angularrxjsangular-signalsangular18

How to bind an Angular signal/model to a template-driven <input> with an object as a value?


I'm trying to implement two-way binding in Angular18 by binding a property of an object within a signal (model) to ngModel. Specifically, I want to use a model signal as the source and ensure that changes update the corresponding property of the object and trigger signal notification cycle. However, the Angular documentation primarily shows examples with non-object signal values, which has left me confused about how to correctly modify object values.

Here are the approaches I've considered:

  1. Here, I bind the ngModel directly to the properties of the signal's value object. While this allows me to modify the object properties and "updates" the object in the parent component, it does not notify consumers of signal changes. As a result, effect() and the output (personDataChange) will not be triggered.
<input [(ngModel)]="personData().firstName"/>
<input [(ngModel)]="personData().lastName"/>
personData = model.required<personData>();
  1. Here, I use the (ngModelChange) event to manually update the signal. However, this feels too bulky, as I would need to implement this for every form field.
<input [ngModel]="personData().firstName"
       (ngModelChange)="update($event)"/>
update($event: string | undefined) {
  this.personData.update(value => ({...value, firstName: $event}));
}

So, my question is - is there another way of doing it so that the value would update and the update would notify consumers? I would appreciate any suggestions on best practices for this usecase.


Solution

  • In your example, the two-way binding directly mutates the values inside signals without notifying the signal, which can cause issues where updates are not reflected correctly.

    Signal works fine with [(ngModel)]="regularSignal" so there is no need for model input

    Instead of passing a primitive value from the parent, you can pass an object containing signals, as shown below:

    export class PersonalComponent {
      personData = input.required<{
        firstName: Signal<string>;
        lastName: Signal<string>;
      }>();
    
      _ = effect(() => {
        console.log(`[PERSONAL] Component ${this.personData().firstName()}`);
      });
    }
    

    ParentComponent

    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [PersonalComponent],
      template: `
        <app-personal [personData]="pData" />
      `,
    })
    export class App {
      pData = { firstName: signal('ng'), lastName: signal('dev') };
    
      _ = effect(() => {
        console.log(`[APP] Component ${this.pData.firstName()}`);
      });
    }
    

    Working Example