I'm seeking guidance on the best practice for using two-way binding with signals, particularly for complex objects in template-driven forms. Our team extensively uses template-driven forms and appreciates the simplicity of two-way binding for inputs. We're also excited about the new "model" input, which seems tailored for this purpose.
Currently, we use a viewModel signal to hold the state of our forms. These viewModels are typically objects, which appears to cause issues with two-way binding.
The problem:
- Two-way binding on an object property doesn't update the signal.
- There's no straightforward way to control this without abandoning two-way binding or using separate signals for each input, which becomes cumbersome for larger forms.
Example code: you can just paste the code here: 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);
Questions:
- Is there a recommended approach to handle this scenario?
- Are there any plans to address this in future Angular releases?
- If not, what's the best current practice for managing complex form state with signals while maintaining the convenience of two-way binding?
Any insights or recommendations would be greatly appreciated. Thank you!