Stable Release of the New Control Flow Syntax

Angular 18 rolled out on Wednesday, May 22, 2024, and with it comes the final, production-ready status for the revamped control flow syntax that made its debut in the prior release. This syntax is handled natively by the template compiler, which means the old structural directives are now optional:

  • ngIf
  • ngFor
  • ngSwitch / ngSwitchCase
<!-- old way -->
<div *ngIf="user">{{ user.name }}</div>

<!-- new way -->
@if(user) {
  <div>{{ user.name }}</div>
}
Enter fullscreen mode Exit fullscreen mode

This API is now considered stable, and the recommendation from the core team is to adopt this new syntax in your projects.

For those looking to make the switch, a schematics-based migration tool is provided.

ng g @angular/core:control-flow
Enter fullscreen mode Exit fullscreen mode

It's important to note that the new @for syntax, which takes over from the ngFor directive, now requires the track option. This is a key part of optimizing list rendering and preventing unnecessary full re-renders when data changes.

In development mode, two new warnings have been introduced:

  • One triggers when the tracking key is a duplicate. This happens when the chosen key value appears more than once within your collection.
  • Another occurs when the tracking key is the entire object and selecting it leads to the destruction and recreation of the whole list. This fires when the framework considers the operation too expensive, although the threshold for this is relatively low.

Defer Syntax Achieves Stability

Angular 18 also marks the @defer syntax as stable. This feature lets you load a section of your view, along with its associated directives, pipes, and libraries, only once a stated condition becomes true.

Check out this example of how it works:


@defer(when user.name === 'Angular') {
  <app-angular-details />
}@placeholder {
  <div>displayed until user.name is not equal to Angular</div>
}@loading(after: 100ms; minimum 1s) {
  <app-loader />
}@error {
  <app-error />
}
Enter fullscreen mode Exit fullscreen mode

To recap how the blocks within this syntax operate:

  • The @placeholder block is shown as long as the @defer condition hasn't been fulfilled.
  • The @loading block appears while the browser is fetching the content for the @defer block. In the example given, it appears if the download lasts longer than 100ms and is then shown for no less than one full second.
  • The @error block will show up when the download of the @defer block fails.

The Future of Zone.js

A major shift in Angular 18 is the introduction of a new mechanism for triggering change detection. In the past, Zone.js was the engine that handled all of this. Now, the framework takes on this responsibility itself.

To enable this, a new ChangeDetectionScheduler was added internally to the framework. This scheduler operates independently of Zone.js and is now the default in Angular 18.

With this new scheduler, a change detection pass is triggered when these events occur:

  • An event listener in a template or on a host is fired
  • A view gets attached or detached
  • The async pipe is given a new value
  • The markForCheck method gets invoked
  • The state of a signal is modified, and so on.

As a piece of trivia: this change detection is triggered by the ApplicationRef.tick function behind the scenes.

Since Angular 18 now relies on this new scheduler by default, migrating your application shouldn't lead to any breakage. Perhaps the framework will receive the trigger for change detection from Zone.js, or from the new scheduler, or from both concurrently.

If you want to go back to the behavior from before Angular 18, you can configure your app with the provideZoneChangeDetection function, making sure to set the ignoreChangesOutsideZone option to true.

bootstrapApplication(AppComponent, {
  providers: [
    provideZoneChangeDetection({ ignoreChangesOutsideZone: true })
  ]
});
Enter fullscreen mode Exit fullscreen mode

Alternatively, if you're ready to commit to a setup without Zone.js, the provideExperimentalZonelessChangeDetection function allows you to depend exclusively on the new scheduler.

bootstrapApplication(AppComponent, {
  providers: [
    provideExperimentalZonelessChangeDetection()
  ]
});
Enter fullscreen mode Exit fullscreen mode

By configuring your app with provideExperimentalZonelessChangeDetection, Angular will no longer require Zone.js. This opens up the possibility to:

  • Drop the Zone.js dependency, assuming no other package in your project relies on it
  • Eliminate zone.js from the polyfills section in your angular.json file

HttpClientModule Deprecation

Since Angular 14 made standalone components available, the use of modules has been elective for most functionality. Now, we are starting to see the first module officially enter retirement, starting with HttpClientModule.

This module's job was to set up your entire application to use a singleton HttpClient, along with your registered interceptors.

This module is now easily swapped out in favor of the provideHttpClient function, which offers options for enabling XSRF and JSONP support.

For testing purposes, there's a companion function called provideHttpClientTesting.

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient()
  ]
});
Enter fullscreen mode Exit fullscreen mode

Following standard practice, the Angular team has created schematics to facilitate this migration.

When you run the ng update @angular/core @angular /cli command, the tooling will detect the use of HttpClientModule in your codebase and prompt you to migrate it.

Fallback for ng-content

For developers creating shared or generic components, ng-content is a cornerstone feature of Angular.

This tag is what allows you to slot your own content into a component. But it had one notable drawback: there wasn't a straightforward way to supply default content.

With version 18, that's all changed. You can now put content directly inside the ng-content tag. This default content is what gets projected only when the developer doesn't provide any of their own.

Consider an example of a button component:

<button>
  <ng-content select=".icon">
   <i aria-hidden="true" class="material-icons">send</i>
  </ng-content>
  <ng-content></ng-content>
</button> 
Enter fullscreen mode Exit fullscreen mode

In this setup, the send icon will be rendered unless the consuming developer passes in their own element bearing the icon class.

The community has long requested an API that consolidates the various events occurring within a form. These events include:

  • pristine
  • touched
  • status change
  • reset
  • submit

Angular 18 now offers a new events property on the AbstractControl class. Because FormControl, FormGroup, and FormArray all inherit from this class, they each gain access to this property, which returns an observable.

@Component()
export class AppComponent {
  login = new FormControl<string | null>(null);

  constructor() {
   this.login.events.subscribe(event => {
    if (event instanceof TouchedChangeEvent) {
        console.log(event.touched);
      } else if (event instanceof PristineChangeEvent) {
        console.log(event.pristine);
      } else if (event instanceof StatusChangeEvent) {
        console.log(event.status);
      } else if (event instanceof ValueChangeEvent) {
        console.log(event.value);
      } else if (event instanceof FormResetEvent) {
        console.log('Reset');
      } else if (event instanceof FormSubmitEvent) {
        console.log('Submit');
      }
   })
  }
}
Enter fullscreen mode Exit fullscreen mode

Routing: Function-Based Redirects

In previous versions, the redirectTo property only accepted a string value when you needed to redirect users to a different path.

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMath: 'full' },
  { path: 'home', component: HomeComponent }
];
Enter fullscreen mode Exit fullscreen mode

It is now possible to supply a function to this property. This function receives an ActivatedRouteSnapshot as its argument, which allows you to access queryParams or params from the URL. Furthermore, this function executes within an injection context, so you can inject services directly.

const routes: Routes = [
  { path: '', redirectTo: (data: ActivatedRouteSnapshot) => {
    const queryParams = data.queryParams
    if(querParams.get('mode') === 'legacy') {
      const urlTree = router.parseUrl('/home-legacy');
      urlTree.queryParams = queryParams;
      return urlTree;
    }
    return '/home';
  }, pathMath: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'home-legacy', component: HomeLegacyComponent }
];
Enter fullscreen mode Exit fullscreen mode

Server-Side Rendering: Two Major Enhancements

Angular 18 brings two highly anticipated features to server-side rendering:

  • event replay
  • internationalization

Event Replay

In a server-side rendered application, the initial HTML is sent to the browser as a static page. This page becomes interactive only after the hydration process. During hydration, the application cannot respond to user interactions, meaning any clicks or inputs are lost until hydration completes.

Angular now has the capability to record these interactions while hydration is underway and then replay them once the application is fully functional and interactive.

This feature, which remains in developer preview, can be enabled by using the server-side feature function withReplayEvents.

providers: [
  provideClientHydration(withReplayEvents())
]
Enter fullscreen mode Exit fullscreen mode

Internationalization

With Angular 16, the framework shifted from destructive hydration to progressive hydration. However, support for internationalization was notably absent—elements marked with i18n were skipped during the hydration process.

This omission has been addressed in the latest version. Keep in mind that this feature is still in development preview and requires activation through the withI18nSupport function.

providers: [
  provideClientHydration(withI18nSupport())
]
Enter fullscreen mode Exit fullscreen mode

Internationalization

Angular's guidance is to leverage the native JavaScript INTL API for any internationalization needs within your application.

Following this recommendation, the helper functions exposed by the @angular/common package have been deprecated. Consequently, functions such as getLocaleDateFormat should no longer be used.

New Builder Package and Deprecations

Since Vite was introduced, the builder responsible for compiling Angular applications has resided in the @angular-devkit/build-angular package.

This package bundles Vite, Webpack, and Esbuild, making it quite heavy—especially considering that future applications might rely solely on Vite and Esbuild.

With this future direction in mind, a new, lighter package named @angular/build has been created, containing only Vite and Esbuild.

When migrating to Angular 18, an optional schematic is available for projects that don't depend on Webpack—for example, those not using Karma for unit tests. This schematic updates the angular.json file to reference the new package and adjusts the package.json by adding the new dependency and removing the old one.

It's important to note that the existing package remains valid, as it serves as an alias to the newly introduced one.

For styling, Angular has always supported Less, Sass, CSS, and PostCSS out of the box, provided the necessary dependencies are present in your project's node_modules.

With the introduction of the @angular/build package, Less and PostCSS are now considered optional. They must be explicitly listed as dev dependencies in your package.json.

If you opt to migrate to the new package, these dependencies will be added automatically.

No More Async/Await Downleveling

Zone.js doesn't play well with the JavaScript async/await syntax.

To ensure developers could still use this feature, the Angular CLI would transform code containing async/await into standard Promises.

This process is known as downleveling, where, for example, ES2017 code gets converted to ES2015.

With the emergence of applications that don't rely on Zone.js—even though this is still experimental—Angular will skip this downleveling step if Zone.js isn't listed in your polyfills. The result is a build that's both faster and more compact.

A New Alias: ng dev

Running the ng dev command will now start your application in development mode.

Essentially, this command is just an alias for the well-known ng serve command.

This alias was introduced to better align with the Vite ecosystem, particularly the standard npm run dev workflow.

Looking Ahead

The Angular team has once again shipped a release packed with features that promise to significantly improve the developer experience and signal a promising future for the framework.

What lies ahead?

We can likely anticipate continued progress in both performance and developer tooling.

Also on the horizon are signal-based forms and components, along with the long-awaited @let block syntax for declaring template variables.