将装饰器迁移到输入、查询和输出函数

input()、output()、viewChild()、viewChildren()、contentChild()、contentChildren()、outputFromObservable() 和 outputToObservable() 在 Angular 19 中处于稳定状态。生产应用程序可以开始使用 Angular 原理图将装饰器转换为信号。

Angular 19 将信号输入、输出和查询迁移合并为单个信号迁移。开发人员可以打开终端,执行原理图,然后从文本菜单中选择一个或所有迁移。

让我们在这篇博文中逐步进行迁移。

您可以在此功能分支中找到装饰器版本:https://github.com/railsstudent/ithome2024-demos/tree/refactor/day40-decorators/projects/day40-migration-schematic-demo

示例 1:迁移到信号输入和新输出

@Component({
 selector: 'app-some',
 standalone: true,
 imports: [FormsModule],
 template: `
   

bgColor: {{ bgColor }}

name: {{ name }}

`, }) export class SomeComponent { @Input({ required: true, alias: 'backgroundColor'}) bgColor!: string; @Input({ transform: (x: string) => x.toLocaleUpperCase() }) name: string = 'input decorator'; }

`SomeComponent` 组件使用 `Input` 装饰器从父组件接收输入。`bgColor` 是必需输入,别名为 `backgroundColor`。`name` 是将传入值转换为大写的输入。

export class SomeComponent {
 @Output() triple = new EventEmitter();
 @Output('cube') powerXBy3 = new EventEmitter();

 numSub = new BehaviorSubject(2);
 @Output() double = this.numSub.pipe(map((n) => n * 2));
}

同一个组件还有三个应用了 `Output` 装饰器的事件发射器。`triple` 是一个发出数字的事件发射器。`powerXBy3` 也是一个别名为 `cube` 的​​数字事件发射器。`double` 使用 `numSub` behaviorSubject 的值,乘以 2,并将结果发射到父组件。

在终端中运行 Angular 信号示意图来迁移演示。

将输入装饰器迁移到信号输入

ng g @angular/core:signals

在选择菜单中,选择所有迁移并继续下一步。

信号输入迁移后,

readonly bgColor = input.required({ alias: "backgroundColor" }); 
readonly name = input('input decorator', 
{ transform: (x: string) => x.toLocaleUpperCase() });

对于 `bgColor`,使用 alias 选项调用 `input.required` 函数。对于 `name`,使用初始值和 transform 选项调用输入,以使输入文本大写。迁移对父组件没有影响。

@Component({
 selector: 'app-some',
 standalone: true,
 imports: [FormsModule],
 template: `
   

bgColor: {{ bgColor() }}

name: {{ name() }}

`, }) export class SomeComponent {}

`bgColor` 和 `name` 是信号输入;HTML 模板调用信号函数来显示它们的值。

输出迁移后,

readonly triple = output();
readonly powerXBy3 = output({ alias: 'cube' });

numSub = new BehaviorSubject(2); 
double = outputFromObservable(this.numSub.pipe(map((n) => n * 2)));

`triple` 调用输出函数,发出一个数字。`powerX3` 调用输出函数,并将别名选项作为第一个参数传递。别名的值是 `cube`,与之前相同。`double` 使用 `outputFromObservable` 函数将 Observable 转换为输出。迁移对父组件也没有影响。

将 OutputRef 转换为输出

num = signal(2);
double = output();

updateNum(value: number) {
   this.num.set(value);
   this.double.emit(value * 2);
}

`numSub` BehaviorSubject 转换为信号,`double` OutputRef 转换为输出函数。我定义了一个 `updateNum` 方法来覆盖 `num` 信号并将该值发送到 `double` 自定义事件。

Num:

新值不会发送给“numSub”主题,而是传递给“updateNum”方法。

示例 2:迁移查询修饰器

export class QueriesComponent implements AfterContentInit {
 @ContentChild('header') header!: ElementRef;
 @ContentChildren('p') body!: QueryList>;

appendHeader = '';
list = '';

ngAfterContentInit(): void {
      this.appendHeader = `${this.header.nativeElement.textContent} Appended`;
      this.list = this.body.map((p) => p.nativeElement.textContent).join('---');
 }
}

`QueriesComponent` 组件应用 `ContentChild` 和 `ContentChildren` 装饰器来查询投射到 `` 元素。

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;
 }
}

`AppComponent` 组件使用 `ViewChild` 装饰器查询 `QueriesComponent` 的第一次出现。它使用 `ViewChildren` 装饰器查询与模板变量 a 匹配的所有 `QueriesComponent` 组件。

将查询修饰器迁移到查询函数

查询迁移后,

export class QueriesComponent implements AfterContentInit {
 readonly header = contentChild.required>('header');
 readonly body = contentChildren>('p');

 appendHeader = '';
 list = '';

 ngAfterContentInit(): void {
   this.appendHeader = `${this.header().nativeElement.textContent} Appended`;
   this.list = this.body().map((p) => p.nativeElement.textContent).join('---');
 }
}
Appendheader: {{ appendHeader() }}
List: {{ list() }}

该原理图将 `ContentChild` 装饰器迁移到具有正确类型的 `contentChild` 函数。同样,该原理图将 `ContentChildren` 装饰器迁移到 `contentChildren` 函数。`contentChild` 和 `contentChildren` 函数返回一个信号;因此,`ngAfterContentInit` 生命周期方法中的代码也被修改了。该方法在访问属性并将结果分配给变量之前调用信号函数。

将实例成员转换为计算信号

appendHeader = computed(() => `${this.header().nativeElement.textContent} Appended`);

list = computed(() => this.body().map((p) => p.nativeElement.textContent).join('---'));

组件移除了 `fterContentInit` 接口和 `ngAfterContentInit` 方法,将 `appendHeader` 和 `list` 转换为计算信号,HTML 模板调用函数显示 `appenderHeader` 和 `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;
  }
}

该原理图将 `ViewChild` 装饰器迁移到具有正确类型的 `viewChild` 函数。同样,该原理图将 `ViewChildren` 装饰器迁移到 `viewChildren` 函数。`viewChild` 和 `viewChildren` 函数返回一个信号;因此,`ngAfterViewInit` 生命周期方法中的代码也被修改了。该方法在访问属性并将结果分配给变量之前调用信号函数。

将实例成员转换为计算信号

viewChildName = computed(() => this.queries().name);
numAComponents = computed(() => this.aComponents().length);

ViewChildName: {{ viewChildName() }}

numAComponents: {{ numAComponents() }}

组件移除了 `AfterViewInit` 接口和 `ngAfterViewInit` 方法,将 `viewChildName` 和 `numAComponents` 转换为计算信号,最后 HTML 模板调用函数显示值。

参考:

  • 信号迁移:https://github.com/angular/angular/tree/main/packages/core/schematics/ng-generate/signals
  • 装饰器分支:https://github.com/railsstudent/ithome2024-demos/tree/refactor/day40-decorators/projects/day40-migration-schematic-demo
  • 主分支:https://github.com/railsstudent/ithome2024-demos/tree/main/projects/day40-migration-schematic-demo