我正在寻求有关使用信号双向绑定的最佳实践的指导,特别是对于模板驱动形式的复杂对象。我们的团队广泛使用模板驱动的表单,并欣赏双向绑定输入的简单性。我们也对新的“模型”输入感到兴奋,它似乎是为此目的量身定制的。
目前,我们使用 viewModel 信号来保存表单的状态。这些 viewModel 通常是对象,这似乎会导致双向绑定问题。
问题:
示例代码: 您可以将代码粘贴到此处:https://angular.dev/playground
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
signal,
WritableSignal
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';
@Component({
selector: 'app-root',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [FormsModule],
template: `
<!-- using two-way-binding with an object signal -->
<!-- this will never update the signal "properly" -->
<input type="text" [(ngModel)]="viewModel().name" />
<!-- is this the recommended way? -->
<!-- of course I could use primitive values for two-way-bindings only -->
<!-- but with larger forms this seems not so nice -->
<!-- especially when using multiple components with the model input -->
<input
type="text"
[ngModel]="viewModel().name"
(ngModelChange)="nameChange($event)"
/>
<br />
<!-- this will be updated because of the change detection that gets triggered by the input -->
<!-- the signal never notifies, because the reference is not changed -->
{{ viewModel().name }}
<br />
<button (click)="onClick()">click</button>
computed: {{ testComputed().name }}
`
})
export class CookieRecipe {
viewModel: WritableSignal<{ name: string }> = signal({ name: 'startName' });
testComputed = computed(() => {
// this will not be triggered, because the reference of the signal value is not changed.
console.warn('inside computed', this.viewModel());
return this.viewModel();
});
constructor() {
effect(() => {
// this will not be triggered, because the reference of the signal value is not changed.
console.warn('inside effect', this.viewModel());
});
}
onClick() {
console.warn('button clicked', this.viewModel());
// the set here will change the reference and therefore the signal will update the effect, the computed and the view
this.viewModel.set({ name: 'buttonClick' });
}
nameChange(name: string) {
this.viewModel.set({ ...this.viewModel, name });
}
ngDoCheck() {
console.warn('inside ngDoCheck');
}
}
bootstrapApplication(CookieRecipe);
问题:
任何见解或建议将不胜感激。谢谢!
一种方法是采取自下而上的方法。
model
,这将处理绑定问题。name = model('startName');
password = model('startNamePassword');
<label for="name">Name:</label>
<input type="text" [(ngModel)]="name"
id="name"
name="name" />
<br/>
<label for="password">Password:</label>
<input
type="text"
id="password"
name="password"
[(ngModel)]="password"
/>
computed
,收集各个模型的所有单独发射并为您提供计算值。 viewModel: Signal<{ name: string; password: string }> = computed(() => {
console.warn('inside computed');
return {
name: this.name(),
password: this.password(),
};
});
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
signal,
WritableSignal,
Signal,
model,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';
@Component({
selector: 'app-root',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [FormsModule],
template: `
<br/>
<label for="name">Name:</label>
<input type="text" [(ngModel)]="name"
id="name"
name="name" />
<br/>
<label for="password">Password:</label>
<input
type="text"
id="password"
name="password"
[(ngModel)]="password"
/>
<br />
{{ viewModel().name }}
<br />
<button (click)="onClick()">click</button>
computed: {{ viewModel().name }}
`,
})
export class CookieRecipe {
name = model('startName');
password = model('startNamePassword');
viewModel: Signal<{ name: string; password: string }> = computed(() => {
console.warn('inside computed');
return {
name: this.name(),
password: this.password(),
};
});
constructor() {
effect(() => {
// this will not be triggered, because the reference of the signal value is not changed.
console.warn('inside effect', this.viewModel());
});
}
onClick() {
console.warn('button clicked', this.viewModel());
}
ngDoCheck() {
console.warn('inside ngDoCheck');
}
}
bootstrapApplication(CookieRecipe);