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 a ComponentRef.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 function dispatchEvent 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 type IterableDiffer 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 extend AsyncPipe 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 you

Although 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 the Title 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 of Router.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.