What's new in Angular 17?
Angular v17 is here!
For French-speaking people, I talked about the release on the Angular Devs France YouTube channel.
This is a major release packed with features: let’s dive in!
angular.dev
The Angular team has been cranking it communication-wise lately, with a live event to unveil the new features of Angular v17, and a new website called angular.dev, which will be the future official website. It features the same documentation but with a new interactive tutorial, and a playground to try Angular without installing anything (as Vue or Svelte do as well).
Angular also has a new logo that you can see at the top of this post!
Control flow syntax
Even if it is only a “developer preview” feature, this is a big one! Angular templates are evolving to use a new syntax for control flow structures.
We wrote a dedicated blog post about this feature:
An experimental migration allows you to give it a try in your project. The syntax should become stable in v18, and be the recommended way to write templates at that point.
Deferrable views
Another big feature is the introduction of deferrable views using @defer
in templates.
We wrote a dedicated blog post about this feature:
This is a “developer preview” feature as well and should become stable in v18. It’s probably going to be less impactful than the control flow syntax, but it’s still interesting to have a way to easily lazy-load parts of a template.
Signals are now stable!
The Signals API is now marked as stable 🎉.
Except effect()
, and the RxJS interoperability functions toSignal
and toObservable
which might change and are still marked as “developer preview”.
The API has not changed much since our blog post about Signals, but some notable things happened.
mutate has been dropped
mutate()
has been dropped from the API.
You were previously able to write something like:
users.mutate(usersArray => usersArray.push(newUser));
And you’ll now have to write:
users.update(usersArray => [...usersArray, newUser]);
The mutate()
method was introducing some issues with other libraries,
and was not worth the trouble as it can be replaced by update()
quite easily.
template diagnostic
A new compiler diagnostic is available to help you spot missing signal invocations in your templates.
Let’s say you have a count
signal used in a template, but forgot the ()
:
<div>{{ count }}</div>
throws with:
NG8109: count is a function and should be invoked: count()
flushEffects
A new method is available (as a developer preview)
on the TestBed
class to trigger pending effects: flushEffects
TestBed.flushEffects();
This is because effect timing has changed a bit:
they are no longer triggered by change detection but scheduled via the microtask queue
(like setTimeout()
or Promise.resolve()
).
So while you could previously trigger them by calling detectChanges()
on the fixture,
you now have to call TestBed.flushEffects()
.
afterRender and afterNextRender phases
The afterRender
and afterNextRender
functions introduced in Angular v16.2
can now specify a phase
option.
Angular uses this phase to schedule callbacks to improve performance.
There are 4 possible values, and they run in the following order:
EarlyRead
(when you need to read the DOM before writing to the DOM)Write
(needed if you want to write to the DOM, for example, to initialize a chart using a third-party library)MixedReadWrite
(default, but should be avoided if possible to use a more specific phase)Read
(recommended if you only need to read the DOM)
I think we should be able to use Read
and Write
in most cases.
EarlyRead
and MixedReadWrite
degrade performances, so they should be avoided if possible.
export class ChartComponent {
@ViewChild('canvas') canvas!: ElementRef<HTMLCanvasElement>;
constructor() {
afterNextRender(() => {
const ctx = this.canvas.nativeElement;
new Chart(ctx, { type: 'line', data: { ... } });
}, { phase: AfterRenderPhase.Write });
}
}
Performances
The internal algorithm changed to use a ref-counting mechanism instead of a mechanism based on bi-directional weak references. It should be more performant than it was in many cases.
It’s also worth noting that the change detection algorithm has been improved to be more efficient when using Signals.
Previously, when reading a signal in a template, Angular was marking the component
and all its ancestors as dirty when the signal was updated
(as it currently does with when OnPush
components are marked for check).
It’s now a bit smarter and only marks the component as dirty when the signal is updated and not all its ancestors.
It will still check the whole application tree,
but the algorithm will be faster because some components will be skipped.
We don’t have a way to write pure signal-based components yet, with no need for ZoneJS, but it should be coming eventually!
styleUrls as a string
The styleUrls
and styles
properties of the @Component
decorator can now be a string instead of an array of strings.
A new property called styleUrl
has also been introduced.
You can now write:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
export class AppComponent {}
View Transitions router support
The View Transitions API is a fairly new browser API that allows you to animate the transition between two views. It is only supported in recent versions of Chrome, Edge, and Opera (see caniuse.com stats) but not in Firefox yet. It works by taking a screenshot of the current view and animating it to the new view.
I’m not very familiar with this API, but there is a great article about it on developer.chrome.com and cool demos on this site (open it with a browser that supports this API of course).
Angular v17 adds support for this API in the router.
This is an experimental feature, and you’ll have to enable it by using withTransitionViews()
:
bootstrapApplication(AppComponent, {
providers: [{ provideRouter(routes, withTransitionViews()) }]
});
By default, you get a nice fade-in/fade-out transition between views when navigating from one route to another. You can customize the animation using CSS, animate the whole view or skip part of it, or indicate which DOM elements are in fact the same entities in the old and new views: the browser will then do its best to animate between the states.
It is possible to skip the initial transition by using the skipInitialTransition
option:
bootstrapApplication(AppComponent, {
providers: [{ provideRouter(routes, withTransitionViews({ skipInitialTransition: true })) }]
});
More advanced scenarios require to add/remove CSS classes to the views,
so the router also lets you run an arbitrary function when the transition is done
if you use the onViewTransitionCreated
option to define a callback.
Http
The fetch backend (introduced in Angular v16.1) has been promoted to stable.
When using SSR, it is now possible to customize the transfer cache, using withHttpTransferCacheOptions(options)
.
The options can be:
filter
: a function to filter the requests that should be cachedincludeHeaders
: the list of headers to include (none by default)includePostRequests
: whether or not to cache POST requests (by default only GET and HEAD requests are cached)
For example:
bootstrapApplication(AppComponent, {
providers: [provideHttpClient({
withHttpTransferCacheOptions({ includePostRequests: true })
})
});
Devtools
The devtools received some love as well, and they now allow you to inspect the dependency injection tree.
Animations
No new feature for this part of Angular,
but it is now possible to lazy-load the animations package.
In a standalone application, you can use provideAnimationsAsync()
instead of
using provideAnimations()
and the necessary code for animations will be loaded asynchronously.
The application should work the same, but you should see an extra chunk appear when building the application. That’s a few kilobytes of JavaScript that you don’t have to load upfront 🚀.
You can disable animations by providing 'noop'
as the value of provideAnimationsAsync()
:
bootstrapApplication(AppComponent, {
providers: [provideAnimationsAsync('noop')]
});
Performances
In dev mode, you’ll now get a warning if you load an oversized image or if an image is the “Largest Contentful Paint element” in the page and is lazy-loaded (which is a bad idea, see the explanations here).
For example:
An image with src image.png has intrinsic file dimensions much larger than its
rendered size. This can negatively impact application loading performance.
For more information about addressing or disabling this warning, see
https://angular.io/errors/NG0913
You can configure this behavior via dependency injection, for example, if you want to turn off these warnings:
{
provide: IMAGE_CONFIG, useValue:
{
disableImageSizeWarning: false,
disableImageLazyLoadWarning: false
}
}
TypeScript 5.2 and Node.js v18
It’s worth noting that Angular now requires TypeScript 5.2 and Node.js v18. Support for older versions has been dropped.
Angular CLI
A lot happened in the CLI!
👉 Check out our dedicated blog post about the CLI v17 for more details.
Summary
That’s all for this release, stay tuned!
All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!
What's new in Angular CLI 17.0?
Angular CLI 17.0.0 is out!✨
If you want to upgrade to 17.0.0 without pain (or to any other version, by the way), I have created a Github project to help: angular-cli-diff. Choose the version you’re currently using (16.2.0 for example), and the target version (17.0.0 for example), and it gives you a diff of all files created by the CLI: angular-cli-diff/compare/16.2.0…17.0.0.
It can be a great help along with the official ng update @angular/core @angular/cli
command.
You have no excuse for staying behind anymore!
Let’s see what we’ve got in this release.
Standalone applications with Vite by default!
The --standalone
flag is now the default behavior of the CLI.
This means generating a new project with ng new
now uses standalone components by default,
and that the ng generate component/pipe/directive
command now generates standalone components/pipes/directives.
Another notable change to ng new
is that the routing is now enabled by default.
But the most important change is that the CLI now uses Vite out-of-the-box!
A new builder called application
has been introduced, and is used when generating a new project.
This builder has a very similar configuration to the browser
builder,
so the migration is quite easy if you want to use Vite in an existing project
You have to change the builder from browser
to application
in the angular.json
file,
rename the main
property to browser
,
and remove a few options from the development configuration (buildOptimizer
, vendorChunk
).
Once migrated, the ng serve
command will use Vite instead of Webpack.
Build time should be faster, especially for cold starts (I saw 2-3x times improvement on my machine).
There is no HMR by default yet, but the global style changes are detected and applied automatically without reloading the page.
Note that the output of the ng build
Vite-based command is now in dist/my-project/browser
instead of dist/my-project
.
The browser-esbuilder
builder still exists, but will be removed in the future.
You should use the application
builder instead.
ng new –ssr
A new flag --ssr
has been added to the ng new
command to generate a new project
with SSR enabled out of the box.
It generates a project similar to what you usually get and then runs the @angular/ssr
schematics
(you can also use the schematics directly on an existing project with ng add @angular/ssr
).
@angular/ssr
is a new package and replaces the Angular Universal package.
If you were using the Angular Universal package, ng update
migrates your configuration to use @angular/ssr
automatically.
This schematic does the following:
- adds the
@angular/ssr
package - adds the
@angular/platform-server
package - adds the
express
and@types/express
packages - adds the
main.server.ts
file (entry point for the application when running on the server) - adds the
app.config.server.ts
file (providers for the application when running on the server) - adds the
tsconfig.server.json
file - adds the
server.ts
file (the Express server, responsible for serving the application)
It updates the angular.json
configuration to add the following options to the build
target:
"server": "src/main.server.ts",
"prerender": true,
"ssr": {
"entry": "server.ts"
}
and adds the provideClientHydration()
to the (browser) application providers,
to have a smooth transition between the server and the client.
This is a new feature of Angular v16, and we talked about it in our article about the v16 release.
When running ng build
, the CLI will now build the server bundle (in dist/my-project/server
) and the client bundle (in dist/my-project/browser
).
You can then run the generated server with:
node dist/my-project/server/main.server.mjs
This starts an Express server on port 4000 by default, which serves the rendered pages.
The rendered pages are in the browser
folder, and are named ${page}/index.html
:
dist/my-project/browser/index.html
dist/my-project/browser/login/index.html
dist/my-project/browser/register/index.html
If you use localize
in your application, the CLI will also build the localized bundles (in dist/my-project/server/${lang}
).
The prerendering mechanism should be quite accurate now,
as it uses the Angular router under the hood to navigate to each route and render it
(routes with parameters or redirections are skipped).
When prerendering is enabled, the CLI generates a prerendered-routes.json
file
that contains all the prerendered routes.
This is useful if you deploy on the cloud as this file is usually recognized by providers
to serve these files as static.
{
"routes": [
"/",
"/login",
"/register"
...
]
}
You can disable the auto-discovery of routes by setting the discoverRoutes
option to false
in the angular.json
file. You can also provide your own list of routes in this file by defining routeFiles
:
"ssr": {
"discoverRoutes": false,
"routeFiles": "ssg-routes.txt"
}
This file must contain a list of routes that you want to render (and can contain parameterized routes).
When running ng serve
, the CLI serves the application via Vite,
and only pre-renders the requested page (the one you’re currently on).
You can also use a new option to CommonEngine
called enablePerformanceProfiler
to trace the performance of each step of the rendering:
const commonEngine = new CommonEngine({
enablePerformanceProfiler: true
});
When using SSR, it is recommended to use the Fetch version of the HTTP client,
by using provideHttpClient(withFetch())
(as introduced in Angular v16.1).
This is for performance and compatibility reasons.
NG02801: Angular detected that `HttpClient` is not configured to use `fetch` APIs. It's strongly recommended to enable `fetch` for applications that use Server-Side Rendering for better performance and compatibility. To enable `fetch`, add the `withFetch()` to the `provideHttpClient()` call at the root of the application.
Functional HTTP interceptors by default
The CLI now generates functional interceptors by default,
without the need to specify --functional
anymore.
Class-based interceptors are still available with the --no-functional
option,
but you’re now encouraged to use the functional ones.
Summary
That’s all for the CLI v17.0 release! You’ll find more interesting features in our article about the framework v17.0.0 release.
All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!
A guide to Angular Deferrable Views with @defer
With the introduction of the Control flow syntax, the Angular team has also introduced a new way to load components lazily (as a developer preview for now). We already have lazy-loading in Angular, but it is mainly based on the router.
Angular v17 adds a new way to load components lazily, using the @defer
syntax in your templates.
@defer
lets you define a block of template that will be loaded lazily when a condition is met
(with all the components, pipes, directives, and libraries used in this block lazily loaded as well).
Several conditions can be used.
For example, it can be “as soon as possible (no condition)”,
“when the user scrolls to that section”,
“when the user clicks on that button” or “after 2 seconds”.
Let’s say your home page displays a “heavy” ChartComponent
that uses a charting library
and some other dependencies, like a FromNow
pipe:
@Component({
selector: 'ns-chart',
template: '...',
standalone: true,
imports: [FromNowPipe],
})
export class ChartComponent {
// uses chart.js
}
This component is used in the home page:
import { ChartComponent } from './chart.component';
@Component({
selector: 'ns-home',
template: `
<!-- some content -->
<ns-chart />
`,
standalone: true,
imports: [ChartComponent]
})
export class HomeComponent {
// ...
}
When the application is packaged, the ChartComponent
will be included in the main bundle:
+------------------------+
| main-xxxx.js - 300KB |
+------------------------+
| home.component.ts |
| chart.component.ts |
| from-now.pipe.ts |
| chart.js |
+------------------------+
Let’s say that the component is not visible at first on the home page, maybe because it is at the bottom of the page, or because it is in a tab that is not active. It makes sense to avoid loading this component eagerly because it would slow down the initial loading of the page.
With @defer
, you can load this component only when the user really needs it.
Just wrapping the ChartComponent
in a @defer
block will do the trick:
import { ChartComponent } from './chart.component';
@Component({
selector: 'ns-home',
template: `
<!-- some content -->
@defer (when isVisible) {
<ns-chart />
}
`,
standalone: true,
imports: [ChartComponent]
})
The Angular compiler will rewrite the static import of the ChartComponent
to a dynamic import (() => import('./chart.component')
),
and the component will be loaded only when the condition is met.
As the component is now imported dynamically,
it will not be included in the main bundle.
The bundler will create a new chunk for it:
+------------------------+
| main-xxxx.js - 100KB |
+------------------------+ +-------------------------+
| home.component.ts |------>| chunk-xxxx.js - 200KB |
+------------------------+ +-------------------------+
| chart.component.ts |
| from-now.pipe.ts |
| chart.js |
+-------------------------+
The chunk-xxxx.js
file will only be loaded when the condition is met,
and the ChartComponent
will be displayed.
Before talking about the various kinds of conditions that can be used with @defer
,
let’s see how to use another interesting feature:
displaying a placeholder until the deferred block is loaded.
@placeholder
, @loading
, and @error
You can define a placeholder template with @placeholder
that will be displayed until the loading condition is met.
Then, while the block is loading, you can display a loading template with @loading
.
If no @loading
block is defined, the placeholder stays there until the block is loaded.
You can also define an error template with @error
that will be displayed if the block fails to load.
@defer (when show) {
<ns-chart />
}
@placeholder {
<div>Something until the loading starts</div>
}
@loading {
<div>Loading...</div>
}
@error {
<div>Something went wrong</div>
}
When using server-side rendering, only the placeholder will be rendered on the server (the defer conditions will never trigger).
after
and minimum
As the @defer
block loading can be quite fast,
there is a risk that the loading block is displayed and hidden too quickly,
causing a “flickering” effect.
To avoid this, you can use the after
option to specify after how many milliseconds
the loading should be displayed.
If the block takes less than this delay to load, then the @loading
block is never displayed.
You can also use the minimum
option to specify a minimum duration for the loading.
If the loading is faster than the minimum duration,
then the loading will be displayed for the minimum duration (this only applies if the loading is ever displayed).
You can of course combine all these options:
@defer (when show) {
<ns-chart />
}
@placeholder {
<div>Something until the loading starts</div>
}
@loading (after 500ms; minimum 500ms) {
<div>Loading...</div>
}
You can also specify a minimum
duration for the placeholder.
It can be useful when the loading condition is immediate (for example, when no condition is specified).
In that case, the placeholder will be displayed for the minimum duration,
even if the block is loaded immediately,
to avoid a “flickering” effect.
@defer (when show) {
<ns-chart />
}
@placeholder (minimum 500ms) {
<div>Something until the loading starts</div>
}
@loading (after 500ms; minimum 500ms) {
<div>Loading...</div>
}
Conditions
Several conditions can be used with @defer
,
let’s see them one by one.
No condition or on idle
The simplest condition is to not specify any condition at all:
in this case, the block will be loaded when the browser is idle
(the loading is scheduled using requestIdleCallback
).
@defer {
<ns-chart />
}
This is equivalent to using the on idle
condition:
@defer (on idle) {
<ns-chart />
}
Simple boolean condition with when
You can also use a boolean condition to load a block of the template with when
.
Here, we display the defer block only when the show
property of the component is true:
@defer (when show) {
<ns-chart />
}
Note that this is not the same as using *ngIf
on the block,
as the block will not be removed even if the condition becomes false later.
on immediate
The on immediate
condition triggers the loading of the block immediately.
It does not display a placeholder, even if one is defined.
on timer
The on timer
condition triggers the loading of the block after a given duration,
using setTimeout
under the hood.
@defer (on timer(2s)) {
<ns-chart />
}
on hover
Other conditions are based on user interactions. These conditions can specify the element of the interaction using a template reference variable, or none to use the placeholder element. In the latter case, the placeholder element must exist and have a single child element that will be used as the element of the interaction.
The on hover
condition triggers the loading of the block when the user hovers the element.
Under the hood, it listens to the mouseenter
and focusin
events.
<span #trigger>Hover me</span>
@defer (on hover(trigger)) {
<ns-chart />
}
or using the placeholder element:
@defer (on hover) {
<ns-chart />
}
@placeholder {
<span>Hover me</span>
}
on interaction
The on interaction
condition triggers the loading of the block when the user interacts with the element.
Under the hood, it listens to the click
and keydown
events.
on viewport
The on viewport
condition triggers the loading of the block when the element becomes visible in the viewport.
Under the hood, it uses an intersection observer.
Multiple conditions
You can also combine multiple conditions using a comma-separated list:
<!-- Loads if the user hovers the placeholder, or after 1 minute -->
@defer (on hover, timer(60s)) {
<ns-chart />
}
@placeholder {
<span>Something until the loading starts</span>
}
Prefetching
@defer
allows you
to separate the loading of a component from its display.
You can use the same conditions we previously saw to load a component using prefetch
,
and then display it with another condition.
For example, you can prefetch the lazy-loaded content on idle
and then display it on interaction
:
@defer (on interaction; prefetch on idle) {
<ns-chart />
}
@placeholder {
<button>Show me</button>
}
Note that the @loading
block will not be displayed if the deferred block is already prefetched
when the loading condition is met.
How to test deferred loading?
When a component uses defer blocks in its template, you’ll have to do some extra work to test it.
The TestBed
API has been extended to help you with that.
The configureTestingModule
method now accepts a deferBlockBehavior
option.
By default, this option is set to DeferBlockBehavior.Manual
,
which means that you’ll have to manually trigger the display of the defer blocks.
But let’s start with the other option instead.
You can change this behavior by using DeferBlockBehavior.Playthrough
.
Playthrough means that the defer blocks will be displayed automatically
when a condition is met, as they would when the application runs in the browser.
beforeEach(() => {
TestBed.configureTestingModule({
deferBlockBehavior: DeferBlockBehavior.Playthrough
});
});
In that case, the defer blocks will be displayed automatically when a condition is met,
after calling await fixture.whenStable()
.
So if we test a component with a deferred block that is visible after clicking on a button, we can use:
// Click the button to trigger the deferred block
fixture.nativeElement.querySelector('button').click();
fixture.detectChanges();
// Wait for the deferred block to render
await fixture.whenStable();
// Check its content
const loadedBlock = fixture.nativeElement.querySelector('div');
expect(loadedBlock.textContent).toContain('Some lazy-loaded content');
If you want to use the DeferBlockBehavior.Manual
behavior,
you’ll have to manually trigger the display of the defer blocks.
To do so, the fixture returned by TestBed.createComponent
now has an async getDeferBlocks
method
that returns an array of DeferBlockFixture
objects.
Each of these fixtures has a render
method that you can call to display the block
in a specific state, by providing a DeferBlockState
parameter.
DeferBlockState
is an enum with the following values:
DeferBlockState.Placeholder
: display the placeholder state of the blockDeferBlockState.Loading
: display the loading state of the blockDeferBlockState.Error
: display the error state of the blockDeferBlockState.Complete
: display the defer block as if the loading was complete
This allows a fine-grained control of the state of the defer blocks. If we want to test the same component as before, we can do:
const deferBlocks = await fixture.getDeferBlocks();
// only one defer block should be found
expect(deferBlocks.length).toBe(1);
// Render the defer block
await deferBlocks[0].render(DeferBlockState.Complete);
// Check its content
const loadedBlock = fixture.nativeElement.querySelector('div');
expect(loadedBlock.textContent).toContain('Some lazy-loaded content');
All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!
Angular templates got better - the Control Flow syntax
Angular v17 introduces a new “developer preview” feature called “control flow syntax”.
This feature allows you to use a new template syntax to write control flow statements, like if/else, for, and switch,
instead of using the built-in structural directives (*ngIf
, *ngFor
, and *ngSwitch
).
To understand why this was introduced, let’s see how structural directives work in Angular.
Structural directives under the hood
Structural directives are directives that change the structure of the DOM by adding, removing, or manipulating elements.
They are easy to recognize in Angular because they begin with an asterisk *
.
But how do they really work?
Let’s take a simple template with ngIf
and ngFor
directives as an example:
<h1>Ninja Squad</h1>
<ul *ngIf="condition">
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
If you read the chapter of our ebook about the Angular compiler,
you know that the framework generates JavaScript code from this template.
And maybe you imagine that *ngIf
gets converted to a JavaScript if
and *ngFor
to a for
loop like:
createElement('h1');
if (condition) {
createElement('ul');
for (user of users) {
createElement('li');
}
}
But Angular does not work exactly like that: the framework decomposes the component’s template into “views”. A view is a fragment of the template that has static HTML content. It can have dynamic attributes and texts, but the HTML elements are stable.
So our example generates in fact three views, corresponding to three parts of the template:
Main view:
<h1>Ninja Squad</h1>
<!-- special comment -->
NgIf view:
<ul>
<!-- special comment -->
</ul>
NgFor view:
<li>{{ user.name }}</li>
This is because the *
syntax is in fact syntactic sugar
to apply an attribute directive on an ng-template
element.
So our example is the same as:
<h1>Ninja Squad</h1>
<ng-template [ngIf]="condition">
<ul>
<ng-template ngFor [ngForOf]="users" let-user>
<li>{{ user.name }}</li>
</ng-template>
</ul>
</ng-template>
Here ngIf
and ngFor
are plain directives.
Each ng-template
then generates a “view”.
Each view has a static structure that never changes.
But these views need to be dynamically inserted at some point.
And that’s where the <!-- special comment -->
comes into play.
Angular has the concept of ViewContainer
.
A ViewContainer
is like a box where you can insert/remove child views.
To mark the location of these containers,
Angular uses a special HTML comment in the created DOM.
That’s what ngIf
actually does under the hood:
it creates a ViewContainer
, and then, when the condition given as input changes,
it inserts or removes the child view at the location of the special comment.
This view concept is quite interesting as it will allow Angular to only update views that consume a signal in the future, and not the whole template of a component! Check out out our blog post about the Signal API for more details.
Custom structural directives
You can create your own structural directives if you want to.
Let’s say you want to write a *customNgIf
directive.
You can create a directive that takes a condition as an input and
injects a ViewContainerRef
(the service that allows to create the view)
and a TemplateRef
(the ng-template
on which the directive is applied).
import { Directive, DoCheck, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[customNgIf]',
standalone: true
})
export class CustomNgIfDirective implements DoCheck {
/**
* The condition to check
*/
@Input({ required: true, alias: 'customNgIf' }) condition!: boolean;
/**
* The view created by the directive
*/
conditionalView: EmbeddedViewRef<any> | null = null;
constructor(
/**
* The container where the view will be inserted
*/
private vcr: ViewContainerRef,
/**
* The template to render
*/
private tpl: TemplateRef<any>
) {}
/**
* This method is called every time the change detection runs
*/
ngDoCheck() {
// if the condition is true and the view is not created yet
if (this.condition && !this.conditionalView) {
// create the view and insert it in the container
this.conditionalView = this.vcr.createEmbeddedView(this.tpl);
} else if (!this.condition && this.conditionalView) {
// if the condition is false and the view is created
// destroy the view
this.conditionalView.destroy();
this.conditionalView = null;
}
} }
This works great! And as you can see, it lets developers like us create powerful structural directives if we want to: the built-in directives offered by Angular are not special in any way.
But this approach has some drawbacks:
for example, it is a bit clunky to have an else
alternative with *ngIf
:
<div *ngIf="condition; else elseBlock">If</div>
<ng-template #elseBlock><div>Else</div></ng-template>
elseBlock
is another input of the NgIf
directive,
of type TemplateRef
, that the directive will display if the condition is falsy.
But this is not very intuitive to use, so we often see this instead:
<div *ngIf="condition">If</div>
<div *ngIf="!condition">Else</div>
The structural directives are also not perfect type-checking-wise.
Even if Angular does some magic (with some special fields called ngTemplateGuard
in the directives to help the type-checker),
some cases are too tricky to handle.
For example, the “else” alternative of *ngIf
is not type-checked:
<div *ngIf="!user; else userNotNullBlock">No user</div>
<ng-template #userNotNullBlock>
<div>
<!-- should compile as user is not null here -->
<!-- but it doesn't -->
{{ user.name }}
</div>
</ng-template>
NgSwitch
is even worse, as it consists of 3 separate directives
NgSwitch
, NgSwitchCase
, and NgSwitchDefault
.
The compiler has no idea if the NgSwitchCase
is used in the right context.
<!-- user.type can be `'user' | 'anonymous'` -->
<ng-container [ngSwitch]="user.type">
<div *ngSwitchCase="'user'">User</div>
<!-- compiles even if user.type can't be 'admin' -->
<div *ngSwitchCase="'admin'">Admin</div>
<div *ngSwitchDefault>Unknown</div>
</ng-container>
It’s also worth noting that the *
syntax is not very intuitive for beginners.
And structural directives depend on the ngDoCheck
lifecycle hook,
which is tied to zone.js
.
In a future world where our components use the new Signal API
and don’t need zone.js
anymore,
structural directives would still force us to drag zone.js
in our bundle.
So, to sum up, structural directives are powerful but have some drawbacks. Fixing these drawbacks would require a lot of work in the compiler and the framework.
That’s why the Angular team decided to introduce a new syntax to write control flow statements in templates!
Control flow syntax
The control flow syntax is a new syntax introduced in Angular v17 to write control flow statements in templates.
The syntax is very similar to some other templating syntaxes you may have met in the past,
and even to JavaScript itself.
There have been some debates and polling in the community about the various alternatives,
and the @-syntax
proposal won.
With the control flow syntax, our previous template with *ngIf
and *ngFor
can be rewritten as:
<h1>Ninja Squad</h1>
@if (condition) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
}
This syntax is interpreted by the Angular compiler and creates the same views as the previous template, but without the overhead of creating the structural directives, so it is also a tiny bit more performant (as it uses brand new compiled instructions under the hood in the generated code). As this is not directives, the type-checking is also much better.
And, cherry on the cake, the syntax is more powerful than the structural directives!
The drawback is that this syntax uses @
, {
and }
characters with a special meaning,
so you can’t use these characters in your templates anymore,
and have to use equivalent HTML entities instead (\@
for @
, \{
for {
, and \}
for }
).
If statement
As we saw above, a limitation of NgIf
is that it is a bit clunky to have an else
alternative.
And we can’t have an else if
alternative at all.
That’s no longer a problem with the control flow syntax:
@if (condition) {
<div>condition is true</div>
} @else if (otherCondition) {
<div>otherCondition is true</div>
} @else {
<div>condition and otherCondition are false</div>
}
You can still store the result of the condition in a variable if you want to,
which is really handy when used with an async
pipe for example:
@if (user$ | async; as user) {
<div>User is {{ user.name }}</div>
} @else if (isAdmin$ | async) {
<div>User is admin</div>
} @else {
<div>No user</div>
}
For statement
With the control flow syntax, a for loop needs to specify a track
property,
which is the equivalent of the trackBy
function of *ngFor
.
Note that this is now mandatory, whereas it was optional with *ngFor
.
This is for performance reasons, as the Angular team found that very often,
developers were not using trackBy
when they should have.
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
As you can see, this is a bit easier to use than the trackBy
function of *ngFor
which requires to write a function.
Here we can directly specify the property of the item that is unique,
and the compiler will generate the function for us.
If you don’t have a unique property, you can still use a function or just use the loop variable itself
(which is equivalent to what *ngFor
currently does when no trackBy
is specified).
One of the very useful additions to the control flow syntax is the handling of empty collections.
Previously you had to use an *ngIf
to display a message
if the collection was null
or empty and then use *ngFor
to iterate over the collection.
With the control flow syntax, you can do it with an @empty
clause:
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
} @empty {
<li>No users</li>
}
</ul>
We can still access the variables we used to have with *ngFor
:
$index
to get the index of the current item$first
to know if the current item is the first one$last
to know if the current item is the last one$even
to know if the current item is at an even index$odd
to know if the current item is at an odd index$count
to get the length of the collection
Unlike with *ngFor
, you don’t have to alias these variables to use them,
but you still can if you need to, for example when using nested loops.
<ul>
@for (user of users; track user.id; let isOdd = $odd) {
<li [class.grey]="isOdd">{{ $index }} - {{ user.name }}</li>
}
</ul>
It is also worth noting that the control flow @for
uses
a new algorithm under the hood to update the DOM when the collection changes.
It should be quite a bit faster than the algorithm used by *ngFor
,
as it does not allocate intermediate maps in most cases.
Combined with the required track
property,
for
loops should be way faster in Angular applications by default.
Switch statement
This is probably where the new type-checking shines the most, as using an impossible value in a case will now throw a compilation error!
@switch (user.type) {
@case ('user') {
<div>User</div>
} @case ('anonymous') {
<div>Anonymous</div>
} @default {
<div>Other</div>
}
}
Note that the switch statement does not support fall-through, so you can’t have several cases grouped together. It also does not check if all cases are covered, so you won’t get a compilation error if you forget a case. (but I hope it will, add a 👍 on this issue if you want this as well!).
It’s also noteworthy that the @switch
statement uses strict equality (===
) to compare values,
whereas *ngSwitch
used to use loose equality (==
).
Angular v17 introduced a breaking change, and *ngSwitch
now uses strict equality too,
with a warning in the console during development if you use loose equality:
NG02001: As of Angular v17 the NgSwitch directive
uses strict equality comparison === instead of == to match different cases.
Previously the case value "1" matched switch expression value "'1'",
but this is no longer the case with the stricter equality check.
Your comparison results return different results using === vs. ==
and you should adjust your ngSwitch expression and / or values
to conform with the strict equality requirements.
The future of templating 🚀
The control flow syntax is a new “developer preview” feature introduced in Angular v17, and will probably be the recommended way to write templates in the future (the plan is to make it stable in v18 once it has been battle-tested).
It doesn’t mean that structural directives will be deprecated, but the Angular team will likely focus on the control flow syntax in the future and push them forward as the recommended solution.
We will even have an automated migration to convert structural directives to control flow statements in existing applications. The migration is available in Angular v17 as a developer preview. If you want to give it a try, run:
ng g @angular/core:control-flow
This automatically migrates all your templates to the new syntax!
You can also run the migration against a single file with the --path
option:
ng g @angular/core:control-flow --path src/app/app.component.html
Even though the new control flow is experimental,
v17 comes with a mandatory migration needed to support this new control flow syntax,
which consists in converting the @
, {
and }
characters used in your templates to their HTML entities.
This migration is run automatically when you update the app with ng update
.
The future of Angular is exciting!
All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!
What's new in Angular 16.2?
Angular 16.2.0 is here!
This is a minor release with some nice features: let’s dive in!
Binding inputs of NgComponentOutlet
It used to be cumbersome to pass input data to a dynamic component (you could do it, but you needed to use a provider and inject it). It’s now way easier:
@Component({
selector: 'app-user',
standalone: true,
template: '{{ name }}'
})
export class UserComponent {
@Input({ required: true }) name!: string;
}
Then to dynamically insert the component with inputs:
@Component({
selector: 'app-root',
standalone: true,
imports: [NgComponentOutlet],
template: '<div *ngComponentOutlet="userComponent; inputs: userData"></div>'
})
class AppComponent {
userComponent = UserComponent;
userData = { name: 'Cédric' }
}
afterRender and afterNextRender
The afterRender
and afterNextRender
lifecycle hooks have been added to the framework as developer preview APIs.
They are parts of the Signal API, see the RFC discussion.
They allow to run code after the component has been rendered the first time (afterNextRender
), or after every render (afterRender
).
The first one is useful to run code that needs to access the DOM, like calling a third-party library like we currently do in ngAfterViewInit
.
But, unlike ngAfterViewInit
and other lifecycle methods,
these hooks do not run during server-side rendering,
which makes them easier to use for SSR applications.
You can now write:
import { Component, ElementRef, ViewChild, afterNextRender } from '@angular/core';
@Component({
selector: 'app-chart',
standalone: true,
template: '<canvas #canvas></canvas>'
})
export class ChartComponent {
@ViewChild('canvas') canvas!: ElementRef<HTMLCanvasElement>;
constructor() {
afterNextRender(() => {
const ctx = this.canvas.nativeElement;
new Chart(ctx, { type: 'line', data: { ... } });
});
}
}
RouterTestingHarness
The RouterTestingHarness
, introduced in v15.2 (check out our blog post),
now exposes the underlying fixture, allowing to use its methods and properties and making it compatible with testing libraries that expect a fixture (like ngx-speculoos).
Devtools
Some preliminary work has been done in the framework to trace what is injected in an application in dev mode. This will be used in the future to improve the devtools experience, by providing a way to see what is injected in a component, and where it comes from.
Angular CLI
The CLI has been updated to v16.2.0 as well, with a few new features:
- the esbuild builder now adds preload hints based on its analysis of the application initial files
- the esbuild builder can now build the server bundle
- the esbuild builder now has experimental support for serving the application in SSR mode with the Vite-based dev-server
Summary
That’s all for this release, stay tuned!
All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!
What's new in Angular 16.1?
Angular 16.1.0 is here!
This is a minor release with some nice features: let’s dive in!
TypeScript 5.1 support
Angular v16.1 now supports TypeScript 5.1. This means that you can use the latest version of TypeScript in your Angular applications. You can check out the TypeScript 5.1 release notes to learn more about the new features.
Transform input values
Angular v16.1 introduces a new transform
option in the @Input
decorator.
It allows transforming the value passed to the input before it is assigned to the property.
The transform
option takes a function that takes the value as input and returns the transformed value.
As the most common use cases are to transform a string to a number or a boolean, Angular provides two built-in functions to do that: numberAttribute
and booleanAttribute
in @angular/core
.
Here is an example of using booleanAttribute
:
@Input({ transform: booleanAttribute }) disabled = false;
This will transform the value passed to the input to a boolean so that the following code will work:
<my-component disabled></my-component>
<my-component disabled="true"></my-component>
<!-- Before, only the following was properly working -->
<my-component [disabled]="true"></my-component>
The numberAttribute
function works the same way but transforms the value to a number.
@Input({ transform: numberAttribute }) value = 0;
It also allows to define a fallback value, in case the input is not a proper number (default is NaN):
@Input({ transform: (value: unknown) => numberAttribute(value, 42) }) value = 0;
This can then be used like this:
<my-component value="42"></my-component>
<my-component value="not a number"></my-component>
<!-- Before, only the following was properly working -->
<my-component [value]="42"></my-component>
Fetch backend for the Angular HTTP client
The HTTP client has a new backend implementation based on the Fetch API.
This is an experimental and opt-in feature, that you can enable with:
provideHttpClient(withFetch());
It does not support the progress reports on uploads, and of course, requires a browser that supports the Fetch API. The fetch API is also experimental on Node but available without flags from Node 18 onwards.
This is mainly interesting for server-side rendering, as the XHR implementation is not supported natively in Node and requires a polyfill (which has some issues).
Angular CLI
The CLI now has a --force-esbuild
option that allows forcing the usage of esbuild for ng serve
.
It allows trying the esbuild implementation without switching the builder in angular.json
(and keeping the Webpack implementation for the ng build
command).
The esbuild builder has been improved. It now pre-bundles the dependencies using the underlying Vite mechanism, uses some persistent cache for the TypeScript compilation and Vite pre-bundling, and shows the estimated transfer sizes of the built assets as the Webpack builder does.
Summary
That’s all for this release, stay tuned!
All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!
What's new in Vue 3.3?
Vue 3.3.0 is here!
The last minor release was v3.2.0 in August 2021! Since then, we have seen a lot of patch releases, some coming with new features.
Originally, the v3.3 release was supposed to bring Suspense and the Reactivity Transform APIs out of their experimental state.
Is that the case? Let’s see what we have in this release (and some interesting bits from the 47 patches since v3.2.0)!
Hello Reactivity Transform, and goodbye!
During the last year and a half, the Vue team pursued its experiments with ref sugar (see our previous blog post to catch up).
Currently, without ref sugar, you write code like this:
import { ref, computed, watchEffect } from 'vue';
const quantity = ref(0);
const total = computed(() => quantity.value * 10);
watchEffect(() => console.log(`New total ${total.value}`));
Note the .value
that you need to access the value of the quantity
or total
ref.
If you use the Composition API, you’re used to it.
The reactivity transform experiment introduced new compiler macros like $ref()
and $computed()
.
When using these, the variable becomes reactive:
import { watchEffect } from 'vue';
const quantity = $ref(0);
const total = $computed(() => quantity * 10);
watchEffect(() => console.log(`New total ${total}`));
And .value
was no longer necessary with this syntax!
But it turns out that this experiment is not quite as perfect as hoped initially. It introduced another way to do the same thing, with quite a bit of “magic”, additional pitfalls, and complexity.
So in the end, this experiment is now officially… dropped!
As some teams already started to use it, it will not be removed right away. The plan is to phase these APIs out in a different package, add deprecation warnings in core, and eventually remove them in v3.4.
It doesn’t mean that the team is not thinking about Vue how can be improved. Some new ideas will probably be shared publicly soon.
And a part of the reactivity transform experiment is going to stay: the defineProps
destructuration.
It’s the part I really liked, so I’m quite happy about it 🤓.
defineProps destructuration
defineProps
is the way to declare your props in the script setup syntax
(see our article about script setup).
The syntax plays well with TypeScript, but the declaration of default values was a bit painful:
const props = withDefaults(defineProps<{ name?: string }>(), { name: 'Hello '})
console.log(props.name);
You also can’t destructure the props directly, as it loses the reactivity.
With this new release, you can now give default values while destructuring the props and keeping the reactivity!
const { name = 'Hello' } = defineProps<{ name?: string }>()
console.log(name);
If you try to use a destructured prop directly inside a watcher (or to toRef
),
Vue will issue a warning and indicate to use a getter function instead:
watch(name, () => {});
// "name" is a destructured prop and should not be passed directly to watch().
// Pass a getter () => name instead.
To help with this pattern, a new toValue
helper function has been added
to convert refs and getters to values:
const v1 = toValue(ref('hello')); // 'hello'
const v2 = toValue(() => 'hello'); // 'hello'
If you want to give it a try, you’ll need to enable the propsDestructure
option
in your bundler config. For example, in Vite:
plugins: [
vue({
script: {
propsDestructure: true
}
})
TypeScript improvements
The TypeScript support of defineProps
and other macros has been massively improved,
as pretty much all built-in types are now supported (Extract
, Exclude
, Uppercase
, Parameters
, etc.).
It also can now refer to types and interfaces imported from other files
(whereas it was only resolving local types previously).
defineEmits
has also been improved,
as it now supports a shorter TS declaration.
In Vue v3.2, we used to write the type like this:
const emit = defineEmits<{
(e: 'selected', value: number): void;
}>();
// emit('selected', 14)
There is now a simplified syntax in Vue v3.3. You can use an interface with the events as keys, and the arguments as tuples:
const emit = defineEmits<{
selected: [value: number]
}>();
Vue 3.3 also allows writing TypeScript directly in templates. It can be handy to hint to Volar that a variable is not null, or of a particular type:
<div>
<h2>Welcome {{ (user!.name as string).toLowerCase() }}</h2>
</div>
Generic components
script setup
components can now have a generic parameter,
which works like a generic <T>
in TypeScript:
Volar is then capable to throw an error if
value
is a string
and items
an array of numbers for example.
Component name inference
When using the script setup syntax, the SFC compiler now infers the component name based on the file name.
So a component declared in a file named Home.vue
will automatically have the name Home
since v3.2.34.
defineOptions macro
A new macro (a compile-time helper like defineProps
and defineEmits
) has been introduced
to help declare the options of a component.
This is available only in script setup
component,
and can be handy to declare a few things like the name of a component,
if the inferred name is not good enough
or to set the inheritAttrs
option:
defineOptions({ name: 'Home', inheritAttrs: true });
defineSlots macro
Another macro called defineSlots
(and a slots
option if you’re using defineComponent
)
has been added to the framework to help declare typed slots.
When doing so, Volar will be able to check the slot props of a component.
Let’s say an Alert
component has a default slot that exposes a close
function:
defineSlots<{
default: (props: { close: () => void }) => void;
}>();
If the Alert
component is not used properly, then Volar throws an error:
<Alert><template #default="{ closeAlert }">...</template></Alert>
// error TS2339: Property 'closeAlert' does not exist on type '{ close: () => void; }'.
The returning value of defineProps
can be used
and is the same object as returned by useSlots
.
experimental defineModel macro
When you have a custom form component that just wants to bind the v-model
value to a classic input, the prop/event mechanic we saw can be a bit cumbersome:
<template>
<input :value="modelValue" @input="setValue($event.target.value)" />
</template>
<script setup lang="ts">
defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
function setValue(pickedValue) {
emit('update:modelValue', pickedValue);
}
</script>
It is now possible to simplify this component,
by using the defineModel
(experimental) macro:
<template>
<input v-model="modelValue" />
</template>
<script setup lang="ts">
const modelValue = defineModel<string>();
</script>
defineModel
also accepts a few options:
required: true
indicates that the prop is requireddefault: value
lets specify a default valuelocal: true
indicates that the prop is available and mutable even if the parent component did not pass the matching v-model
A useModel
helper is also available if you don’t use script setup
.
Note that this feature is experimental and opt-in. For example, in Vite:
plugins: [
vue({
script: {
defineModel: true
}
})
default value for toRef
It is now possible to define a default value when using toRef()
:
const order = { quantity: undefined }
const quantity = toRef(order, 'quantity', 1); // quantity is 1
Note that this works only if the value is undefined
.
isShallow
A new utility function called isShallow
is now available.
It allows checking if a variable is deeply reactive (created with ref
or reactive
)
or “shallow” (created with shallowRef
or shallowReactive
).
v-for and ref
Vue 3 now behaves like Vue 2 used to behave when using ref
inside v-for
:
it populates an array of refs.
<script setup>
import { ref } from 'vue'
const divs = ref([])
</script>
<template>
<div v-for="i of 3" ref="divs">{{ i }}</div>
<!-- divs is populated with an array of 3 refs -->
<!-- one for each HTMLDivElement created -->
<div>{{ divs }}</div>
</template>
aliases for vnode hook events
Vue allows you to listen for lifecycle events in templates, both for elements and components.
The syntax in Vue 3 is @vnodeMounted
for example.
In Vue v3.3, it is now possible to use @vue:mounted
instead,
which is a bit more understandable.
@vnode
hooks are now deprecated.
<script setup>
import { ref } from 'vue'
const isMounted = ref(false)
const onDivMounted = () => isMounted.value = true
const condition = ref(false)
setTimeout(() => condition.value = true, 3000)
</script>
<template>
<div>isMounted: {{ isMounted }}</div>
<div @vue:mounted="onDivMounted()" v-if="condition">Hello</div>
</template>
You can try this example in this online demo.
suspensible Suspense
Suspense
is still experimental but gained a new prop called suspensible
.
The prop allows the suspense to be captured by the parent suspense.
That can be useful if you have nested Suspense
components,
as you can see in the PR explanation.
console available in templates
A small (but useful when debugging) improvement in templates is the possibility
to directly use console
:
<input @input="console.log($event.target.value)">
To conclude, let’s see what happened in the ecosystem recently.
create-vue
Since Vue v3.2, the Vue team started a new project called create-vue, which is now the recommended way to start a Vue project. You can use it with
npm init vue@next
create-vue
is based on Vite v4,
and officially replaces Vue CLI.
If you missed it, create-vue
recently added the support of Playwright in addition to Cypress for e2e tests!
It now also supports TypeScript v5 out of the box.
Router
Vue v3.3 introduced a new function on the object returned by createApp
:
runWithContext
.
The function allows using inject
with the app as the active instance,
and get the value provided by the app providers.
const app = createApp(/* ... */);
app.provide('token', 1);
app.runWithContext(() => inject('token'));
If I mention this in the router section,
it’s because it unlocks the possibility to use inject
in global navigation guards
if you use Vue v3.3 and the router v4.2!
router.beforeEach((to, from) => {
console.log(inject('token'));
});
Pinia
Pinia is a state-management library from the author of vue-router Eduardo “@posva”. It was meant as an experiment for Vuex v5, but it turns out to be very good, and it’s now the official recommendation for state-management library in Vue 3 projects.
The project moved into the vuejs organization, and there will be no Vuex version 5. Pinia is a really cool project, with a great composition API and TS support, and one of the cutest logos you’ve ever seen.
We added a complete chapter in our ebook to explain how Pinia works if you’re interested 🤓.
Eduardo also released VueFire, the official Firebase bindings for Vue 3. With this library, you can add Firebase to your Vue or Nuxt projects in a few minutes.
Nuxt
After a long development, Nuxt v3 is now stable! It is a really amazing solution and the Nuxt team has been hard at work to provide a great development experience (with some dark magic under the hood). Give it a try if you’re looking for a meta-framework on top of Vue (for example if you need SSR or SSG for your project).
Volar
Volar reached v1.0 recently after a very intense period of development these past months. The TypeScript support is now better than ever, making it a no-brainer to use in your projects.
Vue Test utils
The testing library has a few typings improvements coming in the v2.4 release,
and now supports SSR testing via renderToString
since v2.3.
Vue 3 in 2023
The Vue team plans to release more frequent minor releases than in the past, so we can expect Vue v3.4 soon. The next releases will be focused on bug fixes and small improvements in the first quarter of the year. Then there should be some improvements for the SSR support in Q2. Finally, the second half of the year should see the first alpha of Vapor. We should hopefully also see Suspense finally getting out of its experimental state.
Vue Vapor is an alternative compilation mode to get better performances.
It’s not public yet, but we already know that it is inspired by what Solidjs does,
as the reactivity system of Vue and Solid are fairly similar.
The idea is to compile a script setup
component differently when the “Vapor” mode is enabled,
resulting in a lighter rendering code (not using VDOM).
Let’s say we have a classic Counter component:
<script setup lang="ts">
let count = ref(0)
</script>
<template>
<div>
<button @click="count++">{{ count }}</button>
</div>
</template>
In the current Vue 3 compilation mode, the template is compiled into a function that produces VDOM which is then diffed and rendered (check out the “Under the hood” chapter of our ebook if you want to learn more). In Vapor mode, the template is compiled into a function that only updates what’s necessary in the DOM.
import { ref, effect } from 'vue';
import { setText, template, on } from 'vue/vapor';
let t0 = template('<div><button>');
export default () => {
const count = ref(0);
let div = t0();
let button = div.firstChild;
let button_text;
effect(() => {
// This is the only part that is executed at runtime when the counter value changes
setText(button, button_text, count.value);
});
on(button, 'click', () => count.value++);
return div;
}
This “Vapor” mode will be opt-in at the component level, probably for “leaf” components first.
To switch a component to Vapor, the current idea is to import it with a .vapor.vue
extension:
<script setup lang="ts">
// 👇 compiles the User component in Vapor mode
// you get an error if the component is not "Vapor" compatible
import User from './User.vapor.vue'
</script>
<template>
<User />
</template>
We’ll be able to enable it for a whole application in the future.
The current idea is to call a different createApp
function from vue/vapor
:
import { createApp } from 'vue/vapor'
import App from './App.vapor.vue'
createApp(App).mount('#app')
When enabled for a full application, the VDOM implementation could be completely dropped from the resulting bundle! We can’t wait to try this!
That’s all for this release. Stay tuned for the next one!
Our ebook, online training and training) are up-to-date with these changes if you want to learn more!
What's new in Angular 16?
Angular 16.0.0 is here!
This is a major release packed with features: let’s dive in!
Angular Signals
As you may have heard, all the hype around Angular is about the addition of Signals to the framework. As this is a big change that will shape how we build Angular applications in the future, we wrote an introduction to Signals, to cover what you can do with them in v16 and what to expect in the future:
Note that Signals are released as a developer preview in v16 and the API may change in the future.
As Signals are progressing, Angular now allows configuring ZoneJS explicitly with provideZoneChangeDetection
:
bootstrapApplication(AppComponent, {
providers: [provideZoneChangeDetection({eventCoalescing: true})],
});
This opens the door to zoneless applications in the near future, where developers could choose to not include ZoneJS in their application.
Required inputs
Angular v16 added the possibility to mark an input as required,
with @Input({ required: true})
:
@Input({ required: true }) user!: UserModel;
In that case, if the parent component does not pass the input, then the compiler will throw an error.
This has been a long-awaited feature, we’re happy to see this land in Angular!
Server-Side Rendering and progressive hydration
Angular has been supporting Server-Side Rendering (SSR) for a while now, but it was a bit limited as it was only possible to render the whole application on the server, and then re-render it on the client when the JavaScript bundle was loaded. This was resulting in a flickering when the application loaded, as the DOM was completely wiped out before being re-rendered.
Angular v16 introduces “progressive hydration”, which allows rendering the application on the server, and then progressively hydrate it on the client.
This means that the server-rendered DOM is not wiped out anymore, and the client-side rendering is done progressively, which results in a much smoother experience for the user.
To enable these new behaviors, you simply add provideClientHydration()
to your providers:
bootstrapApplication(AppComponent, {
providers: [provideClientHydration()]
});
The HttpClient has also been updated to be able
to store the result of a request done on the server,
and then reuse it on the client during the hydration process!
The behavior is enabled by default if you use provideClientHydration()
,
but can be disabled with provideClientHydration(withNoHttpTransferCache())
.
You can also disable the DOM reuse with withNoDomReuse()
.
Note that this is a developer preview, and the API may change in the future.
There are also a few pitfalls to be aware of.
For example, the HTML must be valid when generated on the server (whereas the browser is more forgiving).
The DOM must also be the same on the server and the client,
so you can’t manipulate the server-rendered DOM before sending it to the client.
If some parts of your templates don’t produce the same result on the server and the client, you can skip them by adding ngSkipHydration
to the element or component.
i18n is also not supported yet, but that should come soon.
When running in development mode, the application will output some stats to the console to help you debug the hydration process:
Angular hydrated 19 component(s) and 68 node(s), 1 component(s) were skipped
You can easily give this a try by using Angular Universal. In the long term, this will probably be part of the CLI directly.
DestroyRef
Angular v16 introduces a new DestroyRef
class,
which has only one method called onDestroy
.
DestroyRef
can be injected,
and then used to register code that should run
on the destruction of the surrounding context.
const destroyRef = inject(DestroyRef);
// register a destroy callback
destroyRef.onDestroy(() => doSomethingOnDestroy());
For example, it can be used to execute code on the destruction
of a component or directive (as we do now with ngOnDestroy
).
But this is more useful for cases where you want to execute code
when a component is destroyed, but you don’t have access to the component itself,
for example when defining a utility function.
This is exactly what Angular uses internally to implement takeUntilDestroyed
,
the new RXJS operator introduced in our Signals blog post.
provideServiceWorker
One of the last modules that needed to be transitioned to a standalone provider function was ServiceWorkerModule
. It is now done with provideServiceWorker
:
bootstrapApplication(AppComponent, {
providers: [provideServiceWorker('ngsw-worker.js', { enabled: !isDevMode() })]
});
It, of course, accepts the same options as the ServiceWorkerModule
.
Running ng add @angular/pwa
will now add provideServiceWorker
to your providers
if your application is a standalone one.
TypeScript 5.0 support
Angular v16 now supports TypeScript 5.0. This means that you can use the latest version of TypeScript in your Angular applications. You can check out the TypeScript 5.0 release notes to learn more about the new features.
One important point is that TypeScript now supports the “official” decorator specification.
Their “experimental decorators” (based on a much older specification) are still supported but are now considered legacy. One of the differences between these two specifications is that the legacy one supports decorators on parameters, which is used by Angular for dependency injection (with @Optional
, @Inject
, etc.), and the new one doesn’t.
It is now possible to use the new decorator specification in Angular, but it requires a few changes in your code, as you can’t use decorators on parameters anymore.
This can usually be worked around by using the inject()
function from @angular/core
.
There is no rush to use the new decorators instead of the “legacy” ones, but it’s something to keep in mind as I wouldn’t be surprised if we have to migrate away from experimentalDecorators
in the future
Styles removal opt-in
Angular v16 introduces a new opt-in feature to remove the styles of a component when its last instance is destroyed.
This will be the default behavior in the future, but you can already opt in with:
{ provide: REMOVE_STYLES_ON_COMPONENT_DESTROY, useValue: true }
Router
Angular v15.2 deprecated the usage of class-based guards and resolvers
(check out our blog post for more details).
In Angular v16, a migration will run to remove the guard and resolver interfaces from your code (CanActivate
, Resolve
, etc.).
To help with the conversion, the router now offers helper functions to convert class-based entities to their function-based equivalent:
mapToCanActivate
mapToCanActivateChild
mapToCanDeactivate
mapToCanMatch
mapToResolve
For example, you can now write:
{ path: 'admin', canActivate: mapToCanActivate([AdminGuard]) };
RouterTestingModule
is also getting phased out, and will probably be deprecated and removed in the future. It is not needed anymore, because Angular v16 now provides MockPlatformLocation
in BrowserTestingModule
by default, which was the main reason to use RouterTestingModule
in the first place.
You can now directly use RouterModule.forRoot([])
or providerRouter([])
in your tests.
Last but not least, the router now offers the possibility to bind parameters as inputs.
To do so, you need to configure the router with withComponentInputBinding
:
provideRouter(routes, withComponentInputBinding())
With this option, a component can declare an input with the same name as a route parameter, query parameter or data, and Angular will automatically bind the value of the parameter or data to this input.
export class RaceComponent implements OnChanges {
@Input({ required: true }) raceId!: string;
We can then use this input as a regular input, and react to its change with ngOnChanges or by using a setter for this input:
constructor(private raceService: RaceService) {}
ngOnChanges() {
this.raceModel$ = this.raceService.get(this.raceId);
}
Angular CLI
Check out our dedicated blog post about the CLI for more details.
Summary
That’s all for this release, stay tuned!
All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!
What's new in Angular CLI 16.0?
Angular CLI 16.0.0 is out!✨
If you want to upgrade to 16.0.0 without pain (or to any other version, by the way), I have created a Github project to help: angular-cli-diff. Choose the version you’re currently using (14.2.0 for example), and the target version (16.0.0 for example), and it gives you a diff of all files created by the CLI: angular-cli-diff/compare/14.2.0…16.0.0.
It can be a great help along with the official ng update @angular/core @angular/cli
command.
You have no excuse for staying behind anymore!
Let’s see what we’ve got in this release.
Standalone applications!
It is now possible to generate standalone applications with the CLI,
thanks to the --standalone
flag!
ng new my-app --standalone
This is great to start new projects 😍
now that the option is available, we switched all the exercises of our Angular training to use a standalone application with standalone components.
We also re-wrote our ebook entirely to now introduce standalone concepts from the beginning, and add the concept of modules only when it makes sense!
esbuild builder is now in developer preview!
The new builder that uses esbuild has been introduced in v14 but was experimental. It is now in developer preview, so you can give it a try. Even if some options are still unsupported, most of them are now available (autoprefixer and tailwind support have been added), and the builder should be usable.
To check it out in your project,
replace @angular-devkit/build-angular:browser
with @angular-devkit/build-angular:browser-esbuild
,
and run ng build
.
For a small application, on a cold production build (after ng cache clean
), the build time went from 13s to 6s on my machine.
For a larger application, the build time went from 1m12s to 32s 🤯.
Note that the esbuild builder now uses Vite under the hood for the development server. Even if the integration is not 100% complete and optimized, it is promising to see this Webpack alternative being used in the Angular ecosystem.
Functional guards and resolvers by default
The CLI now generates functional guards and resolvers by default,
without the need to specify --functional
anymore.
Class-based guards and resolvers are still available with the --no-functional
option,
but as they are now deprecated, you’re encouraged to use the functional ones.
Jest experimental support
The CLI now supports Jest as a test runner, but it is still experimental.
To check it out in your project,
replace @angular-devkit/build-angular:karma
with @angular-devkit/build-angular:jest
,
and run ng test
.
The support is far from being complete, but it is promising. It uses the new esbuild builder under the hood, and the support should improve in the next releases.
Jest does not run in a browser and uses JSDOM instead. That means that you don’t have the same experience that you can have with Karma, but as Karma is now deprecated, an alternative is welcome.
The Angular team also announced that they are working on the support of Web Test Runner, to have an alternative to Karma that runs in a browser.
Bye-bye compileComponents!
The compileComponents
call generated in the unit tests of the CLI is now gone,
as it was useless since the very beginning if you were running your tests with the CLI.
My PR to remove it was merged in the framework, and the CLI now generates tests without it 🤓.
Optional migrations
The CLI now supports optional migrations and will ask you if you want to run them when you upgrade to a new version.
SSR support
A ton of work has been done to improve the SSR story with the CLI, along with what the framework team did with the progressive hydration.
For example, it is now possible to add universal or to generate an app-shell with the CLI for a standalone application.
Summary
That’s all for the CLI v16.0 release! You’ll find more interesting features in our article about the framework v16.0.0 release.
All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!
An introduction to Angular Signals
The Angular team has been working on a different way to handle reactivity in applications for the past year. The result of their work is a new concept in Angular called Signals.
Let’s see what they are, how they work, how they interoperate with RxJS, and what they’ll change for Angular developers.
The reasons behind Signals
Signals are a concept used in many other frameworks, like SolidJS, Vue, Preact, and even the venerable KnockoutJS. The idea is to offer a few primitives to define reactive state in applications and to allow the framework to know which components are impacted by a change, rather than having to detect changes on the whole tree of components.
This will be a significant change to how Angular works, as it currently relies on zone.js to detect changes in the whole tree of components by default. Instead, with signals, the framework will only dirty-check the components that are impacted by a change, which of course makes the re-rendering process more efficient.
This opens the door to zoneless applications, i.e. applications where Angular applications don’t need to include Zone.js (which makes them lighter), don’t have to patch all the browser APIs (which makes them start faster) and smarter in their change detection (to only check the components impacted by a change).
Signals are released in Angular v16, as a developer preview API, meaning there might be changes in the naming or behavior in the future. But this preview is only a small part of the changes that will come with signals. In the future, signal-based components with inputs and queries based on signals, and different lifecycle hooks, will be added to Angular. Other APIs like the router parameters and form control values and status, etc. should also be affected.
Signals API
A signal is a function that holds a value that can change over time.
To create a signal, Angular offers a signal()
function:
import { signal } from '@angular/core';
// define a signal
const count = signal(0);
The type of count
is WritableSignal<number>
, which is a function that returns a number.
When you want to get the value of a signal, you have to call the created signal:
// get the value of the signal
const value = count();
This can be done both in TypeScript code and in HTML templates:
<p>{{ count() }}</p>
You can also set the value of a signal:
// set the value of the signal
count.set(1);
Or update it:
// update the value of the signal, based on the current value
count.update((value) => value + 1);
There is also a mutate
method that can be used to mutate the value of a signal:
// mutate the value of the signal (handy for objects/arrays)
const user = signal({ name: 'JB', favoriteFramework: 'Angular' });
user.mutate((user) => user.name = 'Cédric');
You can also create a readonly signal, that can’t be updated, with asReadonly
:
const readonlyCount = count.asReadonly();
Once you have defined signals, you can define computed values that derive from them:
const double = computed(() => count() * 2);
Computed values are automatically computed when one of the signals they depend on changes.
count.set(2);
console.log(double()); // logs 4
Note that they are lazily computed and only re-computed
when one of the signals they depend on produces a new value.
They are not writable, so you can’t use .set()
or .update()
or .mutate()
on them
(their type is Signal<T>
).
Under the hood, a signal has a set of subscribers, and when the value of the signal changes, it notifies all its subscribers. Angular does that in a smart way, to avoid recomputing everything when a signal changes, by using internal counters to know which signals have really changed since the last time a computed value was computed.
Finally, you can use the effect
function to react to changes in your signals:
// log the value of the count signal when it changes
effect(() => console.log(count()));
An effect
returns an object with a destroy
method that can be used to stop the effect:
const effectRef: EffectRef = effect(() => console.log(count()));
// stop executing the effect
effectRef.destroy();
This does look like a BehaviorSubject
, but it has some subtle differences, the most important one being that unsubscribing is unnecessary thanks to the usage of
weak references.
An effect
or a computed
value will re-evaluate when one of the signals they depend on changes,
without any action from the developer. This is usually what you want.
Note that while signals and computed values will be common in Angular applications, effects will be used less often, as they are more advanced and low-level primitive. If you need to react to a signal change, for example, to fetch data from a server, you can use the RxJS interoperability layer that we detail below.
If you want to exclude a signal from the dependencies of an effect or computed value,
you can use the untracked
function:
const multiplier = signal(2);
// `total` will not re-evaluate when `multiplier` changes
// it only re-evaluates when `count` changes
const total = computed(() => count() * untracked(() => multiplier()));
You can’t write to a signal inside a computed value, as it would create an infinite loop:
// this will throw an error NG0600
const total = computed(() => { multiplier.set(2); return count() * multiplier() });
This is the same in effect, but this can be overridden using allowSignalWrites
:
// this will not throw an error
effect(
() => {
if (this.count() > 10) {
this.count.set(0);
}
},
{ allowSignalWrites: true }
);
All these features (and terminology) are fairly common in other frameworks, so you won’t be surprised if you used SolidJS or Vue 3 before.
One feature that I think is fairly unique to Angular is the possibility to pass a ValueEqualityFn
to the signal
function.
To decide if a signal changed (and know if a computed or an effect need to run),
Angular uses Object.is()
by default, but you can pass a custom function to compare the old and new values:
const user = signal({ id: 1, name: 'Cédric' }, { equal: (previousUser, newUser) => previousUser.id === newUser.id });
// upperCaseName will not re-evaluate when the user changes if the ID stays the same
const uppercaseName = computed(() => user().name.toUpperCase());
Signals, components, and change detection
As mentioned above, you can use signals and computed values in your templates:
<p>{{ count() }}</p>
If the counter value changes, Angular detects it and re-renders the component.
But what happens if the component is marked as OnPush
? Until now, OnPush
meant that Angular would only re-render the component if one of its inputs changed, if an async
pipe was used in the template, or if the component used ChangeDetectorRef#markForCheck()
.
The framework now handles another reason to re-render a component: when a signal changes. Let’s consider the following component (not using a signal, but a simple field that will be updated after 2 seconds):
@Component({
selector: 'app-user',
standalone: true,
templateUrl: './user.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
count = 0;
constructor() {
// set the counter to 1 after 2 seconds
setTimeout(() => this.count = 1, 2000);
}
}
As the component is OnPush
, using a setTimeout
will trigger a change detection, but the component will not be re-rendered (as it won’t be marked as “dirty”).
But if we use a signal instead:
@Component({
selector: 'app-user',
standalone: true,
templateUrl: './user.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
count = signal(0);
constructor() {
// set the counter to 1 after 2 seconds
setTimeout(() => this.count.set(1), 2000);
}
}
Then the component will be re-rendered after 2 seconds, as Angular detects the signal change and refresh the template.
Under the hood, if a view reads a signal value, then Angular marks the template of the component as a consumer of the signal. Then, when the signal value changes, it marks the component and all its ancestors as “dirty”.
For us developers, it means that we can use signals in our components as soon as Angular v16 is released, and we don’t need to worry about change detection, even if the components are using OnPush.
Sharing a signal between components
In the previous example, the signal was defined in the component itself. But what if we want to share a signal between multiple components?
In the Vue ecosystem, you frequently encounter the pattern of “composables”: functions that return an object containing signals, computed values, and functions to manipulate them. If a signal needs to be shared, it is defined outside of the function and returned by the function:
const count = signal(0);
export function useCount() {
return {
count,
increment: () => count.set(count() + 1)
};
}
In Angular, we can do the same and we can also use services instead of functions (and it’s probably what we’ll do).
We can define a CountService
as the following:
@Injectable({ providedIn: 'root' })
export class CountService {
count = signal(0);
}
Then, in our components, we can inject the service and use the signal:
@Component({
selector: 'app-user',
standalone: true,
templateUrl: './user.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
count = inject(CountService).count;
}
The service could also define computed values, effects, methods to manipulate the signals, etc.
@Injectable({ providedIn: 'root' })
export class CountService {
count = signal(0);
double = computed(() => this.count() * 2);
constructor() {
effect(() => console.log(this.count()));
}
increment() {
this.count.update(value => value + 1);
}
}
Memory leaks
Signal consumers and producers are linked together using weak references, which means that if all the consumers of a signal cease to exist, then the signal will be garbage collected as well. In other terms: no need to worry about “unsubscribing” from a signal to prevent memory leaks, as we have to do with RxJS observables \o/.
You can also use an effect
in your component (even if that’s probably going to be very rare) to watch a value and react to its changes,
for example, the count
defined in a service like the above.
Note that you don’t have to manually stop an effect when the component is destroyed, as Angular will do it for you to prevent memory leaks.
Under the hood, an effect uses a DestroyRef
, a new feature introduced in Angular v16, to automatically be cleaned up when the component or service is destroyed.
You can change this behavior though, by creating an effect with a specific option:
this.logEffect = effect(() => console.log(count()), { manualCleanup: true });
In this case, you will have to manually stop the effect when the component is destroyed:
ngOnDestroy() {
this.logEffect.destroy();
}
Effects can also receive a cleanup function, that is run when the effect runs again.
This can be handy when you need to stop a previous action before starting a new one.
In the example below, we start an interval that runs every count
milliseconds,
and we want to stop it and start a new one when the count changes:
this.intervalEffect = effect(
(onCleanup) => {
const intervalId = setInterval(() => console.log(`count in intervalEffect ${this.count()}`), this.count());
return onCleanup(() => clearInterval(intervalId))
}
)
Note that effects run during change detection, so they’re not a good place to set signal values. That’s why you get an error from Angular if you try to do so:
// ERROR: Cannot set a signal value during change detection
Effects will probably be used very rarely, but they can be handy in some cases:
- logging / tracing;
- synchronizing state to the DOM or to a storage, etc.
Signals and RxJS interoperability
RxJS is here to stay, even if its usage might be more limited in the future. Angular is not going to remove RxJS, and it’s not going to force us to use signals instead of observables. In fact, RxJS is probably a better way to react to signal changes than using effects.
We can use signals and observables together, and we can convert one into the other. Two functions to do that are available in the brand new @angular/core/rxjs-interop
package.
To convert a signal to an observable, we can use the toObservable
function:
const count$ = toObservable(count);
Note that the created observable will not receive all the value changes of the signal, as this is done using an effect under the hood, and effects are only run during change detection:
const count = signal(0);
const count$ = toObservable(count);
count$.subscribe(value => console.log(value));
count.set(1);
count.set(2);
// logs only 2, not 0 and 1, as this is the value when the under-the-hood effect runs
To convert an observable to a signal, we can use the toSignal
method:
const count = toSignal(count$);
The signal will contain the last value emitted by the observable,
or will throw an error if the observable emits an error.
Note that the subscription created by toSignal()
is automatically unsubscribed
when the component that declared it is destroyed.
As observables can be asynchronous, you can pass an initial value to the function:
const count = toSignal(count$, { initialValue: 0 });
If you do not provide an initial value, the value is undefined
if it is read before the observable emits a value.
You can also use the option requireSync
to make the signal throw an error if it is read before the observable emits a value:
const count = toSignal(count$, { requireSync: true });
Signal-based components
In the future (v17? v18?), we’ll be able to build a component entirely based on signals, even for its inputs and queries. The framework would be notified when an expression has changed thanks to the signals, and would thus only need to dirty-checks the components affected by the change, without having to check for changes on unrelated components, without the need for zone.js. It will even be able to re-render only the part of the template that has changed, instead of checking the whole template of a component as it currently does. But there is a long way ahead, as several things need to be rethought in the framework to make this work (inputs, outputs, queries, lifecycle methods, etc).
An RFC is out with the details of the proposal, and you can follow the progress on the Angular repository.
Currently, the RFC proposes to use signals: true
to mark a component as “Signal-based”:
@Component({
signals: true,
selector: 'temperature-calc',
template: `
<p>C: {{ celsius() }}</p>
<p>F: {{ fahrenheit() }}</p>
`,
})
export class SimpleCounter {
celsius = signal(25);
// The computed only re-evaluates if celsius() changes.
fahrenheit = computed(() => this.celsius() * 1.8 + 32);
}
Inputs, outputs, and queries would be defined via functions instead of decorators in these components, and would return a signal:
firstName = input<string>(); // Signal<string|undefined>
Nothing has been implemented for this part yet, so you can’t try this in v16. But you can already try to use signals in existing components, as we mentioned above (but keep in mind they are not production ready)
It’s anyway quite interesting how frameworks inspire each other, with Angular taking inspiration from Vue and SolidJS for the reactivity part, whereas other frameworks are increasingly adopting the template compilation approach of Angular, with no Virtual DOM needed at runtime.
The future of Angular is exciting!
All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!