Team Angular is not slowing down. Version 18 of the framework enhances functionalities introduced in previous versions. Emphasis is placed on further integration of signals and preparation for the zoneless mode. In addition to this, at ng-conf, we were surprised by the presentation of cooperation between two teams at Google – Angular and Wiz. There were also improvements in DX, such as fallback in ng-content or a new Observable in forms.
Happy reading!
Angular and Wiz
A big surprise at this year’s ng-conf, the largest Angular conference in the world, was the presentation of over a year-long collaboration between two teams at Google: the Angular Team and the Wiz Team. Jeremy Elbourn and Minko Gechev gave us a glimpse into what we can expect from such collaboration.
What is Wiz?
Wiz is an internal Google framework used to create performance-critical applications, such as Google Search, Google Photos, Google Payments, and YouTube. Traffic on such sites is enormous, and a significant number of users do not have access to fast internet. Wiz focuses on delivering the most optimized application with relatively low interactivity. SSR (Server-Side Rendering) is fundamental to Wiz’s operation – all application components are rendered on an optimized streaming solution. JavaScript required for interaction on the page is only loaded when the component is visible to the user.
On the other hand, we have Angular, which we are familiar with, focusing on high interactivity and Developer Experience. With each new version, new features are delivered to optimize the final application, such as control flow with the defer block – this may be thanks to cooperation with the Wiz team.
Signals in Wiz
As part of the collaboration, both frameworks will borrow functionalities from each other – the long-term goal is to merge both solutions. Wiz will transition to an open-source model, which will streamline work on it through community feedback and contributions. The first fruit of the collaboration is the implementation of Angular signals in the mobile web version of the YouTube portal. It may not be the most commonly used way to watch cat videos, but you have to start somewhere 🙂
In the future, we can expect to see changes from Wiz being implemented into Angular. The topic of optimal SSR already seems very interesting. Without a doubt, we will keep you informed about this on staging.angular.love, and today I invite you to read the Angular blog post and watch the ng-conf Keynote.
Further signals integration
A feature awaited by many developers. Signals, which have been with us since v16, in the latest release drive more and more core mechanisms. Previously available in Developer Preview, inputs, queries, and models based on signals are becoming stable in v18.
All these changes are another step towards Angular operating completely in zoneless mode.
input()
We gain access to the new input() function, which will serve as an optimized alternative to the long-standing @Input() decorator.
We receive two types of inputs:
- Optional – Default behavior. Initial values can be defined for these inputs. If not defined, Angular will declare the input value as undefined.
- Required – In this case, the input must be passed from the parent to the child. Initial values cannot be declared.
@Component(...)
export class MyComponent {
// default: undefined
optionalInput = input<number>();
// default: 5
optionalInputWithDefaultValue = input<number>(5);
// parent must pass value trough input
requiredInput = input.required<number>();
// ERROR - setting initial value to required input is not allowed
requiredInputWithDefaultValue = input.required<number>(5);
}
In the template, we use them like other signals.
<p>{{ myInput() }}</p>
Unlike the decorator, signal inputs are read-only. Therefore, we cannot change their values within the component. This provides an additional guarantee of proper data flow. However, in the Angular world, there are many applications where inputs are being changed at the component level. Thus, replacing the decorator with signals may not always be straightforward.
If we do indeed need modified values passed through inputs, we can use the model() function described later in the article, or one of the following methods.
As we already know, the new inputs are powered by signals. Therefore, as with other signals, we have access to the computed() and effect() functions. With the help of the computed() function, we can create a new signal based on the input value.
@Component(...)
export class MyComponent {
age = input(0);
// wiek pomnożony przez 2
ageMultiplied = computed(() => this.age() * 2);
}
Similarly to the @Input() decorator, we also have access to familiar attributes:
- transform – With this, we can modify our input value. In the example below, each time age() is called, the passed value will be multiplied by 2.
alias – It changes the public name of the input. The component declaring the input still uses its original name. However, for the parent using MyComponent, the alias is visible.
@Component(...)
export class MyComponent {
age = input(0, {
transform: (value: number) => value * 2,
alias: 'userAge'
})
}
model()
In short, model() is input() with benefits. You can use it just like an input, but it has several additional functionalities.
Firstly, the value in the signal created using model() can be changed freely using the set() function known from signals.
@Component(...)
export class MyComponent {
myModel = model(false); // ModelSignal<boolean>
myOtherModel = model<string>() // ModelSignal<string | undefined>
toggle(): void {
// model w każdym momencie można zmienić za pomocą set()
this.myModel.set(!this.myModel());
}
}
Similar to input, we can mark a model as required. We can also assign an alias. However, the model does not have access to the transform function.
@Component(...)
export class MyComponent {
myModel = model.required<boolean>(); // ModelSignal<boolean>
}
By using model(), Angular sets up two-way binding mechanisms. Similar to previous solutions, we have access to a special syntax [()], also known as banana-in-a-box. The existing model also works with the input [] syntax. However, when we use it, two-way binding ceases to function, but we still have an input that we can edit in the child component.
Additionally, Angular creates an output in the component where the model is declared. The output’s name consists of the model’s name plus the Change suffix. For example, if my model is called name, the output will be named nameChange. The parent can listen to events using round brackets ().
// child.component.ts
@Component(...)
export class ChildComponent {
// W tym miejscu powstaje input, two-way binding i output o nazwie nameChange
name = model('Marcin');
}
// parent.component.ts
@Component({
...,
template: `
<app-child
(nameChange)="logValue($event)" << Event
[(name)]="nameFromParent" << Two-way binding - banana-in-a-box
></app-child>`,
})
export class AppComponent {
nameFromParent = 'Martin';
logValue(value: string): void {
console.log(value);
}
}
It is worth noting that two-way binding can be used with simple data types, as shown in the example above, as well as with signals. The parent above could look like this, for instance:
// parent.component.ts
@Component({
...,
template: `
<app-child
[(name)]="nameFromParent" << Two-way binding - banana-in-a-box ></app-child>`,
})
export class AppComponent {
nameFromParent = signal('Martin'); // WritableSignal<string>
}
Signal queries
Queries – mechanisms for creating references to components, directives, or DOM elements – also received updates. Four new functions have been added.
viewChild()
The first function introduces an alternative to the @ViewChild() decorator. We use it when looking for a single result in our component. Similar to input() or model(), we can also use the required option here.
@Component({
...,
template: `
<div #el></div>
<div #requiredDiv></div>
<my-child />
`,
})
export class MyComponent {
divEl = viewChild<ElementRef>('el'); // Signal<ElementRef|undefined>
requiredDivEl = viewChild.required<ElementRef>('requiredDiv'); // Signal<ElementRef>
cmp = viewChild(ChildComponent); // Signal<ChildComponent|undefined>
}
viewChildren()
The second function works similarly to viewChild(). However, it looks for multiple elements and returns an array of results.
@Component({
template: `
<div #el></div>
<div #el></div>
<div #el></div>
`,
})
export class MyComponent {
firstSelector = viewChildren<ElementRef>('el');
// Signal<readonly ElementRef<any>[]>
secondSelector = viewChildren<ElementRef<HTMLDivElement>>('el');
// Signal<readonly ElementRef<HTMLDivElement>[]>
}
contentChild() and contentChildren()
The last two additions to Signal Queries are contentChild() and contentChildren(), which work similarly to the other two functions – the difference is that they don’t search the component’s template but the content placed within the ng-content element.
// parent.component.ts
@Component({
template: `<ng-content></ng-content>`, // Zwróćcie uwagę na tag ng-content
standalone: true,
selector: 'app-parent',
})
export class ParentComponent {
content = contentChild(ChildComponent);
// Signal<ChildComponent | undefined>
contentElements = contentChildren(ChildComponent);
// Signal<readonly ChildComponent[]>
}
output()
Outputs have also been enhanced. In version 17.3, the output() function was introduced, which is currently in Developer Preview. When called, it returns an object of type OutputEmitterRef<T>. To emit a value to the parent we call the emit() function.
@Component(...)
export class MyComponent {
valueChanged = output<string>();
onValueChanged(msg: string): void {
// emit() działa tak samo. Jednak nie można już emitować undefined
this.valueChanged.emit(msg);
}
}
It is important to know that, unlike the new inputs, outputs are not based on signals. So why the changes? One of the reasons is to standardize the syntax with the new inputs – the new syntax for both concepts contains less boilerplate and is simply more readable.
@Component(...)
export class MyComponent {
newInput = input<boolean>(); // InputSignal<boolean | undefined>
newOutput = output<string>(); // OutputEmitterRef<string>
}
The second reason is type-safety in the new class OutputEmitterRef<T>. So far, we have been using the EventEmitter<T> class, with emit() function which accepted an argument of type T | undefined. This will no longer be an issue, as TypeScript will catch the error. This resolves a fairly active issue on GitHub.
@Component(...)
export class MyComponent {
@Output() oldOutput = new EventEmitter<string>();
newOutput = output<string>();
onEvent(): void {
this.oldOutput.emit(); // OK
this.newOutput.emit(); // ERROR: Expected 1 arguments, but got 0.
}
}
In the @angular/core/rxjs-interop package, we will find two new helpers to assist with working with the new outputs:
- outputFromObservable(): This allows us to create an output from an Observable. This means that we no longer need to manually create subscriptions and emit values within them. Or even worse, mark the Observable with the @Output decorator and return it to the parent – it was not a supported solution. When we use the new helper, Angular automatically unsubscribes from our Observable when our component is destroyed.
- outputToObservable(): This, on the other hand, converts our output to an Observable.
// child.component.ts
@Component({
selector: 'app-child',
...
})
export class ChildComponent {
active$ = new Observable<boolean>();
activeChanged = outputFromObservable<boolean>(this.active$);
}
// parent.component.ts
@Component({
selector: 'app-parent',
template: `<app-child (activeChanged)="onActiveChanged($event)"></app-child>`,
...
})
export class ParentComponent {
onActiveChanged(val: boolean): void {
console.log(val);
}
}
Fallback in ng-content
Another highly acclaimed addition by the community is the fallback for the ng-content tag. As you know, this tag is used for Content Projection, which is content passed from the parent – this, of course, remains unchanged. But now, when no content is passed, we can handle such situations by presenting the default.
@Component({
selector: 'my-comp',
template: `
Tu będzie użyty fallback
<ng-content select="header">Default header</ng-content>
A tutaj footer z MyApp
<ng-content select="footer">Default footer</ng-content>
`
})
class MyComp {}
@Component({
template: `
<my-comp>
<footer>New footer</footer>
</my-comp>
`
})
class MyApp {}
New Observable in Forms
Reactive Forms, used to control forms behavior in Angular use model-driven design to control form behavior, have introduced a new Observable called events. It emits various types of changes in the form, combining subscriptions to valueChanges and statusChanges. Additionally, it adds events that were not previously available in any form subscriptions.
- ValueChangeEvent – When the value of an input changes.
- PristineChangeEvent – When the pristine status changes – this is the initial state.
- TouchedChangeEvent – When the input is “touched”.
- StatusChangeEvent – When our form becomes VALID or INVALID.
As you can see, we can now listen for changes in the touched and pristine statuses, which was previously impossible.
Hybrid Change Detection
Change Detection in Angular relies on Zone.js, which is responsible for scheduling updates in response to browser API actions such as setTimeout(), setInterval(), Promise.then(), addEventListener(), etc. However, this approach has certain limitations, especially in handling updates outside NgZone, leading to issues with triggering change detection at appropriate times. In some cases, this results in performance problems for the application, forcing developers to use functions like ngZone.runOutsideAngular().
In v18, experimental support for change detection without Zone.js was introduced, which represents a significant departure from the previous approach. This change aims to improve developer experience (DX) and performance for applications using NgZone, as well as those operating without it. Zoneless Change Detection is based on an approach where components directly notify Angular that something has changed without the additional layer of Zone.js.
To try out the experimental change detection, only two modifications are needed.
// main.ts
bootstrapApplication(AppComponent, {
providers: [
// ? Add this line to enable Zoneless Change Detection
provideExperimentalZonelessChangeDetection(),
],
});
// angular.json
{
"projects": {
"app": {
"architect": {
"build": {
"options": {
"polyfills": [
"zone.js" // ? Remove this line
],
}
}
}
}
}
If your components use ChangeDetectionStrategy.OnPush, AsyncPipe and/or signals to render content, everything should work as expected.
Starting from v18, without activating the experimental provider, hybrid Change Detection will be active by default. In hybrid mode, both NgZone and the new zoneless scheduler are utilized. This hybrid approach enhances DX by ensuring that Change Detection is always scheduled, even when updates occur outside NgZone. If any issues arise, you can always revert to the proven change detection mechanism.
// main.ts
bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection({ ignoreChangesOutsideZone: true }),
],
});
More information about the new Change Detection will be available soon on the blog, and in the meantime, I invite you to read Matthieu Riegler’s article.
Other changes in Angular 18
- The lowest supported version of TypeScript is 5.4
- Control Flow syntax is no longer in Developer Preview status; it’s now stable.
- HttpClientModule and similar modules are now deprecated. Developers should use provideHttpClient() instead.