Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

Angular 9 Update Guide

Learn to successfully update to Angular version 9.

Prerequisites

Before you begin updating to Angular version 9 with the new Ivy renderer, there are a few prerequisites you need complete:

  1. NgForm selector.
  2. @ContentChild and @ContentChildren hosts.
  3. Do not assign values to template-only variables.
  4. TypeScript Compiler Updates (optional).
  5. Renderer deprecation.

Let’s break each of these down.

NgForm Selector

First, update all NgForm selectors in your application where you are using the <ng-form> custom element. This does not affect you if you are using the standard <form> element or reactive forms.

Before:

<ngForm #personForm="ngForm">
  <mat-form-field>
    <mat-label>Search</mat-label>
    <input matInput [(ngModel)]="q" name="q" />
  </mat-form-field>
</ngForm>

To be compliant, you need to update from <ngForm> to <ng-form>:

<ng-form #personForm="ngForm">
  <mat-form-field>
    <mat-label>Search</mat-label>
    <input matInput [(ngModel)]="q" name="q" />
  </mat-form-field>
</ng-form>

@ContentChild and @ContentChildren Hosts

The @ContentChild and @ContentChildren decorator queries will no longer be able to match their directive’s own host node.

Before:

@Directive({
  selector: '[swrActions]',
})
export class ActionsDirective implements AfterContentInit {
  // [TODO]: Angular v9 ContentChild will not return host element!!
  @ContentChild(ActionsDirective, { static: true, read: ElementRef })
  selfElementRef: ElementRef;

  constructor(private readonly renderer: Renderer2) {}

  ngAfterContentInit() {
    const el = this.selfElementRef.nativeElement as HTMLElement;
    if (!el) {
      return;
    }
    this.renderer.setStyle(el, 'display', 'flex');
    this.renderer.setStyle(el, 'flexDirection', 'row');
    this.renderer.setStyle(el, 'justifyContent', 'flex-end');
  }
}

Above, we are using the @ContentChild content query to access the host ElementRef for the directive. FWIW, this is not a best practice, which is why this is being deprecated.

Ideally, we are accessing the ElementRef via a dependency that is injected into our directive:

@Directive({
  selector: '[swrActions]',
})
export class ActionsDirective implements AfterContentInit {
  constructor(
    private readonly elementRef: ElementRef,
    private readonly renderer: Renderer2,
  ) {}

  ngAfterContentInit() {
    const el = this.elementRef.nativeElement as HTMLElement;
    if (!el) {
      return;
    }
    this.renderer.setStyle(el, 'display', 'flex');
    this.renderer.setStyle(el, 'flexDirection', 'row');
    this.renderer.setStyle(el, 'justifyContent', 'flex-end');
  }
}

The goal here is to avoid using the @ContentChild() and @ContentChildren() queries for accessing a host element. The migration is to rely on injecting the host’s ElementRef.

Do Not Assign Values to Template-only Variables

This migration is necessary to avoid mutation and assigning unknown properties to the object.

One of the BIG updates for Angular version 9 with the new Ivy renderer is template type checking, and this is directly related to this new functionality. Before Angular version 9 and the Ivy renderer, template-only variables where basically an any type. This means, you could mutate the object, and its properties, as you saw fit. In general, this was an antipattern.

Going forward with Angular version 9 and the Ivy renderer, template-only variables will be strongly typed based on your TypeScript compiler options for strictness of template type checking. Therefore, it is imperative that we do not mutate template-only variables.

Let’s look at an example that does mutate template-only variables:

<button
  #translateBtn
  (click)="translateBtn.translate = !translateBtn.translate"
>
  Translate
</button>

<mat-accordion>
  <mat-expansion-panel *ngFor="let person of people">
    <mat-expansion-panel-header>
      <mat-panel-title>
        {{ person.fields.name | wookiee: translateBtn.translate }}
      </mat-panel-title>
    </mat-expansion-panel-header>
  </mat-expansion-panel>
</mat-accordion>

The translateBtn template-only variable with Angular version 8 is of type any. With Angular version 9, the variable is strongly typed as an HTMLButtonElement. And, the HTMLButtonElement interface does not have a translate property.

Our goal is to remove mutations of template only variables, and to rely on component properties and methods:

<button (click)="ontTranslate()">Translate</button>

<mat-accordion>
  <mat-expansion-panel *ngFor="let person of people">
    <mat-expansion-panel-header>
      <mat-panel-title>
        {{ person.fields.name | wookiee: translateToWookiee }}
      </mat-panel-title>
    </mat-expansion-panel-header>
  </mat-expansion-panel>
</mat-accordion>

We have removed the template-only variables. Note that the template variable notation #translateBtn has been removed.

Now, we simply implement the property and methods in our component’s class:

export class PeopleListComponent {
  /** True if the content should be translated for Chewbaka */
  translateToWookiee = false;

  onTranslate(): void {
    this.translateToWookiee = !this.translateToWookiee;
  }
}

TypeScript Compiler Updates (optional)

Before updating to Angular version 9, you can opt-in to compiler updates that will make your update more seamless. Update your tsconfig.json file as follows:

{
  "compilerOptions": {
    // omitted for brevity
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noFallthroughCasesInSwitch": true,
    "strictNullChecks": true
    // omitted for brevity
  }
}

The key compiler flags to include are:

Renderer Deprecation

This change has been long awaited, so this should be no surprise. The deprecated Renderer is finally being removed from the Angular codebase. This means that if your Angular project relies, via dependency injection, on the deprecated Renderer class, that you need to migrate to Renderer2.

Thankfully, this migration is eased by the ng update command we are going to execute shortly. So, if you’re lazy (like me), then you can skip this for now and let the Angular update migrate this for you.

Most methods are easily updated from Renderer to Renderer2, but those that are not will be fixed for you.

Be sure to review the git diff after updating for any Renderer to Renderer2 migrations.

Update to the Latest Angular 8 Patch

Before you update to Angular version 9, it’s imperative that you first update to the latest stable release of Angular version 8:

ng update @angular/core@8 @angular/cli@8

Update to Angular version 9

We made it! We’re ready to update our Angular project to Angular version 9 via ng update:

ng update @angular/cli @angular/core

While the release is not final, and currently in release candidate stage, you’ll need to append the --next flag:

ng update @angular/cli @angular/core --next

Localization (i18n)

If you are using Angular’s localization (i18n) framework, then you also need to use the ng add command to install a new @angular/localize package:

ng add @angular/localize

When you examine the diff of the angular.json file after updating, you’ll also notice a new i18n configuration section:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-v9": {
      "i18n": {
        "locales": {
          "de": {
            "translation": "src/locale/messages.de.xlf",
            "baseHref": ""
          }
        }
      }
    }
  }
}

There are a few updates to your angular.json configuration you should consider.

First, specify the sourceLocal property. The default value for this is en-US, so you’ll want to specify the source locale for your application based on the locale of the source code:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-v9": {
      "i18n": {
        "sourceLocale": "en-US",
        "locales": {
          "de": {
            "translation": "src/locale/messages.de.xlf",
            "baseHref": ""
          }
        }
      }
    }
  }
}

Second, consider updating the baseHref configuration for each locale based on your needs. In most instances, we’ll serve each locale of our application using either unique subdomains or unique paths.

In my use case, I wanted each locale to be served on unique paths:

As such, I modified the baseHref for the de locale to:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-v9": {
      "i18n": {
        "sourceLocale": "en-US",
        "locales": {
          "de": {
            "translation": "src/locale/messages.de.xlf",
            "baseHref": "/de/"
          }
        }
      }
    }
  }
}

Next, my Angular version 8 project used mulitple build configurations for each locale. Based on my experience so far, you can remove these as they are no longer necessary:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-v9": {
      ...
      "architect": {
        "build": {
          ...
          "configurations": {
            "de": {
              ...
            },
            "production-de": {
              ...
            }
          }
        }
      }
    }
  }
}

I removed both the de and production-de configurations.

Finally, update your production build command in the project’s package.json file to include the new --localize flag to build all localizations:

{
  "name": "angular-v9",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    ...
    "serve:dist": "http-server -p 8080 -c-1 dist/angular-v9",
    "build": "ng build --prod --localize",
  },

Note:

Let me also mention that you can create additional configuration for building either a specific locale or a group of locales.

Post Update Checklist

After we have successfully updated to Angular version 9, there are two minor things we need to do:

  1. Migration from the deprecated TestBed.get() method to the new TestBed.inject<T>() method.
  2. Remove unnecessary entryComponents properties in our @NgModule() decorators object.

Migrate to TestBed.inject()

Post the Angular version 9 update, our first task is to migration from TestBed.get() to TestBed.inject<T>().

Before:

describe('FilmService', () => {
  let service: FilmService;

  beforeEach(() =>
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
    }),
  );

  it('should be created', () => {
    service = TestBed.get(FilmService);
    expect(service).toBeTruthy();
  });
});

After:

describe('FilmService', () => {
  let service: FilmService;

  beforeEach(() =>
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
    }),
  );

  it('should be created', () => {
    service = TestBed.inject<FilmService>(FilmService);
    expect(service).toBeTruthy();
  });
});

Note:

Remove entryComponents

With Angular version 9 and the new Ivy renderer, we no long need to explicitly instruct the compiler of components that are outside the component dependency graph. A good example of components that need to be declared in the entryComponents array are dialogs.

This update is simple, and who doesn’t love deleting code. Just remove the entryComponents array from all @NgModule() decorators in your application.

Before:

@NgModule({
  declarations: [
    // omitted for brevity
    PersonFilmsDialogComponent,
    PersonHomePlanetDialogComponent,
  ],
  entryComponents: [
    PersonFilmsDialogComponent,
    PersonHomePlanetDialogComponent,
  ],
})
export class PeopleModule {}

And after we have removed the unnecessary entryComponents array:

@NgModule({
  declarations: [
    // omitted for brevity
    PersonFilmsDialogComponent,
    PersonHomePlanetDialogComponent,
  ],
})
export class PeopleModule {}

Success!

We have successfully updated to Angular version 9 and the new Ivy rendering engine.

Have questions? Need help? Let me know!