Input signalsComputed Signals and Effects as a Replacement for Life Cycle HooksTwo-Way Data Binding with Model SignalsView Queries with SignalsQueries and ViewContainerRef
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
}
}