Removing Roadblocks in the Move to Angular 4
Tyler Davis
Reading time: about 7 min
Topics:
Angular has become the de facto front-end MVC framework of the Web. We had been slowly adopting Angular 1 here at Lucidchart, but the vast majority of our crucial components were built in jQuery and vanilla JavaScript. We were one of the early pioneers of Angular 2. We found Angular 2 compelling because of the improved performance over Angular 1, and the structure, consistency, and productivity that we gained was refreshing after stepping out of the jungle of jQuery. However, we have experienced a few pain points with Angular 2. We use the closure compiler for advanced minification and optimizations, and Angular 2 did not produce closure compiler compatible JavaScript. As a result, we had to fork Angular and add closure compatibility. Our bundles that use Angular 2 are also larger than the equivalent jquery bundles—although they still generally load faster. We were excited for the Angular 4 release because it addresses both of those major pain points.
Running our application on Angular 4
We have a fairly large Angular codebase with over 400 components, modules, directives, and pipes. We also use a wide variety of the APIs provided by Angular. I always assume that after an upgrade, some things will be broken. I wasn’t wrong. We ran into quite a few issues with our move to Angular 4. Some of them were documented in the Angular changelog and others weren’t.ERROR TypeError: this._trackByFn is not a function
This runtime bug was documented in the Angular changelog.core: KeyValueDifferFactory and IterableDifferFactory no longer have ChangeDetectorRef as a parameter. It was not used and has been there for historical reasons. If you call DifferFactory.create(...) remove the ChangeDetectorRef argument. Introduced by (#14311).Passing a
ChangeDetectorRef
into the DifferFactory.create(...)
method was throwing an error. This problem was extremely easy to fix. I changed
this.differ = this.iterableDiffers.find(value).create(this.cdr);
to
this.differ = this.iterableDiffers.find(value).create((value) => value);
Providers in a structural directive are no longer available in the templates created by the directive
We were relying on classes provided in the providers of a structural directive. I filed a bug with the Angular team for this one. Working around this issue was also fairly easy. I just created a dumb directive that provides the necessary classes, which can be added on the parent where needed. You can read more here.Pop-ups not getting removed after use
This bug was a result of code written about a year ago in one of the early betas that relied on an implementation detail. So it isn’t surprising that it failed. Fixing it was simply changing this for loop that found the index of aComponentRef.hostView
in a ViewContainerRef
and then removed it:
for (var i = 0; i < this.viewContainer.length; i++) {
if (this.viewContainer.get(i) === componentRef.hostView) {
this.viewContainer.remove(i);
break;
}
}
to
componentRef.destroy();
Dispatch event no longer exported
A few of our tests were importing the functiondispatchEvent
from '@angular/platform-browser/testing/browser_util';
in order to dispatch events on elements. This function was no longer exported from that module. We already have an internal MockInteractions
class that has an equivalent method, so I updated those instances to use that class instead.
Generic type 'IterableDiffer<V>' requires 1 type argument(s)
The typeIterableDiffer
imported from @angular/core
was given a type parameter. Giving the class that used the differ a type parameter and then applying that same type to the differ (IterableDiffer<T>
) fixed this problem.
Directive inheritance started working
We were previously using version 2.2.1, which means we didn’t have working component or directive inheritance. However, we did have some directives that would inherit from each other and then manually define the inputs. Something we did not expect is that having two directives on the same component—one that inherits from the other—meant that both directives would receive the inputs meant just for the base class. We did not have this problem before because inheriting inputs did not work, but when we moved to Angular 4.0 the inputs of base classes started applying. This issue manifested itself with some odd behavior. To fix this problem, I created a small (hopefully temporary) workaround to keep the tooltip from applying its base classes inputs.// this is a hack because directive inheritance is now working correctly
// the real fix is to fix our popup inheritance tree
set pinned(value: boolean) {/* DO NOTHING */}
get pinned(): boolean { return false; }
set pinnable(value: boolean) {/* DO NOTHING */}
get pinnable(): boolean { return false; }
Warning: Can't resolve all parameters for <ClassName> ... This will become an error in Angular v5.x
After moving to Angular 4, we started getting these warnings in a few places. Some of these warnings were caused by the@Injectable
directive getting placed on classes that weren’t actually injectable. Fixing these mistakes was as simple as deleting the @Injectable
annotation. In other places, ngc
could not resolve the dependency because we relied on its dependency being provided in a different @NgModule
than what was being provided. Fixing the remaining warnings will either involve adding a default provided value or refactoring where things are provided.
Had to update class that extends AsyncPipe
According to the Angular changelog, directives that extendAsyncPipe
may not compile correctly after updating.
common: Classes that derive from AsyncPipe and override transform() might not compile correctly. The much more common use of async pipe in templates is unaffected. We expect no or little impact on apps from this change, file an issue if we break youAlthough we didn’t file an issue, they did break our pipe that extended
AsyncPipe
.
Title now requires document
In one of our components, we were creating theTitle
service which is exported from @angular/platform-browser
. The interface for Title
changed, resulting in a compile error. The correct fix for this compile error is to inject the service instead of creating it.
Property 'url' does not exist on type 'Event'.
The type ofRouter.events
was updated. There are now multiple types of events that are listened to when observing events. We were only interested in the NavigationStart
event, so we filtered out all other events.
this.sub = this.router.events.subscribe((event) => {
this.routeChange(event.url)
});
to
this.sub = this.router.events.filter(event => event instanceof NavigationStart).subscribe((event: NavigationStart) => {
this.routeChange(event.url)
});
Promises type resolution was improved
Better type resolution is a great improvement in the TypeScript compiler. It found a few places where we had subtle mistakes in our type definitions. Can you spot the change?- sendPendingInvitations(): Option<Promise<(Collaborator|PromiseRejection[])>>
+ sendPendingInvitations(): Option<Promise<(Collaborator | PromiseRejection)[]>>
Assigning to a new
Typescript’s type checking also became more strict about assigning a newly created object without a type declaration to a specific type. Fixing these instances was simple.- @Output() onColumnSelectionChange: EventEmitter<(number|null)[]> = new EventEmitter();
- @Output() onColumnNumberChange: EventEmitter = new EventEmitter();
+ @Output() onColumnSelectionChange: EventEmitter<(number|null)[]> = new EventEmitter<(number|null)[]>();
+ @Output() onColumnNumberChange: EventEmitter = new EventEmitter();
Results
After upgrading to Angular 4, our largest gzipped bundle was 15% smaller. Teams that upgrade to Angular 4 that aren’t using the closure compiler might see even better results. We have a large application and had quite a few issues, but hopefully the upgrade goes more smoothly for you! If you are interested in trying out our Angular application, click here.About Lucid
Lucid Software is a pioneer and leader in visual collaboration dedicated to helping teams build the future. With its products—Lucidchart, Lucidspark, and Lucidscale—teams are supported from ideation to execution and are empowered to align around a shared vision, clarify complexity, and collaborate visually, no matter where they are. Lucid is proud to serve top businesses around the world, including customers such as Google, GE, and NBC Universal, and 99% of the Fortune 500. Lucid partners with industry leaders, including Google, Atlassian, and Microsoft. Since its founding, Lucid has received numerous awards for its products, business, and workplace culture. For more information, visit lucid.co.