angular 用于响应式和基于模板的表单的信号

taor4pac  于 6个月前  发布在  Angular
关注(0)|答案(2)|浏览(52)

@angular/* package(s) are relevant/related to the feature request?

forms

Description

虽然当前的表单模块已经支持使用 valueChangesstatusChanges 支持响应式,并且我们可以使用 RxJS / Signal 互操作性帮助程序与我们的响应式表单中的信号一起工作,但在处理新信号 API 的表单方面仍然存在相当大的差距。
虽然我相信需要对更符合信号简单 API 设计、基于注入帮助程序和基于信号的组件帮助程序的完全重写来重构 Forms 模块,但这也需要大量的时间来反思和构建。我喜欢 @ShacharHarshuv #51786 的提案,它提供了一些关于与信号保持一致的简单 API 的好想法。然而,仍然有很多事情需要考虑(自定义控件值是非原始类型,可能会与组(TValue first vs TControl first 在 Typed forms discussion 中的讨论)。
与此同时,如果能够更新现有的表单模块以便在处理表单时使用基于信号的状态,或者将任何基于表单的状态公开为响应式值,而不是有时有限的 valueChangesstatusChanges,那就太好了。这将允许结合使用 computed 和执行 side-effects 的 effect 以满足许多表单处理用例。

提议的解决方案

提议的解决方案是在 AbstractControl 中包含所有相关表单状态的信号,并在表单控件的本地状态更新时进行更新。这样,开发人员就可以从响应式和模板驱动表单中受益于新的响应式表单状态,因为 NgForm / NgModel 在底层使用了 AbstractControl。这是一个可行且不侵入性的替代方案,可以在可能重写 Forms 模块之前使用。
在此提案中,表示表单控件状态的 AbstractControl 信号被分组到 signals 对象中,该对象位于 AbstractControl 内部。这允许常规表单状态和信号共存,而不会引起名称冲突或过长的冗长前缀。这也鼓励使用解构 const {value, valid} = myControl.signals; 从特定信号中提取。
对于使用 NgFormNgModel 的模板驱动表单,当前的想法是提供一种方法,将您的信号从组件注入到控件中,使用 ngFormOptionsngModelOptions。由于开发人员无法控制模板驱动表单中的表单控件创建,因此这是连接在模板中创建的表单与组件中的某些信号的最简单的方法。另一种想法是从组件中获取 NgModel 指令,然后从暴露的控件接收信号。然而,这需要等待 AfterViewInit 甚至 AfterViewChecked ,这会损害声明性方法,可以通过手动创建信号传递给控件来实现这一点。
为了提供使用外部表单控件信号所需的便利性,我添加了一个辅助函数 createFormSignals 。在当前 PR 的版本中,我还正在尝试使 value 信号成为 WritableSignal。这样,最简单的方式是将此提案与 NgModel / NgForm 结合使用,可以使用值信号从组件内以编程方式更新表单值。目前,这是通过抽象控件内的 effect 实现的,需要进行大量测试以使其足够健壮并确保一致的状态,同时不引起循环。

@Component({
  selector: 'app-capacity',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
<p>Reservations: {{reservations()}}</p>
<p>Capacity: <input type="number" [formControl]="capacity" /></p>
`
})
export class CapacityComponent {
  capacity = new FormControl(5, {nonNullable: true});
  reservations = signal(0);
  interval = setInterval(() => this.reservations.update(count => count + 1), 1000);
  hasCapacity = computed(() => this.capacity.signals.value() > this.reservations());
  capacityEffect = effect(() => console.log(this.hasCapacity() ? 'We have some capacity!' : 'No capacity!'));
}

这个简单的具有响应式表单的示例表明,即使 FormControl 不本机使用信号,我们仍然可以从新暴露的信号对象中受益,并可以以非常声明式的构造方式创建更大的响应式结构。
对于模板驱动表单,我们可以通过使用 ngFormOptionsngModelOptions 指令输入来实现类似的响应式处理。由于 NgModel 或 NgForm 模型绑定是可选的,当使用此提案创建基于信号的表单时,现有的模板驱动表单可以成为默认工具。

@Component({
  selector: 'app-simple-template',
  standalone: true,
  imports: [FormsModule],
  template: `
<input type="text" reqired ngModel [ngModelOptions]="{signals: searchQuery}" />
<div>Results: {{results()}}</div>
`
})
export class SimpleTemplateComponent {
  searchService = inject(SearchService);
  searchQuery = createFormSignals('');
  results = signal([] as string[]);
  searchEffect = effect(async () => {
    if (this.searchQuery.value().length > 3) {
      this.results.set(await lastValueFrom(this.searchService.search(this.searchQuery.value())));
      // Use the writable value signal to update the form programmatically (currently experimenting with this)
      this.searchQuery.value.set('');
    }
  }, {allowSignalWrites: true});
}

或者一个稍微复杂的模板驱动表单:

@Component({
  selector: 'app-signal-based-form',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
<form #form="ngForm" [ngFormOptions]="{signals}">
<label>
First Name:
<input type="text" required name="firstName" ngModel/>
</label>
<label>
Last Name:
<input type="text" name="lastName" ngModel/>
</label>

<p>Full name: {{ fullName() }}</p>
@if (showErrors()) {
<p>Form contains errors</p>
}
<button (click)="updateFormValue()">Update form</button>
</form>
`,
})
export class SignalBasedFormComponent {
  signals = createFormSignals({ firstName: '', lastName: '' });
  fullName = computed(
    () => `${this.signals.value().firstName} ${this.signals.value().lastName}`
  );
  showErrors = computed(
    () => this.signals.invalid() && this.signals.dirty()
  );
  logFullname = effect(() => {
    console.log(`Full name: ${this.fullName()}`);
  });
  updateFormValue() {
    this.signals.value.set({
      firstName: 'Bob',
      lastName: 'Smith'
    });
  }
}

我已经创建了包含所需更改的新功能 PR。该 PR 不包含任何测试、没有文档,可能还有一些错误。 #53481
我也发布了包含这些更改的表单模块到一个临时 NPM 包中,并将其包含在一个 stackblitz 中供您玩耍: https://stackblitz.com/edit/stackblitz-starters-uyjg2x?file=src%2Freactive-form.component.ts,src%2Ftemplate-driven-form-signals-option.component.ts,src%2Ftemplate-driven-form.component.ts,src%2Fsignal-based-forms.component.ts
我希望听到您的反馈、想法!
祝好!
Gion

mctunoxg

mctunoxg1#

我认为这是一个将信号的好处快速传递给Angular表单的绝佳方法。我也同意在Angular中可能有一个完全重新思考表单的地方,并简化围绕它们的DX(例如在my proposition中)。

sczxawaw

sczxawaw2#

我刚刚更新了提案,以包含一种更简洁的方式来创建用于在NgForm和NgModel中使用的表单信号。我还实验性地将value更改为WritableSignal,并将其添加到AbstractControlSignals上。示例已更新,NPM库和Stackblitz也已更新。

相关问题