What's new in Angular 17.2?

Angular 17.2.0 is here!

Angular logo

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:browser",
  "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!

Angular logo

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 string
  • binary which treats the content as a Uint8Array
  • file which emits the file and provides the runtime location of the file
  • empty 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!


2023-12-29

What's new in Vue 3.4?

Vue 3.4.0 is here!

Vue logo

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 :

<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!

Angular logo

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:

👉 Angular Control Flow Syntax

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:

👉 Angular Deferrable Views

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 cached
  • includeHeaders: the list of headers to include (none by default)
  • includePostRequests: whether or not to cache POST requests (by default only GET and HEAD requests are cached)

For example:

bootstrapApplication(AppComponent, { 
  providers: [provideHttpClient({
    withHttpTransferCacheOptions({ includePostRequests: true })
  })
});

Devtools

The devtools received some love as well, and they now allow you to inspect the dependency injection tree.

Animations

No new feature for this part of Angular, but it is now possible to lazy-load the animations package. In a standalone application, you can use provideAnimationsAsync() instead of using provideAnimations() and the necessary code for animations will be loaded asynchronously.

The application should work the same, but you should see an extra chunk appear when building the application. That’s a few kilobytes of JavaScript that you don’t have to load upfront 🚀.

You can disable animations by providing 'noop' as the value of provideAnimationsAsync():

bootstrapApplication(AppComponent, { 
  providers: [provideAnimationsAsync('noop')] 
});

Performances

In dev mode, you’ll now get a warning if you load an oversized image or if an image is the “Largest Contentful Paint element” in the page and is lazy-loaded (which is a bad idea, see the explanations here).

For example:

An image with src image.png has intrinsic file dimensions much larger than its 
rendered size. This can negatively impact application loading performance. 
For more information about addressing or disabling this warning, see  
https://angular.io/errors/NG0913

You can configure this behavior via dependency injection, for example, if you want to turn off these warnings:

{
  provide: IMAGE_CONFIG, useValue:
  {
    disableImageSizeWarning: false,
    disableImageLazyLoadWarning: false
  }
}

TypeScript 5.2 and Node.js v18

It’s worth noting that Angular now requires TypeScript 5.2 and Node.js v18. Support for older versions has been dropped.

Angular CLI

A lot happened in the CLI!

👉 Check out our dedicated blog post about the CLI v17 for more details.

Summary

That’s all for this release, stay tuned!

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


What's new in Angular CLI 17.0?

Angular CLI 17.0.0 is out!✨

If you want to upgrade to 17.0.0 without pain (or to any other version, by the way), I have created a Github project to help: angular-cli-diff. Choose the version you’re currently using (16.2.0 for example), and the target version (17.0.0 for example), and it gives you a diff of all files created by the CLI: angular-cli-diff/compare/16.2.0…17.0.0. It can be a great help along with the official ng update @angular/core @angular/cli command. You have no excuse for staying behind anymore!

Let’s see what we’ve got in this release.

Standalone applications with Vite by default!

The --standalone flag is now the default behavior of the CLI. This means generating a new project with ng new now uses standalone components by default, and that the ng generate component/pipe/directive command now generates standalone components/pipes/directives.

Another notable change to ng new is that the routing is now enabled by default.

But the most important change is that the CLI now uses Vite out-of-the-box! A new builder called application has been introduced, and is used when generating a new project. This builder has a very similar configuration to the browser builder, so the migration is quite easy if you want to use Vite in an existing project You have to change the builder from browser to application in the angular.json file, rename the main property to browser, and remove a few options from the development configuration (buildOptimizer, vendorChunk).

Once migrated, the ng serve command will use Vite instead of Webpack. Build time should be faster, especially for cold starts (I saw 2-3x times improvement on my machine). There is no HMR by default yet, but the global style changes are detected and applied automatically without reloading the page.

Note that the output of the ng build Vite-based command is now in dist/my-project/browser instead of dist/my-project.

The browser-esbuilder builder still exists, but will be removed in the future. You should use the application builder instead.

ng new –ssr

A new flag --ssr has been added to the ng new command to generate a new project with SSR enabled out of the box.

It generates a project similar to what you usually get and then runs the @angular/ssr schematics (you can also use the schematics directly on an existing project with ng add @angular/ssr). @angular/ssr is a new package and replaces the Angular Universal package. If you were using the Angular Universal package, ng update migrates your configuration to use @angular/ssr automatically.

This schematic does the following:

  • adds the @angular/ssr package
  • adds the @angular/platform-server package
  • adds the express and @types/express packages
  • adds the main.server.ts file (entry point for the application when running on the server)
  • adds the app.config.server.ts file (providers for the application when running on the server)
  • adds the tsconfig.server.json file
  • adds the server.ts file (the Express server, responsible for serving the application)

It updates the angular.json configuration to add the following options to the build target:

"server": "src/main.server.ts",
"prerender": true,
"ssr": {
  "entry": "server.ts"
}

and adds the provideClientHydration() to the (browser) application providers, to have a smooth transition between the server and the client. This is a new feature of Angular v16, and we talked about it in our article about the v16 release.

When running ng build, the CLI will now build the server bundle (in dist/my-project/server) and the client bundle (in dist/my-project/browser). You can then run the generated server with:

node dist/my-project/server/main.server.mjs

This starts an Express server on port 4000 by default, which serves the rendered pages.

The rendered pages are in the browser folder, and are named ${page}/index.html:

dist/my-project/browser/index.html
dist/my-project/browser/login/index.html
dist/my-project/browser/register/index.html

If you use localize in your application, the CLI will also build the localized bundles (in dist/my-project/server/${lang}).

The prerendering mechanism should be quite accurate now, as it uses the Angular router under the hood to navigate to each route and render it (routes with parameters or redirections are skipped). When prerendering is enabled, the CLI generates a prerendered-routes.json file that contains all the prerendered routes. This is useful if you deploy on the cloud as this file is usually recognized by providers to serve these files as static.

{
  "routes": [
    "/",
    "/login",
    "/register"
    ...
  ]
}

You can disable the auto-discovery of routes by setting the discoverRoutes option to false in the angular.json file. You can also provide your own list of routes in this file by defining routeFiles:

"ssr": {
  "discoverRoutes": false,
  "routeFiles": "ssg-routes.txt"
}

This file must contain a list of routes that you want to render (and can contain parameterized routes).

When running ng serve, the CLI serves the application via Vite, and only pre-renders the requested page (the one you’re currently on).

You can also use a new option to CommonEngine called enablePerformanceProfiler to trace the performance of each step of the rendering:

const commonEngine = new CommonEngine({
  enablePerformanceProfiler: true
});

When using SSR, it is recommended to use the Fetch version of the HTTP client, by using provideHttpClient(withFetch()) (as introduced in Angular v16.1). This is for performance and compatibility reasons.

NG02801: Angular detected that `HttpClient` is not configured to use `fetch` APIs. It's strongly recommended to enable `fetch` for applications that use Server-Side Rendering for better performance and compatibility. To enable `fetch`, add the `withFetch()` to the `provideHttpClient()` call at the root of the application.

Functional HTTP interceptors by default

The CLI now generates functional interceptors by default, without the need to specify --functional anymore. Class-based interceptors are still available with the --no-functional option, but you’re now encouraged to use the functional ones.

Summary

That’s all for the CLI v17.0 release! You’ll find more interesting features in our article about the framework v17.0.0 release.

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


A guide to Angular Deferrable Views with @defer

With the introduction of the Control flow syntax, the Angular team has also introduced a new way to load components lazily (as a developer preview for now). We already have lazy-loading in Angular, but it is mainly based on the router.

Angular v17 adds a new way to load components lazily, using the @defer syntax in your templates.

@defer lets you define a block of template that will be loaded lazily when a condition is met (with all the components, pipes, directives, and libraries used in this block lazily loaded as well). Several conditions can be used. For example, it can be “as soon as possible (no condition)”, “when the user scrolls to that section”, “when the user clicks on that button” or “after 2 seconds”.

Let’s say your home page displays a “heavy” ChartComponent that uses a charting library and some other dependencies, like a FromNow pipe:

@Component({
  selector: 'ns-chart',
  template: '...',
  standalone: true,
  imports: [FromNowPipe],
})
export class ChartComponent {
  // uses chart.js
}

This component is used in the home page:

import { ChartComponent } from './chart.component';

@Component({
  selector: 'ns-home',
  template: `
    <!-- some content -->
    <ns-chart />
  `,
  standalone: true,
  imports: [ChartComponent]
})
export class HomeComponent {
  // ...
}

When the application is packaged, the ChartComponent will be included in the main bundle:

+------------------------+
| main-xxxx.js  -  300KB |
+------------------------+
| home.component.ts      |
| chart.component.ts     |
| from-now.pipe.ts       |
| chart.js               |
+------------------------+

Let’s say that the component is not visible at first on the home page, maybe because it is at the bottom of the page, or because it is in a tab that is not active. It makes sense to avoid loading this component eagerly because it would slow down the initial loading of the page.

With @defer, you can load this component only when the user really needs it. Just wrapping the ChartComponent in a @defer block will do the trick:

import { ChartComponent } from './chart.component';

@Component({
  selector: 'ns-home',
  template: `
    <!-- some content -->
    @defer (when isVisible) {
      <ns-chart />
    }
  `,
  standalone: true,
  imports: [ChartComponent]
})

The Angular compiler will rewrite the static import of the ChartComponent to a dynamic import (() => import('./chart.component')), and the component will be loaded only when the condition is met. As the component is now imported dynamically, it will not be included in the main bundle. The bundler will create a new chunk for it:

+------------------------+
| main-xxxx.js  -  100KB |
+------------------------+       +-------------------------+
| home.component.ts      |------>| chunk-xxxx.js - 200KB   |
+------------------------+       +-------------------------+
                                 | chart.component.ts      |
                                 | from-now.pipe.ts        |
                                 | chart.js                |
                                 +-------------------------+

The chunk-xxxx.js file will only be loaded when the condition is met, and the ChartComponent will be displayed.

Before talking about the various kinds of conditions that can be used with @defer, let’s see how to use another interesting feature: displaying a placeholder until the deferred block is loaded.

@placeholder, @loading, and @error

You can define a placeholder template with @placeholder that will be displayed until the loading condition is met. Then, while the block is loading, you can display a loading template with @loading. If no @loading block is defined, the placeholder stays there until the block is loaded. You can also define an error template with @error that will be displayed if the block fails to load.

@defer (when show) {
  <ns-chart />
}
@placeholder {
  <div>Something until the loading starts</div>
}
@loading {
  <div>Loading...</div>
}
@error {
  <div>Something went wrong</div>
}

When using server-side rendering, only the placeholder will be rendered on the server (the defer conditions will never trigger).

after and minimum

As the @defer block loading can be quite fast, there is a risk that the loading block is displayed and hidden too quickly, causing a “flickering” effect.

To avoid this, you can use the after option to specify after how many milliseconds the loading should be displayed.

If the block takes less than this delay to load, then the @loading block is never displayed.

You can also use the minimum option to specify a minimum duration for the loading. If the loading is faster than the minimum duration, then the loading will be displayed for the minimum duration (this only applies if the loading is ever displayed).

You can of course combine all these options:

@defer (when show) {
  <ns-chart />
}
@placeholder {
  <div>Something until the loading starts</div>
}
@loading (after 500ms; minimum 500ms) {
  <div>Loading...</div>
}

You can also specify a minimum duration for the placeholder. It can be useful when the loading condition is immediate (for example, when no condition is specified). In that case, the placeholder will be displayed for the minimum duration, even if the block is loaded immediately, to avoid a “flickering” effect.

@defer (when show) {
  <ns-chart />
}
@placeholder (minimum 500ms) {
  <div>Something until the loading starts</div>
}
@loading (after 500ms; minimum 500ms) {
  <div>Loading...</div>
}

Conditions

Several conditions can be used with @defer, let’s see them one by one.

No condition or on idle

The simplest condition is to not specify any condition at all: in this case, the block will be loaded when the browser is idle (the loading is scheduled using requestIdleCallback).

@defer {
  <ns-chart />
}

This is equivalent to using the on idle condition:

@defer (on idle) {
  <ns-chart />
}

Simple boolean condition with when

You can also use a boolean condition to load a block of the template with when. Here, we display the defer block only when the show property of the component is true:

@defer (when show) {
  <ns-chart />
}

Note that this is not the same as using *ngIf on the block, as the block will not be removed even if the condition becomes false later.

on immediate

The on immediate condition triggers the loading of the block immediately. It does not display a placeholder, even if one is defined.

on timer

The on timer condition triggers the loading of the block after a given duration, using setTimeout under the hood.

@defer (on timer(2s)) {
  <ns-chart />
}

on hover

Other conditions are based on user interactions. These conditions can specify the element of the interaction using a template reference variable, or none to use the placeholder element. In the latter case, the placeholder element must exist and have a single child element that will be used as the element of the interaction.

The on hover condition triggers the loading of the block when the user hovers the element. Under the hood, it listens to the mouseenter and focusin events.

<span #trigger>Hover me</span>

@defer (on hover(trigger)) {
  <ns-chart />
}

or using the placeholder element:

@defer (on hover) {
  <ns-chart />
}
@placeholder {
  <span>Hover me</span>
}

on interaction

The on interaction condition triggers the loading of the block when the user interacts with the element. Under the hood, it listens to the click and keydown events.

on viewport

The on viewport condition triggers the loading of the block when the element becomes visible in the viewport. Under the hood, it uses an intersection observer.

Multiple conditions

You can also combine multiple conditions using a comma-separated list:

<!-- Loads if the user hovers the placeholder, or after 1 minute -->
@defer (on hover, timer(60s)) {
  <ns-chart />
}
@placeholder {
  <span>Something until the loading starts</span>
}

Prefetching

@defer allows you to separate the loading of a component from its display. You can use the same conditions we previously saw to load a component using prefetch, and then display it with another condition.

For example, you can prefetch the lazy-loaded content on idle and then display it on interaction:

@defer (on interaction; prefetch on idle) {
  <ns-chart />
}
@placeholder {
  <button>Show me</button>
}

Note that the @loading block will not be displayed if the deferred block is already prefetched when the loading condition is met.

How to test deferred loading?

When a component uses defer blocks in its template, you’ll have to do some extra work to test it.

The TestBed API has been extended to help you with that. The configureTestingModule method now accepts a deferBlockBehavior option. By default, this option is set to DeferBlockBehavior.Manual, which means that you’ll have to manually trigger the display of the defer blocks. But let’s start with the other option instead.

You can change this behavior by using DeferBlockBehavior.Playthrough. Playthrough means that the defer blocks will be displayed automatically when a condition is met, as they would when the application runs in the browser.

beforeEach(() => {
  TestBed.configureTestingModule({
    deferBlockBehavior: DeferBlockBehavior.Playthrough
  });
});

In that case, the defer blocks will be displayed automatically when a condition is met, after calling await fixture.whenStable().

So if we test a component with a deferred block that is visible after clicking on a button, we can use:

// Click the button to trigger the deferred block
fixture.nativeElement.querySelector('button').click();
fixture.detectChanges();

// Wait for the deferred block to render
await fixture.whenStable();

// Check its content
const loadedBlock = fixture.nativeElement.querySelector('div');
expect(loadedBlock.textContent).toContain('Some lazy-loaded content');

If you want to use the DeferBlockBehavior.Manual behavior, you’ll have to manually trigger the display of the defer blocks. To do so, the fixture returned by TestBed.createComponent now has an async getDeferBlocks method that returns an array of DeferBlockFixture objects. Each of these fixtures has a render method that you can call to display the block in a specific state, by providing a DeferBlockState parameter.

DeferBlockState is an enum with the following values:

  • DeferBlockState.Placeholder: display the placeholder state of the block
  • DeferBlockState.Loading: display the loading state of the block
  • DeferBlockState.Error: display the error state of the block
  • DeferBlockState.Complete: display the defer block as if the loading was complete

This allows a fine-grained control of the state of the defer blocks. If we want to test the same component as before, we can do:

const deferBlocks = await fixture.getDeferBlocks();
// only one defer block should be found
expect(deferBlocks.length).toBe(1);

// Render the defer block
await deferBlocks[0].render(DeferBlockState.Complete);

// Check its content
const loadedBlock = fixture.nativeElement.querySelector('div');
expect(loadedBlock.textContent).toContain('Some lazy-loaded content');

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


Angular templates got better - the Control Flow syntax

Angular v17 introduces a new “developer preview” feature called “control flow syntax”. This feature allows you to use a new template syntax to write control flow statements, like if/else, for, and switch, instead of using the built-in structural directives (*ngIf, *ngFor, and *ngSwitch).

To understand why this was introduced, let’s see how structural directives work in Angular.

Structural directives under the hood

Structural directives are directives that change the structure of the DOM by adding, removing, or manipulating elements. They are easy to recognize in Angular because they begin with an asterisk *.

But how do they really work?

Let’s take a simple template with ngIf and ngFor directives as an example:

<h1>Ninja Squad</h1>
<ul *ngIf="condition">
  <li *ngFor="let user of users">{{ user.name }}</li>
</ul>

If you read the chapter of our ebook about the Angular compiler, you know that the framework generates JavaScript code from this template. And maybe you imagine that *ngIf gets converted to a JavaScript if and *ngFor to a for loop like:

createElement('h1');
if (condition) {
  createElement('ul');
  for (user of users) {
    createElement('li');
  }
}

But Angular does not work exactly like that: the framework decomposes the component’s template into “views”. A view is a fragment of the template that has static HTML content. It can have dynamic attributes and texts, but the HTML elements are stable.

So our example generates in fact three views, corresponding to three parts of the template:

Main view:

<h1>Ninja Squad</h1>
<!-- special comment -->

NgIf view:

<ul>
  <!-- special comment -->
</ul>

NgFor view:

<li>{{ user.name }}</li>

This is because the * syntax is in fact syntactic sugar to apply an attribute directive on an ng-template element. So our example is the same as:

<h1>Ninja Squad</h1>
<ng-template [ngIf]="condition">
<ul>
  <ng-template ngFor [ngForOf]="users" let-user>
    <li>{{ user.name }}</li>
  </ng-template>
</ul>
</ng-template>

Here ngIf and ngFor are plain directives. Each ng-template then generates a “view”. Each view has a static structure that never changes. But these views need to be dynamically inserted at some point. And that’s where the <!-- special comment --> comes into play.

Angular has the concept of ViewContainer. A ViewContainer is like a box where you can insert/remove child views. To mark the location of these containers, Angular uses a special HTML comment in the created DOM.

That’s what ngIf actually does under the hood: it creates a ViewContainer, and then, when the condition given as input changes, it inserts or removes the child view at the location of the special comment.

This view concept is quite interesting as it will allow Angular to only update views that consume a signal in the future, and not the whole template of a component! Check out out our blog post about the Signal API for more details.

Custom structural directives

You can create your own structural directives if you want to. Let’s say you want to write a *customNgIf directive. You can create a directive that takes a condition as an input and injects a ViewContainerRef (the service that allows to create the view) and a TemplateRef (the ng-template on which the directive is applied).

import { Directive, DoCheck, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[customNgIf]',
  standalone: true
})
export class CustomNgIfDirective implements DoCheck {
  /**
  * The condition to check
  */
  @Input({ required: true, alias: 'customNgIf' }) condition!: boolean;

  /**
  * The view created by the directive
  */
  conditionalView: EmbeddedViewRef<any> | null = null;

  constructor(
    /**
    * The container where the view will be inserted
    */
    private vcr: ViewContainerRef,
    /**
    * The template to render
    */
    private tpl: TemplateRef<any>
  ) {}

  /**
  * This method is called every time the change detection runs
  */
  ngDoCheck() {
    // if the condition is true and the view is not created yet
    if (this.condition && !this.conditionalView) {
      // create the view and insert it in the container
      this.conditionalView = this.vcr.createEmbeddedView(this.tpl);
    } else if (!this.condition && this.conditionalView) {
      // if the condition is false and the view is created
      // destroy the view
      this.conditionalView.destroy();
      this.conditionalView = null;
    }
  } }

This works great! And as you can see, it lets developers like us create powerful structural directives if we want to: the built-in directives offered by Angular are not special in any way.

But this approach has some drawbacks: for example, it is a bit clunky to have an else alternative with *ngIf:

<div *ngIf="condition; else elseBlock">If</div>
<ng-template #elseBlock><div>Else</div></ng-template>

elseBlock is another input of the NgIf directive, of type TemplateRef, that the directive will display if the condition is falsy. But this is not very intuitive to use, so we often see this instead:

<div *ngIf="condition">If</div>
<div *ngIf="!condition">Else</div>

The structural directives are also not perfect type-checking-wise. Even if Angular does some magic (with some special fields called ngTemplateGuard in the directives to help the type-checker), some cases are too tricky to handle. For example, the “else” alternative of *ngIf is not type-checked:

<div *ngIf="!user; else userNotNullBlock">No user</div>
<ng-template #userNotNullBlock>
  <div>
    <!-- should compile as user is not null here -->
    <!-- but it doesn't -->
    {{ user.name }}
  </div>
</ng-template>

NgSwitch is even worse, as it consists of 3 separate directives NgSwitch, NgSwitchCase, and NgSwitchDefault. The compiler has no idea if the NgSwitchCase is used in the right context.

<!-- user.type can be `'user' | 'anonymous'` -->
<ng-container [ngSwitch]="user.type">
  <div *ngSwitchCase="'user'">User</div>
  <!-- compiles even if user.type can't be 'admin' -->
  <div *ngSwitchCase="'admin'">Admin</div>
  <div *ngSwitchDefault>Unknown</div>
</ng-container>

It’s also worth noting that the * syntax is not very intuitive for beginners. And structural directives depend on the ngDoCheck lifecycle hook, which is tied to zone.js. In a future world where our components use the new Signal API and don’t need zone.js anymore, structural directives would still force us to drag zone.js in our bundle.

So, to sum up, structural directives are powerful but have some drawbacks. Fixing these drawbacks would require a lot of work in the compiler and the framework.

That’s why the Angular team decided to introduce a new syntax to write control flow statements in templates!

Control flow syntax

The control flow syntax is a new syntax introduced in Angular v17 to write control flow statements in templates.

The syntax is very similar to some other templating syntaxes you may have met in the past, and even to JavaScript itself. There have been some debates and polling in the community about the various alternatives, and the @-syntax proposal won.

With the control flow syntax, our previous template with *ngIf and *ngFor can be rewritten as:

<h1>Ninja Squad</h1>
@if (condition) {
  <ul>
    @for (user of users; track user.id) {
      <li>{{ user.name }}</li>
    }
  </ul>
}

This syntax is interpreted by the Angular compiler and creates the same views as the previous template, but without the overhead of creating the structural directives, so it is also a tiny bit more performant (as it uses brand new compiled instructions under the hood in the generated code). As this is not directives, the type-checking is also much better.

And, cherry on the cake, the syntax is more powerful than the structural directives!

The drawback is that this syntax uses @, { and } characters with a special meaning, so you can’t use these characters in your templates anymore, and have to use equivalent HTML entities instead (\&#64; for @, \&#123; for {, and \&#125; for }).

If statement

As we saw above, a limitation of NgIf is that it is a bit clunky to have an else alternative. And we can’t have an else if alternative at all.

That’s no longer a problem with the control flow syntax:

@if (condition) {
  <div>condition is true</div>
} @else if (otherCondition) {
  <div>otherCondition is true</div>
} @else {
  <div>condition and otherCondition are false</div>
}

You can still store the result of the condition in a variable if you want to, which is really handy when used with an async pipe for example:

@if (user$ | async; as user) {
  <div>User is {{ user.name }}</div>
} @else if (isAdmin$ | async) {
  <div>User is admin</div>
} @else {
  <div>No user</div>
}

For statement

With the control flow syntax, a for loop needs to specify a track property, which is the equivalent of the trackBy function of *ngFor. Note that this is now mandatory, whereas it was optional with *ngFor. This is for performance reasons, as the Angular team found that very often, developers were not using trackBy when they should have.

<ul>
  @for (user of users; track user.id) {
    <li>{{ user.name }}</li>
  }
</ul>

As you can see, this is a bit easier to use than the trackBy function of *ngFor which requires to write a function. Here we can directly specify the property of the item that is unique, and the compiler will generate the function for us. If you don’t have a unique property, you can still use a function or just use the loop variable itself (which is equivalent to what *ngFor currently does when no trackBy is specified).

One of the very useful additions to the control flow syntax is the handling of empty collections. Previously you had to use an *ngIf to display a message if the collection was null or empty and then use *ngFor to iterate over the collection.

With the control flow syntax, you can do it with an @empty clause:

<ul>
  @for (user of users; track user.id) {
    <li>{{ user.name }}</li>
  } @empty {
    <li>No users</li>
  }
</ul>

We can still access the variables we used to have with *ngFor:

  • $index to get the index of the current item
  • $first to know if the current item is the first one
  • $last to know if the current item is the last one
  • $even to know if the current item is at an even index
  • $odd to know if the current item is at an odd index
  • $count to get the length of the collection

Unlike with *ngFor, you don’t have to alias these variables to use them, but you still can if you need to, for example when using nested loops.

<ul>
  @for (user of users; track user.id; let isOdd = $odd) {
    <li [class.grey]="isOdd">{{ $index }} - {{ user.name }}</li>
  }
</ul>

It is also worth noting that the control flow @for uses a new algorithm under the hood to update the DOM when the collection changes. It should be quite a bit faster than the algorithm used by *ngFor, as it does not allocate intermediate maps in most cases. Combined with the required track property, for loops should be way faster in Angular applications by default.

Switch statement

This is probably where the new type-checking shines the most, as using an impossible value in a case will now throw a compilation error!

@switch (user.type) {
  @case ('user') {
    <div>User</div>
  } @case ('anonymous') {
    <div>Anonymous</div>
  } @default {
    <div>Other</div>
  }
}

Note that the switch statement does not support fall-through, so you can’t have several cases grouped together. It also does not check if all cases are covered, so you won’t get a compilation error if you forget a case. (but I hope it will, add a 👍 on this issue if you want this as well!).

It’s also noteworthy that the @switch statement uses strict equality (===) to compare values, whereas *ngSwitch used to use loose equality (==). Angular v17 introduced a breaking change, and *ngSwitch now uses strict equality too, with a warning in the console during development if you use loose equality:

NG02001: As of Angular v17 the NgSwitch directive 
uses strict equality comparison === instead of == to match different cases. 
Previously the case value "1" matched switch expression value "'1'", 
but this is no longer the case with the stricter equality check.
Your comparison results return different results using === vs. ==
and you should adjust your ngSwitch expression and / or values
to conform with the strict equality requirements.

The future of templating 🚀

The control flow syntax is a new “developer preview” feature introduced in Angular v17, and will probably be the recommended way to write templates in the future (the plan is to make it stable in v18 once it has been battle-tested).

It doesn’t mean that structural directives will be deprecated, but the Angular team will likely focus on the control flow syntax in the future and push them forward as the recommended solution.

We will even have an automated migration to convert structural directives to control flow statements in existing applications. The migration is available in Angular v17 as a developer preview. If you want to give it a try, run:

ng g @angular/core:control-flow

This automatically migrates all your templates to the new syntax! You can also run the migration against a single file with the --path option:

ng g @angular/core:control-flow --path src/app/app.component.html

Even though the new control flow is experimental, v17 comes with a mandatory migration needed to support this new control flow syntax, which consists in converting the @, { and } characters used in your templates to their HTML entities. This migration is run automatically when you update the app with ng update.

The future of Angular is exciting!

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


What's new in Angular 16.2?

Angular 16.2.0 is here!

Angular logo

This is a minor release with some nice features: let’s dive in!

Binding inputs of NgComponentOutlet

It used to be cumbersome to pass input data to a dynamic component (you could do it, but you needed to use a provider and inject it). It’s now way easier:

@Component({
  selector: 'app-user',
  standalone: true,
  template: '{{ name }}'
})
export class UserComponent {
  @Input({ required: true }) name!: string;
}

Then to dynamically insert the component with inputs:

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [NgComponentOutlet],
  template: '<div *ngComponentOutlet="userComponent; inputs: userData"></div>'
})
class AppComponent {
  userComponent = UserComponent;
  userData = { name: 'Cédric' }
}

afterRender and afterNextRender

The afterRender and afterNextRender lifecycle hooks have been added to the framework as developer preview APIs. They are parts of the Signal API, see the RFC discussion.

They allow to run code after the component has been rendered the first time (afterNextRender), or after every render (afterRender).

The first one is useful to run code that needs to access the DOM, like calling a third-party library like we currently do in ngAfterViewInit. But, unlike ngAfterViewInit and other lifecycle methods, these hooks do not run during server-side rendering, which makes them easier to use for SSR applications.

You can now write:

import { Component, ElementRef, ViewChild, afterNextRender } from '@angular/core';

@Component({
  selector: 'app-chart',
  standalone: true,
  template: '<canvas #canvas></canvas>'
})
export class ChartComponent {
  @ViewChild('canvas') canvas!: ElementRef<HTMLCanvasElement>;

  constructor() {
    afterNextRender(() => {
      const ctx = this.canvas.nativeElement;
      new Chart(ctx, { type: 'line', data: { ... } });
    });
  }
}

RouterTestingHarness

The RouterTestingHarness, introduced in v15.2 (check out our blog post), now exposes the underlying fixture, allowing to use its methods and properties and making it compatible with testing libraries that expect a fixture (like ngx-speculoos).

Devtools

Some preliminary work has been done in the framework to trace what is injected in an application in dev mode. This will be used in the future to improve the devtools experience, by providing a way to see what is injected in a component, and where it comes from.

Angular CLI

The CLI has been updated to v16.2.0 as well, with a few new features:

  • the esbuild builder now adds preload hints based on its analysis of the application initial files
  • the esbuild builder can now build the server bundle
  • the esbuild builder now has experimental support for serving the application in SSR mode with the Vite-based dev-server

Summary

That’s all for this release, stay tuned!

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


What's new in Angular 16.1?

Angular 16.1.0 is here!

Angular logo

This is a minor release with some nice features: let’s dive in!

TypeScript 5.1 support

Angular v16.1 now supports TypeScript 5.1. This means that you can use the latest version of TypeScript in your Angular applications. You can check out the TypeScript 5.1 release notes to learn more about the new features.

Transform input values

Angular v16.1 introduces a new transform option in the @Input decorator. It allows transforming the value passed to the input before it is assigned to the property. The transform option takes a function that takes the value as input and returns the transformed value. As the most common use cases are to transform a string to a number or a boolean, Angular provides two built-in functions to do that: numberAttribute and booleanAttribute in @angular/core.

Here is an example of using booleanAttribute:

@Input({ transform: booleanAttribute }) disabled = false;

This will transform the value passed to the input to a boolean so that the following code will work:

<my-component disabled></my-component>
<my-component disabled="true"></my-component>
<!-- Before, only the following was properly working -->
<my-component [disabled]="true"></my-component>

The numberAttribute function works the same way but transforms the value to a number.

@Input({ transform: numberAttribute }) value = 0;

It also allows to define a fallback value, in case the input is not a proper number (default is NaN):

@Input({ transform: (value: unknown) => numberAttribute(value, 42) }) value = 0;

This can then be used like this:

<my-component value="42"></my-component>
<my-component value="not a number"></my-component>
<!-- Before, only the following was properly working -->
<my-component [value]="42"></my-component>

Fetch backend for the Angular HTTP client

The HTTP client has a new backend implementation based on the Fetch API.

This is an experimental and opt-in feature, that you can enable with:

provideHttpClient(withFetch());

It does not support the progress reports on uploads, and of course, requires a browser that supports the Fetch API. The fetch API is also experimental on Node but available without flags from Node 18 onwards.

This is mainly interesting for server-side rendering, as the XHR implementation is not supported natively in Node and requires a polyfill (which has some issues).

Angular CLI

The CLI now has a --force-esbuild option that allows forcing the usage of esbuild for ng serve. It allows trying the esbuild implementation without switching the builder in angular.json (and keeping the Webpack implementation for the ng build command).

The esbuild builder has been improved. It now pre-bundles the dependencies using the underlying Vite mechanism, uses some persistent cache for the TypeScript compilation and Vite pre-bundling, and shows the estimated transfer sizes of the built assets as the Webpack builder does.

Summary

That’s all for this release, stay tuned!

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


2023-05-15

What's new in Vue 3.3?

Vue 3.3.0 is here!

Vue logo

The last minor release was v3.2.0 in August 2021! Since then, we have seen a lot of patch releases, some coming with new features.

Originally, the v3.3 release was supposed to bring Suspense and the Reactivity Transform APIs out of their experimental state.

Is that the case? Let’s see what we have in this release (and some interesting bits from the 47 patches since v3.2.0)!

Hello Reactivity Transform, and goodbye!

During the last year and a half, the Vue team pursued its experiments with ref sugar (see our previous blog post to catch up).

Currently, without ref sugar, you write code like this:

import { ref, computed, watchEffect } from 'vue';
const quantity = ref(0);
const total = computed(() => quantity.value * 10);
watchEffect(() => console.log(`New total ${total.value}`));

Note the .value that you need to access the value of the quantity or total ref. If you use the Composition API, you’re used to it.

The reactivity transform experiment introduced new compiler macros like $ref() and $computed(). When using these, the variable becomes reactive:

import { watchEffect } from 'vue';
const quantity = $ref(0);
const total = $computed(() => quantity * 10);
watchEffect(() => console.log(`New total ${total}`));

And .value was no longer necessary with this syntax!

But it turns out that this experiment is not quite as perfect as hoped initially. It introduced another way to do the same thing, with quite a bit of “magic”, additional pitfalls, and complexity.

So in the end, this experiment is now officially… dropped!

As some teams already started to use it, it will not be removed right away. The plan is to phase these APIs out in a different package, add deprecation warnings in core, and eventually remove them in v3.4.

It doesn’t mean that the team is not thinking about Vue how can be improved. Some new ideas will probably be shared publicly soon.

And a part of the reactivity transform experiment is going to stay: the defineProps destructuration. It’s the part I really liked, so I’m quite happy about it 🤓.

defineProps destructuration

defineProps is the way to declare your props in the script setup syntax (see our article about script setup).

The syntax plays well with TypeScript, but the declaration of default values was a bit painful:

const props = withDefaults(defineProps<{ name?: string }>(), { name: 'Hello '})
console.log(props.name);

You also can’t destructure the props directly, as it loses the reactivity.

With this new release, you can now give default values while destructuring the props and keeping the reactivity!

const { name = 'Hello' } = defineProps<{ name?: string }>()
console.log(name);

If you try to use a destructured prop directly inside a watcher (or to toRef), Vue will issue a warning and indicate to use a getter function instead:

watch(name, () => {});
// "name" is a destructured prop and should not be passed directly to watch().
// Pass a getter () => name instead.

To help with this pattern, a new toValue helper function has been added to convert refs and getters to values:

const v1 = toValue(ref('hello')); // 'hello'
const v2 = toValue(() => 'hello'); // 'hello'

If you want to give it a try, you’ll need to enable the propsDestructure option in your bundler config. For example, in Vite:

plugins: [
  vue({
    script: {
      propsDestructure: true
    }
  })

TypeScript improvements

The TypeScript support of defineProps and other macros has been massively improved, as pretty much all built-in types are now supported (Extract, Exclude, Uppercase, Parameters, etc.). It also can now refer to types and interfaces imported from other files (whereas it was only resolving local types previously).

defineEmits has also been improved, as it now supports a shorter TS declaration. In Vue v3.2, we used to write the type like this:

const emit = defineEmits<{
  (e: 'selected', value: number): void;
}>();
// emit('selected', 14)

There is now a simplified syntax in Vue v3.3. You can use an interface with the events as keys, and the arguments as tuples:

const emit = defineEmits<{
  selected: [value: number]
}>();

Vue 3.3 also allows writing TypeScript directly in templates. It can be handy to hint to Volar that a variable is not null, or of a particular type:

<div>
  <h2>Welcome {{ (user!.name as string).toLowerCase() }}</h2>
</div>

Generic components

script setup components can now have a generic parameter, which works like a generic <T> in TypeScript:

Volar is then capable to throw an error if value is a string and items an array of numbers for example.

Component name inference

When using the script setup syntax, the SFC compiler now infers the component name based on the file name.

So a component declared in a file named Home.vue will automatically have the name Home since v3.2.34.

defineOptions macro

A new macro (a compile-time helper like defineProps and defineEmits) has been introduced to help declare the options of a component. This is available only in script setup component, and can be handy to declare a few things like the name of a component, if the inferred name is not good enough or to set the inheritAttrs option:

defineOptions({ name: 'Home', inheritAttrs: true });

defineSlots macro

Another macro called defineSlots (and a slots option if you’re using defineComponent) has been added to the framework to help declare typed slots. When doing so, Volar will be able to check the slot props of a component. Let’s say an Alert component has a default slot that exposes a close function:

defineSlots<{
  default: (props: { close: () => void }) => void;
}>();

If the Alert component is not used properly, then Volar throws an error:

<Alert><template #default="{ closeAlert }">...</template></Alert>
// error TS2339: Property 'closeAlert' does not exist on type '{ close: () => void; }'.

The returning value of defineProps can be used and is the same object as returned by useSlots.

experimental defineModel macro

When you have a custom form component that just wants to bind the v-model value to a classic input, the prop/event mechanic we saw can be a bit cumbersome:

<template>
  <input :value="modelValue" @input="setValue($event.target.value)" />
</template>

<script setup lang="ts">
defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
function setValue(pickedValue) {
  emit('update:modelValue', pickedValue);
}
</script>

It is now possible to simplify this component, by using the defineModel (experimental) macro:

<template>
  <input v-model="modelValue" />
</template>
<script setup lang="ts">
  const modelValue = defineModel<string>();
</script>

defineModel also accepts a few options:

  • required: true indicates that the prop is required
  • default: value lets specify a default value
  • local: true indicates that the prop is available and mutable even if the parent component did not pass the matching v-model

A useModel helper is also available if you don’t use script setup.

Note that this feature is experimental and opt-in. For example, in Vite:

plugins: [
  vue({
    script: {
      defineModel: true
    }
  })

default value for toRef

It is now possible to define a default value when using toRef():

const order = { quantity: undefined }
const quantity = toRef(order, 'quantity', 1); // quantity is 1

Note that this works only if the value is undefined.

isShallow

A new utility function called isShallow is now available. It allows checking if a variable is deeply reactive (created with ref or reactive) or “shallow” (created with shallowRef or shallowReactive).

v-for and ref

Vue 3 now behaves like Vue 2 used to behave when using ref inside v-for: it populates an array of refs.

<script setup>
import { ref } from 'vue'
const divs = ref([])
</script>

<template>
  <div v-for="i of 3" ref="divs">{{ i }}</div>
  <!-- divs is populated with an array of 3 refs -->
  <!-- one for each HTMLDivElement created -->
  <div>{{ divs }}</div>
</template>

aliases for vnode hook events

Vue allows you to listen for lifecycle events in templates, both for elements and components. The syntax in Vue 3 is @vnodeMounted for example. In Vue v3.3, it is now possible to use @vue:mounted instead, which is a bit more understandable. @vnode hooks are now deprecated.

<script setup>
import { ref } from 'vue'

const isMounted = ref(false)
const onDivMounted = () => isMounted.value = true

const condition = ref(false)
setTimeout(() => condition.value = true, 3000)
</script>

<template>
  <div>isMounted: {{ isMounted }}</div>
  <div @vue:mounted="onDivMounted()" v-if="condition">Hello</div>
</template>

You can try this example in this online demo.

suspensible Suspense

Suspense is still experimental but gained a new prop called suspensible.

The prop allows the suspense to be captured by the parent suspense. That can be useful if you have nested Suspense components, as you can see in the PR explanation.

console available in templates

A small (but useful when debugging) improvement in templates is the possibility to directly use console:

<input @input="console.log($event.target.value)">

To conclude, let’s see what happened in the ecosystem recently.

create-vue

Since Vue v3.2, the Vue team started a new project called create-vue, which is now the recommended way to start a Vue project. You can use it with

npm init vue@next

create-vue is based on Vite v4, and officially replaces Vue CLI.

If you missed it, create-vue recently added the support of Playwright in addition to Cypress for e2e tests! It now also supports TypeScript v5 out of the box.

Router

Vue v3.3 introduced a new function on the object returned by createApp: runWithContext. The function allows using inject with the app as the active instance, and get the value provided by the app providers.

const app = createApp(/* ... */);
app.provide('token', 1);
app.runWithContext(() => inject('token'));

If I mention this in the router section, it’s because it unlocks the possibility to use inject in global navigation guards if you use Vue v3.3 and the router v4.2!

router.beforeEach((to, from) => {
  console.log(inject('token'));
});

Pinia

Pinia is a state-management library from the author of vue-router Eduardo “@posva”. It was meant as an experiment for Vuex v5, but it turns out to be very good, and it’s now the official recommendation for state-management library in Vue 3 projects.

The project moved into the vuejs organization, and there will be no Vuex version 5. Pinia is a really cool project, with a great composition API and TS support, and one of the cutest logos you’ve ever seen.

We added a complete chapter in our ebook to explain how Pinia works if you’re interested 🤓.

Eduardo also released VueFire, the official Firebase bindings for Vue 3. With this library, you can add Firebase to your Vue or Nuxt projects in a few minutes.

Nuxt

After a long development, Nuxt v3 is now stable! It is a really amazing solution and the Nuxt team has been hard at work to provide a great development experience (with some dark magic under the hood). Give it a try if you’re looking for a meta-framework on top of Vue (for example if you need SSR or SSG for your project).

Volar

Volar reached v1.0 recently after a very intense period of development these past months. The TypeScript support is now better than ever, making it a no-brainer to use in your projects.

Vue Test utils

The testing library has a few typings improvements coming in the v2.4 release, and now supports SSR testing via renderToString since v2.3.

Vue 3 in 2023

The Vue team plans to release more frequent minor releases than in the past, so we can expect Vue v3.4 soon. The next releases will be focused on bug fixes and small improvements in the first quarter of the year. Then there should be some improvements for the SSR support in Q2. Finally, the second half of the year should see the first alpha of Vapor. We should hopefully also see Suspense finally getting out of its experimental state.

Vue Vapor is an alternative compilation mode to get better performances. It’s not public yet, but we already know that it is inspired by what Solidjs does, as the reactivity system of Vue and Solid are fairly similar. The idea is to compile a script setup component differently when the “Vapor” mode is enabled, resulting in a lighter rendering code (not using VDOM).

Let’s say we have a classic Counter component:

<script setup lang="ts">
  let count = ref(0)
</script>
<template>
  <div>
    <button @click="count++">{{ count }}</button>
  </div>
</template>

In the current Vue 3 compilation mode, the template is compiled into a function that produces VDOM which is then diffed and rendered (check out the “Under the hood” chapter of our ebook if you want to learn more). In Vapor mode, the template is compiled into a function that only updates what’s necessary in the DOM.

import { ref, effect } from 'vue';
import { setText, template, on } from 'vue/vapor';

let t0 = template('<div><button>');

export default () => {
  const count = ref(0); 
  let div = t0();
  let button = div.firstChild;
  let button_text;
  effect(() => {
    // This is the only part that is executed at runtime when the counter value changes
    setText(button, button_text, count.value);
  });
  on(button, 'click', () => count.value++);
  return div;
}

This “Vapor” mode will be opt-in at the component level, probably for “leaf” components first. To switch a component to Vapor, the current idea is to import it with a .vapor.vue extension:

<script setup lang="ts">
  // 👇 compiles the User component in Vapor mode
  // you get an error if the component is not "Vapor" compatible
  import User from './User.vapor.vue'
</script>
<template>
  <User />
</template>

We’ll be able to enable it for a whole application in the future. The current idea is to call a different createApp function from vue/vapor:

import { createApp } from 'vue/vapor'
import App from './App.vapor.vue'
createApp(App).mount('#app')

When enabled for a full application, the VDOM implementation could be completely dropped from the resulting bundle! We can’t wait to try this!

That’s all for this release. Stay tuned for the next one!

Our ebook, online training and training) are up-to-date with these changes if you want to learn more!


Posts plus anciens