What's new in Angular 15.2?

Angular 15.2.0 is here!

Angular logo

This is a minor release with some interesting features and some big news: let’s dive in!

Easily migrate to standalone components!

The Angular team is releasing a set of schematics to automatically migrate your application to standalone components. It does an amazing job at analyzing your code, migrating your components/pipes/directives to their standalone versions, and removing the obsolete modules of your application 😍.

Sounds interesting? We wrote a guide about it:

👉 Migrate to standalone components with Angular schematics

Angular Signals

The Angular team has been working on a different way to handle reactivity in your application for the past year. The first step of the result has been publicly released (even if there is nothing to use yet, as the API will only be available in v16): the discussion about Angular Signals.

Signals are a concept that is used in many other frameworks, like SolidJS, Vue, Preact and even the venerable KnockoutJS. The idea is to offer a few primitives to define reactive state in your application and to allow the framework to know which components are impacted by a change, rather than having to detect changes on the whole tree of components.

This would be a significative change to how Angular works, as it currently relies on zone.js to detect changes in the whole tree of components by default. Instead, with signals, the framework would only re-render the components that are impacted by a change.

This also opens the door to zoneless applications, i.e. applications where Angular applications don’t need to include Zone.js (which makes them lighter), and don’t have to patch all the browser APIs (which makes them start faster).

The first draft of the API is available and looks like this:

// define a signal
const count = signal(0);
// get the value of the signal
const value = count();
// set the value of the signal
count.set(1);
// update the value of the signal, based on current value
count.update((value) => value + 1);
// mutate the value of the signal (handy for objects/arrays)
const user = signal({ name: 'JB', favoriteFramework: 'Angular' });
user.mutate((user) => user.name = 'Cédric');

Once you have defined signals, you can define computed values that derive from them:

const double = computed(() => count() * 2);

Computed values are automatically computed when one of the signals they depend on changes.

count.set(2);
console.log(double()); // logs 4

Note that they are lazily computed and only re-computed when one of the signals they depend on produces a new value.

Finally, you can use the effect function to react to changes in your signals:

// log the value of the count signal when it changes
effect(() => console.log(count()));

This does look like a BehaviorSubject, but it has some subtle differences, the most important one being that unsubscribing is unnecessary thanks to the usage of weak references.

That’s pretty much it for now! The next step is to integrate this API with the framework, and make it interoperate with RxJS.

In an ideal future, we may be able to build a component with fields that are signals and computed values used in the template. The framework would be notified when an expression has changed thanks to the signals, and would thus only need to re-render the components affected by the change, without having to check for changes on unrelated components, without the need for zone.js. But there is a long way ahead, as several things needs to be rethought in the framework to make this work (what about inputs, outputs, queries, lifecycle methods, etc?).

This is anyway an exciting project, and it’s quite interesting how frameworks inspire each others, with Angular taking inspiration from Vue and SolidJS for the reactivity part, whereas other frameworks are increasingly adopting the template compilation approach of Angular, with no Virtual DOM needed at runtime.

Deprecation of class-based guards and resolvers

The class-based guards and resolvers are now officially deprecated on a route definition. As you may know, it is possible to write them as functions since Angular v14.2 (check out our blog post about that).

You can migrate your guards and resolvers to functions fairly easily or you can simply wrap the class with inject() as a quick way to get rid of the deprecation warning:

{ path: 'users', component: UsersComponent, canActivate: () => inject(LoggedInGuard).canActivate() }

Note that the CanActivate, CanDeactivate, etc interfaces will be deleted in a future version of Angular.

RouterTestingHarness

The RouterTestingModule now provides a RouterTestingHarness that can be used to write tests. It can be handy to test components that expect an ActivatedRoute for example, or when you want to trigger navigations in your tests to test guards or resolvers.

RouterTestingHarness has a static method create that can be called with an optional initial navigation. This method returns a promise of the created harness, that can then be used to trigger navigations, using navigateByUrl.

// load the routes in the TestBed
TestBed.configureTestingModule({
  imports: [RouterTestingModule.withRoutes(routes)],
});
// create the harness
const harness = await RouterTestingHarness.create();
// explicitly cast the component returned with `<UserComponent>`
const component = await harness.navigateByUrl<UserComponent>('/users/1');
// or pass the type as the second argument
// in that case, the test fails if the component is not of the expected type when navigating to /users/1
const component = await harness.navigateByUrl('/users/1', UserComponent);

The harness provides a routeDebugElement property that returns the DebugElement of the component you navigated to, and a routeNativeElement property that returns the native element of the component. If you want to get the component instance, you can either get it as the return value of navigateByUrl, or by accessing harness.routeDebugElement.componentInstance.

The harness does not have a property to access the ComponentFixture as we usually have in tests, but directly provides a detectChanges method that will trigger change detection on the component.

const harness = await RouterTestingHarness.create();
const component = await harness.navigateByUrl('/users/1', UserComponent);
component.name = 'Cédric';
harness.detectChanges();
expect(harness.routeNativeElement!.querySelector('#name')!.textContent).toBe('Cédric');

withNavigationErrorHandler

A new feature called withNavigationErrorHandler has been added to the router. It can be used in provideRouter to provide a custom error handler for navigation errors.

provideRouter(routes, withNavigationErrorHandler((error: NavigationError) => {
  // do something with the error
}))

This is roughly equivalent to the (now deprecated) errorHandler you could configure on the RouterModule.

NgOptimizedImage

NgOptimizedImage has a new loaderParams input that accepts an object.

<!-- params = { isBlackAndWhite: true } for example -->
<img [ngSrc]="source" [loaderParams]="params"></img>

This object will be passed to your custom loader when it is called, as a property loaderParams in the ImageLoaderConfig.

const customLoader = (config: ImageLoaderConfig) => {
    const { loaderParams } = config;
    // do something with loaderParams        
};

Performances

The NgClass directive has been rewritten to improve performances. Its algorithm is now a bit smarter and triggers less change detections and DOM updates. You don’t have to change anything, you’ll get that for free when upgrading 😍.

Angular CLI

The CLI had few changes in this release, so no dedicated article this time.

The esbuild builder now supports Less stylesheets, CommonJS dependency checks and node modules license extraction. Maybe more importantly, it now uses the new incremental rebuild of esbuild, introduced in esbuild v0.17. Watch mode should now be even faster.

Another tiny new feature: ng update now logs the number of files modified by the migrations.

Summary

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!


How to migrate an Angular application to standalone components?

Angular 14 introduced standalone components and optional modules. But it can be quite a daunting task to migrate an existing application to this new model!

Angular logo

This guide supposes that you are familiar with the new standalone components and optional modules introduced in Angular 14. If not, you can read our dedicated article.

The Angular team has been working on a collection of schematics to help you migrate your application to standalone components. These schematics are available in Angular v15.2.0 and above. So the first step is to update your application to the latest version of Angular. Then we’re good to go!

Let’s dive in.

Schematics to the rescue

The schematics are available in the @angular/core package.

To run them, enter:

ng generate @angular/core:standalone

The schematics expects two arguments:

  • the path to the application you want to migrate (by default ‘./’)
  • the mode of the schematic (by default ‘convert-to-standalone’)

There are three modes available:

  • convert-to-standalone: this is the default mode, and it will convert all your components to standalone components, except the ones declared in your main module.
  • prune-ng-modules: this mode will remove all the modules that aren’t necessary anymore.
  • standalone-bootstrap: this mode will bootstrap your application with the bootstrapApplication function, and migrate the components referenced in your main module.

To fully run a migration, you need to run the schematics in the three modes consecutively.

Convert to standalone

The first mode will convert all your components to standalone components, except the ones referenced in the bootstrap field of your main module. It also updates the related unit tests.

As this is the default mode, you can run:

ng generate @angular/core:standalone --defaults

The schematic is quite smart, as it compiles the template of each component to detect what the standalone version of the component needs to import.

For example, if you have a component that uses the NgIf, RouterLink and FormControl directives, the schematic will add NgIf, RouterLink (as they are standalone directives) and ReactiveFormModule (as FormControl is available via this module) to the list of imports of the standalone component (and add the necessary TypeScript imports). It also works with your own components, pipes and directives, and the ones from third-party libraries of course.

Be warned though: the schematic can’t target a specific component or module, so it generates a ton of changes in your application. It also generates some “noise”: some files are modified but not really changed, because the schematic sometimes reformats the code.

To avoid this, I strongly advise you to add a formatter to your project, for example Prettier. If you want to learn how, we have a dedicated article about how to add ESLint and Prettier to your Angular project.

This allows you to run ng lint --fix after the schematic, to only focus on the real changes.

All your entities are now standalone components, pipes and directives. The schematic also updates the modules of your application, by moving the migrated entities from the declarations array, to the imports array.

At the end of this step, you’ll have most of your components migrated to standalone components, but you’ll still have your existing modules. The application should still work if you run ng serve, ng test, etc.

Prune the modules

The second mode will remove all the modules that aren’t necessary anymore.

To run it, enter:

ng generate @angular/core:standalone --defaults --mode=prune-ng-modules

The schematic can remove a module only if:

  • it doesn’t have any declarations, providers or bootstrap
  • it doesn’t have any code in its constructor, or other methods
  • it doesn’t have any imports that reference a ModuleWithProviders

If your module has providers, you can usually move them.

This last one means that modules that import a module with providers (like RouterModule.forChild) can’t be removed without a bit of work first.

Typically, if you lazy-loaded modules, you have a module that looks like this:

@NgModule({
  imports: [RouterModule.forChild(adminRoutes)]
})
export class AdminModule { }

With the routes of the module declared like this:

export const adminRoutes: Routes = [
  { path: '', component: AdminComponent }
];

And a main route file that lazy-load the module:

const routes: Routes = [
  { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }
];

You then need to manually migrate this to lad the routes directly:

const routes: Routes = [
  { path: 'admin', loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes)
];

When this is done, you can remove RouterModule.forChildfrom the imports of the admin module, and manually delete the AdminModule if it isn’t referenced elsewhere.

Sometimes, your module is referenced somewhere else in your application. In that case, the call won’t be removed by the schematic. The schematic adds a comment with a TODO

console.log(/* TODO(standalone-migration): clean up removed NgModule reference manually */ AdminModule);

You can look into your codebase to see where AdminModule is referenced and remove it manually.

Bootstrap the application

The last mode will update your application to use the bootstrapApplication function. It will convert the main module of your application and all its components/pipes/directives to standalone components. It also converts the imports of other modules to importProvidersFrom calls. When it can, it uses the appropriate provide...() function to import the providers: for example provideRouter() for the RouterModule, or provideHttpClient() for the HttpClientModule 😍.

To run it, enter:

ng generate @angular/core:standalone --defaults --mode=standalone-bootstrap

After this step, your application is fully migrated to standalone components. You can then do a bit of cleanup in your codebase with ng lint --fix and check that everything is still working.

Your tests will need to be updated though: the schematics can’t analyze them (as tests are not compiled in AoT mode). The schematics tries to do its best to update them, and moves the declarations to the imports of the testing module, but you usually have to do some manual work to make them work again.

A strategy for large applications

As this can be a daunting task in a large project, where thousands of tests are affected, you can try to approach this migration in small steps.

Even if the migration runs on the whole project, you can then use your version control system to revert the changes on all modules except one. I usually start with the “shared” module (that almost all projects have), which usually contains components/pipes/directives that are used in many places and fairly easy to migrate as they are “leaves” of the application.

Then, I lint the code, commit the changes, run the tests and fix them.

Once this is done, I can move on to the next module, starting with the small ones and progressively moving to the bigger ones. One a module is migrated, I migrate its routes configuration, in order to delete the module.

Rinse and repeat until you’re done! Your application now uses standalone components 🎉.

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

Angular 15.1.0 is here!

Angular logo

This is a minor release with some interesting features: let’s dive in!

TypeScript 4.9

TypeScript 4.9 is now supported by Angular. This new version of TypeScript brings some new features, as you can see in the official blog post, including the new satisfies operator.

Templates

It is now possible to use self-closing tags on custom elements. This is a small but nice improvement, as it allows to write:

<my-component />

instead of:

<my-component></my-component>

This is also applicable to ng-content and ng-container for example.

Fun fact: this might not seem like a big change, but this is actually the first time that Angular allows a syntax that is not HTML compliant in templates. Until now, templates were always valid HTML (yes, even, the binding syntax [src]!). But as Angular templates are never parsed by the browser, the Angular team decided to allow this syntax, and it will probably be extended in the future to other non-HTML compliant syntaxes that can improve the developer experience.

Router

The CanLoad guard is now officially deprecated and replaced by the recently introduced CanMatch guard. They both achieve the same goal (prevent loading the children of a route), but the CanMatch guard can also match another route when it rejects. CanLoad was also only running once, whereas CanMatch runs on every navigation as the CanActivate guard.

It is now possible to define the onSameUrlNavigation option for a specific navigation to specify what to do when the user navigates to the same URL as the current one, with two possible values: reload and ignore. This was previously only possible globally with the RouterConfigOptions of the router (or withRouterConfig if you’re using the standalone router providers).

You can now do something like:

this.router.navigateByUrl('/user', { onSameUrlNavigation: 'reload' })

The router also gained a new event NavigationSkipped that is emitted when a navigation is skipped because the user navigated to the same URL as the current one or if UrlHandlingStrategy ignored it.

A new withHashLocation() function has been added to the router to configure the router to use a hash location strategy. It was previously configured via DI { provide: LocationStrategy, useClass: HashLocationStrategy }. You can now write:

providers: [provideRouter(routes, withHashLocation())]

Core

A new function isStandalone() was added to check if a component, directive or pipe is standalone or not.

const isStandalone = isStandalone(UserComponent);

Tests

The TestBed now has a new method runInInjectionContext to easily run a function that uses inject(). This was already possible via the verbose TestBed.inject(EnvironmentInjector).runInContext(). This is especially useful when you want to test a functional guard or resolver for example, and this is what the CLI now generates by default for the tests of these entities.

Angular CLI

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

👉 Angular CLI v15.1

Summary

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.1?

Angular CLI 15.1.0 is out!✨

If you want to upgrade to 15.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 (14.2.0 for example), and the target version (15.1.0 for example), and it gives you a diff of all files created by the CLI: angular-cli-diff/compare/14.2.0…15.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.

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. It now supports Sass inline styles. Less is not supported yet. It also supports the --stats-json option which is useful to generate a JSON file with the build stats.

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 🤯.

ng generate environments

A new schematic has been added to add environment files to an application. If you are a returning reader, you know that the environment files have been removed from a new application in v15, and you now have to add them yourself.

The new schematic can be used to simplify the task:

ng generate environments

This will create the environments folder with the environment.ts and environment.development.ts files, and update the angular.json file to use the development environment file when using the development configuration:

"fileReplacements": [
  {
    "replace": "src/environments/environment.ts",
    "with": "src/environments/environment.development.ts"
  }
]

Notice that this is different from what the CLI used to generate by default, where the environment.ts file was used for the development configuration, and an environment.prod.ts file was used for the production configuration. The CLI team thinks this is more aligned with the current naming choices, as prod is not a configuration.

ng generate config

Another new schematic has been added to generate the configuration files that are now hidden by default since v15.

You can use:

ng generate config karma

to generate the karma.conf.js file, and:

ng generate config browserslist

to generate the .browserslistrc file.

ng generate interceptor

ng generate interceptor can now generate an interceptor with the --functional option. This will generate a functional interceptor, which is a new feature in Angular v15.

ng generate interceptor --functional authentication

ng generate guard

ng generate guard now supports the --guardType option, which is the same as --implements, but makes more sense when you’re generating a functional guard (as they are not implementing an interface).

ng generate guard --functional --guardType CanActivate logged-in

The guards as classes are probably going to be deprecated really soon, so --functional will be the default and --implements will be removed.

Summary

That’s all for the CLI v15.1 release! You’ll find more interesting features in our article about the framework v15.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 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!


Posts plus anciens