Angular 17: What’s new?

Share on facebook
Share on google
Share on twitter
Share on linkedin

Angular has since the latest major version started what the Angular team calls a renaissance, which means a radical renewal of the framework. The main goals of the latest updates have been to improve the developer experience and performance so it aligns more with the other leading front-end frameworks in the space by introducing new features such as signals and standalone components.

This post will cover the latest features of Angular 17.

Signals

Signals was introduced as a developer preview in Angular 16. It is a reactive state management solution for synchronous reactivity with a lower learning curve than RxJS.

Angular 17 comes with more mature upgrades to Signals and it is now ready to be used in production.

Local change detection

One of the main reasons for signals was a more fine-grained way of doing change detection only on the parts of the DOM that should change from the state change. With the traditional ZoneJS change detection, Angular was doing full top-down change detection every time one of the ZoneJS patched events was triggered:

This can be optimized using OnPush change detection so it performs less change detection:

But still, there is a lot of dirty checking and change detection being performed on unrelated components eg. when interacting with a component and all the parent components have to be dirty changed and performed change detection on.

Obviously, this is not very efficient, especially if you compare it to the other leading frontend frameworks this made Angular look like a dinosaur. So they borrowed Signals from SolidJS and you will since Angular 17 get local change detection when you use Signals in a component with OnPush change detection (skipping the parent checks).

Now, if you combine OnPush change detection with Signals you can have local change detection for only the affected component:

As said, other components with default change detection will still be checked but the components behind OnPush change detection are not concerned. Therefore, always use OnPush as your change detection strategy in your components for the best performance.

All you have to do to get this local change detection is to declare the change detection strategy of a component as OnPush and use signals as state sources in the component template.

New control flow in templates

Previously, Angular used to only allow control flows using structural directives eg. *ngIf and *ngFor. The problem with these existing control flows was that they had to be attached to a DOM element and didn’t stand out from the HTML code.

The new approach looks more like how we know it from server rendering frameworks like ASP.NET with the @ syntax:

@Component({
	selector: 'app-todo-list',
	template: `@if (this.isLoading() === false) {

<div class="todo-list-wrapper">
	<div class="mx-auto col-10">
		<h5>{{ 'todo-list' | translate }}</h5>
		<hr />
		<app-cards-list [tableRef]="todoListRef" [cardRef]="todoItemCardRef" [data]="todoList()"></app-cards-list>

		<hr />
		<div>
			{{ 'todo-list-section.todos-duedate-today' | translate }}:
			{{ todoList() | duedateTodayCount }}
		</div>
		<hr />
		<app-add-todo-reactive-forms [currentTodo]="selectedTodo$ | async" [isSavingTodo]="isSavingTodo$ | async"
			(saveTodo)="onSaveTodo($event)"></app-add-todo-reactive-forms>
	</div>
</div>
} @else {
<app-spinner [message]="'Getting todo items'"></app-spinner>
}

<ng-template #todoItemCardRef let-todo="data">
	<app-todo-item-card [todoItem]="todo" (todoDelete)="deleteTodo($event)" (todoEdit)="selectTodoForEdit($event)"
		(todoCompleteToggled)="todoCompleteToggled($event)"></app-todo-item-card>
</ng-template>

<ng-template #todoListRef let-todos="data">
	<ul class="list-group mb-3">
		@for (todo of todos; track todo.id) {
		<app-crud-item [todoItem]="todo" (todoDelete)="deleteTodo($event)" (todoEdit)="selectTodoForEdit($event)"
			(todoCompleteToggled)="todoCompleteToggled($event)"></app-crud-item>
		}
	</ul>
</ng-template>`,
	standalone: true,
	imports: [SharedModule, DuedateTodayCountPipe],
})
export class TodoListComponent {
	public selectedTodo$ = this.todoListFacadeService.selectedTodo$;
	public todoList = this.todoListFacadeService.todoList;
	public isLoading = this.todoListFacadeService.isLoading;
	public isSavingTodo$ = this.todoListFacadeService.isSavingTodo$;

We see how it stands out more clearly from the existing code.

If else

Before, doing an if else in Angular was rather… Clumsy. You had to reference another template reference in the else which made the code rather incoherent.

With the new control flow, it is much more intuitive to how an if/else control flow would like in any other programming language:

@if (this.isLoading() === false) {
		<!-- Some content -->
} @else {
<app-spinner [message]="'Getting todo items'"></app-spinner>
}

For loop

The for loop is also more intuitive and aligned with how you would do in other frameworks:

		@for (todo of todos; track todo.id) {
		<app-crud-item [todoItem]="todo" (todoDelete)="deleteTodo($event)" (todoEdit)="selectTodoForEdit($event)"
			(todoCompleteToggled)="todoCompleteToggled($event)"></app-crud-item>
		}

Switch case

Switch case especially shows how the new control flow is beneficial as it makes the HTML code stand out even with this control flow:

@switch (getQuestionRendering(question).toString()) {
  @case ('TEXTBOX') {
    <input [formControlName]="question.externalQuestionId" [id]="question.externalQuestionId" />
  }
  @case ('NUMBER') {
   <input appNumberInput [formControlName]="question.externalQuestionId" [id]="question.externalQuestionId" />
  }

Migrate application to new control flow

You can automatically migrate your application to the new control flow using this schematic:

ng g @angular/core:control-flow

Delayed loading

A common technique to make your websites load faster is to defer everything “below the fold” (outside of the viewport). Before, you had to use many libraries for this or do it yourself with a directive and the intersection observer API.

Since Angular 17 Angular now has this built into the new control flow with @defer. We can then simply defer the load until it is visible in the viewport like:

import { Component } from '@angular/core';

@Component({
    selector: 'app-home',
    template: `
        <app-navbar></app-navbar>
        <app-above-page-content></app-above-page-content>

        @defer (on viewport) {
            <app-page-content>
            </app-page-content>
        } @placeholder {
            <app-skeleton></app-skeleton>
        } @error {
            <app-error message="loading failed"></app-error>
        }
    `
})

export class HomeComponent {
}

Using @defer delays the loading of the enclosed content until a specific event happens.

Defer API


The defer control flow follows this syntax:

@defer (on/when *trigger*; prefetch on *trigger*)

The @defer triggers encompass two distinct types:

  1. Declarative (on): Utilizes one of the available behaviors, as detailed below.
  2. Imperative (when): Relies on custom logic, such as a component property, method, Signal, RxJs stream, etc., returning true or false.

Declarative “on” triggers: Explore the declarative @defer (on <trigger>) {} triggers, ordered from the most eager to the laziest or custom options:

  • immediate: Triggers component lazy loading immediately during the parent component template execution.
  • idle (default): Angular lazily loads the component on the first available requestIdleCallback (browser API), enabling background and low-priority tasks on the main event loop.
  • timer(delay): Loads the component after a specified delay.
  • viewport (target): Activates when the @placeholder (explained below) or an optional target is in the viewport, detected using the browser’s IntersectionObserver API.
  • hover (target?): Initiates when the @placeholder or optional target is hovered by the user, with Angular considering mouseenter and focusin DOM events.
  • interaction (target?): Triggers when the @placeholder or optional target is interacted with by the user, with Angular recognizing click and keydown DOM events for this purpose.

Build performance with esbuild

Previously Angular relied on Webpack for bundling but there are faster alternatives. Angular now supports esbuild which is a much faster bundler:

You can simply switch to esbuild by changing your Angular build executor:

"builder" : "@angular-devkit/build-angular:browser-esbuild"

SSR improvements

It’s simple to set up an Angular app with SSR. simply run ng new new-app –ssr and you have a new Angular app with SSR.

You can also add SSR to an existing app by simply run:

ng add @angular/ssr

Conclusion

There are many interesting new features in Angular 17. Not only are Signals more mature and come with more benefits such as local change detection but also the control flow of the framework is more aligned with what you see from other frameworks. The defer feature is something that you before had to use third-party libraries to accomplish but now it’s all built in.

Do you want to become an Angular architect? Check out Angular Architect Accelerator.

Related Posts and Comments

How to do Cypress component testing for Angular apps with MSW

In this post, we will cover how to do Cypress Component testing with MSW (mock service worker) and why it’s beneficial to have a mock environment with MSW. The mock environment My recommendation for most enterprise projects is to have a mocking environment as it serves the following purposes : * The front end can

Read More »

Handling Authentication with Supabase, Analog and tRPC

In this video, I cover how to handle authentication with Supabase, Analog and tRPC. It’s based on my Angular Global Summit talk about the SPARTAN stack you can find on my blog as well. Code snippets Create the auth client Do you want to become an Angular architect? Check out Angular Architect Accelerator.

Read More »