wertzui
wertzui

Reputation: 5740

Angular Signals: How to destructure model

I have two Angular (v19) components.

I want to have an instance of ComponentB inside the template of ComponentA. ComponentB.parameters should be bound to the individual fields of ComponentA in such a way that a change to ComponentB.parameters updates the individual fields of ComponentA and an update to any of the fields of ComponentA should update ComponentB.parameters.

How to to this?

a.component.ts

@Component({
    selector: 'app-a',
    templateUrl: './a.component.html',
    standalone: true,
    imports: [ComponentB]
})
export class ComponentA {
    public readonly filter = model<string>();
    public readonly orderby = model<string>();
    public readonly skip = model<number>();
    public readonly top = model<number>();
}

b.component.ts

@Component({
    selector: 'app-b',
    standalone: true,
})
export class ComponentB {
    public readonly parameters = model<Parameters>()
}

parameters.ts

export interface Parameters {
    filter?: string;
    orderby?: string;
    skip?: string;
    top?: string;
}

a.component.html

<app-b [(parameters)]="???"></app-b>

Upvotes: 3

Views: 61

Answers (2)

Thomas
Thomas

Reputation: 1140

Using a computed in A component for building Parameters and split two ways binding to destructure Parameter output.

a.component.ts

@Component({
    selector: 'app-a',
    templateUrl: './a.component.html',
    standalone: true,
    imports: [ComponentB]
})
export class ComponentA {
    public readonly filter = model<string>();
    public readonly orderby = model<string>();
    public readonly skip = model<number>();
    public readonly top = model<number>();
    protected parameters= computed(() => ({filter : filters(), orderby : orderby(), skip :skip(), top :top()}));

  protected onParametersChange(params: Parameters | undefined) {
    if (params) {
      filter.set(params.filter);
      orderby .set(params.orderby);
      skip .set(params.skip);
      top .set(params.top);
    }
  }
}

b.component.ts

@Component({
    selector: 'app-b',
    standalone: true,
})
export class ComponentB {
    public readonly parameters = model<Parameters>()
}

parameters.ts

export interface Parameters {
    filter?: string;
    orderby?: string;
    skip?: string;
    top?: string;
}

a.component.html

<app-b [parameters]="parameters()" (parametersChange)="onParametersChange($event)"></app-b>

Note: working because filter, orderby, skip and top are primitive, if there are object, you will need to change equal signal function.

Upvotes: 0

Naren Murali
Naren Murali

Reputation: 57986

We can update the parameters to contain the individual signals, so that you can bind them to [(ngModel)] inside the child component.

export interface Parameters {
  filter: ModelSignal<string>;
  orderby: ModelSignal<string>;
  skip: ModelSignal<number>;
  top: ModelSignal<number>;
}

Then we do not require the child component to accept a model signal, but an input.required signal instead. I am using required because otherwise I need to wrap each element that uses the inner signal like filter inside a if condition, because it can be undefined, so input.required is easier, but let me know if you would like to know how that code would be.

@Component({
  selector: 'app-b',
  standalone: true,
  imports: [FormsModule],
  template: ` <br/><br/><br/>
  Child<br/>
    <input [(ngModel)]="parameters().filter"/><br/>
    <input [(ngModel)]="parameters().orderby"/><br/>
    <input [(ngModel)]="parameters().skip"/><br/>
    <input [(ngModel)]="parameters().top"/><br/>
  `,
})
export class ComponentB {
  public readonly parameters = input.required<Parameters>();
}

Now we can simply, pass an object called parameters which contains all the individual signals, into the child, hence we have two way binded between parent and child component.

@Component({
  selector: 'app-root',
  imports: [ComponentB, FormsModule],
  template: `
    Parent<br/>
    <input [(ngModel)]="filter"/><br/>
    <input [(ngModel)]="orderby"/><br/>
    <input [(ngModel)]="skip"/><br/>
    <input [(ngModel)]="top"/><br/>
    <br/><br/><br/>
    <app-b [parameters]="parameters"></app-b>
  `,
})
export class App {
  public readonly filter = model<string>('');
  public readonly orderby = model<string>('');
  public readonly skip = model<number>(1);
  public readonly top = model<number>(2);
  parameters: Parameters = {
    filter: this.filter,
    orderby: this.orderby,
    skip: this.skip,
    top: this.top,
  };
}

Full Code:

import { Component, ModelSignal, input, model } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

export interface Parameters {
  filter: ModelSignal<string>;
  orderby: ModelSignal<string>;
  skip: ModelSignal<number>;
  top: ModelSignal<number>;
}

    @Component({
      selector: 'app-b',
      standalone: true,
      imports: [FormsModule],
      template: ` <br/><br/><br/>
      Child<br/>
        <input [(ngModel)]="parameters().filter"/><br/>
        <input [(ngModel)]="parameters().orderby"/><br/>
        <input [(ngModel)]="parameters().skip"/><br/>
        <input [(ngModel)]="parameters().top"/><br/>
      `,
    })
    export class ComponentB {
      public readonly parameters = input.required<Parameters>();
    }

@Component({
  selector: 'app-root',
  imports: [ComponentB, FormsModule],
  template: `
    Parent<br/>
    <input [(ngModel)]="filter"/><br/>
    <input [(ngModel)]="orderby"/><br/>
    <input [(ngModel)]="skip"/><br/>
    <input [(ngModel)]="top"/><br/>
    <br/><br/><br/>
    <app-b [parameters]="parameters"></app-b>
  `,
})
export class App {
  public readonly filter = model<string>('');
  public readonly orderby = model<string>('');
  public readonly skip = model<number>(1);
  public readonly top = model<number>(2);
  parameters: Parameters = {
    filter: this.filter,
    orderby: this.orderby,
    skip: this.skip,
    top: this.top,
  };
}

bootstrapApplication(App);

Stackblitz Demo

Upvotes: 0

Related Questions