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!


What's new in Angular 16?

Angular 16.0.0 is here!

Angular logo

This is a major release packed with features: let’s dive in!

Angular Signals

As you may have heard, all the hype around Angular is about the addition of Signals to the framework. As this is a big change that will shape how we build Angular applications in the future, we wrote an introduction to Signals, to cover what you can do with them in v16 and what to expect in the future:

👉 Angular Signals

Note that Signals are released as a developer preview in v16 and the API may change in the future.

As Signals are progressing, Angular now allows configuring ZoneJS explicitly with provideZoneChangeDetection:

bootstrapApplication(AppComponent, {
  providers: [provideZoneChangeDetection({eventCoalescing: true})],
});

This opens the door to zoneless applications in the near future, where developers could choose to not include ZoneJS in their application.

Required inputs

Angular v16 added the possibility to mark an input as required, with @Input({ required: true}):

@Input({ required: true }) user!: UserModel;

In that case, if the parent component does not pass the input, then the compiler will throw an error.

This has been a long-awaited feature, we’re happy to see this land in Angular!

Server-Side Rendering and progressive hydration

Angular has been supporting Server-Side Rendering (SSR) for a while now, but it was a bit limited as it was only possible to render the whole application on the server, and then re-render it on the client when the JavaScript bundle was loaded. This was resulting in a flickering when the application loaded, as the DOM was completely wiped out before being re-rendered.

Angular v16 introduces “progressive hydration”, which allows rendering the application on the server, and then progressively hydrate it on the client.

This means that the server-rendered DOM is not wiped out anymore, and the client-side rendering is done progressively, which results in a much smoother experience for the user.

To enable these new behaviors, you simply add provideClientHydration() to your providers:

bootstrapApplication(AppComponent, {
  providers: [provideClientHydration()]
});

The HttpClient has also been updated to be able to store the result of a request done on the server, and then reuse it on the client during the hydration process! The behavior is enabled by default if you use provideClientHydration(), but can be disabled with provideClientHydration(withNoHttpTransferCache()). You can also disable the DOM reuse with withNoDomReuse().

Note that this is a developer preview, and the API may change in the future. There are also a few pitfalls to be aware of. For example, the HTML must be valid when generated on the server (whereas the browser is more forgiving). The DOM must also be the same on the server and the client, so you can’t manipulate the server-rendered DOM before sending it to the client. If some parts of your templates don’t produce the same result on the server and the client, you can skip them by adding ngSkipHydration to the element or component. i18n is also not supported yet, but that should come soon.

When running in development mode, the application will output some stats to the console to help you debug the hydration process:

Angular hydrated 19 component(s) and 68 node(s), 1 component(s) were skipped

You can easily give this a try by using Angular Universal. In the long term, this will probably be part of the CLI directly.

DestroyRef

Angular v16 introduces a new DestroyRef class, which has only one method called onDestroy.

DestroyRef can be injected, and then used to register code that should run on the destruction of the surrounding context.

const destroyRef = inject(DestroyRef);
// register a destroy callback
destroyRef.onDestroy(() => doSomethingOnDestroy());

For example, it can be used to execute code on the destruction of a component or directive (as we do now with ngOnDestroy). But this is more useful for cases where you want to execute code when a component is destroyed, but you don’t have access to the component itself, for example when defining a utility function.

This is exactly what Angular uses internally to implement takeUntilDestroyed, the new RXJS operator introduced in our Signals blog post.

provideServiceWorker

One of the last modules that needed to be transitioned to a standalone provider function was ServiceWorkerModule. It is now done with provideServiceWorker:

bootstrapApplication(AppComponent, {
  providers: [provideServiceWorker('ngsw-worker.js', { enabled: !isDevMode() })]
});

It, of course, accepts the same options as the ServiceWorkerModule. Running ng add @angular/pwa will now add provideServiceWorker to your providers if your application is a standalone one.

TypeScript 5.0 support

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

One important point is that TypeScript now supports the “official” decorator specification. Their “experimental decorators” (based on a much older specification) are still supported but are now considered legacy. One of the differences between these two specifications is that the legacy one supports decorators on parameters, which is used by Angular for dependency injection (with @Optional, @Inject, etc.), and the new one doesn’t.

It is now possible to use the new decorator specification in Angular, but it requires a few changes in your code, as you can’t use decorators on parameters anymore. This can usually be worked around by using the inject() function from @angular/core. There is no rush to use the new decorators instead of the “legacy” ones, but it’s something to keep in mind as I wouldn’t be surprised if we have to migrate away from experimentalDecorators in the future

Styles removal opt-in

Angular v16 introduces a new opt-in feature to remove the styles of a component when its last instance is destroyed.

This will be the default behavior in the future, but you can already opt in with:

{ provide: REMOVE_STYLES_ON_COMPONENT_DESTROY, useValue: true }

Router

Angular v15.2 deprecated the usage of class-based guards and resolvers (check out our blog post for more details). In Angular v16, a migration will run to remove the guard and resolver interfaces from your code (CanActivate, Resolve, etc.).

To help with the conversion, the router now offers helper functions to convert class-based entities to their function-based equivalent:

  • mapToCanActivate
  • mapToCanActivateChild
  • mapToCanDeactivate
  • mapToCanMatch
  • mapToResolve

For example, you can now write:

{ path: 'admin', canActivate: mapToCanActivate([AdminGuard]) };

RouterTestingModule is also getting phased out, and will probably be deprecated and removed in the future. It is not needed anymore, because Angular v16 now provides MockPlatformLocation in BrowserTestingModule by default, which was the main reason to use RouterTestingModule in the first place.

You can now directly use RouterModule.forRoot([]) or providerRouter([]) in your tests.

Last but not least, the router now offers the possibility to bind parameters as inputs.

To do so, you need to configure the router with withComponentInputBinding:

provideRouter(routes, withComponentInputBinding())

With this option, a component can declare an input with the same name as a route parameter, query parameter or data, and Angular will automatically bind the value of the parameter or data to this input.

export class RaceComponent implements OnChanges {
  @Input({ required: true }) raceId!: string;

We can then use this input as a regular input, and react to its change with ngOnChanges or by using a setter for this input:

constructor(private raceService: RaceService) {}

ngOnChanges() {
  this.raceModel$ = this.raceService.get(this.raceId);
}

Angular CLI

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

Summary

That’s all for this release, stay tuned!

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


What's new in Angular CLI 16.0?

Angular CLI 16.0.0 is out!✨

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

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

Standalone applications!

It is now possible to generate standalone applications with the CLI, thanks to the --standalone flag!

ng new my-app --standalone

This is great to start new projects 😍

now that the option is available, we switched all the exercises of our Angular training to use a standalone application with standalone components.

We also re-wrote our ebook entirely to now introduce standalone concepts from the beginning, and add the concept of modules only when it makes sense!

esbuild builder is now in developer preview!

The new builder that uses esbuild has been introduced in v14 but was experimental. It is now in developer preview, so you can give it a try. Even if some options are still unsupported, most of them are now available (autoprefixer and tailwind support have been added), and the builder should be usable.

To check it out in your project, replace @angular-devkit/build-angular:browser with @angular-devkit/build-angular:browser-esbuild, and run ng build. For a small application, on a cold production build (after ng cache clean), the build time went from 13s to 6s on my machine. For a larger application, the build time went from 1m12s to 32s 🤯.

Note that the esbuild builder now uses Vite under the hood for the development server. Even if the integration is not 100% complete and optimized, it is promising to see this Webpack alternative being used in the Angular ecosystem.

Functional guards and resolvers by default

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

Jest experimental support

The CLI now supports Jest as a test runner, but it is still experimental.

To check it out in your project, replace @angular-devkit/build-angular:karma with @angular-devkit/build-angular:jest, and run ng test.

The support is far from being complete, but it is promising. It uses the new esbuild builder under the hood, and the support should improve in the next releases.

Jest does not run in a browser and uses JSDOM instead. That means that you don’t have the same experience that you can have with Karma, but as Karma is now deprecated, an alternative is welcome.

The Angular team also announced that they are working on the support of Web Test Runner, to have an alternative to Karma that runs in a browser.

Bye-bye compileComponents!

The compileComponents call generated in the unit tests of the CLI is now gone, as it was useless since the very beginning if you were running your tests with the CLI. My PR to remove it was merged in the framework, and the CLI now generates tests without it 🤓.

Optional migrations

The CLI now supports optional migrations and will ask you if you want to run them when you upgrade to a new version.

SSR support

A ton of work has been done to improve the SSR story with the CLI, along with what the framework team did with the progressive hydration.

For example, it is now possible to add universal or to generate an app-shell with the CLI for a standalone application.

Summary

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

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


An introduction to Angular Signals

The Angular team has been working on a different way to handle reactivity in applications for the past year. The result of their work is a new concept in Angular called Signals.

Let’s see what they are, how they work, how they interoperate with RxJS, and what they’ll change for Angular developers.

The reasons behind Signals

Signals are a concept used in many other frameworks, like SolidJS, Vue, Preact, and even the venerable KnockoutJS. The idea is to offer a few primitives to define reactive state in applications and to allow the framework to know which components are impacted by a change, rather than having to detect changes on the whole tree of components.

This will be a significant change to how Angular works, as it currently relies on zone.js to detect changes in the whole tree of components by default. Instead, with signals, the framework will only dirty-check the components that are impacted by a change, which of course makes the re-rendering process more efficient.

This opens the door to zoneless applications, i.e. applications where Angular applications don’t need to include Zone.js (which makes them lighter), don’t have to patch all the browser APIs (which makes them start faster) and smarter in their change detection (to only check the components impacted by a change).

Signals are released in Angular v16, as a developer preview API, meaning there might be changes in the naming or behavior in the future. But this preview is only a small part of the changes that will come with signals. In the future, signal-based components with inputs and queries based on signals, and different lifecycle hooks, will be added to Angular. Other APIs like the router parameters and form control values and status, etc. should also be affected.

Signals API

A signal is a function that holds a value that can change over time. To create a signal, Angular offers a signal() function:

import { signal } from '@angular/core';
// define a signal
const count = signal(0);

The type of count is WritableSignal<number>, which is a function that returns a number.

When you want to get the value of a signal, you have to call the created signal:

// get the value of the signal
const value = count();

This can be done both in TypeScript code and in HTML templates:

<p>{{ count() }}</p>

You can also set the value of a signal:

// set the value of the signal
count.set(1);

Or update it:

// update the value of the signal, based on the current value
count.update((value) => value + 1);

There is also a mutate method that can be used to mutate the value of a signal:

// mutate the value of the signal (handy for objects/arrays)
const user = signal({ name: 'JB', favoriteFramework: 'Angular' });
user.mutate((user) => user.name = 'Cédric');

You can also create a readonly signal, that can’t be updated, with asReadonly:

const readonlyCount = count.asReadonly();

Once you have defined signals, you can define computed values that derive from them:

const double = computed(() => count() * 2);

Computed values are automatically computed when one of the signals they depend on changes.

count.set(2);
console.log(double()); // logs 4

Note that they are lazily computed and only re-computed when one of the signals they depend on produces a new value. They are not writable, so you can’t use .set() or .update() or .mutate() on them (their type is Signal<T>).

Under the hood, a signal has a set of subscribers, and when the value of the signal changes, it notifies all its subscribers. Angular does that in a smart way, to avoid recomputing everything when a signal changes, by using internal counters to know which signals have really changed since the last time a computed value was computed.

Finally, you can use the effect function to react to changes in your signals:

// log the value of the count signal when it changes
effect(() => console.log(count()));

An effect returns an object with a destroy method that can be used to stop the effect:

const effectRef: EffectRef = effect(() => console.log(count()));
// stop executing the effect
effectRef.destroy();

This does look like a BehaviorSubject, but it has some subtle differences, the most important one being that unsubscribing is unnecessary thanks to the usage of weak references.

An effect or a computed value will re-evaluate when one of the signals they depend on changes, without any action from the developer. This is usually what you want.

Note that while signals and computed values will be common in Angular applications, effects will be used less often, as they are more advanced and low-level primitive. If you need to react to a signal change, for example, to fetch data from a server, you can use the RxJS interoperability layer that we detail below.

If you want to exclude a signal from the dependencies of an effect or computed value, you can use the untracked function:

const multiplier = signal(2);
// `total` will not re-evaluate when `multiplier` changes
// it only re-evaluates when `count` changes
const total = computed(() => count() * untracked(() => multiplier()));

You can’t write to a signal inside a computed value, as it would create an infinite loop:

// this will throw an error NG0600
const total = computed(() => { multiplier.set(2); return count() * multiplier() });

This is the same in effect, but this can be overridden using allowSignalWrites:

// this will not throw an error
effect(
  () => {
    if (this.count() > 10) {
      this.count.set(0);
    }
  },
  { allowSignalWrites: true }
);

All these features (and terminology) are fairly common in other frameworks, so you won’t be surprised if you used SolidJS or Vue 3 before.

One feature that I think is fairly unique to Angular is the possibility to pass a ValueEqualityFn to the signal function. To decide if a signal changed (and know if a computed or an effect need to run), Angular uses Object.is() by default, but you can pass a custom function to compare the old and new values:

const user = signal({ id: 1, name: 'Cédric' }, { equal: (previousUser, newUser) => previousUser.id === newUser.id });
// upperCaseName will not re-evaluate when the user changes if the ID stays the same
const uppercaseName = computed(() => user().name.toUpperCase());

Signals, components, and change detection

As mentioned above, you can use signals and computed values in your templates:

<p>{{ count() }}</p>

If the counter value changes, Angular detects it and re-renders the component.

But what happens if the component is marked as OnPush? Until now, OnPush meant that Angular would only re-render the component if one of its inputs changed, if an async pipe was used in the template, or if the component used ChangeDetectorRef#markForCheck().

The framework now handles another reason to re-render a component: when a signal changes. Let’s consider the following component (not using a signal, but a simple field that will be updated after 2 seconds):

@Component({
  selector: 'app-user',
  standalone: true,
  templateUrl: './user.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
  count = 0;

  constructor() {
    // set the counter to 1 after 2 seconds        
    setTimeout(() => this.count = 1, 2000);
  }
}

As the component is OnPush, using a setTimeout will trigger a change detection, but the component will not be re-rendered (as it won’t be marked as “dirty”).

But if we use a signal instead:

@Component({
  selector: 'app-user',
  standalone: true,
  templateUrl: './user.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
  count = signal(0);

  constructor() {
    // set the counter to 1 after 2 seconds        
    setTimeout(() => this.count.set(1), 2000);
  }
}

Then the component will be re-rendered after 2 seconds, as Angular detects the signal change and refresh the template.

Under the hood, if a view reads a signal value, then Angular marks the template of the component as a consumer of the signal. Then, when the signal value changes, it marks the component and all its ancestors as “dirty”.

For us developers, it means that we can use signals in our components as soon as Angular v16 is released, and we don’t need to worry about change detection, even if the components are using OnPush.

Sharing a signal between components

In the previous example, the signal was defined in the component itself. But what if we want to share a signal between multiple components?

In the Vue ecosystem, you frequently encounter the pattern of “composables”: functions that return an object containing signals, computed values, and functions to manipulate them. If a signal needs to be shared, it is defined outside of the function and returned by the function:

const count = signal(0);
export function useCount() {
  return {
    count,
    increment: () => count.set(count() + 1)
  };
}

In Angular, we can do the same and we can also use services instead of functions (and it’s probably what we’ll do). We can define a CountService as the following:

@Injectable({ providedIn: 'root' })
export class CountService {
  count = signal(0);
}

Then, in our components, we can inject the service and use the signal:

@Component({
  selector: 'app-user',
  standalone: true,
  templateUrl: './user.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
  count = inject(CountService).count;
}

The service could also define computed values, effects, methods to manipulate the signals, etc.

@Injectable({ providedIn: 'root' })
export class CountService {
  count = signal(0);
  double = computed(() => this.count() * 2);

  constructor() {
    effect(() => console.log(this.count()));
  }

  increment() {
    this.count.update(value => value + 1);
  }
}

Memory leaks

Signal consumers and producers are linked together using weak references, which means that if all the consumers of a signal cease to exist, then the signal will be garbage collected as well. In other terms: no need to worry about “unsubscribing” from a signal to prevent memory leaks, as we have to do with RxJS observables \o/.

You can also use an effect in your component (even if that’s probably going to be very rare) to watch a value and react to its changes, for example, the count defined in a service like the above.

Note that you don’t have to manually stop an effect when the component is destroyed, as Angular will do it for you to prevent memory leaks. Under the hood, an effect uses a DestroyRef, a new feature introduced in Angular v16, to automatically be cleaned up when the component or service is destroyed.

You can change this behavior though, by creating an effect with a specific option:

this.logEffect = effect(() => console.log(count()), { manualCleanup: true });

In this case, you will have to manually stop the effect when the component is destroyed:

ngOnDestroy() {
  this.logEffect.destroy();
}

Effects can also receive a cleanup function, that is run when the effect runs again. This can be handy when you need to stop a previous action before starting a new one. In the example below, we start an interval that runs every count milliseconds, and we want to stop it and start a new one when the count changes:

this.intervalEffect = effect(
  (onCleanup) => {
    const intervalId = setInterval(() => console.log(`count in intervalEffect ${this.count()}`), this.count());
    return onCleanup(() => clearInterval(intervalId))
  }
)

Note that effects run during change detection, so they’re not a good place to set signal values. That’s why you get an error from Angular if you try to do so:

// ERROR: Cannot set a signal value during change detection

Effects will probably be used very rarely, but they can be handy in some cases:

  • logging / tracing;
  • synchronizing state to the DOM or to a storage, etc.

Signals and RxJS interoperability

RxJS is here to stay, even if its usage might be more limited in the future. Angular is not going to remove RxJS, and it’s not going to force us to use signals instead of observables. In fact, RxJS is probably a better way to react to signal changes than using effects.

We can use signals and observables together, and we can convert one into the other. Two functions to do that are available in the brand new @angular/core/rxjs-interop package.

To convert a signal to an observable, we can use the toObservable function:

const count$ = toObservable(count);

Note that the created observable will not receive all the value changes of the signal, as this is done using an effect under the hood, and effects are only run during change detection:

const count = signal(0);
const count$ = toObservable(count);
count$.subscribe(value => console.log(value));
count.set(1);
count.set(2);
// logs only 2, not 0 and 1, as this is the value when the under-the-hood effect runs

To convert an observable to a signal, we can use the toSignal method:

const count = toSignal(count$);

The signal will contain the last value emitted by the observable, or will throw an error if the observable emits an error. Note that the subscription created by toSignal() is automatically unsubscribed when the component that declared it is destroyed. As observables can be asynchronous, you can pass an initial value to the function:

const count = toSignal(count$, { initialValue: 0 });

If you do not provide an initial value, the value is undefined if it is read before the observable emits a value. You can also use the option requireSync to make the signal throw an error if it is read before the observable emits a value:

const count = toSignal(count$, { requireSync: true });

Signal-based components

In the future (v17? v18?), we’ll be able to build a component entirely based on signals, even for its inputs and queries. The framework would be notified when an expression has changed thanks to the signals, and would thus only need to dirty-checks the components affected by the change, without having to check for changes on unrelated components, without the need for zone.js. It will even be able to re-render only the part of the template that has changed, instead of checking the whole template of a component as it currently does. But there is a long way ahead, as several things need to be rethought in the framework to make this work (inputs, outputs, queries, lifecycle methods, etc).

An RFC is out with the details of the proposal, and you can follow the progress on the Angular repository.

Currently, the RFC proposes to use signals: true to mark a component as “Signal-based”:

@Component({
  signals: true,
  selector: 'temperature-calc',
  template: `
    <p>C: {{ celsius() }}</p>
    <p>F: {{ fahrenheit() }}</p>
  `,
})
export class SimpleCounter {
  celsius = signal(25);

  // The computed only re-evaluates if celsius() changes.
  fahrenheit = computed(() => this.celsius() * 1.8 + 32);
}

Inputs, outputs, and queries would be defined via functions instead of decorators in these components, and would return a signal:

firstName = input<string>(); // Signal<string|undefined>

Nothing has been implemented for this part yet, so you can’t try this in v16. But you can already try to use signals in existing components, as we mentioned above (but keep in mind they are not production ready)

It’s anyway quite interesting how frameworks inspire each other, with Angular taking inspiration from Vue and SolidJS for the reactivity part, whereas other frameworks are increasingly adopting the template compilation approach of Angular, with no Virtual DOM needed at runtime.

The future of Angular is exciting!

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


What's new in Angular 15.2?

Angular 15.2.0 is here!

Angular logo

This is a minor release with some interesting features and some big news: let’s dive in!

Easily migrate to standalone components!

The Angular team is releasing a set of schematics to automatically migrate your application to standalone components. It does an amazing job at analyzing your code, migrating your components/pipes/directives to their standalone versions, and removing the obsolete modules of your application 😍.

Sounds interesting? We wrote a guide about it:

👉 Migrate to standalone components with Angular schematics

Angular Signals

The Angular team has been working on a different way to handle reactivity in your application for the past year. The first step of the result has been publicly released (even if there is nothing to use yet, as the API will only be available in v16): the discussion about Angular Signals.

Signals are a concept that is used in many other frameworks, like SolidJS, Vue, Preact and even the venerable KnockoutJS. The idea is to offer a few primitives to define reactive state in your application and to allow the framework to know which components are impacted by a change, rather than having to detect changes on the whole tree of components.

This would be a significative change to how Angular works, as it currently relies on zone.js to detect changes in the whole tree of components by default. Instead, with signals, the framework would only re-render the components that are impacted by a change.

This also opens the door to zoneless applications, i.e. applications where Angular applications don’t need to include Zone.js (which makes them lighter), and don’t have to patch all the browser APIs (which makes them start faster).

The first draft of the API is available and looks like this:

// define a signal
const count = signal(0);
// get the value of the signal
const value = count();
// set the value of the signal
count.set(1);
// update the value of the signal, based on current value
count.update((value) => value + 1);
// mutate the value of the signal (handy for objects/arrays)
const user = signal({ name: 'JB', favoriteFramework: 'Angular' });
user.mutate((user) => user.name = 'Cédric');

Once you have defined signals, you can define computed values that derive from them:

const double = computed(() => count() * 2);

Computed values are automatically computed when one of the signals they depend on changes.

count.set(2);
console.log(double()); // logs 4

Note that they are lazily computed and only re-computed when one of the signals they depend on produces a new value.

Finally, you can use the effect function to react to changes in your signals:

// log the value of the count signal when it changes
effect(() => console.log(count()));

This does look like a BehaviorSubject, but it has some subtle differences, the most important one being that unsubscribing is unnecessary thanks to the usage of weak references.

That’s pretty much it for now! The next step is to integrate this API with the framework, and make it interoperate with RxJS.

In an ideal future, we may be able to build a component with fields that are signals and computed values used in the template. The framework would be notified when an expression has changed thanks to the signals, and would thus only need to re-render the components affected by the change, without having to check for changes on unrelated components, without the need for zone.js. But there is a long way ahead, as several things needs to be rethought in the framework to make this work (what about inputs, outputs, queries, lifecycle methods, etc?).

This is anyway an exciting project, and it’s quite interesting how frameworks inspire each others, with Angular taking inspiration from Vue and SolidJS for the reactivity part, whereas other frameworks are increasingly adopting the template compilation approach of Angular, with no Virtual DOM needed at runtime.

Deprecation of class-based guards and resolvers

The class-based guards and resolvers are now officially deprecated on a route definition. As you may know, it is possible to write them as functions since Angular v14.2 (check out our blog post about that).

You can migrate your guards and resolvers to functions fairly easily or you can simply wrap the class with inject() as a quick way to get rid of the deprecation warning:

{ path: 'users', component: UsersComponent, canActivate: () => inject(LoggedInGuard).canActivate() }

Note that the CanActivate, CanDeactivate, etc interfaces will be deleted in a future version of Angular.

RouterTestingHarness

The RouterTestingModule now provides a RouterTestingHarness that can be used to write tests. It can be handy to test components that expect an ActivatedRoute for example, or when you want to trigger navigations in your tests to test guards or resolvers.

RouterTestingHarness has a static method create that can be called with an optional initial navigation. This method returns a promise of the created harness, that can then be used to trigger navigations, using navigateByUrl.

// load the routes in the TestBed
TestBed.configureTestingModule({
  imports: [RouterTestingModule.withRoutes(routes)],
});
// create the harness
const harness = await RouterTestingHarness.create();
// explicitly cast the component returned with `<UserComponent>`
const component = await harness.navigateByUrl<UserComponent>('/users/1');
// or pass the type as the second argument
// in that case, the test fails if the component is not of the expected type when navigating to /users/1
const component = await harness.navigateByUrl('/users/1', UserComponent);

The harness provides a routeDebugElement property that returns the DebugElement of the component you navigated to, and a routeNativeElement property that returns the native element of the component. If you want to get the component instance, you can either get it as the return value of navigateByUrl, or by accessing harness.routeDebugElement.componentInstance.

The harness does not have a property to access the ComponentFixture as we usually have in tests, but directly provides a detectChanges method that will trigger change detection on the component.

const harness = await RouterTestingHarness.create();
const component = await harness.navigateByUrl('/users/1', UserComponent);
component.name = 'Cédric';
harness.detectChanges();
expect(harness.routeNativeElement!.querySelector('#name')!.textContent).toBe('Cédric');

withNavigationErrorHandler

A new feature called withNavigationErrorHandler has been added to the router. It can be used in provideRouter to provide a custom error handler for navigation errors.

provideRouter(routes, withNavigationErrorHandler((error: NavigationError) => {
  // do something with the error
}))

This is roughly equivalent to the (now deprecated) errorHandler you could configure on the RouterModule.

NgOptimizedImage

NgOptimizedImage has a new loaderParams input that accepts an object.

<!-- params = { isBlackAndWhite: true } for example -->
<img [ngSrc]="source" [loaderParams]="params"></img>

This object will be passed to your custom loader when it is called, as a property loaderParams in the ImageLoaderConfig.

const customLoader = (config: ImageLoaderConfig) => {
    const { loaderParams } = config;
    // do something with loaderParams        
};

Performances

The NgClass directive has been rewritten to improve performances. Its algorithm is now a bit smarter and triggers less change detections and DOM updates. You don’t have to change anything, you’ll get that for free when upgrading 😍.

Angular CLI

The CLI had few changes in this release, so no dedicated article this time.

The esbuild builder now supports Less stylesheets, CommonJS dependency checks and node modules license extraction. Maybe more importantly, it now uses the new incremental rebuild of esbuild, introduced in esbuild v0.17. Watch mode should now be even faster.

Another tiny new feature: ng update now logs the number of files modified by the migrations.

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!


How to migrate an Angular application to standalone components?

Angular 14 introduced standalone components and optional modules. But it can be quite a daunting task to migrate an existing application to this new model!

Angular logo

This guide supposes that you are familiar with the new standalone components and optional modules introduced in Angular 14. If not, you can read our dedicated article.

The Angular team has been working on a collection of schematics to help you migrate your application to standalone components. These schematics are available in Angular v15.2.0 and above. So the first step is to update your application to the latest version of Angular. Then we’re good to go!

Let’s dive in.

Schematics to the rescue

The schematics are available in the @angular/core package.

To run them, enter:

ng generate @angular/core:standalone

The schematics expects two arguments:

  • the path to the application you want to migrate (by default ‘./’)
  • the mode of the schematic (by default ‘convert-to-standalone’)

There are three modes available:

  • convert-to-standalone: this is the default mode, and it will convert all your components to standalone components, except the ones declared in your main module.
  • prune-ng-modules: this mode will remove all the modules that aren’t necessary anymore.
  • standalone-bootstrap: this mode will bootstrap your application with the bootstrapApplication function, and migrate the components referenced in your main module.

To fully run a migration, you need to run the schematics in the three modes consecutively.

Convert to standalone

The first mode will convert all your components to standalone components, except the ones referenced in the bootstrap field of your main module. It also updates the related unit tests.

As this is the default mode, you can run:

ng generate @angular/core:standalone --defaults

The schematic is quite smart, as it compiles the template of each component to detect what the standalone version of the component needs to import.

For example, if you have a component that uses the NgIf, RouterLink and FormControl directives, the schematic will add NgIf, RouterLink (as they are standalone directives) and ReactiveFormModule (as FormControl is available via this module) to the list of imports of the standalone component (and add the necessary TypeScript imports). It also works with your own components, pipes and directives, and the ones from third-party libraries of course.

Be warned though: the schematic can’t target a specific component or module, so it generates a ton of changes in your application. It also generates some “noise”: some files are modified but not really changed, because the schematic sometimes reformats the code.

To avoid this, I strongly advise you to add a formatter to your project, for example Prettier. If you want to learn how, we have a dedicated article about how to add ESLint and Prettier to your Angular project.

This allows you to run ng lint --fix after the schematic, to only focus on the real changes.

All your entities are now standalone components, pipes and directives. The schematic also updates the modules of your application, by moving the migrated entities from the declarations array, to the imports array.

At the end of this step, you’ll have most of your components migrated to standalone components, but you’ll still have your existing modules. The application should still work if you run ng serve, ng test, etc.

Prune the modules

The second mode will remove all the modules that aren’t necessary anymore.

To run it, enter:

ng generate @angular/core:standalone --defaults --mode=prune-ng-modules

The schematic can remove a module only if:

  • it doesn’t have any declarations, providers or bootstrap
  • it doesn’t have any code in its constructor, or other methods
  • it doesn’t have any imports that reference a ModuleWithProviders

If your module has providers, you can usually move them.

This last one means that modules that import a module with providers (like RouterModule.forChild) can’t be removed without a bit of work first.

Typically, if you lazy-loaded modules, you have a module that looks like this:

@NgModule({
  imports: [RouterModule.forChild(adminRoutes)]
})
export class AdminModule { }

With the routes of the module declared like this:

export const adminRoutes: Routes = [
  { path: '', component: AdminComponent }
];

And a main route file that lazy-load the module:

const routes: Routes = [
  { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }
];

You then need to manually migrate this to lad the routes directly:

const routes: Routes = [
  { path: 'admin', loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes)
];

When this is done, you can remove RouterModule.forChildfrom the imports of the admin module, and manually delete the AdminModule if it isn’t referenced elsewhere.

Sometimes, your module is referenced somewhere else in your application. In that case, the call won’t be removed by the schematic. The schematic adds a comment with a TODO

console.log(/* TODO(standalone-migration): clean up removed NgModule reference manually */ AdminModule);

You can look into your codebase to see where AdminModule is referenced and remove it manually.

Bootstrap the application

The last mode will update your application to use the bootstrapApplication function. It will convert the main module of your application and all its components/pipes/directives to standalone components. It also converts the imports of other modules to importProvidersFrom calls. When it can, it uses the appropriate provide...() function to import the providers: for example provideRouter() for the RouterModule, or provideHttpClient() for the HttpClientModule 😍.

To run it, enter:

ng generate @angular/core:standalone --defaults --mode=standalone-bootstrap

After this step, your application is fully migrated to standalone components. You can then do a bit of cleanup in your codebase with ng lint --fix and check that everything is still working.

Your tests will need to be updated though: the schematics can’t analyze them (as tests are not compiled in AoT mode). The schematics tries to do its best to update them, and moves the declarations to the imports of the testing module, but you usually have to do some manual work to make them work again.

A strategy for large applications

As this can be a daunting task in a large project, where thousands of tests are affected, you can try to approach this migration in small steps.

Even if the migration runs on the whole project, you can then use your version control system to revert the changes on all modules except one. I usually start with the “shared” module (that almost all projects have), which usually contains components/pipes/directives that are used in many places and fairly easy to migrate as they are “leaves” of the application.

Then, I lint the code, commit the changes, run the tests and fix them.

Once this is done, I can move on to the next module, starting with the small ones and progressively moving to the bigger ones. One a module is migrated, I migrate its routes configuration, in order to delete the module.

Rinse and repeat until you’re done! Your application now uses standalone components 🎉.

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 15.1?

Angular 15.1.0 is here!

Angular logo

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

TypeScript 4.9

TypeScript 4.9 is now supported by Angular. This new version of TypeScript brings some new features, as you can see in the official blog post, including the new satisfies operator.

Templates

It is now possible to use self-closing tags on custom elements. This is a small but nice improvement, as it allows to write:

<my-component />

instead of:

<my-component></my-component>

This is also applicable to ng-content and ng-container for example.

Fun fact: this might not seem like a big change, but this is actually the first time that Angular allows a syntax that is not HTML compliant in templates. Until now, templates were always valid HTML (yes, even, the binding syntax [src]!). But as Angular templates are never parsed by the browser, the Angular team decided to allow this syntax, and it will probably be extended in the future to other non-HTML compliant syntaxes that can improve the developer experience.

Router

The CanLoad guard is now officially deprecated and replaced by the recently introduced CanMatch guard. They both achieve the same goal (prevent loading the children of a route), but the CanMatch guard can also match another route when it rejects. CanLoad was also only running once, whereas CanMatch runs on every navigation as the CanActivate guard.

It is now possible to define the onSameUrlNavigation option for a specific navigation to specify what to do when the user navigates to the same URL as the current one, with two possible values: reload and ignore. This was previously only possible globally with the RouterConfigOptions of the router (or withRouterConfig if you’re using the standalone router providers).

You can now do something like:

this.router.navigateByUrl('/user', { onSameUrlNavigation: 'reload' })

The router also gained a new event NavigationSkipped that is emitted when a navigation is skipped because the user navigated to the same URL as the current one or if UrlHandlingStrategy ignored it.

A new withHashLocation() function has been added to the router to configure the router to use a hash location strategy. It was previously configured via DI { provide: LocationStrategy, useClass: HashLocationStrategy }. You can now write:

providers: [provideRouter(routes, withHashLocation())]

Core

A new function isStandalone() was added to check if a component, directive or pipe is standalone or not.

const isStandalone = isStandalone(UserComponent);

Tests

The TestBed now has a new method runInInjectionContext to easily run a function that uses inject(). This was already possible via the verbose TestBed.inject(EnvironmentInjector).runInContext(). This is especially useful when you want to test a functional guard or resolver for example, and this is what the CLI now generates by default for the tests of these entities.

Angular CLI

As usual, you can check out our dedicated article about the new CLI version:

👉 Angular CLI v15.1

Summary

The roadmap includes work on the CLI to be able to generate standalone applications without modules. It also mentions some efforts on the server-side rendering story, which is not the strong suit of Angular (compared to other mainstream frameworks) and the possibility to use Angular without zone.js.

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 15.1?

Angular CLI 15.1.0 is out!✨

If you want to upgrade to 15.1.0 without pain (or to any other version, by the way), I have created a Github project to help: angular-cli-diff. Choose the version you’re currently using (14.2.0 for example), and the target version (15.1.0 for example), and it gives you a diff of all files created by the CLI: angular-cli-diff/compare/14.2.0…15.1.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.

esbuild builder improvements

The new experimental builder that uses esbuild has been introduced in v14, but with some missing features.

Even if it is still not feature complete, the esbuild builder is making progress. It now supports Sass inline styles. Less is not supported yet. It also supports the --stats-json option which is useful to generate a JSON file with the build stats.

To check it out in your project, replace @angular-devkit/build-angular:browser with @angular-devkit/build-angular:browser-esbuild, and run ng build. For a small application, on a cold production build (after ng cache clean), the build time went from 13s to 6s on my machine. For a larger application, the build time went from 1m12s to 32s 🤯.

ng generate environments

A new schematic has been added to add environment files to an application. If you are a returning reader, you know that the environment files have been removed from a new application in v15, and you now have to add them yourself.

The new schematic can be used to simplify the task:

ng generate environments

This will create the environments folder with the environment.ts and environment.development.ts files, and update the angular.json file to use the development environment file when using the development configuration:

"fileReplacements": [
  {
    "replace": "src/environments/environment.ts",
    "with": "src/environments/environment.development.ts"
  }
]

Notice that this is different from what the CLI used to generate by default, where the environment.ts file was used for the development configuration, and an environment.prod.ts file was used for the production configuration. The CLI team thinks this is more aligned with the current naming choices, as prod is not a configuration.

ng generate config

Another new schematic has been added to generate the configuration files that are now hidden by default since v15.

You can use:

ng generate config karma

to generate the karma.conf.js file, and:

ng generate config browserslist

to generate the .browserslistrc file.

ng generate interceptor

ng generate interceptor can now generate an interceptor with the --functional option. This will generate a functional interceptor, which is a new feature in Angular v15.

ng generate interceptor --functional authentication

ng generate guard

ng generate guard now supports the --guardType option, which is the same as --implements, but makes more sense when you’re generating a functional guard (as they are not implementing an interface).

ng generate guard --functional --guardType CanActivate logged-in

The guards as classes are probably going to be deprecated really soon, so --functional will be the default and --implements will be removed.

Summary

That’s all for the CLI v15.1 release! You’ll find more interesting features in our article about the framework v15.1.0 release.

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


Posts plus anciens