input(), output(), viewChild(), viewChildren(), contentChild(), contentChildren(), outputFromObservable(), and outputToObservable() are in the stable status in Angular 19. Production applications can start using the Angular schematics to convert from the decorators to the signals.
Angular 19 combines the signal input, output, and query migrations into a single signal migration. Developers can open a terminal, execute the schematic, and select one or all migrations from the text menu.
Let's do a step-by-step migration in this blog post.
You can find the decorator version in this feature branch: https://github.com/railsstudent/ithome2024-demos/tree/refactor/day40-decorators/projects/day40-migration-schematic-demo
Example 1: Migration to signal inputs and new output
@Component({
selector: 'app-some',
standalone: true,
imports: [FormsModule],
template: `
<div>
<p>bgColor: {{ bgColor }}</p>
<p>name: {{ name }}</p>
</div>
`,
})
export class SomeComponent {
@Input({ required: true, alias: 'backgroundColor'}) bgColor!: string;
@Input({ transform: (x: string) => x.toLocaleUpperCase() }) name: string = 'input decorator';
}
The SomeComponent
component uses the Input
decorator to receive inputs from the parent component. bgColor
is a required input with an alias backgroundColor
. name
is an input that transforms the incoming value to uppercase.
export class SomeComponent {
@Output() triple = new EventEmitter<number>();
@Output('cube') powerXBy3 = new EventEmitter<number>();
numSub = new BehaviorSubject<number>(2);
@Output() double = this.numSub.pipe(map((n) => n * 2));
}
The same component also has three event emitters that apply the Output
decorator. triple
is an event emitter that emits a number. powerXBy3
is also a number event emitter with an alias cube
. double
consumes the value of the numSub
behaviorSubject, multiples by 2, and emits the result to the parent component.
Run the Angular signal schematics in a terminal to migrate the demo.
Migrate Input decorators to signal inputs
ng g @angular/core:signals
In the selection menu, select all the migrations and proceed to the next step.
After the signal input migration,
readonly bgColor = input.required<string>({ alias: "backgroundColor" });
readonly name = input<string, string>('input decorator',
{ transform: (x: string) => x.toLocaleUpperCase() });
For bgColor
, the input.required
function is invoked with the alias option. For name
, the input is invoked with an initial value and the transform option to capitalize the input text. The migration has no impact on the parent component.
@Component({
selector: 'app-some',
standalone: true,
imports: [FormsModule],
template: `
<div>
<p>bgColor: {{ bgColor() }}</p>
<p>name: {{ name() }}</p>
`,
})
export class SomeComponent {}
bgColor
and name
are signal inputs; the HTML template calls the signal functions to display their values.
After the output migration,
readonly triple = output<number>();
readonly powerXBy3 = output<number>({ alias: 'cube' });
numSub = new BehaviorSubject<number>(2);
double = outputFromObservable(this.numSub.pipe(map((n) => n * 2)));
triple
calls the output function that emits a number. powerX3
calls the output function and passes the alias option as the first argument. The value of alias is cube
, same as before. double
uses the outputFromObservable
function to convert the Observable into an output. The migration also has no impact on the parent component.
Convert the OutputRef to output
num = signal(2);
double = output<number>();
updateNum(value: number) {
this.num.set(value);
this.double.emit(value * 2);
}
The numSub
BehaviorSubject is converted to a signal, and the double
OutputRef is converted to the output function. I defined a updateNum
method to overwrite the num
signal and emit that value to the double
custom event.
<div>
Num: <input type="number" [ngModel]="n" (ngModelChange)="updateNum($event)" />
</div>
Instead of emitting the value to the numSub
Subject, the new value is passed to the updateNum
method.
Example 2: Migrate the query decorators
export class QueriesComponent implements AfterContentInit {
@ContentChild('header') header!: ElementRef<HTMLDivElement>;
@ContentChildren('p') body!: QueryList<ElementRef<HTMLParagraphElement>>;
appendHeader = '';
list = '';
ngAfterContentInit(): void {
this.appendHeader = `${this.header.nativeElement.textContent} Appended`;
this.list = this.body.map((p) => p.nativeElement.textContent).join('---');
}
}
The QueriesComponent
component applies the ContentChild
and ContentChildren
decorators to query the HTML elements projected to the <ng-content>
elements.
export class AppComponent implements AfterViewInit {
@ViewChild(QueriesComponent) queries!: QueriesComponent;
@ViewChildren('a') aComponents!: QueriesComponent[];
viewChildName = '';
numAComponents = 0;
ngAfterViewInit(): void {
this.viewChildName = this.queries.name;
this.numAComponents = this.aComponents.length;
}
}
The AppComponent
component uses the ViewChild
decorator to query the first occurrence of the QueriesComponent
. It uses the ViewChildren
decorator to query all the QueriesComponent
components matching the template variable, a.
Migrate the query decorators to the query functions
After the query migration,
export class QueriesComponent implements AfterContentInit {
readonly header = contentChild.required<ElementRef<HTMLDivElement>>('header');
readonly body = contentChildren<ElementRef<HTMLParagraphElement>>('p');
appendHeader = '';
list = '';
ngAfterContentInit(): void {
this.appendHeader = `${this.header().nativeElement.textContent} Appended`;
this.list = this.body().map((p) => p.nativeElement.textContent).join('---');
}
}
<div>Appendheader: {{ appendHeader() }}</div>
<div>List: {{ list() }}</div>
The schematic migrates the ContentChild
decorator to the contentChild
function with the correct type. Similarly, the schematic migrates the ContentChildren
decorator to the contentChildren
function. The contentChild
and contentChildren
functions return a signal; therefore, the codes in the ngAfterContentInit
lifecycle method are also modified. The method invokes the signal function before accessing the properties and assigning the results to the variables.
Convert instance members to computed signals
appendHeader = computed(() => `${this.header().nativeElement.textContent} Appended`);
list = computed(() => this.body().map((p) => p.nativeElement.textContent).join('---'));
The component removes the fterContentInit
interface and the ngAfterContentInit
method. The appendHeader
and list
are converted to the computed signals. The HTML template calls the functions to display the value of appenderHeader
and list
.
export class AppComponent implements AfterViewInit {
readonly queries = viewChild.required(QueriesComponent);
readonly aComponents = viewChildren('a');
viewChildName = '';
numAComponents = 0;
ngAfterViewInit(): void {
this.viewChildName = this.queries().name;
this.numAComponents = this.aComponents().length;
}
}
The schematic migrates the ViewChild
decorator to the viewChild
function with the correct type. Similarly, the schematic migrates the ViewChildren
decorator to the viewChildren
function. The viewChild
and viewChildren
functions return a signal; therefore, the codes in the ngAfterViewInit
lifecycle method are also modified. The method invokes the signal function before accessing the properties and assigning the results to the variables.
Convert instance members to computed signals
viewChildName = computed(() => this.queries().name);
numAComponents = computed(() => this.aComponents().length);
<p>ViewChildName: {{ viewChildName() }}</p>
<p>numAComponents: {{ numAComponents() }}</p>
The component removes the AfterViewInit
interface and the ngAfterViewInit
method. The viewChildName
and numAComponents
are converted to the computed signals. Finally, the HTML template calls the functions to display the values.
References:
- Signal migration: https://github.com/angular/angular/tree/main/packages/core/schematics/ng-generate/signals
- Decorators branch: https://github.com/railsstudent/ithome2024-demos/tree/refactor/day40-decorators/projects/day40-migration-schematic-demo
- Main branch: https://github.com/railsstudent/ithome2024-demos/tree/main/projects/day40-migration-schematic-demo
Top comments (0)