New Features of Angular 15
Mohammed Mahmoud
Posted on February 26, 2023
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);
- 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>
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>
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 {
}
And then use your directive like this 🎉:
<button appButton
tooltip="Info"
[tooltipHideDelay]="delay"
[dragDisabled]="isDisabled">
</button>
- 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.
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]
};
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()]
};
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 { ... }
Before this change, to lazy load a standalone component you had to:
{
path: 'lazy',
loadComponent: () => import('./lazy-file').then(m => m.LazyComponent),
}
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'),
}
- 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
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)
- 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!
Posted on February 26, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.