Image for post: Angular 17: more than a new release!

Angular 17: more than a new release!

Angular 17, the latest version of the popular front-end framework, has arrived with a host of new features and improvements that are set to revolutionize the way developers build web applications. Introduced and maintained by Google, this release represents a significant step forward in the framework's evolution, addressing the ever-evolving needs of modern software development.

Streamlined control flow

One of the standout features of Angular 17 is the introduction of a new declarative control flow mechanism. The framework now offers built-in constructs such as @for, @if, and @switch, which closely resemble the syntax used in JavaScript and Python. This shift from the traditional *ngIf and *ngFor directives not only makes the code more readable and maintainable but also significantly enhances its performance.

The new @if Syntax

The new @if block replaces the traditional *ngIf directive with a more intuitive syntax:

Before (Angular 16 and earlier):

<div *ngIf="user; else loading">
  <h2>Welcome, {{ user.name }}</h2>
</div>
<ng-template #loading>
  <p>Loading user data...</p>
</ng-template>

After (Angular 17):

@if (user) {
  <div>
    <h2>Welcome, {{ user.name }}</h2>
  </div>
} @else {
  <p>Loading user data...</p>
}

You can also chain conditions with @else if:

@if (status === 'loading') {
  <app-spinner />
} @else if (status === 'error') {
  <app-error-message [error]="errorDetails" />
} @else if (status === 'success') {
  <app-dashboard [data]="responseData" />
} @else {
  <p>Unknown state</p>
}

The new @for syntax

The @for block replaces *ngFor with enhanced performance through built-in track optimization:

Before (Angular 16 and earlier):

<ul>
  <li *ngFor="let item of items; trackBy: trackByFn">
    {{ item.name }}
  </li>
</ul>

After (Angular 17):

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

The track expression is now mandatory, encouraging better performance practices. You can also use the @empty block to handle empty collections:

<div class="product-grid">
  @for (product of products; track product.id) {
    <app-product-card [product]="product" />
  } @empty {
    <p>No products available at this time.</p>
  }
</div>

The new @switch Syntax

The @switch block provides a cleaner alternative to ngSwitch:

Before (Angular 16 and earlier):

<div [ngSwitch]="userRole">
  <admin-panel *ngSwitchCase="'admin'"></admin-panel>
  <editor-panel *ngSwitchCase="'editor'"></editor-panel>
  <viewer-panel *ngSwitchCase="'viewer'"></viewer-panel>
  <guest-panel *ngSwitchDefault></guest-panel>
</div>

After (Angular 17):

@switch (userRole) {
  @case ('admin') {
    <admin-panel />
  }
  @case ('editor') {
    <editor-panel />
  }
  @case ('viewer') {
    <viewer-panel />
  }
  @default {
    <guest-panel />
  }
}

Blazing-fast build times

Angular 17 has integrated the esbuild bundler, which has demonstrated remarkable improvements in build speed. Benchmarks have shown up to a 67% increase in build performance for client-side rendering and an 87% boost for hybrid rendering. This optimization ensures that developers can iterate on their projects more efficiently, reducing the time spent waiting for builds to complete.

Configuring esbuild in Angular 17

The new build system is enabled by default in new Angular 17 projects. For existing projects, you can opt in by updating your angular.json:

{
  "projects": {
    "your-app": {
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser-esbuild",
          "options": {
            "outputPath": "dist/your-app",
            "index": "src/index.html",
            "main": "src/main.ts",
            "tsConfig": "tsconfig.app.json"
          }
        }
      }
    }
  }
}

Improved internationalization and accessibility

The latest version of Angular brings enhanced support for internationalization and accessibility. Developers can now leverage improved tooling and APIs to create applications that cater to diverse user needs, ensuring a more inclusive and accessible web experience.

Enhanced i18n example

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideI18n } from '@angular/localize';

export const appConfig: ApplicationConfig = {
  providers: [
    provideI18n({
      locales: ['en-US', 'es-ES', 'fr-FR'],
      defaultLocale: 'en-US'
    })
  ]
};
<!-- Component template with i18n -->
<h1 i18n="welcome-message|A welcome message@@welcomeMsg">
  Welcome to our application!
</h1>

<p i18n>
  You have {itemCount, plural, =0 {no items} =1 {one item} other {{{itemCount}} items}} in your cart.
</p>

Deferrable views for performance optimization

Angular 17 introduces deferrable views with the @defer block, allowing you to lazily load components and their dependencies:

@defer (on viewport) {
  <app-heavy-component />
} @loading (minimum 500ms) {
  <app-skeleton-loader />
} @placeholder {
  <div>Component will load when visible</div>
} @error {
  <p>Failed to load component</p>
}

Defer triggers

Angular 17 supports multiple trigger conditions:

// Load on idle
@defer (on idle) {
  <app-analytics-dashboard />
}

// Load on interaction
@defer (on interaction) {
  <app-comments-section />
}

// Load on timer
@defer (on timer(5s)) {
  <app-promotional-banner />
}

// Load when specific element is in viewport
@defer (on viewport(triggerElement)) {
  <app-lazy-content />
}

<div #triggerElement>Scroll to load content below</div>

Precise change detection with Signals

The framework now employs a more targeted approach to change detection using Signals, marking only the components directly affected by changes in data-bound Signals as dirty. This optimization can lead to significant performance improvements.

Using Signals in components

import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <div>
      <p>Count: {{ count() }}</p>
      <p>Double: {{ doubleCount() }}</p>
      <button (click)="increment()">Increment</button>
      <button (click)="reset()">Reset</button>
    </div>
  `
})
export class CounterComponent {
  count = signal(0);
  doubleCount = computed(() => this.count() * 2);

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

  reset() {
    this.count.set(0);
  }
}

Signals with Effects

import { Component, signal, effect } from '@angular/core';

@Component({
  selector: 'app-user-preferences',
  standalone: true,
  template: `
    <div>
      <label>
        <input type="checkbox" [checked]="darkMode()" 
               (change)="toggleDarkMode()" />
        Dark Mode
      </label>
    </div>
  `
})
export class UserPreferencesComponent {
  darkMode = signal(false);

  constructor() {
    // Effect runs whenever darkMode changes
    effect(() => {
      const isDark = this.darkMode();
      document.body.classList.toggle('dark-theme', isDark);
      localStorage.setItem('theme', isDark ? 'dark' : 'light');
    });
  }

  toggleDarkMode() {
    this.darkMode.update(value => !value);
  }
}

Streamlined tooling and developer experience

Angular 17 includes several quality-of-life improvements that contribute to a more streamlined development experience.

Functional toute guards and interceptors

Generate functional interceptors and guards using the Angular CLI:

ng generate interceptor auth --functional
ng generate guard admin --functional

Functional Interceptor Example:

// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authToken = localStorage.getItem('auth_token');
  
  if (authToken) {
    const authReq = req.clone({
      setHeaders: {
        Authorization: `Bearer ${authToken}`
      }
    });
    return next(authReq);
  }
  
  return next(req);
};

Functional Guard Example:

// admin.guard.ts
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';

export const adminGuard: CanActivateFn = (route, state) => {
  const router = inject(Router);
  const userRole = localStorage.getItem('user_role');
  
  if (userRole === 'admin') {
    return true;
  }
  
  return router.parseUrl('/unauthorized');
};

Standalone components as default

Angular 17 embraces standalone components, making them the default for new projects:

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { authInterceptor } from './app/interceptors/auth.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(
      withInterceptors([authInterceptor])
    )
  ]
});

A fresh look and improved documentation

Alongside the technical advancements, Angular 17 introduces a brand-new visual identity, reflecting the framework's future-oriented direction. The updated branding is accompanied by a revamped documentation website, angular.dev, which provides a more intuitive and interactive learning experience with built-in tutorials, playground environments, and modernized examples.

Conclusion

Angular 17 represents a significant leap forward in the framework's evolution, delivering a host of features and improvements that empower developers to build faster, more efficient, and more accessible web applications. With its streamlined control flow syntax, blazing-fast build times powered by esbuild, revolutionary deferrable views, and enhanced Signals-based reactivity, Angular 17 is poised to shape the future of front-end development.

The framework's commitment to developer experience through functional programming patterns, standalone components, and improved tooling makes it easier than ever to build modern web applications. Whether you're starting a new project or migrating an existing one, Angular 17 offers compelling reasons to upgrade and embrace the future of Angular development.