The
Ninja Squad
Blog

What's new in Angular 21.1?

Angular 21.1.0 is here!

Angular logo

Two months after the huge v21 release, the Angular team delivers a new minor update.

Let's dive in!

Signal Forms

A meaningful change in Signal Forms is the renaming of the [field] directive to [formField]. The [field] selector was too generic and likely to cause naming collisions with existing components. Therefore, the Angular team decided to rename both the directive class (from Field to FormField) and the directive selector (from [field] to [formField]).

You should now use [formField] instead of [field] to bind form controls:

<input [formField]="form.name" />

The Field directive has been removed, so you'll have some renaming to do if you already use experimental Signal Forms.

Another name change in Signal Forms is the renaming of the field property to fieldTree in validation error interfaces and field contexts. If you're writing custom validators, you'll need to update your code to use fieldTree instead of field:

// Before
const error: ValidationError = {
  // πŸ‘‡ targets the password field
  field: context.fieldTree.password,
  kind: 'password-different-from-email',
  message: 'Password should not be the same as email'
};

// After v21.1
const error: ValidationError = {
  // πŸ‘‡ targets the password field
  fieldTree: context.fieldTree.password,
  kind: 'password-different-from-email',
  message: 'Password should not be the same as email'
}

This change affects the ValidationError interface and the FieldContext interface.

Signal Forms gained a new feature in v21.1: the ability to automatically apply CSS classes to fields based on their state πŸš€.

One of the missing features of signal forms compared to the classic reactive/template-driven forms was the automatic CSS classes that were added to form controls (ng-valid, ng-dirty, ng-touched, etc.).

This is now possible using the provideSignalFormsConfig() function in your application configuration:

provideSignalFormsConfig({
  classes: {
    'is-invalid': field => field.state().invalid() && field.state().touched()
  }
})

In this example, the is-invalid CSS class will be automatically added to any field that is invalid and touched. You can define as many classes as you want, and use any property of the Field/FormField directive, like its state (invalid(), touched(), dirty(), etc.) or its host element (a new property added in this version) to determine when the class should be applied.

This makes it easier to style your forms, especially when using CSS frameworks like Bootstrap or Tailwind that rely on specific CSS classes to style form inputs.

Note that you can have the "old" behavior with ng-valid/ng-invalid/ng-dirty/ng-touched... classes by using:

provideSignalFormsConfig({
  classes: NG_STATUS_CLASSES,
});

Signal Forms now also support custom control directives! Previously, the [field] binding could only be used with components. Now, you can create directives that implement the FormValueControl or FormCheckboxControl interfaces and bind them with [formField].

<input [formField]="form.name" appCustomControl />

Signal Forms also gained a new focusBoundControl() method on the field state. This is particularly useful for accessibility or when you want to focus a specific field after a validation error.

This allows you to programmatically focus the input element associated with a form field, for example when a submission failed:

protected async register(event: SubmitEvent) {
    event.preventDefault();
    await submit(this.userForm, async form => {
      // ...
    });
    // πŸ‘‡ automatically focus the first field with an error
    const firstError = this.userForm().errorSummary()[0];
    if (firstError?.fieldTree) {
      firstError.fieldTree().focusBoundControl();
    }

This will automatically focus the first input/select/textarea associated with a field that has an error, but the cool thing is that it can also work with custom control components! You just have to implement a focus() method in your custom control, and focusBoundControl() will automatically call it:

@Component({
  // ...
})
export class BirthYearInput implements FormValueControl<number | null> {
  // ...
  // πŸ‘‡
  focus() {
    this.birthYearInput().nativeElement.focus();
  }

NOTE: We added a Signal Forms exercise to our online workshop, check it out!

Control flow

The @switch control flow now supports multiple consecutive @case blocks matching a single content block.

Previously, each @case required its own content. Now you can specify multiple conditions for a single block, similar to fall-through behavior in traditional switch statements:

@switch (status) {
  @case ('draft')
  @case ('pending') {
    <p>Your document is not yet published</p>
  }
  @case ('published') {
    <p>Your document is live</p>
  }
  @default {
    <p>Unknown status</p>
  }
}

In this example, both 'draft' and 'pending' statuses will display the same message, making the code more concise when multiple conditions should produce the same result.

Template spread operator

Angular templates now support the spread operator (...)! This allows you to spread an object into an object or an array into another array:

@let users = [currentUser, ...admins];

It also supports the spread syntax in function calls. This is particularly useful for functions that use rest parameters, a syntax that allows a function to accept an indefinite number of arguments as an array:

<button (click)="sum(...counters)">Sum all</button>

Spoiler for the next version: we will get arrow function support in templates in v21.2!

Router

The router introduces a new standalone isActive() function that returns a computed signal indicating whether a given URL is currently active.

This new function is a more tree-shakeable alternative to the existing Router.isActive() method, which is now deprecated in v21.1:

import { isActive } from '@angular/router';

const active = isActive(
  '/home',
  this.router, 
  { paths: 'exact', queryParams: 'exact', fragment: 'ignored', matrixParams: 'ignored' }
);

The function returns a signal that automatically updates when the router state changes, making it easier to reactively track active routes.

The router also gained better memory management capabilities in v21.1 if you use a custom RouteReuseStrategy implementation (which is not that common).

A new experimental feature withExperimentalAutoCleanupInjectors() automatically destroys unused route injectors after navigation, helping to prevent memory leaks in applications with many routes or long-lived sessions.

You can enable it when configuring your routes:

provideRouter(routes, withExperimentalAutoCleanupInjectors())

Additionally, a new destroyDetachedRouteHandle() function is available for manually cleaning up detached route handles in custom RouteReuseStrategy implementations.

Finally, the router introduces an experimental integration with the browser's Navigation API. This API is a modern alternative to the traditional History API, providing a more robust way to handle navigations.

By enabling this experimental feature, the Angular router can:

  • intercept navigations triggered outside the router and convert them to SPA navigations.
  • leverage native browser scroll and focus restoration.
  • communicate ongoing navigations to the browser for better accessibility and user experience (native loading progress, stop button, etc.).

You can enable it using the withExperimentalPlatformNavigation() feature:

provideRouter(routes, withExperimentalPlatformNavigation())

This feature is highly experimental and the native browser support is currently very limited. This won't be stabilized until the Navigation API is more widely supported. When this is done, it may not become a router feature (with...) but rather a different router provider, which would allow to tree-shake the current history-based classes.

Debugging stability

A new debugging utility provideStabilityDebugging() helps developers troubleshoot applications that fail to stabilize during hydration.

If your application doesn't reach a stable state within 9 seconds, this utility logs diagnostic information to the console, including pending tasks and their stack traces. This is particularly useful when debugging hydration timeouts in SSR applications.

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

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

Note that this utility is provided by default in dev mode when using provideClientHydration().

Vitest

The Angular CLI's migration schematic for converting Jasmine tests to Vitest now supports a browserMode option (a contribution from my fellow ninja JB!).

If you're migrating your tests to Vitest and plan to use Vitest's browser mode (you should, read our blog post about Vitest browser mode), you can now use this option to preserve certain assertions that are natively supported in browser mode:

ng generate refactor-jasmine-vitest --browser-mode

When this option is enabled, the migration will keep the toHaveClass matcher in its original form instead of converting it to a different assertion, since Vitest's browser mode provides its own toHaveClass matcher.

This makes the migration smoother for projects that want to run their tests in a real browser environment, ensuring your tests work as expected without manual adjustments after the migration.

The schematic also now generates a detailed migration report by default, creating a markdown file that lists all the TODOs and manual tasks that need to be addressed after the automatic migration. This report includes precise file paths and line numbers, making it easier to quickly identify and fix any remaining migration tasks in large codebases. You can disable this with --no-report if you don't need it.

# Jasmine to Vitest Refactoring Report

Date: 2025-12-17T15:16:43.108Z

## Summary

|                   | Count |
|:------------------|------:|
| Files Scanned     |   159 |
| Files Transformed |   151 |
| Files Skipped     |     8 |
| Total TODOs       |     3 |

## TODO Overview

| Category    | Count |
|:------------|------:|
| addMatchers |     3 |

## Files Requiring Manual Attention
...

AI

The MCP server gained a few new (experimental) tools that enable AI assistants to control Angular projects more effectively:

  • build: compiles the Angular application
  • devserver.start: launches the development server
  • devserver.stop: terminates the running development server
  • devserver.wait_for_build: waits until the dev server completes its build process
  • test: runs unit tests via ng test
  • e2e: runs end-to-end tests via ng e2e

These tools allow AI assistants like Claude to programmatically build your application, manage the development server, and run your tests, making it easier to verify that everything compiles correctly and all tests pass.

You can now enable all experimental tools at once using the all group in your MCP configuration, making it easier to get started with the full suite of AI assistance features: ng mcp --experimental-tool=all.

The already existing ai_tutor tool continues to evolve. A new lesson has been added to the interactive learning experience: Signal Forms!

If you're curious about Signal Forms, the AI tutor can be a nice resource, but you should definitely check out our 2 part series on Signal Forms if you haven't already πŸ˜‰

The Angular team continues to improve the infrastructure for AI assistance. Code examples are now embedded directly in Angular packages (starting with @angular/forms) using a SQLite database, making them easily accessible to the MCP server and other tooling. This builds upon the code examples feature introduced in earlier versions, improving the AI's ability to provide contextual code snippets and guidance, and we'll probably see more code examples added in future releases in the various packages.

Finally, setting up the MCP server is now easier than ever. New Angular workspaces now include a .vscode/mcp.json.template file with a pre-configured setup for the Angular CLI MCP server. This eliminates the need for manual configuration and makes it straightforward to start using AI assistance in your Angular projects. You can learn more about this on the Angular MCP documentation page.

Summary

That's all for this release! Signal Forms continue to improve with CSS class configuration, custom control directives support, the focusBoundControl() method, and naming improvements. The router gained an experimental integration with the Navigation API. The CLI team focused on AI features and the Vitest stabilization.

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

Angular 21.0.0 is here!

Angular logo

The highlights? Signal Forms and zoneless change detection by default. The CLI, with Vitest becoming the default testing framework, also delivers on a long due improvement.

Let's dive in!

Signal Forms (experimental)

Angular 21 introduces a new experimental way to build forms: Signal Forms, in the @angular/forms/signals package. This signal-based approach provides better type safety and easier validation compared to traditional reactive and template-driven forms.

private readonly credentials = signal({ email: '', password: '' });
protected readonly loginForm = form(
  this.credentials,
  // πŸ‘‡ add validators to the fields, with their error messages
  form => {
    // email is mandatory
    required(form.email, { message: 'Email is required' });
    // must be a valid email
    email(form.email, { message: 'Email is not valid' });
    // password is mandatory
    required(form.password, { message: 'Password is required' });
    // should have at least 6 characters
    minLength(form.password, 6, {
      // can also be a function, if you need to access the current value/state of the field
      message: password => `Password should have at least 6 characters but has only ${password.value().length}`
    });
  }
);

We wrote a two-part series to introduce you all the concepts of Signal Forms. The first one covers the basics, while the second one dives into more advanced topics like custom validation, custom form components, etc.

Check them out!

πŸ‘‰ Angular Signal Forms - Part 1

πŸ‘‰ Angular Signal Forms - Part 2

There is also an effort to make the migration to signal forms more progressive, with the introduction of the @angular/forms/signals/compat package. This package offers a compatForm function, which is a version of the form function designed for backwards compatibility with reactive forms by accepting reactive controls as a part of the data. I'm not sure that this will save us from completely rewriting our forms, but we appreciate the effort 🫑.

Zoneless by default πŸš€

Angular applications are now zoneless by default! provideZonelessChangeDetection() is no longer needed in new applications, as Angular will use zoneless change detection automatically. If your application relies on Zone.js, you will need to explicitly add provideZoneChangeDetection() in the root providers. A migration will automatically add it for you if necessary when updating to Angular v21.

The CLI now generates zoneless applications by default, and you'll note that the component tests use await fixture.whenStable() instead of fixture.detectChanges().

Core

The SimpleChanges type that we use for the ngOnChanges parameter is now generic and can be type safe, if you add the type of your component as a type parameter. To keep the backward compatibility, it defaults to any when no type parameter is provided.

export class User implements OnChanges {
  readonly name = input.required<string>();

  // πŸ‘‡ ngOnChanges is now type safe
  ngOnChanges(changes: SimpleChanges<User>): void {
    const nameChange = changes.name;
    // typed as `SimpleChange<string> | undefined`
    if (nameChange) {
      console.log(`Name changed from ${nameChange.previousValue} to ${nameChange.currentValue}`);
    }
  }

Forms

In "classic" forms, a new FormArrayDirective directive has been added, allowing to have an array as the top element of a form. Until now, you had to wrap it in a FormGroup and use the FormGroupDirective.

Http

The big change is that HttpClient is now provided in the root injector by default. We no longer need to use provideHttpClient() in our applications, except when we want to customize the HTTP client (for example, to register interceptors). It also simplifies the testing setup, where it was a bit verbose to have to use both provideHttpClient() and provideHttpClientTesting(). We can now only use the latter.

The HttpClient also gained a few options in its Fetch version:

  • responseType has been added (corresponding to the response type property in the native fetch API, see the MDN docs)
  • referrerPolicy which can be used to specify the referrer information (see the MDN docs) has been added to the client and in the HttpResource options.

Router

The router gained a new scroll option that can be used when navigating to define the scrolling behaviour (allowing to override the scroll restoration behaviour that you can enable in provideRouter thanks to withInMemoryScrolling({ scrollPositionRestoration: 'enabled' })).

The scroll option can be 'manual' (no scrolling even if enabled globally) or 'after-transition' (following the global behaviour). So, using router.navigateByUrl('/', { scroll: 'manual' }) will prevent scrolling even if scroll restoration is enabled globally.

Templates

The control flow migration will automatically be applied when updating to Angular v21, if you did not apply it yet.

Since the updated style guide written for Angular v20, it is now recommended to use class and style bindings instead of the NgClass and NgStyle directives.

Angular v21 offers automatic migrations (optional) to help you with this change!

ng g @angular/core:ngclass-to-class-migration
ng g @angular/core:ngstyle-to-style-migration

Another optional migration has been added to migrate CommonModule imports to the individual directives/pipes standalone imports:

ng g @angular/core:common-to-standalone

Speaking of changes in the templates, RegExp are now supported: you can now write something like <div>{{ /\\d+/.test(id()) }}</div> in your HTML files. I'm not sure why anyone would want to do that, but well...

The @defer trigger on viewport also gained a new option, allowing to define the options of the underlying IntersectionObserver. You can now write a @defer like this:

@defer (on viewport({ trigger, rootMargin: '50px' })) {

Compiler

In v21, the compiler now has its typeCheckHostBindings option enabled by default (see our blog post about v20 to learn more about what it does).

A new diagnostic has also been added to detect when a component reads a required input/model/viewChild/contentChild property before it is initialized. This was already throwing an error at runtime, but now you will get a compile-time error instead.

Another diagnostic has been added to detect unreachable or duplicated or inefficient @defer triggers.

Styles encapsulation

A new strategy for view encapsulation has been added: ExperimentalIsolatedShadowDom. As its name indicates, it is still experimental. It leverages Shadow DOM to provide true encapsulation of styles, but also isolates the component from the rest of the application. Unlike ShadowDom encapsulation, it prevents styles from leaking in or out of the component. This is useful when you want to ensure that the component's styles are completely isolated from the rest of the application, including global styles. There are still some issues with the implementation, so it is not recommended for production use yet.

Vitest is the new default

Big change in the unit testing setup: Vitest is now the default testing framework when creating new applications with the CLI! 😲

Karma/Jasmine has been the default for a long time, even though Karma has been deprecated for a while. The experimental @angular/build:unit-test is now stable and uses Vitest by default. Some efforts have been made to simplify the configuration as much as possible, and the minimal setup in angular.json is now:

"test": {
  "builder": "@angular/build:unit-test"
}

You can see the difference in a generated project on angular-cli-diff. This little project that we maintain proves to be really useful to see what changed between CLI versions when you want to update your projects.

Even if the configuration is simpler, you can still customize it a lot with the following options, either in angular.json or via CLI options when running ng test:

  • coverage (and all coverage related options, like coverageInclude, coverageExclude, coverageThresholds, etc.) to configure code coverage
  • browsers to define which browsers to use for testing. Note that Vitest stabilized the Browser Mode in v4, so you can now run tests in real browsers instead of jsdom/happy-dom if you want to. The browserViewport option allows to define the viewport size for browser tests.
  • reporters to define which reporters to use, and output-file to define where to output the report.
  • setupFiles to define setup files to be run before the tests and providersFile to define a file that provides Angular testing providers.
  • ui if you want to use Vitest's UI mode.
  • watch to enable/disable watch mode (true by default when running ng test, but false in CI environments).
  • filter to run only tests matching a specific pattern in their test suite or test name.
  • list-tests to list all the tests without running them (useful to check what the include/exclude options are doing).

The CLI team also added a runnerConfig option to allow using a custom Vitest configuration file, if you need to customize something not covered by the CLI options. For example, if you want to enable Vitest isolation mode (disabled by default in the CLI as it slows down tests), you can add a vitest-base.config.ts file to your project using: ng g config vitest.

Then customize it like this:

import { defineConfig } from 'vitest/config';
export default defineConfig({
  test: {
    isolate: true,
  },
});

We really like Vitest and its Browser Mode that allows to use real browsers and write more expressive tests using its Locator API. We wrote a dedicated blog post to explain how to write more expressive than ever component unit tests 🀩:

πŸ‘‰ Angular tests with Vitest browser mode

It is still possible to use Karma if you want to, by setting the runner option to "karma". And you can generate a new project with Karma as the default by using ng new my-app --test-runner=karma.

A migration is also available to migrate existing tests from Jasmine to Vitest:

ng generate refactor-jasmine-vitest

This migration is fairly mechanical, so you'll probably have to manually adjust some tests afterward. But it should save you some time, as it replaces all Jasmine-specific functions, types, matchers and imports with their Vitest equivalents! It does not migrate the test setup or dependencies though, so you'll have to do that part manually.

The migration has some useful options:

  • --add-imports to automatically add the Vitest imports in your test files (if you don't want to use global imports, which is the default when generating a new application)
  • --file-suffix if you use a different suffix than .spec.ts for your test files
  • --include to target only specific files or folders

Note that fakeAsync tests need to be rewritten, as fakeAsync relies on a patched Zone.js for Jasmine, and there is no version of it for Vitest. The migration will also add helpful TODO comments in your tests to indicate where it couldn't automatically migrate them.

Vitest is definitely a great addition to Angular, and the way to go if you start a new project! The web-test-runner and jest experimental builders which were introduced as a possible alternative to Vitest will be removed in v22.

ng serve

It is now possible to define variables that will be replaced during the serve process, using the define option in the serve configuration. For example, you can define a VERSION variable that will be replaced with the version of your application from the command line (making it simple to use environment variables):

ng serve --define VERSION="'1.0.0'"

and then use it in your code:

// @ts-expect-error defined with --define flag
console.log(`App version: ${VERSION}`);

This was already possible since v17.2 in builds, but is now also available for ng serve.

Tailwind schematic

A new schematic has been added to easily set up Tailwind CSS in your Angular projects. You can use it when creating a new application:

ng new my-app --style tailwind

This sets up Tailwind CSS with a minimal .postcssrc.json configuration file and adds the required dependencies (tailwindcss, @tailwindcss/postcss, and postcss).

Interestingly, ng add now supports adding a package that is not a schematic and will fall back to installing the NPM package and then try to run its schematic if it has one. So, you can also add Tailwind CSS to an existing project with:

ng add tailwindcss

Style guide 2016

If you're nostalgic about the old Angular file naming conventions from 2016, you can now generate a project following its recommendations by using the --file-name-style-guide=2016 option when creating a new application.

This option adds a retro vibe to your project with file names like user.component.ts, user.pipe.ts, and user.service.ts πŸͺ©.

The component, directive, and services name will also follow the 2016 conventions, with a Component, Directive, or Service suffix in their class names.

ng version

The ng version command has been improved to provide more detailed information about the packages in your Angular project (showing the requested and installed versions). A --json option has also been added to output the information in JSON format, which can be useful for automation scripts.

Accessibility

A new @angular/aria package has been introduced to provide headless and accessible directives that implement common ARIA patterns. They handle keyboard interactions, ARIA attributes, focus management, and screen reader support. These directives are built on top of the CDK and developed by the Angular Material team. The package is in developer preview in v21 and currently contains the following directives: Accordion, Autocomplete, ComboBox, Grid, ListBox, Menu, Multiselect, Select, Tabs, Toolbar, and Tree. We haven't tested them yet, so we'll let you explore them on your own.

AI

The AI section is becoming a regular section of our release review! The CLI team worked hard on the MCP server. It now offers 7 tools:

  • list_projects: lists the Angular projects in the workspace
  • get_best_practices: provides the latest best practices
  • find_examples (now stable): finds code examples for a given topic
  • search_documentation: searches the Angular documentation
  • modernize (still experimental): suggests modernizations for your codebase
  • ai_tutor (new): provides interactive learning to build a "Smart Recipe Box" application
  • onpush_zoneless_migration (new): helps migrate to OnPush and zoneless change detection

find_examples is now stable, and has been improved to support filtering, weighted results, and searching for specific Angular packages and versions. The examples you add to your project can now have a frontmatter description, with a title, summary and keywords. It can also be marked as experimental if they use experimental APIs. By default, the database has just one example for a @if, but we can expect the default database to grow over time.

search_documentation has also been improved to search the documentation for the specific Angular version of your project.

modernize is still experimental,, and now can run migrations directly. So if you ask your favorite AI assistant to modernize a component, the MCP server will be able to run the necessary migrations to update your codebase (control-flow, signal migrations, etc.).

onpush_zoneless_migration is an interesting new tool that analyzes your codebase and provides a step-by-step plan to migrate your application to OnPush and zoneless. It targets components and their tests, helps you identify ZoneJS usages that need to be addressed and suggests how to migrate to zoneless.

Summary

The transition to zoneless by default, the introduction of Signal Forms and Vitest as the new default testing framework are big news for Angular developers.

Migration of existing applications to turn on zoneless change detection will require a big effort though. As well as for Vitest if you have thousands of Jasmine tests like we do.

Signal forms have still some rough edges as they are still experimental, but they look very promising, and are maybe a good choice for new projects? We will keep an eye on their evolution in the coming releases, and let you know what we think about them.

Stay tuned!

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