Migrating Angular v17+ to signal inputs, signal outputs, control flow and more
Posted on
Last update on
TL;DR
In case you just need the commands, here is the overview. Check below for more details. If you are using Nx, you can replace ng
with nx
.
Control flow migration command
npx ng g @angular/core:control-flow
npx ng g @angular/core:control-flow --path=libs/feature-xyz
npx ng add ngxtension
Signal inputs migration command
npx ng g ngxtension:convert-signal-inputs
npx ng g ngxtension:convert-signal-inputs --path=libs/feature-xyz/my-component.component.ts
npx ng g ngxtension:convert-signal-inputs --project=feature-xyz
Signal outputs migration command
npx ng g ngxtension:convert-outputs
npx ng g ngxtension:convert-outputs --path=libs/feature-xyz/my-component.component.ts
npx ng g ngxtension:convert-outputs --project=feature-xyz
Signal queries migration command
npx ng g ngxtension:convert-queries
npx ng g ngxtension:convert-queries --path=libs/feature-xyz/my-component.component.ts
npx ng g ngxtension:convert-queries --project=feature-xyz
Inject function migration command (ngxtension)
npx ng g ngxtension:convert-di-to-inject
npx ng g ngxtension:convert-di-to-inject --path==libs/feature-xyz/my-component.component.ts
npx ng g ngxtension:convert-di-to-inject --project==feature-xyz
Inject function migration command (Angular)
npx ng generate @angular/core:inject-migration
npx ng generate @angular/core:inject-migration --path==libs/feature-xyz
Target audience
This article is intended to help anybody willing to migrate their Angular v17+ project to use signal inputs, signal outputs, the new control flow syntax and inject function.
It does not focus on the specifics of these concepts, just how you can migrate to them. If you want to learn more about them, please check the "Further reading" section at the bottom of this page.
Introduction
We start with a very simple example of a project that uses the well known @Input()
annotation for component inputs and @Output()
for component outputs. Besides that we have a service that is injected using constructor arguments and we are using the typical *ngIf
, *ngFor
and *ngSwitch
structural directives to control the data rendering in our components.
With running a few commands we will be able to automagically migrate these "old" mechanisms to the new input()
, output()
, inject()
and control flow blocks using @if
, @for
and @switch
.
Example Setup
Our example project can be found on Github. It is a very minimal counter Angular project that has 2 inputs, 2 outputs, some state service that holds a writable signal with the current count. Based on the current count we show some information and derived values.
The fully migrated project can be found on a separate branch called migrate-to-angular-v17-plus
. See my Github repository and/or the diff with the main branch for more details.
Migration commands
For updating each of the concepts we have separate commands that will migrate our code automatically. But before we continue, we need to install a new dependency called ngxtension
. This very neat library of migration schematics and other helper utilities will help us migrate to the new Angular v17+ standards.
npx ng add ngxtension
Let's get started!
Control flow syntax
In the HTML of our components we use structural directives such as *ngIf
, *ngFor
and *ngSwitch
, but before they can be used, they need to be imported first. Since standalone components, we can import them separately in the component's or through the CommonModule
.
Component HTML code
<ng-container *ngIf="count === 0; else notZero">
The count is zero.. :(
</ng-container>
<ng-template #notZero>
The count is not zero!
</ng-template>
...
<ng-container [ngSwitch]="count">
<div *ngSwitchCase="0">There are 0 numbers</div>
<div *ngSwitchCase="1">There is only 1 number:{{ count }}</div>
<div *ngSwitchCase="-1">There is only 1 number:{{ count }};</div>
<div *ngSwitchDefault>
There are multiple numbers:
<ul>
<li *ngFor="let i of numbers">{{ i }}</li>
</ul>
</div>
</ng-container>
Component TypeScript code
@Component ({
standalone: true,
imports: [NgIf, NgFor, NgSwitch, NgSwitchCase, NgSwitchDefault],
// imports: [CommonModule],
selector: 'app-current-count',
templateUrl: './current-count.component.html',
styleUrl: './current-count.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
For this migration we don't need the @ngxextension
library yet. To migrate to the new control flow syntax, we need to run the following migration command provided by the Angular CLI. If we want to migrate just a part of our codebase, we can specify the path to limit the migration to:
Control flow migration command
npx ng g @angular/core:control-flow
npx ng g @angular/core:control-flow --path=libs/feature-xyz
After running this command the imports for the structural directives will be removed as this functionality is available through the compiler directly. Our HTML code from above will be transformed to:
Migrated HTML code
@if (count === 0) {
The count is zero.. :(
} @else {
The count is not zero!
}
...
@switch (count) {
@case (0) {
<div>There are 0 numbers</div>
}
@case (1) {
<div>There is only 1 number: {{ count }}</div>
}
@case (-1) {
<div>There is only 1 number: {{ count }}</code></div>
}
@default {
<div>
There are multiple numbers
<ul>
@for (i of numbers; track i) {
<li>{{ i }}</li>
}
</ul>
</div>
}
}
Extra trick: @empty
block
With the new control flow syntax, we can use the @empty
block to handle the case where the list of items to iterate over is empty. This means no more checking for an empty list with an *ngIf
or even @if
condition check first:
@empty block example
<section id="blogposts">
<h1>Blogposts</h1>
@for (blogpost of blogposts; track blogposts.id) {
<article>
<h2>{{ blogpost.title }}</h2>
<p>{{ blogpost.introduction }}</p>
</article>
} @empty {
<p>No blogposts available</p>
}
</section>
Annotated @Input to signal input
Migrating to the new signal inputs is easy with the help of the ngxtension
library. We can run the following command to migrate our @Input()
annotated properties to the new signal input syntax. Same as with the control flow migration, we can specify a path to limit the migration to component or directive file in our project. Alternatively, we can target a specific project as well.
Signal inputs migration command
npx ng g ngxtension:convert-signal-inputs
npx ng g ngxtension:convert-signal-inputs --path=libs/feature-xyz/my-component.component.ts
npx ng g ngxtension:convert-signal-inputs --project=feature-xyz
This results in the code transforming from:
Annotated @Input
import { Input } from '@angular/core';
...
export class CurrentCountComponent {
@Input() count: number = 0;
@Input() numbers: number[] = [];
}
To the new signal input syntax:
Signal inputs
import { input } from '@angular/core';
...
export class CurrentCountComponent {
count = input(0);
numbers = input([]);
}
Annotated @Output to signal output
Similar steps and options apply to the migration of annotated @Outputs to signal based outputs.
Signal outputs migration command
npx ng g ngxtension:convert-outputs
npx ng g ngxtension:convert-outputs --path=libs/feature-xyz/my-component.component.ts
npx ng g ngxtension:convert-outputs --project=feature-xyz
This results in the code transforming from:
Annotated @Output
import { Output } from '@angular/core';
...
export class OperationsComponent {
@Output() increment = new EventEmitter();
@Output() decrement = new EventEmitter();
}
To the new signal based outputs. Note that both input
and output
are exported from the same package, namely @angular/core
, with just a subtle difference; lower input/output instead of Input/Output:
Signal outputs
import { output } from '@angular/core';
...
export class OperationsComponent {
increment = output();
decrement = output();
}
Annotated Queries
To make sure we have a uniform experience with signals, starting from Angular v17, we can also migrate to signal based queries using the ngxtension
tooling library.
Signal queries migration command
npx ng g ngxtension:convert-queries
npx ng g ngxtension:convert-queries --path=libs/feature-xyz/my-component.component.ts
npx ng g ngxtension:convert-queries --project=feature-xyz
The following code, querying a list of images in our component, will be transformed from:
Signal outputs
export class AppComponent {
@ViewChildren('my-images') myImages: QueryList>;
}
into:
Signal outputs
export class AppComponent {
myImages = viewChildren>('my-images');
}
Read more in the documentation of @ngxtension for examples and more.
Constructor injection to inject function
While this is not a v17+ specific feature, but available from v14, it is worth highlighting. In Angular you can make use of the TypeScript constructor
to inject services into your components. The compiler will then apply it's magic to run the dependency injection mechanism to make sure you get the right instance of the provided arguments.
The syntax in the code snippet below makes the stateService
publicly available on the instance of the app component class.
Component TypeScript code
export class AppComponent {
constructor(public readonly stateService: StateService) { }
}
The only requirement here is that the class that you are injecting has the @Injectable
annotation or is provided manually. To run the migration we can again leverage the ngxtension
library:
Inject function migration command (ngxtension)
npx ng g ngxtension:convert-di-to-inject
npx ng g ngxtension:convert-di-to-inject --path==libs/feature-xyz/my-component.component.ts
npx ng g ngxtension:convert-di-to-inject --project==feature-xyz
Running the command above will result in the following code transformation:
Component TypeScript code
export class AppComponent {
public readonly stateService = inject(StateService);
}
Update (September 2024): Because of popular demand, the Angular team created a native migration for the inject function.
Inject function migration command (Angular)
npx ng generate @angular/core:inject-migration
npx ng generate @angular/core:inject-migration --path==libs/feature-xyz
Conclusion
With these simple commands provided by the Angular team and the maintainers of ngxtension
we can easily migrate our Angular projects to keep them up-to-date and be aligned with the new standards.
Further reading
By reading this article I hope you can find a solution for your problem. If it still seems a little bit unclear, you can hire me for helping you solve your specific problem or use case. Sometimes even just a quick code review or second opinion can make a great difference.