In this blog post, we're going to learn about all the brand-new features introduced in the latest Angular 17.2 release.
We're going to be learning about the new viewChild
, viewChildren
, contentChild
, and contentChildren
signal-based alternatives to the decorators of the same name.
Let's learn why these decorators make the existing lifecycle hooks
AfterContentInit
and AfterViewInit
no longer necessary.
We are also going to be covering a new model()
API that will allow us to do two-way data binding with a signal-based approach.
So without further ado, let's dive right into the Angular 17.2 release!
Table Of Contents
- viewChild and viewChildren Signal-based Queries
- contentChild and contentChildren Signal-based Queries
- Two-way data binding model() API
- Summary
If you would like to learn about this release in video format, check out this video on the Angular University YouTube channel:
Angular 17.2: SIGNAL COMPONENTS One Step Closer:
Check out also the release post on the Angular Blog concerning the Angular 17.2 release.
viewChild Signal-based Queries
Let's start by talking about the new alternative to the @ViewChild
decorator.
As you know, the existing @ViewChild decorator allows you to grab a reference to a child element in your template:
@Component({
selector: "signals-demo",
template: `
<p>Parent counter {{ parentCounter }}</p>
<signal-counter [(count)]="parentCounter" />
`,
standalone: true,
imports: [CounterComponent],
})
export class SignalsDemoComponent
implements AfterViewInit {
parentCounter = 0;
@ViewChild(SignalCounter)
counter: SignalCounter;
ngAfterViewInit() {
console.log(
"counter component:", this.counter);
}
}
In this example, we are using a SignalCounter component in our template, and we want a reference to it here in our component.
Before Angular 17.2, we would create a @ViewChild query:
@ViewChild(SignalCounter)
counter: SignalCounter;
But now in Angular 17.2, we now have a signal-based alternative!
Let's remove the use of the @ViewChild
decorator and the AfterViewInit lifecycle hook.
This lifecycle hook should normally no longer be necessary:
@Component({
selector: "signals-demo",
template: `
<p>Parent counter
{{ parentCounter }}</p>
<signal-counter #counter
[(count)]="parentCounter" />
`,
standalone: true,
imports: [CounterComponent],
})
export class SignalsDemoComponent {
parentCounter = 0;
counter = viewChild(CounterComponent);
constructor() {
effect(() => {
console.log("counter component:",
this.counter());
});
}
}
This works in the same way, but as you can see, the new API looks much simpler.
We can also see that the use of the AfterViewInit lifecycle hook has been replaced by the use of a simple signal effect()
.
Notice that the counter signal is now a signal of type Signal of CounterComponent.
This is just like any other regular Angular signal.
You can use anything in the Signals API to interact with it.
You can use the computed API to compute derived signals, and you can also use effects, as we have done here, to be notified when new values are emitted.
Now, you are probably wondering about all the typical options of the ViewChild decorator.
Well, these are also present in its signal-based alternative.
You have an options object that you can pass in. You can pass in the "read" property, just like the standard ViewChild decorator:
counter = viewChild(CounterComponent, {
read: true,
});
You can also query the view using a template variable. We have a #counter
template variable. We can pass in the name of the variable:
counter = viewChild("counter");
And this would give us the same result.
If we want to make this viewChild
property required, then you can do so using
viewChild.required
.
counter = viewChild.required("counter");
The viewChildren signal-based API
Let's cover one more template query, the viewChildren query.
Let's say we have a component that uses multiple child components of the same type on its template.
We can query all the children in one go using the new viewChildren query:
@Component({
selector: "signals-demo",
template: `
<p>Parent counter {{ parentCounter }}</p>
<signal-counter [(count)] />
<signal-counter [(count)] />
<signal-counter [(count)] />
`,
standalone: true,
imports: [CounterComponent],
})
export class SignalsDemoComponent {
parentCounter = 0;
counters = viewChildren(CounterComponent);
constructor() {
effect(() => {
console.log("counters component:", this.counters());
});
}
}
Again, notice that we don't need a lifecycle hook anymore, a simple effect will do.
The contentChild and ContentChildren queries
There are also new signal-based alternatives to the existing @ContentChild and @ContentChildren decorators, that work in the exact same way as explained for @ViewChild and @ViewChildren.
The model() two-way binding API
Let's now focus on the new Model API for doing bidirectional data binding with a signal-based approach.
If you remember the traditional bi-directional data binding ngModel-based syntax,
I'm talking about the "bananas in a box" [()]
syntax with the square brackets outside and the round brackets inside.
This can now be done also with a signal-based approach using the new model
API:
@Component({
selector: "signal-counter",
template: `
<div>
<div>Counter value: {{ count() }}</div>
<button (click)="onIncrement()">Increment</button>
</div>
`,
standalone: true,
})
export class CounterComponent {
count = model(0);
onIncrement() {
this.count.update((val) => val + 1);
}
}
As you can see, we are just receiving a signal, which is the counter itself.
And whenever we click on the button, we increment the value of the counter.
If we check the type of this count
variable, we can see that this is a
ModelSignal<number>
.
This is a special type of Writable signal, that allows us to do bi-directional data-binding from the point of view of the parent component.
So, if I go back to the parent component:
@Component({
selector: "signals-demo",
template: `
<p>Parent counter {{ parentCounter }}</p>
<signal-counter [(count)]="parentCounter" />
`,
standalone: true,
imports: [CounterComponent],
})
export class SignalsDemoComponent {
parentCounter = 0;
}
We can see that we now are applying the two-way binding [()]
syntax to the count property:
<signal-counter [(count)]="parentCounter" />
The parentCounter
variable in now kept in sync with the counter variable from inside the SignalCounter
automatically.
Remember that the binding goes two ways. So, if I initialize parentCounter
with 100, then the counter
value inside the SignalCounter
is going to be initialized
with the value of 100.
Then, if we start incrementing, the values start being incremented one by one, starting at the value of 100, 101, and 102, as expected.
I hope that you enjoyed this post, to get notified when new similar posts like this are out, I invite you to subscribe to my newsletter:
You will also get up-to-date news on the Angular ecosystem.
And if you want a deep dive into all the features of Angular Core like Signals
and others, have a look at the Angular Core Deep Dive Course:
Summary
So these are the new APIs that we have available in Angular 17.2! These are going to make building signal-based components much easier.
Let me know if you have questions or comments in the comments section below.
I would be happy to help, cheers everyone.