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:
NgForm
selector.@ContentChild
and@ContentChildren
hosts.- Do not assign values to template-only variables.
- TypeScript Compiler Updates (optional).
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:
noImplicitAny
prevent the compiler from implying that the type of an argument, variable, or assignment is of typeany
, which is the default when the compiler either cannot infer a type or the code not does explicitly declare the type.noImplicitReturns
prevents you from falling through a function to the end unnoticed, meaning, without your knowledge the function can returnany
implicitly. This ensure that you are correctly returning the proper type, no matter the branch of the code you are within where you do return a value.noImplicitThis
ensure that we don’t assume the value ofthis
. As JavaScript developers we know that thethis
value is dependent upon the execution context of a function. This compiler flag protects us from assuming, or allowing the compiler to imply the context ofthis
.noFallthroughCasesInSwitch
ensures that eachcase
statement is guarded by an appropriatebreak
statement to prevent the case from falling through to thecase
statement.strictNullChecks
ensures that we are not allowing bothundefined
andnull
values from being globally available or assignable unless explicitly declared in our TypeScript. This flag is highly recommended as it prevents bugs that can result fromnullish
coalescing and other truthy/falsey coercions.
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
toRenderer2
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:
- US English: /en-US/
- Deutsch: /de/
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:
- First, I have a new
serve:dist
command where I use the http-server Node module to serve a localized version of my production build. - Second, I updated the
build
command to include the new--localize
flag to build all localizations.
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:
- Migration from the deprecated
TestBed.get()
method to the newTestBed.inject<T>()
method. - 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:
- We change
TestBed.get()
toTestBed.inject<T>()
- We specify the generic of the type that is returned from the
inject()
method.
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!