DEV Community

Connie Leung
Connie Leung

Posted on

Migrate decorators to the input, query, and output functions

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';
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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() });
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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)));
Enter fullscreen mode Exit fullscreen mode

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);
} 
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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('---');
 }
}
Enter fullscreen mode Exit fullscreen mode

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;
 }
}
Enter fullscreen mode Exit fullscreen mode

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('---');
 }
}
Enter fullscreen mode Exit fullscreen mode
<div>Appendheader: {{ appendHeader() }}</div>
<div>List: {{ list() }}</div>
Enter fullscreen mode Exit fullscreen mode

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('---'));
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
<p>ViewChildName: {{ viewChildName() }}</p>
<p>numAComponents: {{ numAComponents() }}</p>
Enter fullscreen mode Exit fullscreen mode

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:

Top comments (0)