What's new in Angular 15?

Angular 15.0.0 is here!

Angular logo

This is a major release with a ton of interesting features: let’s dive in!

Standalone components are stable! ✨

Here we are: standalone components are now stable! You can officially build Angular applications without modules if you want to.

Check out our guide to standalone components:

👉 A guide to standalone components in Angular

A few improvements landed in v15.

The HTTP support evolved, and we can now use provideHttpClient to provide HttpClient without using HttpClientModule (see below).

We can now use provideHttpClientTesting() to provide HttpClient in tests, and… provideLocationMocks() to test components using the router:

TestBed.configureTestingModule({
  providers: [
    // 👇 ~ similar to RouterTestingModule
    // (some providers are missing compared to RouterTestingModule)
    provideRouter([]), 
    provideLocationMocks(), 
    // 👇 similar to HttpClientTestingModule
    provideHttpClient(),
    provideHttpClientTesting()
  ],
});

The NgForOf directive is now aliased as NgFor which makes it simpler to import it in your standalone components, as you previously had to know ngIf was the NgIf directive, and ngFor was the NgForOf directive 😅.

The same kind of thing has been done with the RouterLink directive in the router: there was previously a RouterLink directive and a RouterLinkWithHref directive. They are now merged into one, making it a no-brainer to import it. A schematic will automatically migrate your code if you were using RouterLinkWithHref.

The language service (used for the autocompletion and type-checking in your IDE) has been improved for standalone components, and it now automatically offers to import a standalone directive/component/pipe in your component, if you use one in its template ✨.

HTTP with provideHttpClient

The HTTP support evolves and adapts to the new world of Angular 15, where modules are optional. It’s now possible to provide the HttpClient using provideHttpClient(). HTTP interceptors are also evolving and can now be defined as functions.

We wrote a dedicated article about this:

👉 HTTP in a standalone Angular application with provideHttpClient

Directive composition API

The other big feature of Angular v15 is the directive composition API. We also wrote a dedicated article about this:

👉 Directive Composition API in Angular

NgOptimizedImage is stable

The NgOptimizedImage directive is now stable and can be used in production. Introduced in Angular v14.2, it allows you to optimize images. You can check out our explanation in our blog post about v14.2.

Note that there is a change in the API: the NgOptimizedImage directive now has inputs named ngSrc and ngSrcset (whereas they were originally called rawSrc and rawSrcset).

<img [ngSrc]="imageUrl" />

Another input called sizes has also been added. When you provide it a value, then the directive will automatically generate a responsive srcset for you.

<img [ngSrc]="imageUrl" sizes="100vw"> />

It uses the default breakpoints [16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840] (and thus generates a big srcset with all these values) but they can be configured.

providers: [
  {
    provide: IMAGE_CONFIG, useValue: { breakpoints: [1080, 1200] }
  },
]

This generates the following srcset: https://example.com/image.png 1080w, https://example.com/image.png 1200w.

This behavior can be disabled via the disableOptimizedSrcset input of the directive.

The directive also gained a new fill boolean input, which removes the requirements for height and width on the image, adds inline styles to cause the image to fill its containing element and adds a default sizes value of 100vw which will cause the image to have a responsive srcset automatically generated:

<img [ngSrc]="imageUrl" fill />

Last but not least, the directive triggers the generation of a preload link in the head of your document for priority images when used in SSR/Angular Universal.

Dependency injection

The providedIn: NgModule syntax of the @Injectable() decorator is now deprecated. You generally want to use providedIn: 'root'. If providers should truly be scoped to a specific NgModule, use NgModule.providers instead. The providedIn: 'any' syntax is also deprecated.

Router

The router now auto-unwraps default exports from lazy-loaded modules, routes or components. You can replace:

loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
// of for routes
loadChildren: () => import('./admin/admin.routes').then(c => c.adminRoutes)
// or for component
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)

with the shorter:

loadChildren: () => import('./admin/admin.module')
// of for routes
loadChildren: () => import('./admin/admin.routes')
// or for component
loadComponent: () => import('./admin/admin.component')

if AdminModule, AdminComponent and adminRoutes are default exports.

Forms

Some utility functions have been added to the forms package: isFormControl, isFormGroup, isFormRecord, isFormArray.

They are particularly useful when you want to write a custom validator, as custom validators have AbstractControl in their signature, but you often know that the validator you write is for a specific FormControl, FormGroup, etc.

positiveValues(control: AbstractControl) {
  if (!isFormArray(control)) {
    return null;
  }
  // check that every value is positive
  // we can use `control.controls` here \o/
  if (control.controls.some(c => c.value < 0)) {
    return { positiveValues: true };
  }
  return null;
}

Common

Angular v13 introduced the toke DATE_PIPE_DEFAULT_TIMEZONE to configure the default timezone of the DatePipe (see our blog post about v13).

This token has been deprecated in v15 and replaced with DATE_PIPE_DEFAULT_OPTIONS which accepts an object with a timezone property and a dateFormat property to specify the default date format that the pipe should use.

providers: [{ provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: { dateFormat: 'shortDate'} }]

Devtools

The devtools now allow you to inspect the source code of a directive

Angular CLI

As usual, you can check out our dedicated article about the new CLI version:

👉 Angular CLI v15

Summary

This release is packed with features as you can see, and the future is exciting with the standalone APIs. The roadmap includes work on the CLI to be able to generate standalone applications without modules. It also mentions some efforts on the server-side rendering story, which is not the strong suit of Angular (compared to other mainstream frameworks) and the possibility to use Angular without zone.js.

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!


What's new in Angular CLI 15.0?

Angular CLI 15.0.0 is out!✨

If you want to upgrade to 15.0.0 without pain (or to any other version, by the way), I have created a Github project to help: angular-cli-diff. Choose the version you’re currently using (14.2.0 for example), and the target version (15.0.0 for example), and it gives you a diff of all files created by the CLI: angular-cli-diff/compare/14.2.0…15.0.0. It can be a great help along with the official ng update @angular/core @angular/cli command. You have no excuse for staying behind anymore!

Let’s see what we’ve got in this release.

Deprecations and breaking changes

The CLI now requires at least NodeJS v14.20 (as it relies on the crypto module). It now officially supports NodeJS v18 as well.

TypeScript 4.6 and 4.7 are no longer supported: you need to upgrade to TS v4.8.2+.

SCSS imports starting with a tilde, like @import '~font-awesome/scss/font-awesome';, are now deprecated. You’ll see a warning message when you build your application:

'src/styles.scss' imports '~font-awesome/scss/font-awesome' with a tilde. Usage of '~' in imports is deprecated.

The solution is to simply remove the tilde: @import 'font-awesome/scss/font-awesome';.

This was dictated by the migration to the modern Sass API and compiler, which is faster. You can still use the legacy API with NG_BUILD_LEGACY_SASS=1 ng build, but this will go away in the next versions.

Speaking of styles, the support of Stylus has been removed. It was deprecated since Angular CLI v12.

esbuild builder improvements

The new experimental builder that uses esbuild has been introduced in v14, but with some missing features.

Even if it is still not feature complete, the esbuild builder is making progress. You can now use the watch mode with --watch (and polling with --poll). The builder is now also able to rebuild incrementally as the webpack-based one!

The code and styles are now processed in parallel, so the bundling should be a bit faster.

The esbuild builder also supports fileReplacements, which was missing to really give it a try in an existing application.

To check it out in your project, replace @angular-devkit/build-angular:browser with @angular-devkit/build-angular:browser-esbuild, and run ng build. For a small application, on a cold production build (after ng cache clean), the build time went from 13s to 6s on my machine. For a larger application, the build time went from 1m12s to 32s 🤯.

New project simplifications

The CLI now generates fewer files in a new project:

  • the environment files are gone, so you’ll no longer see environment.ts and environment.prod.ts in your project. The fileReplacements configuration has been removed from the angular.json file. You can still use these files if you want, but they are not generated by default anymore: you have to manually create them and add the relevant fileReplacements configuration in the angular.json file.
  • the enableProdMode function call has been removed in main.ts. It was only used for JiT mode, which is not really used anymore. In AoT mode, the CLI already sets ngDevMode when building in production mode, which has the same purpose.
  • the .browserslistrc file has been removed. You can still add one, but the file is no longer generated by default. A migration will remove it from your project if you use the default one. Note that the default or custom .browserslistrc of your project is now used by the CLI to determine the ECMAScript version of the output generated from your TypeScript code (instead of relying on the target defined in tsconfig.json). That’s why the target is set to es2022 in new projects.
  • the polyfills.ts file has been removed. The polyfills option in angular.json now directly accepts an array of imports. To add zone.js to your application, the angular.json file now specifies polyfills: ["zone.js"].
  • the karma.conf.js file has been removed. Same here, you can add one to customize the unit test behavior, but it is not generated by default anymore.
  • the test.ts file has been removed, as it was bringing no special value. You can still add one and specify it in the angular.json file if you need to. A migration will run and simplify this file in your project (but not remove it, although you can).

Note that the default test.ts now uses:

getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
  errorOnUnknownElements: true,
  errorOnUnknownProperties: true
});

These options (added here and in the framework by this blog post author 🤓) check that all elements and properties used in your tests are known by Angular. See our blog post about Angular v14 for more details.

See you ngOnInit

The CLI used to generate a component with an empty ngOnInit method. As this was not always useful, this is no longer the case!

Generate library

ng generate library now accepts a --project-root option to generate the library in a different folder than the default projects one.

Summary

That’s all for the CLI v15 release! You’ll find more interesting features in our article about the framework v15.0.0 release.

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


HTTP in a standalone Angular application with provideHttpClient

Angular v14 introduced the concept of standalone components/directives/pipes and allows writing applications without modules.

How do you use and test HTTP in a standalone application?

provideHttpClient

For a long time, the HttpClient was provided by the HttpClientModule that you imported into your application module.

@NgModule({
  imports: [BrowserModule, HttpClientModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

When the standalone APIs were introduced in Angular v14, it opened the door to writing applications without modules.

The Angular team introduced an importProvidersFrom() function, that you could use in the bootstrapApplication function to import providers from an existing module, as most of the ecosystem was structured around modules.

So to provide the HttpClient in a standalone application, you could do:

import { bootstrapApplication, importProvidersFrom } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

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

But since Angular v15, this can be replaced by provideHttpClient(), a new function that does the same thing as importProvidersFrom(HttpClientModule):

import { bootstrapApplication } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app.component';

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

HttpClient is then available for injection in your application.

provideHttpClient() is more “tree-shakable” than importing HttpClientModule, as you can enable the features you want by giving it some parameters.

For example, if you want JSONP support, you can write:

import { bootstrapApplication } from '@angular/core';
import { provideHttpClient, withJsonpSupport } from '@angular/common/http';
import { AppComponent } from './app.component';

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

In the same vein, Angular provides XSRF protection (cross-site request forgery) out-of-the-box, by adding a custom header containing a random token provided by the server in a cookie (which is a common technic to mitigate these attacks).

As you probably want to keep this security, it’s enabled by default in provideHttpClient(), but you can configure it with withXsrfConfiguration() to specify a custom header name and cookie name:

import { bootstrapApplication } from '@angular/core';
import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http';
import { AppComponent } from './app.component';

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient(withXsrfConfiguration({
    cookieName: 'TOKEN', // default is 'XSRF-TOKEN'
    headerName: 'X-TOKEN' // default is 'X-XSRF-TOKEN'
  }))]
});

or you can disable it completely with withNoXsrfProtection():

import { bootstrapApplication } from '@angular/core';
import { provideHttpClient, withNoXsrfProtection } from '@angular/common/http';
import { AppComponent } from './app.component';

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

There is another feature that you can enable, but first I need to introduce the concept of functional interceptors.

Functional interceptors

In Angular, interceptors are classes that implement the HttpInterceptor interface. They are used to intercept HTTP requests and responses and can be used to add headers, log requests, etc.

@Injectable()
export class LoggerInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log(`Request is on its way to ${req.url}`);
    return next.handle(req);
  }
}

Since Angular v15, you can also use functional interceptors. They are functions that take an HttpRequest and a HttpHandlerFn as parameters:

export function loggerInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
  console.log(`Request is on its way to ${req.url}`);
  return next(req);
}

As this is a function, you can’t use the usual dependency injection via constructor parameters to inject services in it. But you can use the inject() function:

export const loggerInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> => {
  const logger = inject(Logger);
  logger.log(`Request is on its way to ${req.url}`);
  return next(req);
}

Functional interceptors have to be registered via withInterceptors():

import { bootstrapApplication } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app.component';

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient(withInterceptors([loggerInterceptor]))]
});

Note that you can also register class-based interceptors via withInterceptorsFromDi():

import { bootstrapApplication } from '@angular/core';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { AppComponent } from './app.component';

bootstrapApplication(AppComponent, {
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: LoggerInterceptor, multi: true },
    provideHttpClient(withInterceptorsFromDi())
  ]
});

But this API may be phased out in the future, so it’s better to use withInterceptors() and functional interceptors.

Interceptors and lazy-loading

A long-standing issue with Angular was that interceptors are not inherited in lazy-loaded modules (see this issue for more context).

For example, let’s say we have a lazy-loaded part of the application for the administration of our website:

{
  path: 'admin',
  loadChildren: () => import('./admin/admin.routes').then(c => c.ADMIN_ROUTES)
},

If it provides the HttpClient as well (which is not a good idea to be honest, but let’s say that’s the case for this example):

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    pathMatch: 'prefix',
    providers: [provideHttpClient()], // <--
    children: [{ path: '', component: AdminComponent }]
  }
];

then all requests made in the AdminComponent will not be intercepted by the interceptors registered in our application.

If we want them to go through our loggerInterceptor, we can use withRequestsMadeViaParent():

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    pathMatch: 'prefix',
    providers: [provideHttpClient(withRequestsMadeViaParent(), withInterceptors([adminInterceptor()]))],
    children: [{ path: '', component: AdminComponent }]
  }
];

Then the requests made in the AdminComponent will then go through the adminInterceptor and are then handed off to the parent HttpClient, and will be intercepted by the loggerInterceptor registered at the application bootstrap.

Testing HTTP

When using HttpClientModule in your application, you can import the HttpClientTestingModule in your tests to mock HTTP requests.

But if you use provideHttpClient() instead, you can use provideHttpClientTesting() to mock HTTP requests in your tests (in addition to provideHttpClient()):

import { TestBed } from '@angular/core/testing';
import { provideHttpClientTesting } from '@angular/common/http/testing';

beforeEach(() =>
  TestBed.configureTestingModule({
    providers: [provideHttpClient(), provideHttpClientTesting()]
  })
);

You can then inject HttpController to mock HTTP requests as you usually do.

Summary

The provideHttpClient() API is the way to go if you work with an Angular application and don’t want to use NgModule. When migrating an existing application to the standalone APIs, you will need to replace the usage of HttpClientModule with provideHttpClient() and the usage of HttpClientTestingModule with provideHttpClientTesting() in your tests. You can also gradually migrate your class-based interceptors to functional interceptors.

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


Directive Composition API in Angular

Angular 15.0 introduces a new API to easily compose directives. This feature has been pushed by Kristiyan Kostadinov who shares its time between the Material and Framework teams. It solves a pain that is particularly felt by the Angular Material team.

The problem

One of the most powerful mechanics of Angular is its directive system: you can apply a directive to an element to give it a special behavior.

For example, Material provides a MatTooltip directive that you can apply to an element to display a tooltip:

<button matTooltip="Info" [matTooltipHideDelay]="delay">Click me</button>

or a CdkDrag directive to make an element draggable:

<div cdkDrag [cdkDragDisabled]="isDisabled">Drag me!</div>

Let’s say that you built a nice button directive appButton (or a component), that probably does something amazing, and you always want to apply the MatTooltip and CdkDrag directives at the same time.

You also want to let the user of your directive decide if the button is draggable or not, and what the text and delay of the tooltip should be. But you don’t want your users to have to write:

<button appButton
  matTooltip="Info"
  [matTooltipHideDelay]="delay"
  cdkDrag
  [cdkDragDisabled]="isDisabled">
    Click me
</button>

Here it is a burden on the developers to remember to add matTooltip and cdkDrag every time and to configure them properly.

Ideally, you’d want:

<button appButton
  tooltip="Info"
  [tooltipHideDelay]="delay"
  [dragDisabled]="isDisabled">
    Click me
</button>

When you want to compose behaviors like this, you can currently use inheritance (but you can only inherit from one directive) or mixins (with a pattern I’ve only seen in Angular Material).

In v15, the Angular team introduces a new API to compose directives, called the Directive Composition API. A new property is available in the @Directive (or @Component) decorator: hostDirectives. It accepts an array of standalone directives, and will apply them on the host component.

Note: my following example is not working yet at the time of writing, as the Angular Material directives aren’t available as standalone directives. But they will probably be soon.

@Directive({
  selector: 'button[appButton]',
  hostDirectives: [
    { 
      directive: MatTooltip, 
      inputs: ['matTooltip', 'matTooltipHideDelay']
    },
    {
      directive: CdkDrag,
      inputs: ['cdkDragDisabled']
    }
  ]
})
export class ButtonComponent {
}

You can specify which inputs should be exposed (by default, none are). They are exposed with the same name, but you can rename them:

@Directive({
  selector: 'button[appButton]',
  hostDirectives: [
    { 
      directive: MatTooltip, 
      inputs: [
        'matTooltip: tooltip',
        'matTooltipHideDelay: tooltipHideDelay'
      ]
    },
    {
      directive: CdkDrag,
      inputs: ['cdkDragDisabled: dragDisabled']
    }
  ]
})
export class ButtonDirective {
}

And then use your directive like this 🎉:

<button appButton
  tooltip="Info"
  [tooltipHideDelay]="delay"
  [dragDisabled]="isDisabled">
</button>

You can of course do the same with the outputs. The type-checking will properly work, host bindings will be applied, DI will work (you can even inject the host directives into your directive/component), etc. You can override lifecycle hooks if you want to.

The host directives are picked by view/content queries as well, so this works:

// picks our ButtonDirective 🤯
@ViewChild(CdkDrag) drag!: CdkDrag;

If you want to dive deeper into this topic, check out this talk from Kristiyan.

Currently, the biggest limitation is that you can only apply standalone directives.

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

Angular 14.2.0 is here!

Angular logo

This is a minor release, but it is packed with interesting features: let’s dive in!

Typescript 4.8

TypeScript v4.8 has just been released, and Angular is already compatible \o/. Check out the Typescript v4.8 blog post to learn more about the new features.

NgOptimizedImage

The biggest new feature in Angular 14.2 is the new NgOptimizedImage directive. It is a standalone directive available as an experiment in the @angular/common package.

This directive helps you to optimize your images in your application. To enable it, add it to the imports of one of your modules or standalone components:

imports: [NgOptimizedImage]

Then to use it, you can simply replace the src of an image with rawSrc (update: this is now ngSrc since Angular v15):

<img [rawSrc]="imageUrl" />

The directive then does its best to enforce best practices for this image. For example, did you know that it is recommended to set the width and height attributes on the img tag to prevent layout shifts? See this web.dev article for more information.

If you use the NgOptimizedImage directive, then you get an error if width and height are not properly set:

NG02954: The NgOptimizedImage directive (activated on an <img> element with the `rawSrc="/avatar.png"`) has detected that these required attributes are missing: "width", "height". Including "width" and "height" attributes will prevent image-related layout shifts. To fix this, include "width" and "height" attributes on the image tag.

This forces us to properly set the width and height attributes on the img tag. But note that you need to set a width/height ratio coherent with your image’s intrinsic size. For example, if the image is 800x600 pixels, then you need to set the width and height attributes to 800 and 600 respectively, or to values that respect the same ratio, like 400 and 300.

<img [rawSrc]="imageUrl" width="400" height="300" />

Otherwise, you get a warning letting you know that the image is distorted:

NG02952: The NgOptimizedImage directive (activated on an <img> element with the `rawSrc="/avatar.png"`) has detected that the aspect ratio of the image does not match the aspect ratio indicated by the width and height attributes. Intrinsic image size: 800w x 600h (aspect-ratio: 1.3333333333333333). Supplied width and height attributes: 300w x 300h (aspect-ratio: 1). To fix this, update the width and height attributes.

But the directive does more than just screaming at you 😉.

It automatically sets the fetchpriority attribute on the img tag. This attribute is used by modern browsers to determine how it should prioritize the fetching of the image (see the MDN docs). The directive will set the fetchpriority attribute to high if the image has the priority attribute (so the browser will fetch it right away), or to auto otherwise.

It also sets the loading attribute (see the MDN docs) to eager if the image has the priority attribute, or to lazy otherwise.

This means that by default, the browser will only load images when they’re about to be visible in the viewport.

It checks a few more things when running in dev mode (ng serve). If the image is treated by the browser as a Largest Contentful Paint (LCP) element (typically the case for above-the-fold images), then it checks that the image has the priority attribute. If that’s not the case you get a warning in the console (NG02955).

Last but not least, the directive comes with the concept of “loaders”. By default, the image is loaded from the src directory of your application, as usual. But you can specify another loader if you are using a service like Cloudflare Image Resizing, Cloudinary, ImageKit or Imgix. To do so, you can define one of the provided loaders in your providers:

providers: [
  provideCloudflareLoader("https://ninja-squad.com/"),
  // or `provideCloudinaryLoader`, `provideImageKitLoader`, `provideImgixLoader`
]

It is of course possible to create your own loader:

providers: [
  {
    provide: IMAGE_LOADER,
    useValue: (config: ImageLoaderConfig) => {
      return `https://example.com/${config.src}-${config.width}.jpg}`;
    }
  }
]

The directive also supports width or density descriptors, like 400w or 2x, with rawSrcset (update: this is now ngSrcset since Angular v15).

<img rawSrcset="avatar.png" rawSrcset="100w, 200w" />

This directive is probably only useful in some specific cases, but it enforces best practices that we don’t always know as web developers. It has been pushed by the Aurora team which is a collaboration between Chrome and open-source web frameworks. Give it a try (and keep in mind this is experimental). That’s why you can see similar work in other frameworks, like Nuxt Image for example.

The Aurora team wrote an in-depth article if you want to learn more.

Core

A new function createComponent() has been added to the framework to help create components dynamically. This is a replacement for the ComponentFactory that was usually used until it was deprecated in Angular v13.

const app = await bootstrapApplication(AppComponent);
const homeComponent = createComponent(HomeComponent, app.injector);
app.attachView(homeComponent.hostView);

Another new function called createApplication has been introduced to let developers start an application without bootstrapping a component (unlike bootstrapApplication). This can be useful if you want to render multiple root components in your application, or if you are using Angular Elements like in the following example:

const app = await createApplication();
const HomeNgElementCtor = createCustomElement(HomeComponent, { injector: app.injector });
customElements.define('app-home', HomeNgElementCtor);

A low-level utility function called reflectComponentType() has also been added to the framework to help get the component metadata from a component type.

const mirror = reflectComponentType(UserComponent)!;

mirror is a ComponentMirror object, which contains the metadata of the component:

  • selector, for example app-user
  • type, for example UserComponent
  • inputs, for example [{ propName: 'userModel', templateName: 'userModel' }]
  • outputs, for example [{ propName: 'userSaved', templateName: 'userSaved' }]
  • ngContentSelectors, for example ['*']
  • isStandalone, for example false

Similarly, there is now a provideRouterForTesting() function that can be used in tests instead of RouterTestingModule.

Forms

Angular v14 introduced a new form element called FormRecord. You can read more about it in our blog post about Strictly typed forms.

But there was no method to create a FormRecord with the FormBuilder. This is now fixed in Angular v14.2 (a small contribution from me 👉👈), and you can use fb.record({}):

this.form = this.fb.group({
  languages: this.fb.record({
    english: true,
    french: false
  })
});

Standalone components

Angular v14.1 introduced the common directives and pipes as standalone entities, v14.2 now introduces the router directives as standalone entities!

You can now import RouterLinkWithHref (for a routerLink), RouterLinkActive and RouterOutlet directly instead of importing the whole RouterModule:

@Component({
  standalone: true,
  templateUrl: './user.component.html',
  imports: [RouterLinkWithHref] // -> you can now use `routerLink` in the template
})
export class UserComponent {

Related to standalone components, the router is now usable without using RouterModule, thanks to the new provideRouter function.

So instead of using:

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(HttpClientModule),
    importProvidersFrom(RouterModule.forRoot(ROUTES, { preloadingStrategy: PreloadAllModules }))
  ]
});

you can now write:

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(HttpClientModule),
    provideRouter(ROUTES,
      withPreloading(PreloadAllModules)
    )
  ]
});

Other with... functions are available to enable router features:

  • withDebugTracing
  • withDisabledInitialNavigation
  • withEnabledBlockingInitialNavigation
  • withInMemoryScrolling
  • withRouterConfig

These changes allow tree-shaking parts of the router module that aren’t actually used, thus reducing the main bundle size.

Router

The router introduced the possibility of defining a page title on the route in Angular v14 (see our blog post). With this v14.2 release, it is now possible to retrieve the resolved title on the ActivatedRoute and ActivatedRouteSnapshot:

constructor(private route: ActivatedRoute) {
  this.title = route.snapshot.title;
}

It is now also possible to define guards and resolvers as simple functions. You can now write something like:

{
  path: '/user/:id/edit', 
  component: EditUserComponent,
  canDeactivate: [(component: EditUserComponent) => !component.hasUnsavedChanges]
}

The RouterLink directive received a tiny improvement that is noticeable: all its boolean inputs (preserveFragment, skipLocationChange and replaceUrl) now accept a string and coerce it to a boolean. This means you can now write:

<a [routerLink]="['/user', user.id, 'edit']" skipLocationChange='true'>Edit</a>
<!-- or even -->
<a [routerLink]="['/user', user.id, 'edit']" skipLocationChange>Edit</a>

instead of:

<a [routerLink]="['/user', user.id, 'edit']" [skipLocationChange]="true">Edit</a>

Angular CLI

The new CLI version does not have many features this time.

One notable addition is the ability for ng serve to serve service workers. It is enabled automatically if you have the option "serviceWorker": true in your builder configuration (which is the case by default when you add @angular/pwa to your application). This is handy as it allows us to use the usual ng serve to test the PWA behavior, whereas we previously had to build the application and serve it with another HTTP server to check it.

The work on the esbuild builder continues, and it is now faster to downlevel the JS code. In the esbuild builder as well, the Sass compilation now uses the “modern API” of Sass, which is faster than the legacy one. The classic webpack builder still uses the legacy Sass API, but should switch to the modern one soon as well.

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!


What's new in Angular 14.1?

Angular 14.1.0 is here!

Angular logo

This is a minor release, but it is packed with interesting features: let’s dive in!

Router new guard type: CanMatch

The router gained a new guard type in this release: CanMatch.

The existing CanActivate guard decides whether or not a navigation can go through. CanLoad guards decide if a module/component can be loaded. But there is no guard that allows matching a route depending on business logic: that’s what the CanMatch guard fixes.

It is now possible to define the same route several times, with different CanMatch guards, and to navigate to a specific one:

[
  { path: '', component: LoggedInHomeComponent, canMatch: [IsLoggedIn] },
  { path: '', component: HomeComponent }
]

Note that a CanMatch guard that returns false does not cancel the navigation: the route is skipped and the router simply continues matching other potential routes.

Here, navigating to / will render LoggedInHomeComponent if the user is logged in and will render HomeComponent otherwise. Note that the URL will remain / in both cases.

@Injectable({
  providedIn: 'root'
})
export class IsLoggedIn implements CanMatch {
  constructor(private userService: UserService) {}

  canMatch(route: Route, segments: Array<UrlSegment>): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
    return this.userService.isLoggedIn();
  }
}

A CanMatch guard can also redirect to another route like other guards do. To do so, you can return an UrlTree.

@Injectable({
  providedIn: 'root'
})
export class IsLoggedIn implements CanMatch {
  constructor(private userService: UserService, private router: Router) {}

  canMatch(route: Route, segments: Array<UrlSegment>): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
    return this.userService.isLoggedIn() || this.router.parseUrl('/');
  }
}

As the route is not even considered when the CanMatch guard returns false, it can be used to replace the CanLoad guard (and may even replace it in the future).

Router navigation events

The router now indicates why a navigation was canceled in a dedicated code field of the NavigationCancel event. Previously, you could use the reason field of the event to get the same information, but this was more a workaround than an intended feature. The code can now be used, and the reason field should only be used for debugging purposes. The code property can have the following values: NavigationCancellationCode.Redirect, NavigationCancellationCode.SupersededByNewNavigation, NavigationCancellationCode.NoDataFromResolver, or NavigationCancellationCode.GuardRejected.

The NavigationError also received a small improvement: the target of the navigation is now available in the event.

Standalone components

The built-in Angular directives and pipes offered by CommonModule (NgIf, NgFor, DatePipe, DecimalPipe, AsyncPipe, etc.) are now available as standalone!

You can now import them directly, without having to import CommonModule:

@Component({
  standalone: true,
  templateUrl: './user.component.html',
  imports: [NgIf, DecimalPipe] // -> you can now use `*ngIf` and `| number` in the template
})
export class UserComponent {

A new function called provideAnimations() is also available to add the animation providers to your application, instead of importing BrowserAnimationModule. Similarly, you can use provideNoopAnimations instead of importing the BrowserNoopAnimationsModule:

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

inject function

The new inject function introduced in Angular v14.0 allows injecting dependencies (see our last blog post for more info), and a second parameter can be used to define the injection flag. In v14, the second parameter was a bit field: InjectFlags.Host, InjectFlags.Optional, InjectFlags.Self, or InjectFlags.SkipSelf. In v14.1, this signature has been deprecated in favor of a more ergonomic one with an object as the second parameter:

value = inject(TOKEN, { optional: false });

The cool thing is that it improved the type safety of the function. Previously, TypeScript had no idea of the flag signification, and the return type was always T | null, even if the injection was not optional. This is now working properly, and the above example has a return type T.

runInContext function

The inject function mentioned above only works in the constructor, or to initialize a field, or in a factory function.

So, how can you use it in a method/function that is not a constructor? You can use the EnvironmentInjector.runInContext function that has been introduced for this purpose in v14.1!

For example, this doesn’t work:

export class AppComponent implements OnInit {
  ngOnInit() {
    console.log('AppComponent initialized', inject(UserService));
  }
}

But this does, thanks to runInContext:

export class AppComponent {
  constructor(private injector: EnvironmentInjector) {}

  ngOnInit() {
    this.injector.runInContext(() => {
      console.log('AppComponent initialized', inject(UserService));
    });
  }
}

setInput

ComponentRef has a new method called setInput that can be called to set an input. Why is that interesting?

Currently when you are testing an OnPush component, it is not easy to test if the change of an input properly triggers what you want, because manually setting the input does not trigger the change detection.

This is now no longer a problem if you call setInput()! If your UserComponent component has an input called userModel, you can now write the following code in a test:

  const fixture = TestBed.createComponent(UserComponent);
  fixture.componentRef.setInput('userModel', newUser);

setInput() properly sets the input (even if it is aliased), calls the NgOnChanges lifecycle hook and triggers the change detection!

This feature is useful in tests, but also with any kind of dynamic component. It even opens the door for the router to set the inputs of a component dynamically based on route params (a feature that the Vue router has for example). Maybe we’ll see that in a future release!

ContentChild descendants

The ContentChild decorator now supports the descendants option, as ContentChildren does. The default behavior does not change, and if you don’t specify the option, then ContentChild looks for the query in the descendants. This behavior is the same as specifying @ContentChild({ descendants: true }). But you can now change it by specifying @ContentChild({ descendants: false }), in which case Angular will only do a “shallow” search and look for the direct descendants.

Extended template diagnostics

The team added a few more “extended diagnostics”.

The first one is missingControlFlowDirective, and it’s linked to the Standalone Components story.

With this check enabled, the compiler warns us when a ngIf, ngFor, or ngSwitch is used in the template of a standalone component, but the corresponding directive or the CommonModule is not imported:

Error: src/app/register/register.component.html:11:59 - error NG8103: 
The `*ngFor` directive was used in the template, 
but neither the `NgForOf` directive nor the `CommonModule` was imported.
Please make sure that either the `NgForOf` directive or the `CommonModule`
is included in the `@Component.imports` array of this component.

This is a nice addition, as it can be fairly easy to forget to import CommonModule or the directive itself as I pointed out in our guide to standalone components. The message even mentions the directive you need to import, which can be tricky to figure out for *ngFor.

The second extended diagnostics is textAttributeNotBinding. When enabled, the compiler warns us when a class, style, or attr binding does not have the [], or if the value is not interpolated. For example, a template with class.blue="true" yields the following:

Error: src/app/register/register.component.html:2:8 - error NG8104: 
Attribute, style, and class bindings should be  
enclosed with square braces. For example, '[class.blue]="true"'.

Slightly related, the third one is suffixNotSupported. When enabled, the compiler warns us when a suffix like px, % or em is used on attribute binding where it doesn’t work, unlike when used in a style binding:

Error: src/app/register/register.component.html:2:9 - error NG8106: 
The '.px', '.%', '.em' suffixes are only supported on style bindings.

The fourth one is missingNgForOfLet. when enabled, the compiler warns us when a *ngFor is used with the let keyword. For example, *ngFor="user of users" throws with:

Error: src/app/users/users.component.html:1:7 - error NG8105: 
Your ngFor is missing a value. Did you forget to add the `let` keyword?

The fifth and last one is optionalChainNotNullable, and it is slightly similar to the already existing nullishCoalescingNotNullable check. When enabled, the compiler warns us when an unnecessary optional check is used. For example, if user is not nullable, then using `` in a template yields:

Error: src/app/user/user.component.html:2:21 - error NG8107: 
The left side of this optional chain operation does not include 'null' or 'undefined' in its type, 
therefore the '?.' operator can be replaced with the '.' operator.

Zone.js

zone.js has also been released in version v0.11.7, and contains a new feature that improves the debugging of asynchronous tasks, by using an experimental feature of Chrome. You can leverage this new support by importing import 'zone.js/plugins/async-stack-tagging';. When this is enabled, you’ll have nicer stack traces in case of an error in an async task.

Angular CLI

As usual, you can check out our dedicated article about the new CLI version:

👉 Angular CLI v14.1

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!


What's new in Angular CLI 14.1?

Angular CLI 14.1.0 is out!✨

If you want to upgrade to 14.1.0 without pain (or to any other version, by the way), I have created a Github project to help: angular-cli-diff. Choose the version you’re currently using (13.2.0 for example), and the target version (14.1.0 for example), and it gives you a diff of all files created by the CLI: angular-cli-diff/compare/13.2.0…14.1.0. It can be a great help along with the official ng update @angular/core @angular/cli command. You have no excuse for staying behind anymore!

Let’s see what we’ve got in this release.

npm init / yarn create

It is now possible to create a new Angular CLI project by using:

npm init @angular
yarn create @angular

Using this avoids the need to install the Angular CLI package globally. The commands support the same options as ng new.

esbuild builder improvements

The new experimental builder that uses esbuild has been introduced in v14, but with some missing features.

Even if it is still not feature complete, the esbuild builder now supports service workers and Sass files.

It also allows declaring external dependencies with the new externalDependencies option. When dependencies are listed in this option, they are excluded from the generated bundle. Instead, the created bundle relies on these dependencies to be available during runtime.

So for example if you define externalDependencies: ['@angular/core'], then the @angular/core package will not be bundled, and you’ll need to include it on your page in another way.

This is useful if you want to run several Angular applications on the same page, and use import maps to load Angular itself just once (instead of loading it in every bundle).

You’ll find more interesting features in our article about the framework v14.1.0 release.

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

Angular 14.0.0 is here!

Angular logo

This is a major release with really interesting features: let’s dive in!

Strictly typed forms

The most up-voted issue in the Angular repository is solved in Angular v14: we now have strictly typed forms!

As there is quite a bit to explain between the migration, the new API, and the addition of FormRecord and NonNullableFormBuilder, we wrote a dedicated blog post:

👉 Strictly typed forms

TL;DR: with some elbow grease, it’s now possible to have form values perfectly typed, and no longer of type any ✨.

Other forms improvements

It is now possible to use negative indices on FormArray methods, like the Array methods do in JavaScript. For example formArray.at(-1) is now allowed and returns the last control of the form array.

Standalone components (see ya NgModule!)

The other big feature of the release is the addition of the (experimental) standalone APIs. Same here: as there is a lot to cover, we wrote a dedicated blog post:

👉 A guide to standalone components

TL;DR: it’s now possible (but experimental) to get rid of NgModule in your applications, and use the new standalone components/directives and pipes ✨.

inject function

You can now use the inject() function from '@angular/core' (which already existed but has been improved) to inject a token programmatically.

For example you can use it in a component:

constructor() {
  const userService = inject(UserService);
  // ...
}

It can only be called in some specific areas:

  • in a constructor as above;
  • to initialize a class field;
  • in a factory function.

It opens the door to some interesting patterns, especially for library authors.

Router

The router received a lot of attention in this release.

Page title

It’s now possible to set a page title directly in a route declaration:

export const ROUTES: Routes = [
  { path: '', title: 'Ninja Squad | Home', component: HomeComponent },
  { path: 'trainings', title: 'Ninja Squad | Trainings', component: TrainingsComponent }
]

You can also define a resolver for the title:

export const ROUTES: Routes = [
  { path: 'trainings/:trainingId', title: TrainingTitleResolver, component: TrainingComponent }
]

@Injectable({
  providedIn: 'root'
})
export class TrainingTitleResolver implements Resolve<string> {
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): string {
    return `Ninja Squad | Training ${route.paramMap.get('trainingId')}`;
  }
}

This is not super flexible though, as you’ll probably often want to display something more meaningful like Ninja Squad | Angular training and not Ninja Squad | Training 13, and that data lives in the component: the resolver can’t access it. But it’s still a nice addition for adding static (or not too dynamic) titles.

It’s also possible to write a custom strategy to build the title by extending the built-in TitleStrategy. For example, if I want to prepend Ninja Squad | to all titles, I can do:

@Injectable()
export class CustomTitleStrategyService extends TitleStrategy {
  constructor(@Inject(DOCUMENT) private readonly document: Document) {
    super();
  }

  override updateTitle(state: RouterStateSnapshot) {
    const title = this.buildTitle(state);
    this.document.title = `Ninja Squad | ${title}`;
  }
}

Then, use this custom strategy instead of the default one:

@NgModule({
  //...
  providers: [{ provide: TitleStrategy, useClass: CustomTitleStrategyService }]
})
export class AppModule {}

And just define the specific part of the title on each route:

export const ROUTES: Routes = [
  { path: '', title: 'Home', component: HomeComponent },
  { path: 'trainings', title: 'Trainings', component: TrainingsComponent }
]

Types

Some types have been improved in the router. For example, all router events now have a type property, allowing to narrow their type like this:

// send hits to analytics API only on navigation end events
this.router.events
  .pipe(
    filter((event: Event): event is NavigationEnd => event.type === EventType.NavigationEnd),
    mergeMap(event => this.sendHit(event.url))
  )
  .subscribe();

pathMatch is now also more strictly typed and only accepts the two valid options 'full'|'prefix'. That’s why a migration will add the explicit Route or Routes types on your routes declaration if you don’t have them when running ng update. Otherwise, TypeScript will be unhappy with pathMatch: 'full' as it’ll think that 'full' is a string and not a const.

Route providers, standalone routes, loadComponent

A bunch of new things have been added to the router to support the new standalone APIs. For example, you can now define providers directly on a route, or lazy-load just a component. Check out our blog post about standalone components to learn more.

Accessibility

routerLinkActive gained a new input called ariaCurrentWhenActive, which allows to set aria-current a11y property. The possible values are 'page' | 'step' | 'location' | 'date' | 'time' | true | false. For example:

<a class="nav-link" routerLink="/" routerLinkActive="active" ariaCurrentWhenActive="page">Home</a>

TestBed

It’s now possible to configure the TestBed to throw errors on unknown elements or properties found in a template. Currently, Angular has two compilation modes: Just in Time (jit) and Ahead of Time (aot). When you run ng serve or ng build, the aot mode is used. But when running ng test, the jit compilation is used. Weirdly enough, an error in a template results in just a warning in the console when the template compilation fails on an unknown element or property in jit mode. That’s why you can sometimes see NG0303 and NG0304 warnings in the console when you run your tests, typically when you forgot to import or declare a component/directive necessary to test your component.

Starting with Angular v14, it is now possible to configure the TestBed to throw an error for these issues, and thus make sure we don’t miss them. I think this is amazing (but that’s probably because I implemented it 😬):

getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting(),
  { 
    errorOnUnknownElements: true, 
    errorOnUnknownProperties: true 
  }
);

The default for errorOnUnknownElements and errorOnUnknownProperties is false, but we’ll probably change it to true in a future release. You can also enable/disable them in a specific test with TestBed.configureTestingModule({ /*...*/, errorOnUnknownElements: false }).

In the distant future, the tests will maybe use the aot compilation, but that’s not for tomorrow. In the meantime, these new options should be helpful!

Compiler

As you probably know, you can’t use a private member of a component in a template, and you can only use public members. Starting with v14, you can now also use protected members of a component in the template.

Http

This is more a bugfix than a feature, but as it is a breaking change that may have an impact, let’s talk about it: + in query params are now properly encoded as %2B. They used to be ignored by the HttpClient that was otherwise properly encoding the other special characters in query parameters. You had to manually take care of the + signs, but this is no longer necessary: you can now delete the code that was manually encoding them after upgrading to v14.

Zone.js

Zone.js now supports Promise.any(), a new method introduced in ES2021.

Service worker

The versionUpdates observable now emits NoNewVersionDetectedEvent if the service worker did not find a newer version.

Devtools

The Angular Devtools are now available on Firefox as well 🎉.

Typescript and Node

Angular v14 drops the support of TypeScript v4.4 and 4.5 and now supports for v4.7 (recently released). It also drops the support of Node v12.

Angular CLI

As usual, you can check out our dedicated article about the new CLI version:

👉 Angular CLI v14

Summary

This release is packed with features as you can see, and the future is exciting with the standalone APIs. The roadmap now also mentions some efforts on the server-side rendering story, which is not the strong suite of Angular (compared to other mainstream frameworks).

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!


What's new in Angular CLI 14?

Angular CLI 14.0.0 is out!✨

If you want to upgrade to 14.0.0 without pain (or to any other version, by the way), I have created a Github project to help: angular-cli-diff. Choose the version you’re currently using (12.2.0 for example), and the target version (14.0.0 for example), and it gives you a diff of all files created by the CLI: angular-cli-diff/compare/13.2.0…14.0.0. It can be a great help along with the official ng update @angular/core @angular/cli command. You have no excuse for staying behind anymore!

Let’s see what we’ve got in this release.

Autocompletion

The first time you’re going to run an ng command in your terminal, you’ll see that Angular CLI will ask about setting up the autocompletion. If you accept, then pressing <TAB> after typing ng will list the available commands and options. However, this works only for Bash and Zsh shells on macOS and Linux operating systems.

If you don’t want to enable it and then change your mind, you can run ng completion to set it up manually.

esbuild builder

A new experimental builder that uses esbuild instead of Webpack has been introduced.

You can give it a try by replacing @angular-devkit/build-angular:browser with @angular-devkit/build-angular:browser-esbuild in your angular.json file. Note that the new builder is far from being complete. It does not support a bunch of options (and does not understand Sass for example).

These are very early days of course, but you can give it a try if you’re curious. It looks very promising, and we can hope for a faster builder in the future that would rely on esbuild. And, who knows, a CLI that uses Vite instead of Webpack (check out our article about Vite to learn more).

ng cache

A new ng cache command has been added to manage the cache system introduced in CLI v13 (see our article).

This new command has 4 subcommands:

  • ng cache enable to enable the cache.
  • ng cache disable to disable the cache.
  • ng cache clean to delete the cache from disk (useful when switching branches and Webpack gets lost).
  • ng cache info which will print statistics and information about the cache.

ng generate

ng generate has a new --standalone flag to generate standalone components/pipes/directives.

It is now also possible to specify multiple schematics collections with schematicCollections in your angular.json file (instead of the now deprecated defaultCollection).

"cli": {
  "schematicCollections": ["@schematics/angular", "@angular/material"]
}

The CLI will then look for schematics in the specified list, following the order of priority (the first schematic wins).

ng e2e, lint and deploy

The ng e2e, ng lint, and ng deploy don’t come with an implementation as you may know. All these commands now ask you what implementation you’d like to add when you run them for the first time. ng lint only offers ESLint, but ng e2e lets you pick between Cypress/Nightwatch/WebdriverIO, and ng deploy between Amazon S3/Azure/Firebase/Netlify/NPM/GitHub Pages.

Fun with flags

Some deprecated options have been removed and a schematic will automatically take care of migrating your project:

  • --all option from ng update has been removed without replacement
  • --prod option has been removed from all builders. --configuration production/-c production should be used instead.
  • showCircularDependencies option from ng build has been removed without replacement
  • defaultProject workspace option has been deprecated (the default is now the current working directory)

Note that the flag parser of the CLI changed and that camelCase arguments are no longer supported: for example, you need to write ng g c hello --skip-selector instead of ng g c hello --skipSelector.

You’ll find more interesting features in our article about the framework v14.0.0 release.

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


A guide to Standalone Components in Angular

Angular v14 introduces one major (experimental) feature, after months of discussion: the possibility to declare standalone components/pipes/directives, and to get rid of NgModule in your application if you want to 😍.

In this article, we’ll see:

  • how to declare standalone entities,
  • how to use them in existing applications
  • how to get rid of NgModule if you want to
  • how the router has changed to leverage this new feature
  • and more nerdy details!

Disclaimer: this blog post is based on early releases of Angular v14, and some details may change based on the feedback the Angular team gets. That’s why, for once, we write a blog post on a feature before its final release: this is a great opportunity to give it a try and gather feedback!

Standalone components

Components, directives, and pipes can now be declared as standalone.

@Component({
  selector: 'ns-image',
  standalone: true,
  templateUrl: './image.component.html'
})
export class ImageComponent {
}

When that’s the case, the component/directive/pipe can’t be declared in an NgModule. But it can be directly imported into another standalone component. For example, if my ImageComponent above is used in the template of a standalone UserComponent, you have to import ImageComponent in UserComponent:

@Component({
  selector: 'ns-user',
  standalone: true,
  imports: [ImageComponent],
  templateUrl: './user.component.html' 
  // uses `<ns-image>`
})
export class UserComponent {
}

This is true for every component/directive/pipe you use in a standalone component. So if the template of UserComponent also uses a standalone FromNowPipe and a standalone BorderDirective, then they have to be declared into the imports of the component:

@Component({
  selector: 'ns-user',
  standalone: true,
  imports: [ImageComponent, FromNowPipe, BorderDirective],
  templateUrl: './user.component.html' 
  // uses `<ns-image>`, `fromNow` and `nsBorder`
})
export class UserComponent {
}

This is also true for components, directives, and pipes offered by Angular itself. If you want to use ngIf in a template, the directive has to be declared. But ngIf is not a standalone directive: it is offered via the CommonModule. That’s why imports lets you import any NgModule used as well:

@Component({
  selector: 'ns-user',
  standalone: true,
  imports: [CommonModule, RouterModule, ImageComponent, FromNowPipe, BorderDirective],
  templateUrl: './user.component.html' 
  // uses `*ngIf`, `routerLink`, `<ns-image>`, `fromNow` and `nsBorder`
})
export class UserComponent {
}

You can of course import your own existing modules or modules offered by third-party libraries. If you use the DragDropModule from Angular Material for example:

@Component({
  selector: 'ns-user',
  standalone: true,
  imports: [CommonModule, RouterModule, DragDropModule, ImageComponent],
  templateUrl: './user.component.html' 
  // uses `*ngIf`, `routerLink`, `cdkDrag`, `<ns-image>`
})
export class UserComponent {
}

A standalone component can also define schemas if you want to ignore some custom elements in its template with CUSTOM_ELEMENTS_SCHEMA or even ignore all errors with NO_ERRORS_SCHEMA.

Usage in existing applications

This is all great, but how can we use our new standalone UserComponent in an existing application that has no standalone components?

Maybe you guessed it: you can import a standalone component like UserComponent in the imports of an NgModule!

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule, UserComponent], // <---
  bootstrap: [AppComponent]
})
export class AppModule {}

This is probably a sound strategy to start using standalone components, pipes, and directives in existing applications. Angular applications tend to have a SharedModule with commonly used components, directives, and pipes. You can take these and convert them to a standalone version. It’s usually straightforward, as they have few dependencies. And then, instead of importing the full SharedModule in every NgModule, you can import just what you need!

CLI support

The Angular CLI team added a new flag --standalone to ng generate in v14, allowing to create standalone versions of components, pipes, and directives:

ng g component --standalone user

The component skeleton then has the standalone: true option, and the imports are already populated with CommonModule (that will be used in pretty much all components anyway):

@Component({
  selector: 'pr-user',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {

The generated test is also slightly different. A standalone component is declared in the imports option of TestBed.configureTestingModule() instead of in the declarations option.

If you want to generate all components with the --standalone flag, you can set the option directly in angular.json:

"schematics": {
  "@schematics/angular:component": {
    "standalone": true
  }
}

You can of course do the same for the directive and pipe schematics.

Application bootstrap

If you want to, you can go one step further and write an application with only standalone entities, and get rid of all NgModules. In that case, we need to figure out a few details.

First, if we don’t have an Angular module, how can we start the application? A typical main.ts contains a call to platformBrowserDynamic().bootstrapModule(AppModule) which bootstraps the main Angular module of the application.

In a standalone world, we don’t want to use NgModule, so we don’t have an AppModule.

Angular now offers a new function called bootstrapApplication() in @angular/platform-browser. The function expects the root standalone component as a parameter:

bootstrapApplication(AppComponent);

This creates an application and starts it.

For SSR, you can use the new renderApplication function, which renders the application as a string:

const output: string = await renderApplication(AppComponent, { appId: 'app' });

Optional NgModules

NgModule is a weird concept in Angular if you think about it. They fulfill several roles at once. We use them to declare what is usable in the templates of the components, but also to configure the available providers. We can export entities, to make them available in other modules. Modules are eagerly executed, which means you can add code in their constructors if you want to run something on their initialization. They are also necessary if you want to lazy-load parts of your application.

If modules are now optional, how can we do all these tasks?

Providers

NgModules allow defining providers available for components in the module. For example, if you want to use HttpClient, you add HttpClientModule to the imports of your main module.

In an application with no module, you can achieve the same by using the second parameters of bootstrapApplication(), which allows declaring providers:

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

In the long run, Angular will probably offer a function returning the HTTP providers. For now, to bridge the gap with modules that expose providers, we can use importProvidersFrom(module):

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

You can also use importProvidersFrom to configure the router:

bootstrapApplication(AppComponent, { 
  providers: [importProvidersFrom(RouterModule.forRoot([/*...*/]))]
});

Note that the BrowserModule providers are automatically included when starting an application with bootstrapApplication().

It’s also worth noting that you can’t use importProvidersFrom in component providers: it’s only usable in bootstrapApplication(). bootstrapApplication() is now responsible for the Dependency Injection work, and that’s where providers must be declared.

Note: since Angular v15, it’s now possible to use provideRouter() and provideHttpClient() (see our blog post about Angular HTTP in a standalone application).

Lazy loading routes

The lazy-loading story in Angular has always revolved around NgModule. Let’s say you wanted to lazy-load an AdminComponent. You had to write an NgModule like the following:

@NgModule({
  declarations: [AdminComponent],
  imports: [
    CommonModule, 
    RouterModule.forChild([{ path: '', component: AdminComponent }])
  ],
})
export class AdminModule {}

and then load the module with the router function loadChildren:

{ 
  path: 'admin',
  loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}

You can now get rid of AdminModule if AdminComponent is standalone, and directly lazy-load the component with loadComponent:

{ 
  path: 'admin',
  loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)
}

This is a really nice addition! All the lazy-loaded components must be standalone of course. It’s worth noting that this feature exists in all other mainstream frameworks, and Angular was lacking a bit on this.

We can also lazy-load several routes at once, by directly loading the routes config with loadChildren:

{ 
  path: 'admin',
  loadChildren: () => import('./admin/admin.routes').then(c => c.adminRoutes)
}

We now have a nice symmetry between children/loadChildren and component/loadComponent!

But NgModules also allow to define providers for a lazy-loaded module: the providers are then only available in the components of the lazy-loaded module. To achieve the same thing, you can now declare providers directly on a route, and the providers will be available only for this route and its children:

{ 
  path: 'admin',
  providers: [AdminService],
  loadComponent: () => import('./admin/admin.component').then(c => c.AdminComponent)
}

This works with all types of routes (with component, loadComponent, children, loadChildren with routes or NgModule). In my example above, the component is lazy-loaded, but the service is not. If you want to lazy-load the service as well, you can use:

{ 
  path: 'admin',
  loadChildren: () => import('./admin/admin.routes').then(c => c.adminRoutes)
}

and define the providers in adminRoutes:

export const adminRoutes: Routes = [
  { 
    path: '',
    pathMatch: 'prefix',
    providers: [AdminService], // <--
    children: [
      { path: '', component: AdminComponent }
    ]
  }
];

Initialization

An NgModule can also be used to run some initialization logic, as they are eagerly executed:

@NgModule({ /*...*/ })
export class AppModule {
  constructor(currentUserService: CurrentUserService) {
    currentUserService.init();
  }
}

To achieve the same without a module, we can now use a new multi-token ENVIRONMENT_INITIALIZER. All the code registered with this token will be executed during the application initialization.

bootstrapApplication(AppComponent, {
  providers: [
    {
      provide: ENVIRONMENT_INITIALIZER,
      multi: true,
      useValue: () => inject(CurrentUserService).init()
    }
  ]
});

Note that importProvidersFrom(SomeModule) is smart enough to automatically register the initialization logic of SomeModule in ENVIRONMENT_INITIALIZER.

Angular compiler and Vite

On a low level, NgModules are the smallest unit that the compiler can re-compile when running ng serve. Indeed, if you update the selector of a component for example, then the Angular compiler has to check all the templates of the module that contains this component to see if something changed, and also all the modules that import that module. Right now, the Angular compiler is tightly coupled with the TypeScript compiler and does a lot of bookkeeping to only recompile what’s necessary. In an application with no NgModules, the compiler has a more straightforward task: it will for example only recompile the components that directly import the modified component.

This can be good news for the future of Angular tooling. The frontend world has been taken by storm by Vite. We talked about Vite, and the differences with Webpack, in this blog post.

TL;DR: Vite only re-compiles the files needed to display a page and skips the TypeScript compilation to only do a simple transpilation, often in parallel.

This works great for Vue, React, or Svelte, but not so great for Angular, where a lot more needs to be recompiled, and where TypeScript is needed. Standalone components are a nice step in this direction, and may allow a future Angular CLI with Vite instead of Webpack and way faster re-builds.

Caveats

To be honest, the standalone API feels great. We migrated a few applications, and this is really nice to use, and it feels good to get rid of NgModules!

A few pain points though.

  1. There are no “global imports”: you need to import a component/pipe/directive every time you use it. ngIf, ngFor, and friends are available in every standalone component generated by the CLI, as the skeleton includes the import of CommonModule. But routerLink for example is not: you need to import RouterModule if you need it. Other frameworks, like Vue for example, allow registering some components globally, to avoid importing them over and over. That’s not the case in Angular.

  2. Sometimes you forget to add an import, and your template doesn’t work, with no compilation error. For example, adding a link with [routerLink]="['/'] does not compile, but routerLink="/" does compile (and doesn’t work). I feel that these kinds of errors happen more often than they did with NgModule. IDEs will probably help us here, and I suppose typing routerLink in a template will result in an automatic addition of RouterModule in the component’s imports in VS Code/Webstorm/whatever is a few months.

  3. You can’t bootstrap multiple components at once with the new bootstrapApplication() function, whereas it was possible with the NgModule-based bootstrap.

  4. TestBed works with standalone components, but will probably include more specific APIs to simplify tests in the future. Note that it is already easier to test standalone components than classic components, as you don’t have to repeat in configureTestingModule all the dependencies the component needs.

Summary

Six years after the initial release, we can finally get rid of NgModule in Angular if we want to. Their addition to Angular was a bit rushed: they were introduced in Angular v2.0.0-rc.5 two months (!!) before the stable release, mainly to help the ecosystem build libraries. As often in our field, the rushed design resulted in an entity that mixed several concerns, with some concepts quite hard to understand for beginners.

The new “mental model” is easy to grasp: providers are declared on an application level and components just have to import what they need in their templates. It will also probably be easier for newcomers to understand how Angular works.

These standalone APIs are trying to make things clearer, and it looks like they did ♥️.

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


Posts plus anciens