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!



blog comments powered by Disqus