Model Inputs - Signal API that is missing in 2-way binding
Tags: angular, tutorial, frontend, signal
Published: 2024-02-19 14:44:35

##Introduction

In this blog post, I would like to show a new feature in Angular 17.2 that calls model inputs. Model input is a Signal API for 2-way binding between parent and child components. 2-way binding is always available and the official Angular documentation provides the following sizer example:

```typescript
<app-sizer [(size)]="fontSizePx"></app-sizer>

export class SizerComponent {
  @Input() size!: number | string;
  @Output() sizeChange = new EventEmitter<number>();

  dec() {
    this.resize(-1);
  }
  inc() {
    this.resize(+1);
  }

  resize(delta: number) {
    this.size = Math.min(40, Math.max(8, +this.size + delta));
    this.sizeChange.emit(this.size);
  }
}
```

With model inputs,  I can bind the signals of the parent component to the model inputs of the child component using model() function.  Therefore, the child component does not need the input and output decorators to receive input and to emit changes.

In my demo,  I recreated the generic image placeholder site (https://dev.me/products/image-placeholder) twice.  

Solution 1: This is the old way when Angular does not have Signal.  The implementation uses primarily BehaviorSubject, RxJS operators, @Input, and @Output to build the placeholder URL. 

Solution 2:  The implementation uses model inputs, signal double binding, and computed signal to derive the placeholder URL.  

###The old way: BehaviorSubject and RxJS operators

```typescript
// image-placeholder-subject.componen.ts

@Component({
  selector: 'app-image-placeholder-subject',
  standalone: true,
  imports: [FormsModule],
  template: `
    <h3>Redo https://dev.me/products/image-placeholder - with BehaviorSubject</h3>
    <div class="container">
      <div class="field">
        <label for="text">
          <span>Text: </span>
          <input id="text" name="text" [ngModel]="text" 
            (ngModelChange)="textChange.emit($event)"
          />
        </label>
      </div>
      <div class="field">
        <label for="width">
          <span>Width: </span>
          <input id="width" name="width" [ngModel]="width" 
            (ngModelChange)="widthChange.emit($event)" 
            type="number" min="10" />
        </label>
      </div>
      <div class="field">
        <label for="height">
          <span>Height: </span>
          <input id="height" name="height" [ngModel]="height" (ngModelChange)="heightChange.emit($event)" type="number" min="10" />
        </label>
      </div>
      <div class="field">
        <label for="color">
          <span>Color: </span>
          <input id="color" name="color" [ngModel]="color" (ngModelChange)="colorChange.emit($event)" />
        </label>
      </div>
      <div class="field">
        <label for="backgroundColor">
          <span>Background color: </span>
          <input id="backgroundColor" name="backgroundColor" [ngModel]="backgroundColor" (ngModelChange)="backgroundColorChange.emit($event)" />
        </label>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ImagePlaceholderSubjectComponent {  
  @Input()
  text!: string;

  @Output()
  textChange = new EventEmitter<string>();

  @Input()
  width!: number;

  @Output()
  widthChange = new EventEmitter<number>();

  @Input()
  height!: number;

  @Output()
  heightChange = new EventEmitter<number>();

  @Input()
  color!: string;

  @Output()
  colorChange = new EventEmitter<string>();

  @Input()
  backgroundColor!: string;

  @Output()
  backgroundColorChange = new EventEmitter<string>();
}
```

`ImagePlaceholderSubjectComponent` has a template-driven form that allows users to input text, width, height, text color and background color.  Each form field has a `ngModel` that accepts an input and a `ngModelChange` event that emits changes back to the parent component.

```html
<div class="field">
        <label for="text">
          <span>Text: </span>
          <input id="text" name="text" [ngModel]="text" 
            (ngModelChange)="textChange.emit($event)"
          />
        </label>
</div>

@Input()
text!: string;

@Output()
textChange = new EventEmitter<string>();
```

`[ngModel]="text"` receives values from text input decorator while `(ngModelChange)="textChange.emit($event)"` emits the value to `textChange` event emitter.

Then, `ImagePlaceholderSubjectComponent` can bind text to a string primitive or a BehaviorSubject in the parent component.

```html
<app-image-placeholder-subject  [text]="behaviorSubject.getValue()" (textChange)="behaviorSubject.next($event)" />
```

###Create a parent component for 2-way binding

```typescript
// image-placeholder-subject-container.component.ts

@Component({
  selector: 'app-image-plceholder-subject-container',
  standalone: true,
  imports: [ImagePlaceholderSubjectComponent, AsyncPipe],
  template: `
    <h2>Old way - with BehaviorSubject and RxJS</h2>
    <app-image-placeholder-subject 
      [text]="textSub.getValue()"
      (textChange)="textSub.next($event)"
      [width]="widthSub.getValue()" (widthChange)="widthSub.next($event)"
      [height]="heightSub.getValue()" (heightChange)="heightSub.next($event)"
      [color]="colorSub.getValue()" (colorChange)="colorSub.next($event)"
      [backgroundColor]="backgroundColorSub.getValue()"
      (backgroundColorChange)="backgroundColorSub.next($event)"  
    />
    <img [src]="placeholderUrl$| async" alt="generic placeholder" />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ImagePlaceholderSubjectContainerComponent {
  // The way without mode inputs.  I use BehaviorSubject to implement the same behavior
  textSub = new BehaviorSubject('BehaviorSubject');
  widthSub = new BehaviorSubject(250);
  heightSub = new BehaviorSubject(120);
  colorSub = new BehaviorSubject('#fff');
  backgroundColorSub = new BehaviorSubject('#000');

  placeholderUrl$ = this.textSub.pipe(
    combineLatestWith(this.widthSub, this.heightSub, this.colorSub, this.backgroundColorSub),
    map(([text, width, height, color, bgColor]) => {
      const encodedText = text ? encodeURIComponent(text) : `${width} x ${height}`;
      const encodedColor = encodeURIComponent(color);
      const encodedBgColor = encodeURIComponent(bgColor);

      return `https://via.assets.so/img.jpg?w=${width}&h=${height}&&tc=${encodedColor}&bg=${encodedBgColor}&t=${encodedText}`;
    })
  );
}
```

`ImagePlaceholderSubjectContainerComponent` declares `textSub`, `widthSub`, `heightSub`, `colorSub`, and `backgroundColorSub` BehaviorSubject to bind to the inputs of `ImagePlaceholderSubjectComponent`.  I chose BehaviorSubject such that I could combine their values to create a `placeholderUrl$` Observable. In the HTML template, I used `AsyncPipe` to resolve `placeholderUrl$` and assigned the URL to an image element to render the image placeholder. 

###Demo 2: Model Inputs, Signal double binding, and computed Signal

Model inputs are similar to signal inputs except model inputs can do both read and write.  Moreover, model inputs have a similar syntax as signal inputs.

```typescript
text = model('');
```

`text` is a model input with an initial value of an empty string

```typescript
width = model.required<number>();
height = model.required<number>();
```

`width` and `height` are required model inputs; therefore, the parent component needs to provide input values to them

```typescript
textColor = model('#fff', { alias: 'color' });
bgColor = model('#000', { alias: 'backgroundColor' });
```

`textColor` and `bgColor` are model inputs with initial values and aliases. The parent component binds the values to `color` and `backgroundColor` inputs respectively.

```typescript
// image-placeholder-component.ts

@Component({
  selector: 'app-image-placeholder',
  standalone: true,
  imports: [FormsModule],
  template: `
    <h3>Redo https://dev.me/products/image-placeholder - the new way</h3>
    <div class="container">
      <div class="field">
        <label for="text">
          <span>Text: </span>
          <input id="text" name="text" [(ngModel)]="text" />
        </label>
      </div>
      <div class="field">
        <label for="width">
          <span>Width: </span>
          <input id="width" name="width" [(ngModel)]="width" type="number" min="10" />
        </label>
      </div>
      <div class="field">
        <label for="height">
          <span>Height: </span>
          <input id="height" name="height" [(ngModel)]="height" type="number" min="10" />
        </label>
      </div>
      <div class="field">
        <label for="color">
          <span>Color: </span>
          <input id="color" name="color" [(ngModel)]="textColor" />
        </label>
      </div>
      <div class="field">
        <label for="backgroundColor">
          <span>Background color: </span>
          <input id="backgroundColor" name="backgroundColor" [(ngModel)]="bgColor" />
        </label>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ImagePlaceholderComponent {
  text = model('');
  width = model.required<number>();
  height = model.required<number>();
  textColor = model('#fff', { alias: 'color' });
  bgColor = model('#000', { alias: 'backgroundColor' });
}
```

[(ngModel)]="text" is a new feature that calls "Signal double binding", which means NgModel can read from and write to a WritableSignal/ModelSignal.  It is also the shorthand form of `[ngModel]="text()"` and `(ngModelChange)="text.set($event)`. 

```typescript
// image-placeholder-container.component.ts

@Component({
  selector: 'app-image-placeholder-container',
  standalone: true,
  imports: [ImagePlaceholderComponent],
  template: `
    <h2>New way - with model inputs</h2>
    <app-image-placeholder [(text)]="text"
      [(width)]="width" [(height)]="height"
      [(color)]="color" [(backgroundColor)]="backgroundColor"
    />
    <img [src]="placeholderUrl()" alt="generic placeholder" />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ImagePlaceholderContainerComponent {
// The new way with model inputs
  text = signal('Model Inputs');
  width = signal(250);
  height = signal(120);
  color = signal('#fff');
  backgroundColor = signal('#000');  

  placeholderUrl = computed(() => {
    const text = this.text() ? encodeURIComponent(this.text()) : `${this.width()} x ${this.height()}`;
    const color = encodeURIComponent(this.color());
    const backgroundColor = encodeURIComponent(this.backgroundColor());
    
    return `https://via.assets.so/img.jpg?w=${this.width()}&h=${this.height()}&&tc=${color}&bg=${backgroundColor}&t=${text}`;
  });
}
```

`ImagePlaceholderContainerComponent` declares text, width, height, color, and backgroundColor Signal to bind to the inputs of `ImagePlaceholderComponent`.  `placeholderUrl` is a computed signal that constructs the URL based on text, width, height, color, and backgroundColor signals. In the HTML template, I assigned `placeholderUrl` to the image element to render the image placeholder. 

```typescript
// main.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ImagePlaceholderSubjectContainerComponent,
  ImagePlaceholderContainerComponent],
  template: `
    <h1>Angular {{ version }} - model inputs demo </h1>
    <app-image-plceholder-subject-container />
    <app-image-placeholder-container />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  version = VERSION.full;
}

bootstrapApplication(App);
```

App imports both `ImagePlaceholderSubjectContainerComponent` and `ImagePlaceholderContainerComponent`, and both of them achieve the same results.  

###Why I think model inputs are great

- Less boilerplate code. `ImagePlaceholderComponent` has a shorter code than `ImagePlaceholderSubjectComponent`, and the former component is not cluttered by the `@input` and `@Output` decorators.

- Use Signal for synchronous action.  Form entry is a synchronous task; therefore, signals are preferred over Observables.  Other signals derive the `placeholderUrl` computed signal and the HTML template displays the result

- Adoption of [(ngModel)]="signal". With the addition of signal double binding, a signal can easily bind to `[(ngModel)]`.  On the other hand, &lt;behavior subject&gt;.getValue() is passed to `[ngModel]` input and `(ngModelChange)="<behavior subject>.next($event)"` emits a new value to the BehaviorSubject.  Engineers type fewer characters, and `[(ngModel)]` is easier to read and comprehend.    

The following Stackblitz repo shows the final results:
{%embed https://stackblitz.com/edit/stackblitz-starters-gmrhas?file=src%2Fmain.ts %}

This is the end of the blog post that analyzes data retrieval patterns in Angular. I hope you like the content and continue to follow my learning experience in Angular, NestJS and other technologies.

##Resources:

- Stackblitz Demo: https://stackblitz.com/edit/stackblitz-starters-gmrhas?file=src%2Fmain.ts
- Github Repo: https://github.com/railsstudent/ng-model-inputs-demo