What's new in Angular 19.0?
Angular 19.0.0 is here!
This is a major release with a lot of features. Components are now standalone by default and most of the new Signal APIs are stable!
We have been hard at work these past months to completely re-write our “Become a Ninja with Angular” ebook and our online workshop to use signals from the start! 🚀 The update is free if you already have it, as usual 🤗. I can’t believe we have been maintaining this ebook and workshop for nearly 10 years. If you don’t have it already, go grab it now!
TypeScript 5.6 support
Angular v19 now supports TypeScript 5.6. This means that you can use the latest version of TypeScript in your Angular applications. You can check out the TypeScript 5.6 release notes to learn more about the new features. TypeScript 5.4 is no longer supported.
Standalone by default
We no longer need to add standalone: true
in the component/directive/pipe decorator,
as it is now the default behavior!
A migration will take care of removing it for you when running ng update
,
and add standalone: false
to your non-standalone entities if needed.
If you want to make sure all your components are standalone,
you can use the new "strictStandalone": true
option in the angularCompilerOptions
.
If that’s not the case, you’ll see:
TS-992023: Only standalone components/directives are allowed when 'strictStandalone' is enabled.
Unused imports in standalone components
A wonderful extended diagnostic has been added to the Angular compiler, allowing it to detect unused imports in standalone components!
This is a great addition, as it will help you to keep your components clean and tidy. If you forget to remove an import after refactoring your code, you’ll see a message like this:
TS-998113: Imports array contains unused imports [plugin angular-compiler]
src/app/user/users.component.ts:9:27:
9 │ imports: [UserComponent, UnusedComponent],
╵ ~~~~~~~~~~~~~
A code action is provided to remove the unused import for you via the language service (but there is no automatic migration doing it for you, unfortunately).
You can disable the diagnostic if needed with:
"extendedDiagnostics": {
"checks": {
"unusedStandaloneImports": "suppress"
}
}
Signal APIs are stable (well, most of them)
Most of the Signal APIs (and RxJS interoperability functions) are no longer in developer preview and can safely be used.
The input()
, output()
, model()
, viewChild()
, viewChildren()
,
contentChild
, contentChildren()
, takeUntilDestroyed()
, outputFromObservable()
,
and outputToObservable()
are now marked as stable.
Of course, migrating complete applications to these new APIs can be a bit of work. But you know what, the Angular team cooked some automatic migrations! 😍 We will talk about them in the next section.
effect
went through some changes and is still in developer preview.
toObservable
(which uses an effect) and toSignal
are still in developer preview as well.
All effects aren’t handled the same way anymore. Angular distinguishes two kinds of effects:
- component effects, which are created in components or directives;
- root effects, which are created in root services, or with the
forceRoot
option.
Component effects now run during change detection
(just before the actual change detection of their owning component)
and not after it as it was the case before.
This is a breaking change.
You might thus see some changes in their behavior,
for example when an effect is triggered by a change of a view query signal.
To solve this case, a new afterRenderEffect
function has been added.
It is similar to effect
, but its function runs after the rendering rather than before.
Like afterRender
and afterNextRender
(check our blog post if you need a refresher),
it can also specify what should be executed in each rendering phase
but values are propagated from phase to phase as signals instead of as plain values.
As a result, later phases may not need to execute
if the values returned by earlier phases do not change.
All these afterRender
functions are still in developer preview.
Root effects are not tied to a component lifecycle
and are usually used to synchronize the state of the application
with the outside world
(for example, to write it to the local storage).
These effects don’t run as part of the change detection but as a microtask
(and can be triggered in tests using TestBed.flushEffects()
).
Another notable change is that you can now write to signals in effects,
without the need to specify the (now deprecated) option allowSignalWrites: true
.
The team found out that it was not preventing basic usages
but was just making the code more verbose when really needed.
All in all, effects should be stabilized in the next release.
However their usage is still not recommended for most cases,
and it seems like they should be the last resort to solve a problem.
That’s why the framework introduced the new experimental functions linkedSignal
,
resource
, and rxResource
.
Linked signals with linkedSignal()
Angular v19 introduced a new (developer preview) concept called “linked signals”. A linked signal is a writable signal, but it is also a computed signal, as its content can be reset thanks to a computation that depends on another signal (or several ones).
Imagine we have a component that displays a list of items received via an input, and we want our users to select one of them. By default, let’s say we want to select the first item of the list. But every time the list of items changes, the selected item may no longer be valid, so we want to reset the selected item to the first one.
We can imagine a component like this:
export class ItemListComponent {
items = input.required<Array<ItemModel>>();
selectedItem = signal<ItemModel | undefined>(undefined);
pickItem(item: ItemModel) {
this.selectedItem.set(item);
}
}
Using an effect may come to mind to solve the selection problem:
constructor() {
// ⚠️ This is not recommended
effect(() => {
this.selectedItem.set(this.items()[0]);
});
}
Every time the list of items changes, the effect will be triggered and the first item will be selected.
There is a nice trick that I can show you before we dive into the now-recommended solution: we can use a computed value that returns… a signal!
export class ItemListComponent {
items = input.required<Array<ItemModel>>();
selectedItem = computed<WritableSignal<ItemModel | undefined>>(
() => signal(this.items()[0])
);
pickItem(item: ItemModel) {
this.selectedItem().set(item);
}
}
As you can see, the computed value returns a signal that represents the selected item
(whereas they usually return a value directly).
Every time the list of items changes, the computed function is re-evaluated,
and returns a new signal that represents the selected item.
The downside of this solution is that we have to use selectedItem()()
to read the value,
or selectedItem().set()
to update it, which is a bit ugly.
This is where we can use a linkedSignal
:
export class ItemListComponent {
items = input.required<Array<ItemModel>>();
// ✅ This is recommended
selectedItem: WritableSignal<ItemModel> = linkedSignal(() => this.items()[0]);
A linkedSignal
is a WritableSignal
,
but its value can be reset thanks to a computation.
If the items change, then the computation will be re-executed
and the value of the signal will be updated with the result.
The computation can of course depend on several signals.
Here selectedItem
is reset when the items input changes,
but also when the enabled input changes.
export class ItemListComponent {
items = input.required<Array<ItemModel>>();
enabled = input.required<boolean>();
// recomputes if `enabled` or `items` change
selectedItem = linkedSignal(() => (this.enabled() ? this.items()[0] : undefined));
Note that you can use the previous value of the source signal in the computation function if you need to.
For example, if you want to access the previous items value to compare it with the new one,
you can declare the linkedSignal
with the source and computation options.
In that case, the computation function receives the current and previous values of the source as parameters.
export class ItemListComponent {
items = input.required<Array<ItemModel>>();
selectedItem = linkedSignal</* source */ Array<ItemModel>, /* value */ ItemModel>({
source: this.items,
computation: (items, previous) => {
// pick the item the user selected if it's still in the new item list
if (previous !== undefined) {
const previousChoice = previous.value; // previous.source contains the previous items
if (items.map(item => item.name).includes(previousChoice.name)) {
return previousChoice;
}
}
return items[0];
}
});
Async resources with resource() and rxResource()
Most applications need to fetch data from a server, depending on some parameters,
and display the result in the UI: resource
aims to help with that.
This API is experimental, and will go through an RFC process soon: I would not advise you to use it yet.
The resource
function takes an object with a mandatory loader function that returns a promise:
list(): ResourceRef<Array<UserModel> | undefined> {
return resource({
loader: async () => {
const response = await fetch('/users');
return (await response.json()) as Array<UserModel>;
}
});
}
This example doesn’t use the HTTP client, but the native fetch()
function, which returns a promise.
Indeed, the resource()
function is not linked to RxJS,
and can thus use any client that returns promises.
rxResource
, which we will discuss in a few seconds,
is the alternative to resource
that can be used with an Observable-based client.
This is another example of Angular decoupling itself from RxJS,
but still providing interoperability functions allowing you to use it smoothly.
The function returns a ResourceRef
, an object containing:
- an
isLoading
signal that indicates if the resource is loading; - a
value
signal that contains the result of the promise; - an
error
signal that contains the error if the promise is rejected; - a
status
signal that contains the status of the resource.
You can then use these signals in your template:
@if (usersResource.isLoading()) {
<p>Loading...</p>
} @else {
<ul>
@for (user of usersResource.value(); track user.id) {
<li>{{ user.name }}</li>
}
</ul>
}
The status
signal can be:
ResourceStatus.Idle
, the initial state;ResourceStatus.Loading
, when the promise is pending;ResourceStatus.Error
, when the promise is rejected;ResourceStatus.Resolved
, when the promise is resolved;ResourceStatus.Reloading
, when the resource is reloading;ResourceStatus.Local
, when the value is set locally.
The resource also has a reload
method that allows you to reload the resource.
In that case, its status will be set to ResourceStatus.Reloading
.
But the reloading can also be automatic, thanks to the request
option.
When provided, the resource will automatically reload if one of the signals used in the request changes.
Here, for example, the component has a sortOrder
option that is used in the request:
sortOrder = signal<'asc' | 'desc'>('asc');
usersResource = resource({
// 👇 The `sortOrder` signal is used to trigger a reload
request: () => ({ sort: this.sortOrder() }),
loader: async params => {
// 👇 Params also contains the `abortSignal` to cancel the request
// and the previous status of the resource
// here we are only interested in the request
const request = params.request;
const response = await fetch(`/users?sort=${request.sort}`);
return (await response.json()) as Array<UserModel>;
}
});
If the sortOrder
signal changes,
the resource will automatically reload!
You can also cancel the previous request if needed when the resource is reloaded
using the abortSignal
parameter of the loader
(for example to implement a debounce).
You can choose to ignore the reload request
and thus keep the current value by returning undefined
from the request function.
Last but not least, the returned ResourceRef
is in fact writable.
You can use its set
or update
methods to change the value of the resource
(on the value
or on the resource itself, both work).
In that case, its status will be set to ResourceStatus.Local
.
If you’re only interested in reading the resource,
you can use the asReadonly
method to get a read-only version of the resource.
Finally, the ResourceRef
has a destroy method that can be used to stop the resource.
Now, let’s see how we can use an observable-based resource instead of a promised-based one.
You can use the rxResource()
function in that case.
This function is really similar to resource()
,
but its loader must return an observable instead of a promise.
This allows you to use our good old HttpClient
service to fetch data from a server,
using all your interceptors, error handling, etc:
sortOrder = signal<'asc' | 'desc'>('asc');
usersResource = rxResource({
request: () => ({ sort: this.sortOrder() }),
// 👇 RxJS powered loader
loader: ({ request }) => this.httpClient.get<Array<UserModel>>('/users', { params: { sort: request.sort } })
});
Note that the rxResource()
function is from the @angular/core/rxjs-interop package
,
where the resource()
function is from @angular/core
.
Another noteworthy detail is that only the first value of the observable is taken into account,
so you can’t have a stream of values.
Some of you may get a feeling of déjà vu with all this, as it’s quite similar to the TanStack Query library. I must insist that this is experimental and will probably evolve in the future. It will also probably be used by higher-level APIs or libraries. Let’s see what the RFC process will bring us!
Automatic migration for signals
Now that signal inputs, view queries and content queries are stable, why not refactor all our components to use them? That can be automated using the following migration:
ng generate @angular/core:signals
? Which migrations do you want to run? (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
❯◉ Convert `@Input` to the signal-based `input`
◉ Convert `@Output` to the new `output` function
◉ Convert `@ViewChild`/`@ViewChildren` and `@ContentChild`/`@ContentChildren` to the signal-based `viewChild`/`viewChildren` and
`contentChild`/`contentChildren`
You can then choose which directory you want to migrate (all the application by default).
The migration, by default, is conservative.
If it can’t migrate something without breaking the build, it will leave it as it is.
But you can be more aggressive by passing the option --best-effort-mode
as we’ll see.
For a complete list of options, run ng generate @angular/core:signals --help
.
After the migration, a report is displayed,
showing how many inputs/outputs/viewChildren/contentChildren have been migrated.
If you picked the less aggressive option, some of them might not have been migrated,
and you can re-run the migration with --insert-todos
to add explanation comments in the code where the migration couldn’t be done.
For example, an @Input
used on a setter yields the following TODO:
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
Another example that you’ll encounter fairly often is when an @Input
is used in a template inside an @if
,
the migration can’t update it due to type-narrowing issues:
// TODO: Skipped for migration because:
// This input is used in a control flow expression (e.g. `@if` or `*ngIf`)
// and migrating would break narrowing currently.
These incompatibility reasons can then be migrated with the more aggressive option --best-effort-mode
,
but you’ll probably have to fix some errors manually.
The migration works astonishingly well, and you can then enjoy the new Signal APIs!
Of course, it does not refactor the code to use computed
instead of ngOnChanges
or other patterns that could be used with signals,
but it’s a good start and will save you a lot of time.
You should also be able to trigger the migration for a specific file or property from your IDE with the new version of the language service!
After running these migrations on a few projects,
my advice would be to first run the outputs
one,
as it is fairly trivial.
Then you can run the queries
one,
which is a bit more complex but still quite safe
(with a few possible incompatible cases).
Finally, you can run the inputs
one,
which is the most complex and may require manual intervention.
ng generate @angular/core:signals --migrations=outputs --defaults
ng generate @angular/core:signals --migrations=queries --defaults
ng generate @angular/core:signals --migrations=inputs --defaults
You can then lint, build, and test your application to see if everything is still working as expected.
You can then re-run the migration with --insert-todos
to see the reasons why some fields have been skipped.
Then you can re-run the migration with --best-effort-mode
to try to migrate them anyway.
provideAppInitializer instead of APP_INITIALIZER
The APP_INITIALIZER
token is now deprecated in favor of provideAppInitializer
.
This new function is a more elegant way to provide an initializer to your application.
Before v19, you would provide an initializer like this:
{
provide: APP_INITIALIZER,
useValue: () => inject(InitService).init(),
multi: true
},
Now you need to use provideAppInitializer
:
provideAppInitializer(() => inject(InitService).init())
ENVIRONMENT_INITIALIZER
and PLATFORM_INITIALIZER
Are also deprecated in favor of provideEnvironmentInitializer
and providePlatformInitializer
.
As usual, an automatic migration will take care of this for you when running ng update
(you may have to refactor a bit if you want to have a nice function with inject
as I used in the example above).
Templates
The @let
syntax, introduced in Angular v18.1, is now stable.
Expressions in the template can now use the typeof
operator,
so you can write things like @if (typeof user === 'object') {
.
The keyvalue
pipe also has a new option.
This pipe has been around for a long time
and allows you to iterate over the entries of an object.
But it, perhaps surprisingly, orders the entries by their key by default,
as we explained in our Angular 6.1 blog post (back in 2018 😅).
You could already pass a comparator function,
but you can now pass null
to disable the ordering:
@for (entry of userModel() | keyvalue: null; track entry.key) {
<div>{{ entry.key }}: {{ entry.value }}</div>
}
Router
It is now possible to pass data to a RouterOutlet
,
making it easy to share data from a parent component to its nested children.
<router-outlet [routerOutletData]="userModel"></router-outlet>
Then in a child component, you can get the data via DI and the token ROUTER_OUTLET_DATA
:
readonly userModel = inject<Signal<UserModel>>(ROUTER_OUTLET_DATA);
Note that, for the first time I believe, the value you get via DI is not a static value, but a signal!
That means that every time userModel
changes in the parent component,
the signal in the child component will be updated as well.
Service worker
A few features have been added to the service worker support in Angular.
First, it is now possible to specify a maxAge
for the entire application,
via a new configuration option called applicationMaxAge
.
This allows us to configure how long the service worker will cache any requests.
Within the applicationMaxAge
, files will be served from the cache.
Beyond that, all requests will only be served from the network.
This can be particularly useful for the index.html
file,
to make sure a user returning several months later
will get the latest version of the application and not an old cached version.
You can define the applicationMaxAge
in the ngsw-config.json
file:
{
"applicationMaxAge": "1d6h" // 1 day and 6 hours
}
Another new feature is the ability to define a refreshAhead
delay for a specific data group.
When the time before the expiration of a cached resource is less than this refreshAhead
delay,
Angular refreshes the resource.
Fun fact: this feature was already implemented,
but not publicly exposed.
{
"dataGroups": [
{
"name": "api-users",
"urls": ["/api/users/**"],
"cacheConfig": {
"maxAge": "1d",
"timeout": "10s",
"refreshAhead": "10m"
}
}
]
}
SSR
There are tons of changes in the Server-Side Rendering (SSR) part of Angular, both in the framework and in the CLI.
Event Replay is stable
The event replay feature, introduced in Angular v18, is now stable.
The CLI will now generate the necessary withEventReplay()
call for you
when you create a new application with SSR.
Application stability
When working with SSR, Angular needs to know when the application is stable, meaning that all the asynchronous operations have been completed, in order to render the application to a string and send it to the client. Zone.js usually allows knowing this but in a zoneless application, you need to do it yourself.
Angular does the bulk of the work for you,
by internally keeping track of the asynchronous operations it triggers
(an HTTP request done via the HttpClient
, for example),
using a service called PendingTasks
.
It has been renamed from ExperimentPendingTasks
and stabilized in v19,
and an automatic migration will take care of this renaming for you during ng update
.
You can also use PendingTasks
in your application to track your own asynchronous operations.
The service has an add
method to manually create a task that you can later clean,
but a run
method has been added for convenience in v19,
allowing you to directly pass an async function:
const userData = await inject(PendingTasks).run(() => fetch('/api/users'));
//☝️ Angular will wait for the promise to resolve
this.users.set(usersData);
A new (experimental) RxJS operator called pendingUntilEvent
has also been added to the framework (in the @angular/core/rxjs-interop
package):
it allows marking an observable as important for the application stability until a first event is emitted:
this.users = toSignal(users$.pipe(pendingUntilEvent()));
Partial and incremental hydration
Building upon the event replay feature,
and the @defer
feature (check our blog post if you need a refresher),
the Angular team has introduced a new feature called
“incremental hydration”.
With incremental hydration, deferred content is rendered on the server side (instead of rendering the defer placeholder), but skipped over during client-side hydration (it’s left dehydrated, hence the “partial hydration” concept).
It means that the application is fully rendered, but some parts are not yet interactive when the application bootstraps. When a user interacts with a dehydrated component, Angular will download the necessary code and hydrate the component (and its perhaps dehydrated parent components) on the fly, then replay the events that happened while the component was dehydrated, leaving the impression that the component was already active 🤯.
This feature is in developer preview in v19,
and can be activated with withIncrementalHydration()
:
bootstrapApplication(AppComponent, {
providers: [provideClientHydration(withIncrementalHydration())]
});
The syntax to enable it is quite simple and uses the @defer
block
with an additional hydrate
option to define the hydration condition.
The possible hydration triggers are the same as the @defer
conditions
(that we explained in detail in the blog post mentioned above).
@defer(on timer(15s); hydrate on interaction) {
<app-users />
} @placeholder {
<span>Loading</span>
}
Until v19, the loading placeholder would be rendered in SSR.
With the withIncrementalHydration()
option,
the UsersComponent
will be rendered, but not hydrated on the client.
For example, the DOM will look like:
<app-users>
<!-- Dehydrated content -->
<h1>User</h1>
<button jsaction="click:;">Refresh users</button>
<!-- more content -->
</app-users>
When the user clicks on the button,
Angular will download the necessary code for the UsersComponent
,
then hydrate it and replay the events that happened
while the component was dehydrated (here, refreshing the list of users).
This is a powerful feature for those who are looking to improve the performance of their applications, and it highlights the flexibility of the control flow syntax in Angular.
Route configuration for hybrid rendering
Until now, an SSR application with pre-render was pre-rendering the pages with no parameters but ignored parameterized pages. In v19, after an RFC, the CLI team introduced a new feature called “hybrid rendering”.
All this work is part of the ongoing effort to improve the SSR experience in Angular, which includes new APIs (App Engine APIs).
It is now possible to define the rendering mode per route of the application. Instead of adding options to the existing route configuration, the team decided to add a configuration file, dedicated to the server-side, which defines the rendering mode for each route.
Three rendering modes are available:
RenderMode.Prerender
for pre-rendering the page at build time;RenderMode.Server
for rendering the page on the server;RenderMode.Client
for rendering the page on the client.
For example, let’s say you have the following routes in your application:
const routes: Routes = [
// Home component
{ path: '', component: HomeComponent },
// About component
{ path: 'about', component: AboutComponent },
// User component with a parameter
{ path: 'users/:id', component: UserComponent }
];
In v18, ng build
generated a browser
folder with index.html
and about/index.html
files.
In v19, the same configuration throws with:
✘ [ERROR] The 'users/:id' route uses prerendering and includes parameters, but 'getPrerenderParams' is missing. Please define 'getPrerenderParams' function for this route in your server routing configuration or specify a different 'renderMode'.
This means there are 2 solutions.
First, we can add a server routing configuration file, app.routes.server.ts
.
This file is now generated in a project when using ng new --ssr --server-routing
or ng add @angular/ssr --server-routing
.
The --server-routing
option enables both the server routing configuration and the new App Engine APIs
(all these APIs are in developer preview for now).
It also uses a new option in angular.json
called outputMode
to define the output mode of the application:
server
generates a server bundle, enabling server-side rendering (SSR) and hybrid rendering strategies. This mode is intended for deployment on a Node.js server or a serverless environment.static
generates a static output suitable for deployment on static hosting services or CDNs. This mode supports both client-side rendering (CSR) and static site generation (SSG).
ng add @angular/ssr --server-routing
sets the outputMode
to server
by default.
Note that the prerender
and appShell
options are no longer used if you define the outputMode
.
Let’s define the server routes configuration for the previous example:
export const serverRoutes: Array<ServerRoute> = [
{
path: '',
renderMode: RenderMode.Prerender,
},
{
path: 'about',
renderMode: RenderMode.Server,
},
{
path: '**',
renderMode: RenderMode.Client,
}
];
This file is then loaded in the app.config.server.ts
file,
using provideServerRoutesConfig(serverRoutes)
.
The server routes configuration doesn’t need to define all the routes as you can see, with a possible “catch-all” route ‘**’ to define a default behavior. If a route doesn’t exist though, you’ll get an error at build time:
✘ [ERROR] The 'unknown' server route does not match any routes defined in the Angular routing configuration (typically provided as a part of the 'provideRouter' call). Please make sure that the mentioned server route is present in the Angular routing configuration.
The second solution is to define a getPrerenderParams
function, to prerender routes with parameters.
{
path: 'users/:id',
renderMode: RenderMode.Prerender,
async getPrerenderParams(): Promise<Array<Record<string, string>>> {
// API call to get the user IDs
const userIds = await inject(UserService).getUserIds();
// build an array like [{ id: '1' }, { id: '2' }, { id: '3' }]
return userIds.map(id => ({ id }));
}
},
This will prerender the users/:id
route for each user ID found by the UserService
,
generating users/1/index.html
, users/2/index.html
, etc.
A really nice change is that server.ts
is now used during prerendering,
allowing access to locally defined API routes.
server.ts
is now also used by the Vite server during development!
If needed you can disable it with the --no-server
option, for example,
to make a static build: ng build --output-mode static --no-server
.
As you may not want to prerender all the user pages, you can also define a fallback
option,
that can be PrerenderFallback.Client
(falls back to CSR),
PrerenderFallback.Server
(falls back to SSR),
or PrerenderFallback.None
(the server will not handle the request).
When using the RenderMode.Server
mode,
you can also define a status
and headers
options to customize the response:
{
path: '404',
renderMode: RenderMode.Server,
status: 404,
headers: {
'Cache-Control': 'no-cache'
}
}
You can also define which route should serve as the app shell of the application
(see the App shell pattern in the Angular documentation),
by setting the appShellRoute
option: provideServerRoutesConfig(serverRoutes, { appShellRoute: 'shell' })
.
The “App Engine APIs” mentioned earlier are a set of APIs
that allow you to interact with the server-side rendering process,
based on the new AngularAppEngine
and its node version AngularNodeAppEngine
.
They are used in the generated server.ts
file:
createNodeRequestHandler
allows you to create a request handler for the server-side rendering process You can pass it the handler you want to use like an Express app, which is still the default used when usingng add @angular/ssr
, or another like Fastify, Hono, etc. This should make it simpler to use a different server framework than Express.writeResponseToNodeResponse
allows you to write the response from your server of choice to the node response object.
All these functions aim to make the interactions easier between Node.js
and the framework you picked to handle the requests to your Angular application.
This should provide greater flexibility compared to the previous APIs,
and make it easier to deploy Node.js applications, whatever the server framework you want to use.
And you can build your own variants of AngularAppEngine
to fit your needs for other platforms.
The CLI team also wants to make it easier to target other runtimes than Node.js.
That’s why a new option ssr.experimentalPlatform
has been added, which lets you define the platform you want to target:
node
(default) generates a bundle optimized for Node.js;neutral
generates a platform-neutral bundle suitable for environments like edge workers, and other serverless platforms that do not rely on Node.js APIs. As the option name indicates, this is an experimental feature.
Request and response via DI
It is now easy to access the request and response objects in your components during SSR,
thanks to new DI tokens in @angular/core
:
REQUEST
to access the current HTTP request object;REQUEST_CONTEXT
to pass custom metadata or context related to the current request in server-side rendering;RESPONSE_INIT
to access the response initialization options;
Angular CLI
The CLI has also been released in version v19, with some notable features.
If you want to upgrade to 19.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 (18.1.0 for example), and the target version (19.0.0 for example), and it gives you a diff of all files created by the CLI: angular-cli-diff/compare/18.1.0…19.0.0.
It can be a great help along with the official ng update @angular/core @angular/cli
command.
Let’s see what’s new in the CLI!
Better HMR by default
A ton of work has been done to improve the Hot Module Replacement (HMR) experience in Angular.
When using the application
builder for ng serve
,
HMR is now enabled by default for styles!
This means that when you change a style file (or inline style),
the browser will update the styles without reloading the page
and without rebuilding the application.
This is sooo nice to see, as you can now change the styles and see the results in real-time without losing the state of your application. For example, when working in a modal, you won’t have to re-open it after each style change! Definitely a game-changer for day-to-day work.
The work done in the framework goes even further than that,
and we should be able to have HMR that properly works for templates soon.
It is in fact already possible to try it using NG_HMR_TEMPLATES=1 ng serve
(this is experimental as you can guess).
When using this option, the templates will be reloaded, refreshing all component instances, without reloading the page, and the state of the application will be preserved!
Karma can run with esbuild!
Even if Karma is slowly dying, it is still the default testing solution in newly generated Angular CLI projects. The Karma integration in Angular was still relying on Webpack until now, which was a bit sad as all other builders were now using esbuild under the hood.
This is no longer the case as you can use esbuild with Karma as well!
To do so, a new option can be used in the Karma builder options in angular.json
:
builderMode
.
This option can have 3 different values:
browser
which is the same as the current behavior, using Webpack under the hoodapplication
which uses esbuilddetect
which uses the same builder asng serve
When using application
, you can also remove the @angular-devkit/build-angular/plugins/karma
webpack plugin from your karma.conf.js
(if you have one).
Shifting to builderMode: application
is quite a bit faster.
On a project with thousands of tests, the full test suite was ~40% faster,
cutting nearly a minute from the total time.
In watch mode, the difference is also quite noticeable,
shaving a few seconds on each re-run.
Zoneless experiment
A new option --experimental-zoneless
has been added to the ng new
command,
generating a new project without Zone.js.
Unit tests are also generated with the proper providers to make them work without Zone.js.
You can check out our blog post on Angular 18.0 for more information on this experiment.
ng generate component
A new --export-default
option has been added to the ng generate component
command.
It changes the component to use the default export syntax: export default class AdminComponent
.
This can be interesting for lazy-loaded components, as the loadComponent
syntax then allows to write loadComponent: () => import('./admin/admin.component')
instead of the usual loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)
.
Sass deprecation warnings
It’s now possible to silence the deprecation warnings coming from the Sass compiler:
"stylePreprocessorOptions": {
"sass": {
"silenceDeprecations": ["import"]
}
},
It’s also possible to throw an error if a deprecation warning is emitted with fatalDeprecations
and to prepare for future deprecations with futureDeprecations
.
This feature is going to be really useful to all Sass users,
as some APIs are getting deprecated and will be removed in Sass v3,
so you may see a bunch of deprecation warnings appear.
Strict CSP
A new option has been added to the ng build
command to enable a
strict Content Security Policy (CSP) in the generated index.html
file.
This option applies the recommendations from this Web.dev article
and enables automatic generation of a hash-based CSP based on scripts in the index.html
file.
To enable this option, set the security.autoCsp
configuration to true
in your angular.json
file.
Summary
Wow, that was a lot of new features in Angular v19!
v20 will probably continue to stabilize the signals APIs introduced these past months. We can also hope for more news about how the router and forms will integrate with signals. 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.5?
Vue 3.5.0 is here!
The last minor release was v3.4.0 in December. Since then, we have seen quite a few patch releases, and some interesting new features.
Let’s see what we have in this release!
Props destructuration
Props destructuration was introduced as an experiment in Vue 3.3 (as part of the reactive transform experiment) and is now stable in Vue 3.5.
So instead of:
const props = withDefaults(defineProps<{ name?: string }>(), { name: "Hello" });
watchEffect(() => console.log(props.name));
You can now write:
const { name = "Hello" } = defineProps<{ name?: string }>();
watchEffect(() => console.log(name));
// ☝️ This gets compiled to the same code as the previous example
// so we don't lose the reactivity of the props
You no longer need the propsDestructure: true
flag in the compiler options to use this feature,
so you can remove it if you have it.
You can however disable this feature by setting propsDestructure: false
in the compiler options,
or even throw an error if you want to enforce the use of the previous syntax by setting propsDestructure: 'error'
.
Read more about this feature in the RFC.
useTemplateRef
As you probably know, Vue lets you grab a reference to an element in a template, using the ref="key"
syntax. The framework then populates a Ref named key
in the setup of the component.
For example, to initialize a chart, you usually write code looking that:
<canvas ref="chart"></canvas>
// 👇 special ref that Vue populates with the element in the template
const chart = ref<HTMLCanvasElement | null>(null);
onMounted(() => new Chart(chart.value!, /* chart options */));
This API felt a bit awkward, as nothing was pointing out that this ref was “special” at first glance. It also forced developers to pass this ref around to composables.
For example, if you wanted to build a useChart
composable, you had to write it like this:
export function useChart(chartRef: Ref<HTMLCanvasElement | null>) {
onMounted(() => new Chart(chartRef.value!, /* chart options */));
}
And then call it in your component by passing it the ref:
const chart = ref<HTMLCanvasElement | null>(null);
useChart(chart);
Vue v3.5 introduces a new composable called useTemplateRef
to grab a reference in the template:
// useTemplateRef expects the key of the element in the template
const chartRef = useTemplateRef<HTMLCanvasElement>('chart');
onMounted(() => new Chart(chartRef.value!, /* chart options */));
The type of chartRef
is a read-only ShallowRef<HTMLCanvasElement | null>
.
In addition to a more explicit name and usage, the new function is usable directly inside a composable. This simplifies the pattern we saw above, as useChart
can simply be written:
export function useChart(chartKey: string) {
const chartRef = useTemplateRef<HTMLCanvasElement>(key);
onMounted(() => new Chart(chartRef.value!, /* chart options */));
}
and then used in a component:
useChart('chart');
This is a nice improvement!
useId
A new composition function called useId
has been added to generate a unique ID.
This feature is probably already familiar to React developers or Nuxt developers who use the useId
composable.
This can be useful when you need to generate an ID for an HTML element, for example when you use a label with an input, or for accessibility attributes:
<label :for="id">Name</label>
<input :id="id" />
As the component can be rendered many times, you need to ensure that the ID is unique.
This is where useId
comes in:
const id = useId();
useId
guarantees that the generated ID is unique within the application.
By default, Vue generates an ID with a prefix of v-
followed by a unique number
(that increments when new components are rendered).
The prefix can be customized by using app.config.idPrefix
.
useId
also guarantees that the ID is stable between server-side rendering and client-side rendering, to avoid mismatching errors.
Lazy hydration strategies
Asynchronous components, defined with defineAsyncComponent
,
can now control when they should be hydrated using a new hydrate
option.
Vue provides four strategies for hydration in v3.5:
hydrateOnIdle()
: the component will be hydrated when the browser is idle (you can specify a timeout if needed, asrequestIdleCallback
, which is used internally, allows).hydrateOnVisible()
: the component will be hydrated when it becomes visible in the viewport (implemented using anIntersectionObserver
). Additional options supported by theIntersectionObserver
API can be passed to the strategy, likerootMargin
to define the margin around the viewport. So you can usehydrateOnVisible({ rootMargin: '100px' })
to hydrate the component when it is 100px away from the viewport.hydrateOnInteraction(event)
: the component will be hydrated when the user interacts with the component with a defined event, for examplehydrateOnInteraction('click')
. You can also specify an array of events.hydrateOnMediaQuery(query)
: the component will be hydrated when the media query matches. For example,hydrateOnMediaQuery('(min-width: 600px)')
will hydrate the component when the viewport is at least 600px wide.
You can also define a define custom strategy if you want to.
Here is an example of how to use these strategies:
import { defineAsyncComponent, hydrateOnVisible } from 'vue'
const User = defineAsyncComponent({
loader: () => import('./UserComponent.vue'),
hydrate: hydrateOnVisible('100px')
});
data-allow-mismatch
Vue 3.5 now supports a new attribute called data-allow-mismatch
,
that can be added to any element to allow client/server mismatch warnings
to be silenced for that element.
For example, if you have a component that renders the current date like this:
<div>{{ currentDate }}</div>
you might get a warning if the server and the client render the date at different times:
[Vue warn]: Hydration text content mismatch on <div>
- rendered on server: Jul 26, 2024
- expected on client: Jul 27, 2024
You can silence this warning by adding the data-allow-mismatch
attribute:
<div data-allow-mismatch="text">{{ currentDate }}</div>
The value of the attribute can be:
text
to silence the warning for text contentchildren
to silence the warning for children contentclass
to silence the warning for class mismatchstyle
to silence the warning for style mismatchattribute
to silence the warning for attribute mismatch
Better types
A few improvements have been made to help the tooling understand the Vue API better.
For example, components that use expose
will now have a more correct type.
An effort has also been made for directives. It is now possible to specify the allowed modifiers for a directive in the type definition:
// can be used as v-focus.seconds in the template
export const vFocus: Directive<
HTMLInputElement,
boolean,
'seconds' /* 👈 New! only 'seconds' is allowed as modifier */
> = (el, binding) => {
const secondsModifier = binding.modifiers.seconds; // autocompletion works here
...
}
The built-in directives have also been improved to leverage this new feature.
Another improvement concerns the computed
function:
you can now define a getter and a setter with different types (it was already working but TS was complaining):
const user = ref<UserModel>({ name: 'Cédric' });
const json = computed({
get: () => JSON.stringify(user.value),
// 👇 the setter receives a UserModel instead of a string
set: (newUser: UserModel) => user.value = newUser;
}); // typed as ComputedRef<string, UserModel>
console.log(json.value); // 👈 a string
json.value = { name: 'JB' }; // 👈 no error
app.onUnmount()
It is now possible to register a callback that will be called when the app is unmounted
(i.e when the app.unmount()
method is called)
This can be useful to clean up resources or to log something if you unmount your application,
but it is even more useful for plugin developers.
Here is an example:
const myPlugin: Plugin = {
install (app: App) {
function cleanupSomeSideEffect() { /* ...*/ }
// Register the cleanup function to be called when the app is unmounted
app.onUnmount(cleanupSomeSideEffect)
}
Watcher novelties
deep watch
The watch
function had a deep
option since the beginning.
It allows watching deeply nested properties of a ref:
const obj = ref({ super: { nested: { prop: 1 } } });
watch(obj, () => {
// called when the ref or one of its nested properties changes
console.log('nested prop changed');
}, { deep: true });
You don’t need it for a reactive
object though,
as watch
will automatically watch deeply nested properties of a reactive object.
In that case, deep
can be set to false
if you want to disable this behavior.
The novelty introduced in Vue 3.5 is that you can now use deep
with a specific depth:
const obj = ref({ super: { nested: { prop: 1 } } });
watch(obj, () => {
// called when the ref or the first level of its nested properties changes
console.log('nested prop changed');
}, { deep: 1 }); // 👈 deep can now be a number
pause/resume
The watch
and watchEffect
can now be paused and resumed, in addition to being stopped.
Until now, you could only stop a watcher, which would prevent it from being called again:
const stop = watch(obj, () => {
console.log('obj changed');
});
stop(); // 👈 stop the watcher
Now, you can pause and resume a watcher:
const { pause, resume, stop } = watch(obj, () => {
console.log('obj changed');
});
pause(); // 👈 pause the watcher
resume(); // 👈 resume the watcher
stop(); // 👈 stop the watcher
onWatcherCleanup
A new API called onWatcherCleanup
has been added to register a callback that will be called when a watch
/watchEffect
is cleaned up.
This is similar to what the onCleanup
parameter of watchers does,
but it allows to use the cleanup function in functions called inside a watcher.
Before
// starts an interval, called in the watchEffect below
function startInterval(intervalTime, onCleanup) {
const id = window.setInterval(() => console.log('hello'), intervalTime)
// we use onCleanup here to clear the interval
onCleanup(() => window.clearInterval(id));
}
const intervalTime = ref(1000);
watchEffect((onCleanup) => {
console.log('Interval time changed', intervalTime.value);
// we need to pass onCleanup to startInterval
startInterval(intervalTime.value, onCleanup);
});
Now
function startInterval(intervalTime) {
const id = window.setInterval(() => console.log('hello'), intervalTime)
//👇 we can now use onWatcherCleanup
onWatcherCleanup(() => window.clearInterval(id));
}
const intervalTime = ref(1000);
watchEffect(() => {
console.log('Interval time changed', intervalTime.value);
//👇 no need to pass onCleanup anymore
startInterval(intervalTime.value);
});
onWatcherCleanup
throws a warning if there is no current active effect.
This warning can be silenced by passing a second parameter to onWatcherCleanup
:
onWatcherCleanup(() => {
// cleanup code
}, true /* 👈 no warning */);
A similar API has been introduced for the low level effect
function, called onEffectCleanup
.
Trusted types
Vue 3.5 now supports Trusted Types.
It should work out of the box by default.
This is done by automatically converting the strings generated by the compiler into TrustedHTML
when they are used in a context where a Trusted Type is expected.
v-html
is not supported out-of-the-box, but can also be used if you declare a custom policy.
throwUnhandledErrorInProduction
A new option has been added to the app configuration to throw unhandled errors in production. This can be useful to catch errors that are currently not caught because the default behavior is to log them in the console.
const app = createApp(App);
app.config.throwUnhandledErrorInProduction = true;
With this option enabled, you’ll easily catch errors when rendering your application in production.
Note that the default is false
to avoid breaking existing applications.
Teleport
deferred Teleport
It is now possible to add a defer
attribute to Teleport
to mark the component as deferred.
When doing so, the target of the teleportation doesn’t have to already exist:
even if it appears later, the target can still be resolved.
A deferred Teleport waits until all other DOM content in the same update cycle has been rendered before locating the target container.
So we can now use Teleport
with targets located in other components (as long as they are mounted in the same tick), or even use Teleport
and a target inside a Suspense
(whereas you previously had to target a container outside of the Suspense
component).
<Suspense>
<Teleport defer to="#target">...</Teleport>
<div id="target"></div>
</Suspense>
Teleport and Transition
It is now possible to use a Teleport
component directly inside a Transition
component,
thus allowing to animate the appearance and the disappearance of an element in a different place in the DOM. This used to throw an error in Vue 3.4.
You can check out this playground from the PR author edison1105, showcasing this new feature.
Custom elements
Vue v3.5 adds a bunch of features for custom elements. As I don’t personally use them, I’ll just list the most notable here:
defineCustomElement
now supports disabling ShadowDom by setting theshadowRoot
option tofalse
;- a
useHost()
composable has been added to get the host element and auseShadowRoot()
composable has been added to get the shadow root of the custom element (which can be useful for CSS in JS); emit
now supports specifying event options, likeemit('event', { bubbles: true })
;expose
is now available in custom elements;- custom elements can now define a
configureApp
method to configure the associated app instance, for example to use plugins like the router;
Developer experience
The Vue compiler will now emit a warning if you write invalid HTML nesting in your template (for example, a div
inside a p
):
warning: <div> cannot be child of <p>, according to HTML specifications. This can cause hydration errors or potentially disrupt future functionality.
We also have a new warning when a computed
is self-triggering (i.e. it writes to one of its dependencies in its getter):
Computed is still dirty after getter evaluation
likely because a computed is mutating its own dependency in its getter.
State mutations in computed getters should be avoided.
Check the docs for more details: https://vuejs.org/guide/essentials/computed.html#getters-should-be-side-effect-free
This warning is not enabled by default, you need to set app.config.warnRecursiveComputed
to true
in your application configuration.
Performances
A whole lot of optimizations have been made around reactivity. The first notable one is that the reactivity system now uses a new algorithm to track dependencies. It now relies on version counting and doubly linked lists. If I’m not mistaken, this is really similar to what Preact did and explained in this really interesting article. This brings a few performance improvements (most notably around memory usage) and should make the reactivity system more predictable (computed values should now never be stale).
The other notable change is that array manipulation should also be faster, especially for large arrays.
News from the ecosystem
Vapor mode
Vapor (@vue/vapor
) is making progress within its repository. You can play with it using the online REPL here.
Vue router
Vue Router 4.4.0 is out and offers the possibility to have typed routes!
This can be done manually by adding the types yourself to your project
(for example in env.d.ts
):
interface RouteNamedMap {
// a route with no params, named 'users' and matching '/users'
users: RouteRecordInfo<"users", "/users", Record<never, never>>;
// a route with a param named 'id', named 'user' and matching '/user/:id'
user: RouteRecordInfo<
"user",
"/users/:id",
{ id: string | number }, // raw parameters (allows to use the route with a number or a string)
{ id: string } // parameters we get with useRoute
>;
}
declare module "vue-router" {
interface TypesConfig {
RouteNamedMap: RouteNamedMap;
}
}
Then when you use a RouterLink
or router.push
, you get a nice auto-completion
and type-checking:
<!-- 👇 you get an error if the route name has a typo -->
<!-- or if you define a parameter that does not exist or forget to define it -->
<RouterLink :to="{ name: 'users', params: { id: user.id } }">User</RouterLink>
Then when using useRoute
, you get a typed route
object:
// note the name of the route as a parameter
// which is necessary for TypeScript to know the type of the route
const route = useRoute('user');
console.log(route.params.id); // 👈 id is properly typed as a string!
console.log(route.params.other); // 👈 this throws an error
This is a nice addition even if manually defining the types is a bit cumbersome.
Note that if you are into file-based routing, you can use unplugin-vue-router which will automatically generate the types for you! The plugin is still experimental though.
create-vue
A new option has been added to create-vue
to allow you to use the new Devtools plugin,
a Vite plugin that allows you to use the Vue Devtools directly in the browser with an overlay.
npm create vue@latest my-app --devtools
Nuxt
Nuxt v3.12 is out and paves the way for Nuxt 4. You can try the changes using:
export default defineNuxtConfig({
future: {
compatibilityVersion: 4
}
})
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 18.2?
Angular 18.2.0 is here!
This is a minor release with some nice features: let’s dive in!
Automatic flush in fakeAsync
In Angular v18.2 (and zone.js v0.15), the flush()
function is now automatically called at the end of a fakeAsync()
test.
Before this version, you had to call flush()
yourself at the end of your test to flush all pending asynchronous tasks or discardPeriodicTasks()
for periodic tasks. If you didn’t do it, you would get the error 1 periodic timer(s) still in the queue
.
The way to fix it was to call flush()
or discardPeriodicTasks()
at the end of your test:
it('should do something', fakeAsync(() => {
// ...
flush();
}));
This is no longer necessary, as Angular will do it for you
(if you manually set the flush
option to true with Angular v18.2/zone.js v0.14,
and will be done by default by Angular in v19/zone.js v0.15).
it('should do something', fakeAsync(() => {
// ...
// no flush() or discardPeriodicTasks() required!
}, { flush: true }));
When zone.js v0.15 is released, you won’t need to specify the flush
option anymore, as it will be done automatically for you:
it('should do something', fakeAsync(() => {
// ...
flush();
}));
whenStable helper
A new helper method has been added to ApplicationRef
to wait for the application to become stable. whenStable
is really similar to the existing isStable
method, but it returns a promise that resolves when the application is stable instead of an observable.
defaultQueryParamsHandling in router
It is now possible to specify the default query params handling strategy for all routes in the provideRouter()
configuration.
provideRouter(routes, withRouterConfig({ defaultQueryParamsHandling: 'merge' }));
By default, Angular uses the replace
strategy, but you can also use preserve
or merge
.
Previously, you could only specify this strategy on a per-navigation basis (via RouterLink
or router.navigate
options).
Migrations
An optional migration has been added to migrate dependency injection done via the constructor to the inject
function.
ng g @angular/core:inject
This will update your code from:
export class UserComponent {
constructor(private userService: UserService) {}
}
to:
export class UserComponent {
private userService = inject(UserService);
}
Note that you might have some compilation errors after this migration,
most notably if you were using new UserComponent(userService)
in your tests.
There are a few options for this migration, and one can mitigate this issue:
path
, the directory to run the migration. By default:.
;migrateAbstractClasses
, whether to migrate the abstract classes or not (which may break your code and necessitate to manually fixing it). By default:false
;backwardsCompatibleConstructors
: by default, constructors that are empty after the migration are deleted. This can lead to compilation errors like the one I’m mentioning above. To prevent that, you can generate backward-compatible constructors with this option (which looks like:constructor(...args: unknown[]);
). By default:false
;nonNullableOptional
: whether to cast the optional inject sites to be non-nullable or not. By default:false
.
The migration is optional and the Angular team explicitly said that the constructor injection will still be supported in the future.
However, it does indicate that the future of Angular might be to use the inject
function.
Another optional migration has been added to convert standalone components used in routes to be lazy-loaded if that’s not the case:
ng g @angular/core:route-lazy-loading
This will update your code from:
{
path: 'users',
component: UsersComponent
}
to:
{
path: 'users',
loadComponent: () => import('./users/users.component').then(m => m.UsersComponent)
}
The only option for this migration is the path
.
Diagnostics
A new diagnostic has been added to catch uncalled functions in event bindings:
<button (click)="login">Log in</button>
throws: NG8111: Function in event binding should be invoked: login()
.
Another one was added to catch unused @let
declaration (a new feature introduced in Angular 18.1): NG8112: @let user is declared but its value is never read.
Angular CLI
The application builder now supports attribute-based loader configuration
For example, an SVG file can be imported as text via:
// @ts-expect-error TypeScript cannot provide types based on attributes yet
import contents from './some-file.svg' with { loader: 'text' };
This overrides all other configurations (for example a loader
defined in angular.json
).
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 18.1?
Angular 18.1.0 is here!
This is a minor release with some nice features: let’s dive in!
TypeScript 5.5 support
Angular v18.1 now supports TypeScript 5.5. This means that you can use the latest version of TypeScript in your Angular applications. You can check out the TypeScript 5.5 release notes to learn more about the new features: it’s packed with new things! For example, it’s no longer necessary to manually define type guards when filtering arrays, with the new inferred type predicate feature.
@let syntax
The main feature of this release is undoubtedly the new @let
syntax in Angular templates.
This new syntax (in developer preview) allows you to define a template variable in the template itself,
without having to declare it in the component class.
The syntax is @let name = expression;
, where name
is the name of the variable (and can be any valid JavaScript variable name) and expression
is the value of the variable.
Let’s say our component has a count field defined in its class, then we can define a variable in the template like this:
@let countPlustwo = count + 2;
<p>{{ countPlustwo }}</p>
If the count
value changes, then the countPlustwo
value will be updated automatically.
This also works with the async
pipe (and the other ones, of course):
@let user = user$ | async;
@if (user) {
<p>{{ user.name }}</p>
}
Note that you can’t declare several variables (with @let
or #
) with the same name in the same template:
NG8017: Cannot declare @let called 'value' as there is another symbol in the template with the same name.
You also can’t use a variable before its declaration:
NG8016: Cannot read @let declaration 'user' before it has been defined.
which prevents to use it like @let user = user()
in case you thought about unwrapping a signal value into a variable of the same name.
This new feature can be handy when you want to use a value in several places in your template, especially if it is a complex expression. Sometimes you can create a dedicated field in the component class, but sometimes you can’t, for example in a for loop:
@for(user of users; track user.id) {
<div class="name">{{ user.lastName }} {{ user.firstName }}</div>
<div class="address">
<span>{{ user.shippingAddress.default.number }} </span>
<span>{{ user.shippingAddress.default.street }} </span>
<span>{{ user.shippingAddress.default.zipcode }} </span>
<span>{{ user.shippingAddress.default.city }}</span>
</div>
}
This can be written more cleanly using @let
🚀:
@for(user of users; track user.id) {
<div class="name">{{ user.lastName }} {{ user.firstName }}</div>
<div class="address">
@let address = user.shippingAddress.default;
<span>{{ address.number }} </span>
<span>{{ address.street }} </span>
<span>{{ address.zipcode }} </span>
<span>{{ address.city }}</span>
</div>
}
afterRender/afterNextRender APIs
Angular v16.2 introduced two new APIs to run code after the rendering of a component: afterRender
and afterNextRender
.
You can read more about them in our Angular 16.2 blog post.
In Angular v18.1, these APIs have been changed (they are still marked as experimental) to no longer need a phase
parameter, which was introduced in Angular v17, as we explained
in our Angular 17 blog post.
The phase
parameter is now deprecated: we now pass to these functions an object instead of a callback, to specify the phases you want to use.
You can still use the callback form with no phase,
which is equivalent to using the MixedReadWrite
mode.
There are 4 phases available in the new object form:
earlyRead
, mixedReadWrite
, read
, and write
.
If you only need to read something, you can use the read
phase.
If you need to write something that affects the layout, you need to use write
.
For example, to initialize a chart in your template:
export class ChartComponent {
@ViewChild('canvas') canvas!: ElementRef<HTMLCanvasElement>;
constructor() {
afterNextRender({
write: () => {
const ctx = this.canvas.nativeElement;
new Chart(ctx, { type: 'line', data: { ... } });
}
});
}
}
If your write
callback depends on something you need to read first,
you must use earlyRead
to get the information you need before the write
phase,
and Angular will call the write
callback with the value returned by the earlyRead
callback:
afterRender({
earlyRead: () => nativeEl.getBoundingClientRect(),
//👇 The `rect` parameter is the value returned by the `earlyRead` callback
write: (rect) => {
otherNativeEl.style.width = rect.width + "px";
},
});
This exists to avoid intermixing reads and writes if possible,
and thus make things faster by avoiding “layout trashing”.
We previously had to call afterRender
twice, once for the read and once for the write phase,
but now we can do it in a single call, which is more efficient and cleaner.
The phase
option still exists but is now deprecated.
Even if these APIs are an experimental feature, the Angular team provided a migration schematics to update your code to the new syntax 😍.
toSignal equality function
As you may know, if you read our blog post about signals,
a Signal
can define its own equality function (it uses Object.is
by default).
In Angular v18.1, the toSignal
function now also accepts an equal
parameter to specify the equality function to use in the signal it creates.
RouterLink with UrlTree
The RouterLink
directive now accepts an UrlTree
as input. This allows you to pass a pre-constructed UrlTree
to the RouterLink
directive, which can be useful in some cases.
Until now, we could pass either a string or an array to the RouterLink
directive. For example, we could write:
<a [routerLink]="['/users', user.id]">User {{ user.name }}</a>
Now we can also use an UrlTree
:
export class HomeComponent {
userPath = inject(Router).createUrlTree(["/users", user.id]);
}
and in the template:
<a [routerLink]="userPath">User {{ user.name }}</a>
Note that when doing so, you can’t define the queryParams
, fragment
, queryParamsHandling
and relativeTo
inputs of the RouterLink
directive in the template.
You have to define them in the UrlTree
itself, or you’ll see the following error:
Error: NG04016: Cannot configure queryParams or fragment when using a UrlTree as the routerLink input value.
Router browserUrl
The NavigationBehaviorOptions
object,
used for the options of the navigate
/navigateByUrl
of the Router
service
or the RedirectCommand
object introduced in Angular v18
now accepts a browserUrl
option.
This option allows you to specify the URL that will be displayed in the browser’s address bar when the navigation is done, even if the matched URL is different.
This does not affect the internal router state (the url
of the Router
will still be the matched one) but only the browser’s address bar.
const canActivate: CanActivateFn = () => {
const userService = inject(UserService);
const router = inject(Router);
if (!userService.isLoggedIn()) {
const targetOfCurrentNavigation = router.getCurrentNavigation()?.finalUrl;
const redirect = router.parseUrl("/401");
// Redirect to /401 internally but display the original URL in the browser's address bar
return new RedirectCommand(redirect, {
browserUrl: targetOfCurrentNavigation,
});
}
return true;
};
Extended diagnostic for uncalled functions
A new extended diagnostic has been added to warn about uncalled functions in event bindings:
So for example:
<button (click)="addUser">Add user</button>
yields the following error (if you have strictTemplates
enabled):
NG8111: Functions must be invoked in event bindings: addUser()
Angular CLI
faster builds with isolatedModules
TypeScript has an option called isolatedModules
that warns you if you write code that can’t be understood by other tools
by just looking at a single file.
If you enable this option on your project, you should hopefully have no warnings, and you can get a nice boost in build performances, as CLI v18.1 will delegate the transpilation of your TS files into JS files to esbuild instead of the TypeScript compiler 🚀
On a rather large application, ng build
went from 49s to 32s, just by adding isolatedModules: true
in my tsconfig.json
file 🤯.
isolatedModules
will be enabled by default in the new projects generated by the CLI.
WASM support
The CLI now supports the usage of Web Assembly… in zoneless applications! The application can’t use ZoneJS as the CLI needs to use native async/await to load WASM code and ZoneJS prevents that.
Anyway this can be interesting for some people, and you can write code like:
import { hash } from "./hash.wasm";
console.log(hash(myFile));
inspect option
We can note the addition of a --inspect
option for ng serve
/ng dev
, only possible for SSR/SSG applications.
This flag starts a debug process, by default on port 9229,
allowing to attach a debugger to go through the code executed on the server.
You can use Chrome Inspect, VS Code, or your favorite IDE to attach to the process
(see the NodeJS docs).
You can then add breakpoints into your code,
and the debugger will stop when this code is executed on the server when you load the corresponding page in your browser.
You can specify a different host or port if needed with ng serve --inspect localhost:9999
for example.
chunk optimizer
If you want to reduce the number of chunk files,
an experimental optimization has been added to the build step.
You may have noticed that since we shifted from webpack to esbuild as the underlying build tool,
the number of generated chunks has increased quite a bit.
This is because there is no real optimization done on chunks.
So we sometimes end up with really small chunks when running ng build
.
Some build tools have options to specify a minimal size, but esbuild doesn’t.
That’s why the CLI team is experimenting with an additional rollup pass
to optimize the generated chunks and try to merge some of them
or add some directly into the main bundle when it makes sense.
On one of our largest applications, it reduces by half the number of chunks, and the initial page now only needs to load 3 JS files instead of… 118 😲. It does add a bit of build time though.
To try this on your own application, you can run:
NG_BUILD_OPTIMIZE_CHUNKS=1 ng build
This is still experimental for now but will probably be enabled automatically in a future version, based on the initial file entry count and size.
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 18.0?
Angular 18.0.0 is here!
This is a major release with some nice features: let’s dive in!
Control flow syntax is now stable!
The control flow syntax introduced in Angular 17 is no longer a developer preview feature and can safely be used. As it is now the recommended way to write templates, you should consider using it in your applications. You can easily migrate your applications using the provided schematics.
👉 To learn more, check out our dedicated blog post.
Since we wrote this blog post, two warnings have been added to catch potential issues in your templates with for loops.
As you know the track
option is now mandatory in @for
loops.
A new warning has been added in development mode to warn you if you have duplicated keys used by the track
option:
WARN: 'NG0955: The provided track expression resulted in duplicated keys for a given collection.
Adjust the tracking expression such that it uniquely identifies all the items in the collection.
Duplicated keys were:
key "duplicated-key" at index "0" and "1".'
This is a warning that you can see in the browser console or when running unit tests. It typically happens if you pick a property that is not unique in your collection.
Another warning has been added to catch potential issues with the tracking expression. If the tracking expression leads to the destruction and recreation of the complete collection, a warning will be displayed:
WARN: 'NG0956: The configured tracking expression (track by identity)
caused re-creation of the entire collection of size 20.
This is an expensive operation requiring destruction and subsequent creation of DOM nodes, directives, components etc.
Please review the "track expression" and make sure that it uniquely identifies items in a collection.'
This typically happens if you use the track item
option and if you recreate all the collection items when there is a change.
Note that the warning only applies if the repeated element is considered “expensive” to create, but the bar is currently set quite low (a text node with a binding is already considered expensive).
Defer syntax is stable
The @defer
syntax is also stable.
@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).
👉 We wrote a detailed blog post about this feature if you want to learn more about it.
Signal standardization proposal
This is not an Angular v18 news, but as you may have heard, some of the most popular framework authors (included the Angular and Vue team for example) have been working on a proposal to standardize signals in the JavaScript language.
The proposal is at the first stage, so it might take a long time, probably at least several years, or even never happened.
You can deep dive into the proposal or into this interesting blog post to learn more about it.
TL;DR: new Signal.State()
would be the equivalent of signal()
in Angular.
new Signal.Computed()
would be the equivalent of computed()
.
There are no equivalents for effect
:
as all frameworks have slightly different needs, this is left out of the scope of the proposal, and frameworks can implement it as they see fit based on new Signal.subtle.Watcher()
.
Fun fact: the current Signal polyfill in the proposal is based on the Angular implementation!
Zoneless change detection
Angular v18 introduces a new way to trigger change detection. Instead of relying on ZoneJS to know when something has possibly changed, the framework can now schedule a change detection by itself.
To do so, a new scheduler has been added to the framework (called ChangeDetectionScheduler
),
and this scheduler is internally used to trigger change detection.
This new scheduler is enabled by default in v18, even if you use ZoneJS.
However, the goal is to progressively move away from ZoneJS and rely only on this new scheduler.
With this new scheduler, the framework no longer only relies on ZoneJS to trigger change detection.
Indeed, the new scheduler triggers a change detection when a host or template listener is triggered,
when a view is attached or removed, when an async
pipe detects a new emission,
when the markForCheck()
method is called, when you set a signal value, etc.
It does so by calling ApplicationRef.tick()
internally.
Opting out of the new scheduler
The new scheduler is enabled by default in v18.
This means that Angular gets notified of potential changes by ZoneJS (as it used to)
and by the new scheduler (when a signal is set, an async
pipe receives a new value, etc.).
The framework then runs the change detection.
This should not impact your application,
as Angular will only run the change detection once even if notified by several sources.
But if you want to opt out of the new scheduler,
you can use the provideZoneChangeDetection()
function with ignoreChangesOutsideZone
set to true
:
bootstrapApplication(AppComponent, {
providers: [
// this restores the behavior of Angular before v18
// and ignores the new scheduler notifications
provideZoneChangeDetection({ ignoreChangesOutsideZone: true })
]
});
Experimental zoneless change detection
But you can also try to only rely on this new scheduler, and no longer on ZoneJS, to trigger change detection.
This is an experimental feature, and you can enable it by using the provider function provideExperimentalZonelessChangeDetection()
in your application.
bootstrapApplication(AppComponent, {
providers: [
// 👇
provideExperimentalZonelessChangeDetection()
]
});
When doing so, the framework will no longer rely on ZoneJS to trigger change detection.
So you can remove ZoneJS from your application if you want to
(and if you have no dependencies that rely on it, of course).
In that case, you can remove zone.js
from the polyfills
in your angular.json
file.
It should work out of the box if all your components are OnPush
and/or rely on signals! 🚀
I tried it on a small application fully written with signals and it worked like a charm.
Of course, this is not something we will be able to do in all applications,
but it’s a nice step forward towards a zoneless Angular.
In particular, if you use a component library that isn’t ready for zoneless support, you’ll have to wait until it is.
If you want to prepare your application for this new feature,
you can start by progressively moving your components to OnPush
.
Testing
Note that the provideExperimentalZonelessChangeDetection
function can also be used in tests,
so you can test your application without ZoneJS,
and make sure your components are correctly working with this new feature.
You can currently add the provider in each test, or globally to all your tests by adding it in the TestBed
configuration,
in the test.ts
file of your application (this file is no longer generated in new projects, but you can add it back manually):
@NgModule({
providers: [provideExperimentalZonelessChangeDetection()]
})
export class ZonelessTestModule {}
getTestBed().initTestEnvironment(
[BrowserDynamicTestingModule, ZonelessTestModule],
platformBrowserDynamicTesting()
);
Then, instead of relying on fixture.detectChanges()
that triggers the change detection,
you can simply use await fixture.whenStable()
and let Angular trigger the change detection
(as it would when running the application).
This is because the ComponentFixture
used by the framework in zoneless mode
uses the “auto detect changes” strategy by default.
So, similarly to using OnPush
in your components to prepare for the zoneless future,
a good way to prepare your tests is to progressively replace detectChanges()
with await fixture.whenStable()
and enable “auto-detect changes” in your tests.
This is something that has been existing for quite some time in Angular.
If you want to use it in your current tests, even without using provideExperimentalZonelessChangeDetection
,
you can either call fixture.autoDetectChanges()
at the beginning of your test,
or add the following provider to your test configuration:
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
We’re probably going to update our ebook and the tests we provide in our online training to use this strategy.
Note that some testing features that use ZoneJS are not supported with
provideExperimentalZonelessChangeDetection()
, like fakeAsync
and tick()
.
If you need to fake time in your tests, you can use the jasmine.clock
APIs instead.
Debugging existing applications
If you want to check if your current application is ready for zoneless change detection,
you can use provideExperimentalCheckNoChangesForDebug()
:
bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }), // or provideExperimentalZonelessChangeDetection()
provideExperimentalCheckNoChangesForDebug({
interval: 1000, // run change detection every second
useNgZoneOnStable: true, // run it when the NgZone is stable as well
exhaustive: true // check all components
})
]
});
This will run a change detection every second and check if any component has been changed without triggering a change detection.
If such a change is detected, a NG0100: ExpressionChangedAfterItHasBeenCheckedError
error will be thrown in the console.
This should allow you to track down the components that need to be updated to work with zoneless change detection.
Zone.js status
ZoneJS is still a dependency of Angular and will be for a while. It is now officially in maintenance mode, and will not ship new features, but will still be maintained for bug fixes and security issues.
Fallback for ng-content
<ng-content>
is a powerful feature in Angular, but it has a cumbersome limitation: it can’t have fallback content.
This is no longer the case in Angular v18!
We can now add some content inside the <ng-content>
tag, and this content will be displayed if no content is projected in the component.
For example, let’s consider a CardComponent
with a title and a content
that can be projected:
<div class="card">
<div class="card-body">
<h4 class="card-title">
<!-- 👇 If the title is not provided, we display a default title -->
<ng-content select=".title">Default title</ng-content>
</h4>
<p class="card-text">
<ng-content select=".content"></ng-content>
</p>
</div>
</div>
Now, if we use this component without providing a title, the default title will be displayed!
Forms events
The AbstractControl
class (the base class for form controls, groups, arrays, and records) now has a new property called events
.
This field is an observable that emits events when the control’s value, status, pristine state, or touched state changes. It also emits when the form is reset or submitted.
For example, let’s consider a form group for a user with a login and a password:
fb = inject(NonNullableFormBuilder);
userForm = this.fb.group({
login: ['', Validators.required],
password: ['', Validators.required]
});
constructor() {
this.userForm.events.subscribe(event => {
if (event instanceof TouchedChangeEvent) {
console.log('Touched: ', event.touched);
} else if (event instanceof PristineChangeEvent) {
console.log('Pristine: ', event.pristine);
} else if (event instanceof StatusChangeEvent) {
console.log('Status: ', event.status);
} else if (event instanceof ValueChangeEvent) {
console.log('Value: ', event.value);
} else if (event instanceof FormResetEvent) {
console.log('Form reset');
} else if (event instanceof FormSubmitEvent) {
console.log('Form submit');
}
});
}
As you can see, several types of events can be emitted:
TouchedChangeEvent
, PristineChangeEvent
, StatusChangeEvent
, ValueChangeEvent
, FormResetEvent
and FormSubmitEvent
.
If I enter a first character in the login field, the console will display:
Pristine: false
Value: Object { login: "c", password: "" }
Status: INVALID
Touched: true
All events also have a source
property that contains the control that emitted the event
(here the login control). The source
contains the form itself for form reset and submission events.
Router and redirects
The redirectTo
property of a route now accepts a function instead of just a string.
Previously, we were only able to redirect our users to a static route
or a route with the same parameters.
The RedirectFunction
introduced in v18 allows us to access part of the ActivatedRouteSnapshot
to build the redirect URL.
I say “part of” because the activated route is not fully resolved when the function is called. For example, the resolvers haven’t run yet, the child routes aren’t matched, etc.
But we do have access to the parent route params or the query params for example,
which was not previously possible.
This function is also similar to guards, and is run in the environment injector:
this means you can inject services if needed.
The function can return a string or a UrlTree
.
provideRouter([
// ...
{
path: 'legacy-users',
redirectTo: (redirectData) => {
const userService = inject(UserService);
const router = inject(Router);
// You also have access to 'routeConfig', 'url', 'params', 'fragment', 'data', 'outlet', and 'title'
const queryParams = redirectData.queryParams;
// if the user is logged in, keep the query params
if (userService.isLoggedIn()) {
const urlTree = router.parseUrl('/users');
urlTree.queryParams = queryParams;
return urlTree;
}
return '/users';
}
}
])
A similar improvement has been made in guards.
The GuardResult
type returned by a guard has been augmented from boolean | UrlTree
to boolean | UrlTree | RedirectCommand
.
A guard could already return an UrlTree
to redirect the user to another route,
but now it can also return a RedirectCommand
to redirect the user to another route with a specific navigation behavior, as a RedirectCommand
is an object
with two properties: redirectTo
(the UrlTree
to navigate to) and navigationBehaviorOptions
(the navigation behavior to use):
provideRouter([
// ...
{
path: 'users',
component: UsersComponent,
canActivate: [
() => {
const userService = inject(UserService);
return userService.isLoggedIn() || new RedirectCommand(router.parseUrl('/login'), {
state: { requestedUrl: 'users' }
});
}
],
}
])
Resolvers can now also return a RedirectCommand
.
The first resolver to do so will trigger a redirect and cancel the current navigation.
withNavigationErrorHandler()
has also been updated to be able to return a RedirectCommand
.
HttpClientModule deprecation
Now that the ecosystem is moving towards standalone components,
we’re starting to see the deprecation of the first Angular modules.
Starting with v18, HttpClientModule
(and HttpClientTestingModule
, HttpClientXsrfModule
, and HttpClientJsonpModule
) are deprecated.
As you probably know, you can now use provideHttpClient()
(with options for XSRF or JSONP support) and provideHttpClientTesting()
as a replacement.
But, as usual, the Angular team provides a schematic to help you migrate your application. When running ng update @angular/core
, you’ll be prompted to migrate your HTTP modules if you still have some in your application.
Internationalization
The utility functions offered by @angular/common
to work with locale data have been deprecated in favor of the Intl
API.
It is no longer recommended to use getLocaleCurrencyCode()
, getLocaleDateFormat()
,
getLocaleFirstDayOfWeek()
, etc.
Instead, you should use the Intl
API directly,
for example Intl.DateTimeFormat to work with locale dates.
Server-Side Rendering
We have two new features in Angular v18 that are related to Server-Side Rendering (SSR).
SSR and replay events
It is now possible to record user interactions during the hydration phase, and replay them when the application is fully loaded. As you may know, the hydration phase is the phase where the server-rendered HTML is transformed into a fully functional Angular application, where listeners are added to the existing elements.
But during this phase, the user can interact with the application, and these interactions are lost (if the hydration process is not fast enough).
So for some applications, it can be interesting to record these interactions and replay them when the application is fully loaded.
This used to be done via a project called preboot in Angular,
but this project was no longer maintained.
Instead of reviving preboot, the Angular team decided to implement this feature directly in the framework.
But they did not start from scratch:
in fact, they used something that already existed inside Google, in the Wiz framework.
Wiz is not open-source, but it is widely used by Google for their applications (Google Search, Google Photos, etc.).
You can read about the ambitions of the Wiz and Angular teams to “merge” the two frameworks in this blog post on angular.io.
Wiz started to use the signals API from Angular (that’s why Youtube is now using Signals),
and now Angular is using the replay events feature from Wiz.
That’s why these two features are in a packages/core/primitives
directory in the Angular codebase:
they are part of Angular but are shared by the two frameworks.
To enable this feature, you can use the withEventReplay()
(developer preview) function in your server-side rendering configuration:
providers: [
provideClientHydration(withEventReplay())
]
When doing so, Angular will add a JS script at the top of your HTML page,
whose job is to replay events that happened during the hydration phase.
To do so, it adds a listener at the root of the document,
and listens to a set of events that can happen on the page
using event delegation.
It does know which events it needs to listen to,
as Angular collected them when rendering the page on the server.
So for example, if you render a page that contains elements which have a (click)
or a (dblclick)
handler,
Angular will add listeners for these events:
<script>window.__jsaction_bootstrap('ngContracts', document.body, "ng", ["click","dblclick"]);</script>
When the application is loaded and stable, the script then replays the events that happened during the hydration phase, thus triggering the same actions as the user did. Quite a nice feature, even if it is probably useful only for some applications.
SSR and Internationalization
The Angular SSR support is improving with each version.
One year ago, Angular v16 introduced progressive hydration,
as we explained in this blog post.
There was one missing feature at the time:
the internationalization support.
Angular would skip the elements marked with i18n
during SSR.
This is now solved in v18!
If your application uses the builtin i18n support of the framework,
you can now use SSR.
The support is in developer preview, and can be enabled with withI18nSupport()
.
Angular CLI
The CLI has also been released in version v18, with some notable features.
Performance improvements
The CLI now builds projects with a larger number of components faster. The commit mentions some really nice gains but I was not able to reproduce them in my experiments on large projects.
ng dev
The ng serve
command is now aliased to ng dev
(in addition to the existing ng s
).
This aligns with the Vite ecosystem, where the development server is usually started using npm run dev
.
Speaking of commands, the ng doc
command has been removed from the CLI in v18.
New build package
The Angular CLI now has a new package for building applications: @angular/build
.
It contains the esbuild/Vite builders, that were previously in the @angular-devkit/build-angular
package.
This allows the new package to only have Vite and ESBuild as dependencies,
and not Webpack.
The serve
/build
/extract-i18n
builders are now in this new package.
The @angular-devkit/build-angular
package can still be used,
as it provides an alias to the now-moved builders.
You’ll notice that an optional migration can be run when updating your application to v18, to update your angular.json
file to use the new package
(where @angular-devkit/build-angular
is replaced by @angular/build
)
and update your package.json
accordingly (to add @angular/build
and remove @angular-devkit/build-angular
).
This will only be done if you don’t use any Webpack-based builders in your applications, so the migration does nothing if you have tests using Karma for example (as they run using Webpack).
Less and PostCSS dependencies
The CLI supports Sass, Less, and PostCSS out of the box,
and until now, these dependencies were installed in your node_modules
when creating a new application (even if you were not using these dependencies).
Less and PostCSS are now optional dependencies for the new @angular/build
package and need to be installed explicitly if you switch to the new package.
When you update your application to v18, these dependencies will be added automatically by ng update
if you choose to switch to @angular/build
(and if you’re using them of course).
Native async/await in zoneless applications
ZoneJS has a particularity: it can’t work with async/await. So you may not know it, but every time you use async/await in your application, your code is transformed by the CLI to use “regular” promises. This is called downleveling, as it transforms ES2017 code (async/await) into ES2015 code (regular promises).
As we are now able to build applications without ZoneJS (even if it is still experimental),
the CLI doesn’t downlevel async/await when zone.js
is not in the application polyfills.
This should make the build a tiny bit faster and lighter in that case.
New project skeleton updates
If you want to upgrade to v18 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 (17.3.0 for example), and the target version (18.0.0 for example), and it gives you a diff of all files created by the CLI: angular-cli-diff/compare/17.3.0…18.0.0.
It can be a great help along with the official ng update @angular/core @angular/cli
command.
You’ll notice that the assets
folder has been replaced by a public
folder in new projects.
You’ll also note that the app.config.ts
file now contains the provideZoneChangeDetection()
provider by default with the eventCoalescing
option set to true
(which avoids the change detection being triggered several times by ZoneJS when an event bubbles and is listened to by several template listeners).
Summary
That’s all for this release.
v19 will probably be dedicated to stabilizing the signals APIs introduced these past months.
We should also see a new feature to declare variables in the template itself, using @let
, as well as an option to switch to Intl-based internationalization.
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 17.3?
Angular 17.3.0 is here!
This is a minor release with some nice features: let’s dive in!
TypeScript 5.4 support
Angular v17.3 now supports TypeScript 5.4. This means that you can use the latest version of TypeScript in your Angular applications. You can check out the TypeScript 5.4 release notes to learn more about the new features.
New template compiler
Angular now uses a new template compiler! The work on this compiler started more than a year ago and has been done in parallel with the other features we saw in the previous releases to eventually pass all of the existing tests. It’s now the case, and this new compiler is now the default in Angular 17.3.
This compiler is based on an intermediate representation of template operations, a common concept in compilers, for example in LLVM. This IR semantically encodes what needs to happen at runtime to render and change-detect the template. Using an IR allows for different concerns of template compilation to be processed independently, which was not the case with the previous implementation. This new compiler is easier to maintain and extend, so it is a great foundation for future improvements in the framework.
Note that the compiler emits the same code as the previous one, so you should not see any difference in the generated code.
output functions
A new (developer preview) feature was added to allow the declaration
of outputs similarly to the input()
function.
As for inputs, you can use the output()
function to define an output:
ponySelected = output<PonyModel>();
// ^? OutputEmitterRef<PonyModel>
The output()
function returns an OutputEmitterRef<T>
that can be used to emit values.
OutputEmitterRef
is an Angular class,
really similar to a simplified EventEmitter
but that does not rely on RxJS
(to limit the coupling of Angular with RxJS).
The function accepts a parameter to specify options,
the only one available for now is alias
to alias the output.
As with EventEmitter
, you can use the emit()
method to emit a value:
ponySelected = output<PonyModel>();
// ^? OutputEmitterRef<PonyModel>
select() {
this.ponySelected.emit(this.ponyModel());
}
You can also declare an output without a generic type,
and the OutputEmitterRef
will be of type OutputEmitterRef<void>
.
You can then call emit()
without a parameter on such an output.
OutputEmitterRef
also exposes a subscribe method
to manually subscribe to the output.
This is not something you’ll do often, but it can be handy in some cases.
If you manually subscribe to an output,
you’ll have to manually unsubscribe as well.
To do so, the subscribe method returns an OutputRefSubscription
object with an unsubscribe
method.
Two new functions have been added to the rxjs-interop
package to convert an output to an observable,
and an observable to an output.
Angular always had the capability of using observables other than EventEmitter
for outputs.
This is not something that is largely used, but it’s possible.
The new outputFromObservable
function allows you to convert an observable to an output:
ponyRunning$ = new BehaviorSubject(false);
ponyRunning = outputFromObservable(this.ponyRunning$);
// ^? OutputRef<boolean>
The outputFromObservable
function returns an OutputRef<T>
, and not an OutputEmitterRef<T>
,
as you can’t emit values on an output created from an observable.
The output emits every event that is emitted by the observable.
startRunning() {
this.ponyRunning$.next(true);
}
It is also possible to convert an output to an observable using the outputToObservable
function if needed.
You can then use .pipe()
and all the RxJS operators on the converted output.
These interoperability functions will probably be rarely used.
output()
, on the other hand, will become the recommended way to declare outputs in Angular components.
HostAttributeToken
Angular has always allowed to inject the value of an attribute of the host element. For example, to get the type of an input, you can use:
@Directive({
selector: 'input',
standalone: true
})
class InputAttrDirective {
constructor(@Attribute('type') private type: string) {
// type would be 'text' if `<input type="text" />
}
}
Since Angular v14, injection can be done via the inject()
function as well,
but there was no option to get an attribute value with it.
This is now possible by using a special class HostAttributeToken
:
type = inject(new HostAttributeToken('type'));
Note that inject
throws if the attribute is not found (unless you pass a second argument { optional: true }
).
RouterTestingModule deprecation
The RouterTestingModule
is now deprecated.
It is now recommended to provideRouter()
in the TestBed
configuration instead.
New router types
The router now has new types to model the result of guards and resolvers.
For example, the CanActivate guard was declared like this:
export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
This is because the guard can return a boolean to allow or forbid the navigation, or an UrlTree
to trigger a redirection to another route. This result can be synchronous or asynchronous.
The signature has been updated to:
export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => MaybeAsync<GuardResult>;
GuardResult
is a new type equal to boolean | UrlTree
,
and MaybeAsync<T>
is a new generic type equal to T | Observable<T> | Promise<T>
.
A resolver function now also returns a MaybeAsync<T>
.
You can keep using the older signatures but the new ones are more concise.
Angular CLI
Angular CLI v17.3 doesn’t bring a lot of new features,
but we can note that deployUrl
is now supported in the application builder.
It was initially marked as deprecated but was re-introduced after community feedback.
Summary
That’s all for this release. The next stop is v18, where we should see some developer preview features becoming stable. 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 17.2?
Angular 17.2.0 is here!
This is a minor release with some nice features: let’s dive in!
Queries as signals
A new developer preview feature has been added to allow the use of queries as signals. viewChild()
, viewChildren()
, contentChild()
, and contentChildren()
functions have been added in @angular/core and return signals.
Let’s go through a few examples.
You can use viewChild to query the template:
// <canvas #chart></canvas>
canvas = viewChild<ElementRef<HTMLCanvasElement>>('chart');
// ^? Signal<ElementRef<HTMLCanvasElement> | undefined>
// <form></form> with FormsModule
form = viewChild(NgForm);
// ^? Signal<NgForm | undefined>
As you can see, the return type is a Signal containing the queried ElementRef<HTMLElement>
or undefined
, or the queried component/directive or undefined
.
You can specify that the queried element is required to get rid of undefined
:
canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('chart');
// ^? Signal<ElementRef<HTMLCanvasElement>>
If the element is not found, you’ll have a runtime error:
'NG0951: Child query result is required but no value is available.
Find more at https://angular.io/errors/NG0951'
This error can also happen if you try to access the query result too soon, for example in the constructor of the component. You can access the query result in the ngAfterViewInit
/ngAfterViewChecked
lifecycle hooks, or in the afterNextRender
/afterRender
functions.
You can also use viewChildren
to query multiple elements. In that case, you get a Signal containing a readonly array of elements, or an empty array if no element is found (we no longer need QueryList \o/):
chart.component.ts
canvases = viewChildren<ElementRef<HTMLCanvasElement>>('chart');
// ^? Signal<ReadonlyArray<ElementRef<HTMLCanvasElement>>>
The functions accept the same option as @ViewChild
and @ViewChildren
, so you can specify the read
option to query a directive or provider on an element.
As you can imagine, the same is possible for contentChild
and contentChildren
.
For example, if we want to build a TabsComponent
that can be used like this:
<ns-tabs>
<ns-tab title="Races" />
<ns-tab title="About" />
</ns-tabs>
We can build a TabDirective
to represent a tab:
@Directive({
selector: 'ns-tab',
standalone: true
})
export class TabDirective {
title = input.required<string>();
}
then build the TabsComponent
with contentChildren
to query the directives:
@Component({
selector: 'ns-tabs',
template: `
<ul class="nav nav-tabs">
@for (tab of tabs(); track tab) {
<li class="nav-item">
<a class="nav-link">{{ tab.title() }}</a>
</li>
}
</ul>
`,
standalone: true
})
export class TabsComponent {
tabs = contentChildren(TabDirective);
// ^? Signal<ReadonlyArray<TabDirective>>
}
As for the @ViewChild
/@ViewChildren
decorators, we can specify the descendants option to query the tab directives that are not direct children of TabsComponent
:
tabs = contentChildren(TabDirective, { descendants: true });
// ^? Signal<ReadonlyArray<TabDirective>>
<ns-tabs>
<div>
<ns-tab title="Races" />
</div>
<ns-tabgroup>
<ns-tab title="About" />
</ns-tabgroup>
</ns-tabs>
As viewChild
, contentChild
can be required.
model
signal
Signals also allow a fresh take on existing patterns. As you probably know, Angular allows a “banana in a box” syntax for two-way binding. This is mostly used with ngModel
to bind a form control to a component property:
<input name="login" [(ngModel)]="user.login" />
Under the hood, this is because the ngModel
directive has a ngModel
input and a ngModelChange
output.
So the banana in a box syntax is just syntactic sugar for the following:
<input name="login" [ngModel]="user.login" (ngModelChange)="user.login = $event" />
The syntax is, in fact, general and can be used with any component or directive that has an input named something
and an output named somethingChange
.
You can leverage this in your own components and directives, for example, to build a pagination component:
@Input({ required: true }) collectionSize!: number;
@Input({ required: true }) pageSize!: number;
@Input({ required: true }) page!: number;
@Output() pageChange = new EventEmitter<number>();
pages: Array<number> = [];
ngOnChanges(): void {
this.pages = this.computePages();
}
goToPage(page: number) {
this.pageChange.emit(page);
}
private computePages() {
return Array.from({ length: Math.ceil(this.collectionSize / this.pageSize) }, (_, i) => i + 1);
}
The component receives the collection, the page size, and the current page as inputs, and emits the new page when the user clicks on a button.
Every time an input changes, the component recomputes the buttons to display. The template uses a for loop to display the buttons:
@for (pageNumber of pages; track pageNumber) {
<button [class.active]="page === pageNumber" (click)="goToPage(pageNumber)">
{{ pageNumber }}
</button>
}
The component can then be used like:
<ns-pagination [(page)]="page" [collectionSize]="collectionSize" [pageSize]="pageSize"></ns-pagination>
Note that page
can be a number or a signal of a number,
the framework will handle it correctly.
The pagination component can be rewritten using signals,
and the brand new model()
function:
collectionSize = input.required<number>();
pageSize = input.required<number>();
pages = computed(() => this.computePages());
page = model.required<number>();
// ^? ModelSignal<number>;
goToPage(page: number) {
this.page.set(page);
}
private computePages() {
return Array.from({ length: Math.ceil(this.collectionSize() / this.pageSize()) }, (_, i) => i + 1);
}
As you can see, a model()
function is used to define the input/output pair, and the output emission is done using the set()
method of the signal.
A model can be required, or can have a default value, or can be aliased, as it is the case for inputs. It can’t be transformed though. If you use an alias, the output will be aliased as well.
If you try to access the value of the model before it has been set, for example in the constructor of the component, then you’ll have a runtime error:
'NG0952: Model is required but no value is available yet.
Find more at https://angular.io/errors/NG0952'
Defer testing
The default behavior of the TestBed
for testing components using @defer
blocks has changed from
Manual
to Playthrough
.
Check out our blog post about defer for more details.
NgOptimizedImage
The NgOptimizedImage directive (check out our blog post about it) can now automatically display a placeholder while the image is loading, if the provider supports automatic image resizing.
This can be enabled by adding a placeholder
attribute to the directive:
<img ngSrc="logo.jpg" placeholder />
The placeholder is 30px by 30px by default, but you can customize it.
It is displayed slightly blurred to give a hint to the user that the image is loading. The blur effect can be disabled with [placeholderConfig]="{ blur: false }
.
Another new feature is the ability to use Netlify as a provider, joining the existing Cloudflare, Cloudinary, ImageKit, and Imgix providers.
Angular CLI
define support
The CLI now supports a new option named define
in the build
and serve
targets. It is similar to what the esbuild plugin of the same name does: you can define constants that will be replaced with the specified value in TS and JS code, including in libraries.
You can for example define a BASE_URL that will be replaced with the value of https://api.example.com
:
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"define": {
"BASE_URL": "'https://api.example.com'"
},
You can then use it in your code:
return this.http.get(`${BASE_URL}/users`);
TypeScript needs to know that this constant exists (as you don’t import it),
so you need to declare it in a d.ts
file:
declare const BASE_URL: string;
This can be an alternative to the environment files, and it can be even more powerful as the constant is also replaced in libraries.
Bun support
You can now use Bun as a package manager for your Angular CLI projects, in addition to npm, yarn, pnpm and cnpm.
It will be automatically detected, or can be forced with --package-manager=bun
when generating a new project.
clearScreen option
A new option is now supported in the application builder to clear the screen before rebuilding the application.
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"clearScreen": true
},
You then only see the output of the current build, and not from the previous one.
Abbreviated build targets
The angular.json
file now supports abbreviated build targets.
For example, you currently have something like this in your project:
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"development": {
"buildTarget": "app:build:development"
},
This means that ng serve
uses the app:build:development
target to build the application.
This can now be abbreviated to:
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"development": {
"buildTarget": "::development"
},
PostCSS support
The application builder now supports PostCSS, a tool for transforming CSS with JavaScript plugins. You just have to add a postcss.config.json
or .postcssrc.json
file to your project and the CLI will pick it up.
JSON build logs
The CLI now supports a new option to output the build logs in JSON format. This can be useful to integrate the build logs in other tools.
NG_BUILD_LOGS_JSON=1 ng build
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 17.1?
Angular 17.1.0 is here!
This is a minor release with some nice features: let’s dive in!
TypeScript 5.3 support
Angular v17.1 now supports TypeScript 5.3. This means that you can use the latest version of TypeScript in your Angular applications. You can check out the TypeScript 5.3 release notes to learn more about the new features.
Inputs as signals
In Angular v17.1, a new feature was added to allow the use of inputs as signals.
This is a first step towards signal-based components.
The framework team added an input()
function in @angular/core
.
@Component({
standalone: true,
selector: 'ns-pony',
template: `
@if (ponyModel(); as ponyModel) {
<figure>
<img [src]="imageUrl()" [alt]="ponyModel.name" />
<figcaption></figcaption>
</figure>
}
`
})
export class PonyComponent {
ponyModel = input<PonyModel>();
imageUrl = computed(() => `assets/pony-${this.ponyModel()!.color}.gif`);
}
As you can see in the example above, the input()
function returns a signal,
that can be used in the template or in computed values
(which would be the modern equivalent of ngOnChanges
).
It can be undefined though,
hence the @if
in the template and the !
in the computed value.
If an input is mandatory,
you can use the input.required()
version of the function:
@Component({
standalone: true,
selector: 'ns-pony',
template: `
<figure>
<img [src]="imageUrl()" [alt]="ponyModel().name" />
<figcaption>{{ ponyModel().name }}</figcaption>
</figure>
`
})
export class PonyComponent {
ponyModel = input.required<PonyModel>();
imageUrl = computed(() => `assets/pony-${this.ponyModel().color}.gif`);
}
You can also provide a default value, an alias, and a transformer function.
Here the ponySpeed
field is aliased as speed
,
provided with a default value, and transformed to a number (even if the input is a string):
ponySpeed = input(10, {
alias: 'speed',
transform: numberAttribute
});
You can also use the signal as the source of an observable, to trigger an action when the input changes. For example, to fetch data from a server:
export class PonyComponent {
ponyService = inject(PonyService);
ponyId = input.required<number>();
// entity fetched from the server every time the ponyId changes
ponyModel = toSignal(toObservable(this.ponyId)
.pipe(switchMap(id => this.ponyService.get(id))));
imageUrl = computed(() => `assets/pony-${this.ponyModel()!.color}.gif`);
}
When coupled with the recent addition to the router called “Component Input Binding”,
where the router binds the route parameters to the inputs of a component,
it can lead to an interesting pattern.
Note that the input transform
is necessary as the router parameters are strings:
ponyId = input.required<number, string>({
transform: numberAttribute
});
This behavior is enabled via withComponentInputBinding
in the router configuration:
provideRouter(
[
{
path: 'pony/:ponyId',
component: PonyComponent
}
],
withComponentInputBinding()
),
Zoneless change detection
The framework is making progress towards zoneless change detection.
A new private API called ɵprovideZonelessChangeDetection
was added to @angular/core
.
When you add this provider to your application,
the framework no longer relies on Zone.js for change detection (and you can remove it from the application).
So how does it work?
Every time an event is fired, an input is set, an output emits a value, an async
pipe receives a value, a signal is set, markForCheck
is called, etc.,
the framework notifies an internal scheduler that something happened.
It then runs the change detection on the component marked as dirty.
But this doesn’t catch what Zone.js usually does:
a setTimeout
, a setInterval
, a Promise
, an XMLHttpRequest
, etc.
But that shouldn’t be a problem because the idea is that when a setTimeout
, setInterval
or XMLHttpRequest
callback is triggered, and you want it to update the state of the application, you should do it by modifying a signal, which will in turn trigger change detection.
This is far from being complete, as the “private API” part suggests. However, it indicates that the framework is making progress towards zoneless change detection.
Router
The router now has an info
option in the NavigationExtras
that can be used to store information about the navigation.
Unlike the state
option,
this information is not persisted in the session history.
The RouterLink
directive now supports this option as well:
<a [routerLink]="['/pony', pony.id]" [info]="{ ponyName: pony.name }"></a>
Control flow migration
The control flow migration is still experimental but has been improved
with a ton of bug fixes and new features.
It now removes the useless imports from your component imports after the migration.
It also now has a new option format
to reformat your templates after the migration.
The option is true
by default, but can be turned off:
ng g @angular/core:control-flow --path src/app/app.component.html --format=false
INFINITE_CHANGE_DETECTION
This is not a new feature, but this bug fix is worth mentioning. Angular v17.1 fixes a bug for transplanted views, but this will also be useful for signals.
The framework now runs change detection while there are still dirty
views to be refreshed in the tree.
If too many loops are detected, the framework will throw an error: INFINITE_CHANGE_DETECTION
.
This will remind the oldest Angular developers of the good old days of AngularJS, when we had to be careful with infinite digest loops 👴.
Angular v17.1 will throw this error if you have 100 loops in a row at the moment.
Angular CLI
Vite v5
The Angular CLI v17.1 now uses Vite v5 under the hood. Vite v5 was recently released, you can read more about it in the official blog post.
Application builder migration
If you haven’t migrated to the new application builder yet, there is now a migration schematic to help you with that:
ng update @angular/cli --name=use-application-builder
Keyboard shortcuts in dev server
After running ng serve
, you can now see in the terminal the following line:
Watch mode enabled. Watching for file changes...
➜ Local: http://localhost:4200/
➜ press h + enter to show help
If you press ‘h + enter’, you will see the list of available keyboard shortcuts:
Shortcuts
press r + enter to force reload browser
press u + enter to show server url
press o + enter to open in browser
press c + enter to clear console
press q + enter to quit
Quite cool!
Running tests with Web Test Runner
An experimental builder is now available to run tests with
Web Test Runner.
It is very early stage, but you can try it out by replacing the karma
builder with web-test-runner
in the angular.json
file:
"test": {
"builder": "@angular-devkit/build-angular:web-test-runner",
}
You then need to install the @web/test-runner
package,
and here you go!
Running ng test
will now use Web Test Runner instead of Karma
(and bundle the files with the application
builder, which uses esbuild,
and not Webpack as the current karma
builder does).
A lot of options aren’t available yet, so you can’t change the browser for example (it only runs in Chrome for now), or define reporters, or use any kind of configuration.
In the future, we will be able to define a configuration file for Web Test Runner, use other browsers (WTR supports using Playwright to download and run tests in other browsers), etc.
This builder will probably be the default in the future, as Karma is now deprecated.
loader option
The application
builder gained a new loader
option.
It allows defining the type of loader to use for a specified file extension.
The file matching the extension can then be used
within the application code via an import.
The available loaders that can be used are:
text
which treats the content as a stringbinary
which treats the content as a Uint8Arrayfile
which emits the file and provides the runtime location of the fileempty
which considers the content to be empty and will not include it in bundles
For example, to inline the content of SVG files into the bundled application,
you can use the following configuration in the angular.json
file:
loader: {
".svg": "text"
}
Then an SVG file can be imported in your code with:
import content from './logo.svg';
TypeScript needs to be aware of the module type for the import to prevent type-checking errors during the build, so you’ll need to add a type definition for the SVG file:
declare module "*.svg" {
const content: string;
export default content;
}
Output location
It is now possible to customize the output location of the build artifacts:
"outputPath": {
"base": "dist/my-app",
"browser": "",
"server": "node-server",
"media": "resources"
}
Retain special CSS comments
By default, the CLI removes comments from CSS files during the build.
If you want to retain them because you use some tools that rely on them,
you can now set the removeSpecialComments
option to false
in the optimization
section of your angular.json
file:
"optimization": {
"styles": {
"removeSpecialComments": false
}
}
Allowed CommonJS dependencies
You can now specify *
as a package name in the allowedCommonJsDependencies
option to allow all packages in your build:
"allowedCommonJsDependencies": ["*"]
–no-browsers in tests
You can now use the --no-browsers
option when running tests with the CLI.
This will prevent the browser from opening when running tests,
which can be useful if you are inside a container for example.
This was already possible by setting the browsers
option to []
in the karma.conf.js
file, but not from the CLI command.
ng test --no-browsers
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.4?
Vue 3.4.0 is here!
The last minor release was v3.3.0 in May. Since then, we have seen a few patch releases, some coming with new features.
Let’s see what we have in this release!
v-bind shorthand syntax
It is now possible to use a shorthand syntax
for v-bind
when the key and value have the same name!
<!-- before -->
<div v-bind:id="id"></div>
<-- or -->
<div :id="id"></div>
<!-- after -->
<div v-bind:id></div>
<-- or -->
<div :id></div>
Performances improvements for the reactivity system
Johnson Chu, the author of Volar, has done massive work to improve the performance of the reactivity system.
Let’s consider a scenario where you have a computed
A that depends on a computed
B.
In Vue v3.3, if B is re-evaluated, A is also re-evaluated, even if B has the same value as before.
In Vue v3.4, A is not re-evaluated if B has the same value as before.
This is also true for watch
functions.
Other improvements have been made for Arrays mutations, for watchers that depend on multiple computed values, and more (as you can see in the PR description).
This should avoid a whole lot of unnecessary re-renders! 🚀 (and hopefully, don’t introduce any regression 🤞).
computed
previous value
You can now get the previous value in a computed
,
as the first parameter of the getter function.
const count = ref(0);
const double = computed((prev) => {
console.log(prev);
return count.value * 2
});
count.value++;
// logs 0
This can be useful if you want to manually compare object values.
(computed
internally uses Object.is
to compare the previous and current values,
which is not always what you want, see the PR description).
This is especially useful with the new reactivity system improvements.
In v3.4, a computed property will only trigger effects when its computed value has changed from the previous one.
But in the case of a computed that return new objects, Vue thinks that the previous and current values are different.
If you want to avoid triggering effects in that case, you can compare the previous and current values manually.
Performances improvements for the compiler
The Vue compiler has been improved to be faster. Evan rewrote the parser entirely, to avoid using regexes. The code generation has also been improved. They are now nearly 2 times faster!
This should not have a huge impact on your build times, as the Vue compiler is not the only step in the build process (you usually have the TypeScript compiler, the CSS preprocessor, etc.).
Support for import attributes
It is now possible to use import attributes in SFC (both in JS and TS):
import json from "./foo.json" with { type: "json" }
The support for using
has also been added (new feature for explicit resource management in JS, see the proposal here).
watch once
The watch
function gained a new option called once
.
When set to true
, the watcher is removed after the first call.
watch('foo', () => {
console.log('foo changed');
}, { once: true });
It was previously possible to achieve the same result by using the returned stop
function:
const stop = watch('foo', () => {
console.log('foo changed');
stop();
});
Props validation
As you probably know, Vue provides a mechanism to validate props.
defineProps({
min: {
type: Number,
required: true,
validator: (value) => value >= 0,
},
max: {
type: Number,
required: true,
validator: (value) => value >= 0,
}
})
In the above example, the min
and max
props must be positive numbers.
In Vue v3.4, the validator
function is now called with a second argument
containing all the props, allowing to validate the value against other props.
defineProps({
min: {
type: Number,
required: true,
validator: (value) => value >= 0,
},
max: {
type: Number,
required: true,
validator: (value, props) => value >= props.min,
}
})
Then if you try to use the component with max
being lower than min
,
you will get a warning.
Invalid prop: custom validator check failed for prop "max".
SSR hydration mismatch warnings
When using SSR, the client-side hydration will now warn you with a message that includes the mismatching element. It was sometimes hard to find the mismatching element in the DOM, as the warning was in v3.3:
[Vue warn]: Hydration completed but contains mismatches.
In v3.4, the warning now contains the actual element (not only its tag name but the actual DOM element, so you can click on it), allowing us to see where it is in the DOM and why it failed.
[Vue warn]: Hydration text content mismatch in <h2>:
- Server rendered: Hello server
- Client rendered: Hello client
Vue now also warns you if you have a mismatch in classes, styles, or attributes!
You can enable this feature in production as well by using a feature flag
in your Vite config called __VUE_PROD_HYDRATION_MISMATCH_DETAILS__
:
export default defineConfig({
plugins: [vue()],
define: {
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true
}
});
MathML support
It is now possible to write templates using MathML (in addition to HTML and SVG)!
The template below displays a beautiful x²
:
<template>
<math>
<mrow><msup><mi>x</mi><mn>2</mn></msup></mrow>
</math>
</template>
defineModel
The defineModel
function was introduced as an experimental API in v3.3,
is now a stable API.
It is now the recommended way to define custom v-models.
Compared to what we explained in our previous blog post, the local
option has been removed (see this discussion if you want to know why).
The model now automatically adapts based on whether the parent provides a v-model
or not.
Another change it that it is now possible to handle modifiers.
For example, if you want to handle the .number
modifier, you can do:
const [count, countModifiers] = defineModel({
set(value) {
if (countModifiers?.number) {
return Number(value);
}
return value;
}
});
console.log(countModifiers?.number); // true if the .number modifier is used
You can type the modifiers by using defineModel<number, 'number'>({ ... })
.
The first type parameter is the type of the model value,
and the second one is the type of the modifiers (which can be a union type if you want to handle several ones).
You can play with this demo to see how it works.
TypeScript improvements
An effort has been made to sanitize the types (which will be helpful for all libraries in the ecosystem).
A notable improvement for developers is that app.directive
,
used to register global directives, can now be properly typed:
app.directive<HTMLElement, string>('custom', {
mounted(el, binding) {
// el is correctly typed as HTMLElement
// binding is correctly typed as string
}
})
Deprecated features removed
The reactivity transform experiment has been removed. It had been deprecated in v3.3.0 (see our previous blog post).
Vnode hook events written like vnodeMounted
have been deprecated in v3.3
(see our previous blog post)
and they are now no longer supported.
You should use the @vue:
syntax instead, like @vue:mounted
.
The v-is
directive has also been removed.
News from the ecosystem
Vue 2 End of Life
Vue 2 has reached its end of life, and Evan wrote a blog post about it:
👉 https://blog.vuejs.org/posts/vue-2-eol
Vapor mode
Vapor (@vue/vapor
) is making progress with a new repository.
For now, it introduces two work-in-progress packages: a new compiler and a new runtime. They only support the most basic features at the moment, and aren’t easily usable. There is a playground in the repository if you want to try it out.
The biggest difference with Vue 3 is that Vapor generates a rendering function that does not rely on virtual DOM.
For example, the following component:
<script setup lang="ts">
import { ref, computed } from 'vue/vapor'
const count = ref(1)
const double = computed(() => count.value * 2)
const inc = () => count.value++
</script>
<template>
<div>
<h1 class="red">Counter</h1>
<div>8 * 2 = </div>
<button @click="inc">inc</button>
</div>
</template>
generates the following render function:
function _sfc_render(_ctx) {
const t0 = _template('<div><h1 class="red">Counter</h1><div> * 2 = </div><button>inc</button></div>');
const n0 = t0();
const { 0: [, { 1: [n3], 2: [n4] }] } = _children(n0);
const n1 = _createTextNode(_ctx.count);
const n2 = _createTextNode(_ctx.double);
_prepend(n3, n1);
_append(n3, n2);
_on(n4, "click", (...args) => _ctx.inc && _ctx.inc(...args));
_watchEffect(() => {
_setText(n1, void 0, _ctx.count);
});
_watchEffect(() => {
_setText(n2, void 0, _ctx.double);
});
return n0;
}
As you can see the render function is using a different strategy than Vue 3:
it creates the static elements, then it creates the dynamic elements,
and finally it updates the dynamic elements when needed using watchEffect
.
You can check in the project’s README the features that are supported and the ones that are not.
Vue Test Utils
VTU should now have better type-checking support for TypeScript users.
For example wrapper.setProps({ foo: 'bar' })
will now correctly error
if the component has no foo
prop.
create-vue
create-vue
now generates projects using Vite v5,
which was recently released.
Nuxt
Nuxt v3.9 is out as well, with the support of Vue 3.4. It brings a lot of new features and experiments: you can read more in the official blog post.
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 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!