DEV Community

Cover image for New Features of Angular 15
Mohammed Mahmoud
Mohammed Mahmoud

Posted on

New Features of Angular 15

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

Angular v15 brings many improvements and new features. This section only contains some of the innovations in v15.


- Standalone components are stable! ✨

The standalone components API lets you build Angular applications without the need to use NgModules.

As part of making sure standalone APIs were ready to graduate we have ensured that standalone components work across Angular, and they now fully work in HttpClient, Angular Elements, router and more.

The standalone APIs allow you to bootstrap an application using a single component: 👇

import {bootstrapApplication} from '@angular/platform-browser';
import {ImageGridComponent} from'./image-grid';

@Component({
  standalone: true,
  selector: 'photo-gallery',
  imports: [ImageGridComponent],
  template: `
    … <image-grid [images]="imageList"></image-grid>
  `,
})
export class PhotoGalleryComponent {
  // component logic
}

bootstrapApplication(PhotoGalleryComponent);
Enter fullscreen mode Exit fullscreen mode

- Directive composition API ✨

👉 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

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

And then use your directive like this 🎉:

<button appButton
  tooltip="Info"
  [tooltipHideDelay]="delay"
  [dragDisabled]="isDisabled">
</button>
Enter fullscreen mode Exit fullscreen mode

- NgOptimizedImage is stable ✨

The NgOptimizedImagedirective is now stable and can be used in production. Introduced in Angular v14.2, it allows you to optimize images.

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"> />

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.


- Functional router guards ✨

Together with the tree-shakable standalone router APIs we worked on reducing boilerplate in guards. Let’s look at an example where we define a guard which verifies if the user is logged in:

@Injectable({ providedIn: 'root' })
export class MyGuardWithDependency implements CanActivate {
  constructor(private loginService: LoginService) {}

  canActivate() {
    return this.loginService.isLoggedIn();
  }
}

const route = {
  path: 'somePath',
  canActivate: [MyGuardWithDependency]
};
Enter fullscreen mode Exit fullscreen mode

LoginService implements most of the logic and in the guard we only invoke isLoggedIn(). Even though the guard is pretty simple, we have lots of boilerplate code.

With the new functional router guards, you can refactor this code down to:

const route = {
  path: 'admin',
  canActivate: [() => inject(LoginService).isLoggedIn()]
};
Enter fullscreen mode Exit fullscreen mode

We expressed the entire guard within the guard declaration.


- Router unwraps default imports ✨

To make the router simpler and reduce boilerplate further, the router now auto-unwraps default exports when lazy loading.

Let’s suppose you have the following LazyComponent:

@Component({
  standalone: true,
  template: '...'
})
export default class LazyComponent { ... }
Enter fullscreen mode Exit fullscreen mode

Before this change, to lazy load a standalone component you had to:

{
  path: 'lazy',
  loadComponent: () => import('./lazy-file').then(m => m.LazyComponent),
}
Enter fullscreen mode Exit fullscreen mode

Now the router will look for a default export and if it finds it, use it automatically, which simplifies the route declaration to:

{
  path: 'lazy',
  loadComponent: () => import('./lazy-file'),
}
Enter fullscreen mode Exit fullscreen mode

- Better Stack Traces For Debugging Process ✨

We partnered with Chrome DevTools to fix this! Let’s look at a sample stack trace that you may get working on an Angular app:

ERROR Error: Uncaught (in promise): Error
Error
    at app.component.ts:18:11
    at Generator.next (<anonymous>)
    at asyncGeneratorStep (asyncToGenerator.js:3:1)
    at _next (asyncToGenerator.js:25:1)
    at _ZoneDelegate.invoke (zone.js:372:26)
    at Object.onInvoke (core.mjs:26378:33)
    at _ZoneDelegate.invoke (zone.js:371:52)
    at Zone.run (zone.js:134:43)
    at zone.js:1275:36
    at _ZoneDelegate.invokeTask (zone.js:406:31)
    at resolvePromise (zone.js:1211:31)
    at zone.js:1118:17
    at zone.js:1134:33
Enter fullscreen mode Exit fullscreen mode

This snippet suffers from two main problems:

  • There’s only one line corresponding to code that the developer has authored. Everything else is coming from third-party dependencies (Angular framework, Zone.js, RxJS)

  • There’s no information about what user interaction caused the error

The Chrome DevTools team created a mechanism to ignore scripts coming from node_modules by annotating source maps via the Angular CLI. We also collaborated on an async stack tagging API which allowed us to concatenate independent, scheduled async tasks into a single stack trace. Jia Li integrated Zone.js with the async stack tagging API, which allowed us to provide linked stack traces.

These two changes dramatically improve the stack traces developers see in Chrome DevTools:

ERROR Error: Uncaught (in promise): Error
Error
    at app.component.ts:18:11
    at fetch (async)  
    at (anonymous) (app.component.ts:4)
    at request (app.component.ts:4)
    at (anonymous) (app.component.ts:17)
    at submit (app.component.ts:15)
    at AppComponent_click_3_listener (app.component.html:4)
Enter fullscreen mode Exit fullscreen mode

- Release MDC-based components✨

Angular announced the refactoring of the Angular material components based on Material Design Components for Web (MDC) is now done! This change allows Angular to align even closer to the Material Design specification, reuse code from primitives developed by the Material Design team, and enable us to adopt Material 3 once we finalize the style tokens.


- @keyframes name format changes ✨

In v15, @keyframes names are prefixed with the component's scope name.

For example, in a component definition whose scope name is host-my-cmp, a @keyframes rule with a name in v14 of:

@keyframes foo { ... }

becomes in v15:

@keyframes host-my-cmp_foo { ... }


Deprecations

Major releases allow us to evolve the framework towards simplicity, better developer experience, and alignment with the web platform.

After analyzing thousands of projects within Google we found few rarely used patterns which in most cases are misused. As result we’re deprecating providedIn: 'any' is an option which has very limited use apart from a handful of esoteric cases internal to the framework.

We’re also deprecating providedIn: NgModule. It does not have wide usage, and in most cases is used incorrectly, in circumstances where you should prefer providedIn: 'root'. If you should truly scope providers to a specific NgModule, use NgModule.providers instead.


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!

Top comments (0)