🚩

Angular Input Signal in 17.2

Input signals

Let’s display options with ⭐ if it’s featured.
import { Component, booleanAttribute, input } from '@angular/core';

function boolTransformer(value: unknown) {
  return value !== 'no';
}

@Component({
  selector: 'app-option',
  standalone: true,
  template: ` <div class="option">{{ label() }} @if (featured()) { ⭐ }</div> `,
})
export class OptionComponent {
  label = input.required<string>();
  featured = input.required({
    transform: boolTransformer, // booleanAttribute
  });
}
Consume this component:
<app-option label="Option #1" featured="yes"></app-option>
<app-option label="Option #2" featured="no"></app-option>
<app-option label="Option #3" featured="no"></app-option>
If using the default booleanAttribute, attribute without value means true, e.g. below 2 are the same.
<app-option label="Option #1" featured="true"></app-option>
<app-option label="Option #2" featured></app-option>
 
💡
Required Inputs Cannot Have a Default Value!

Computed Signals and Effects as a Replacement for Life Cycle Hooks

Life cycle hooks like ngOnInit and ngOnChanges can now be replaced with computed and effect:
markDownTitle = computed(() => '# ' + this.label());

constructor() {
  effect(() => {
    console.log('label updated', this.label());
    console.log('markdown', this.markDownTitle());
  });
}
 
You cannot directly access it in the constructor. Instead, you can use ngOnInit or ngOnChanges. Also, using inputs within computed or effect is always safe, as they are only first triggered when the component has been initialized:
export class OptionComponent implements OnInit, OnChanges {
  label = input.required<string>();

  // safe
  markDownTitle = computed(() => '# ' + this.label());

  constructor() {
    // this would cause an exception, as data hasn't been bound so far
    console.log('label', this.label());

    effect(() => {
      // safe
      console.log('label', this.label());
    });
  }

  ngOnInit() {
    // safe
    console.log('label', this.label());
  }

  ngOnChanges() {
    // safe
    console.log('label', this.label());
  }
}

Two-Way Data Binding with Model Signals

Input Signals are read-only. If you want to pass a Signal that can be updated by the called component, you need to set up a so-called Model Signal.
 
demo component:
import { Component, signal } from '@angular/core';
import { TabPaneComponent } from './tab-pane.component';
import { TabComponent } from './tab.component';

@Component({
  template: `
    <div class="pane-container">
      <app-tab-pane [(current)]="currentIndex">
        <app-tab title="1st tab">
          Lorem, ipsum dolor sit amet consectetur adipisicing elit. Consectetur
          aliquam at quam facilis ducimus maxime suscipit numquam quidem quis
          autem? Dicta consequuntur a, laudantium iusto unde praesentium
          inventore fugit quibusdam.
        </app-tab>
        <app-tab title="2nd tab">
          Sammas ergo gemma, ipsum dolor sit amet consectetur adipisicing elit.
          Consectetur aliquam at quam facilis ducimus maxime suscipit numquam
          quidem quis autem? Dicta consequuntur a, laudantium iusto unde
          praesentium inventore fugit quibusdam.
        </app-tab>
        <app-tab title="3nd tab">
          Gemma ham ipsum dolor sit amet consectetur adipisicing elit.
          Consectetur aliquam at quam facilis ducimus maxime suscipit numquam
          quidem quis autem? Dicta consequuntur a, laudantium iusto unde
          praesentium inventore fugit quibusdam.
        </app-tab>
      </app-tab-pane>

      <p class="current-info">Current: {{ currentIndex() }}</p>
    </div>
  `,
  imports: [TabPaneComponent, TabComponent],
})
export class DemoComponent {
  // tab-pane component current model will initially be set as 1,
	// when button clicks, current model will update currentIndex
  currentIndex = signal(1); 
}
 
tab-pane component:
import { TabComponent } from './tab.component';

@Component({
  selector: 'app-tab-pane',
  standalone: true,
  template: `
    <div class="pane">
      <div class="nav" role="group">
        <!-- after 3 tabs projection, loop the buttons -->
        @for(tab of tabs(); track tab) {
	        <button
	          [class.secondary]="tab !== currentTab()"
	          (click)="current.set($index)"
	        >
	          {{ tab.title() }}
	        </button>
        }
      </div>
      <article>
        <ng-content></ng-content>
      </article>
    </div>
  `,
})
export class TabPaneComponent {
  current = model(0); // 2-way binding <app-tab-pane [(current)]="currentIndex">, signal(0) won't work.
  tabs = contentChildren(TabComponent); // 3 tabs in <ng-content>
  currentTab = computed(() => this.tabs()[this.current()]);
}
 
tab component:
import { Component, input, inject, computed } from '@angular/core';
import { TabPaneComponent } from './tab-pane.component';

@Component({
  selector: 'app-tab',
  standalone: true,
  template: `
    @if(visible()) {
	    <div class="tab">
	      <h2>{{ title() }}</h2>
	      <ng-content></ng-content>
	    </div>
    }
  `,
})
export class TabComponent {
  title = input.required<string>();

  // get parent component, because it knows currentTab
  pane = inject(TabPaneComponent);

  visible = computed(() => this.pane.currentTab() === this);
}

View Queries with Signals

import { Component, ElementRef, signal, viewChild } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { JsonPipe } from '@angular/common';

@Component({
  standalone: true,
  imports: [FormsModule, JsonPipe],
  template: `
    <form>
      <input [(ngModel)]="userName" name="username" #userNameCtrl required>
      <input [(ngModel)]="password" name="password" type="password" #passwordCtrl required>
      <button (click)="save()">Save</button>
    </form>
  `,
})
export class FormDemoComponent {
  form = viewChild.required(NgForm);
  userNameCtrl = viewChild.required('userNameCtrl', { read: ElementRef });
  passwordCtrl = viewChild.required('passwordCtrl', { read: ElementRef });

  userName = signal('');
  password = signal('');

  save() {
    const form = this.form();

    if (form.controls['userName'].invalid) {
      this.userNameCtrl().nativeElement.focus();
      return;
    }

    if (form.controls['password'].invalid) {
      this.passwordCtrl().nativeElement.focus();
      return;
    }

    console.log('save', this.userName(), this.password())
  }
}

Queries and ViewContainerRef

There are situations where you need to dynamically add a component to a placeholder. Examples are modal dialogs or toasts. An easy way to achieve this is using the *ngComponentOutlet directive. A more flexible way is querying the ViewContainerRef of the placeholder.
You can think about a View Container as an invisible container around each Component and piece of static HTML. After getting hold of it, you can add further Components or Templates.
import { Component, ViewContainerRef, viewChild } from '@angular/core';
import { ToastComponent } from './toast.component';

@Component({
  standalone: true,
  template: `
    <button (click)="show()">Show Toast</button>
    <ng-container #placeholder></ng-container>
  `,
})
export class ToastDemoComponent {
  placeholder = viewChild('placeholder', { read: ViewContainerRef });

  show() {
    const ref = this.placeholder()?.createComponent(ToastComponent);
    ref?.setInput('label', 'Toast Message'); // toast component has a label input
    setTimeout(() => ref?.destroy(), 2000); // destroy toast component
  }
}