Signals
ESBuild
ESLint
Standalone Components
NgImageDirective
No more ViewEngine
Strict (Reactive) Forms
EOL
- TSLint
- Karma
- Protractor
DevTools
Functional
Directive Composition API
Improved Router
Jest
NGCC
inject()
production by default
And that’s just Angular!
// Javascript private class member!
#myVar: string = "foo";
v12
May 2021
Production builds by default
{
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"aot": true,
},
"configurations": {
"production": {
"fileReplacements": [...],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
},
}
}
}
Schematic
npx @angular/cli@12 update @angular/cli@12 --migrate-only production-by-default
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": { ...},
"configurations": {
"production": {
"budgets": [...],
"fileReplacements": [...],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
}
Serve
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "my-app:build:production"
},
"development": {
"browserTarget": "my-app:build:development"
}
},
"defaultConfiguration": "development"
},
EOL
v13
Nov 2021
Test teardown by default
No more `entryComponents: []`
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
{ teardown: { destroyAfterEach: true } }
);
v14
Jun 2022
inject()
vs constructor()
const ENVIRONMENT = new InjectionToken<Environment>('environment');
@Component({
// component metadata
})
export class AppComponent {
// Gebruik de nieuwe ES private class modifier `#` ipv `private`
// type "Environment" is inferred
#env = inject(ENVIRONMENT, { optional: true });
#myService = inject(MyService);
protected myUser$ = inject(UserService).getUser();
}
Angular Dependency Injection is heel erg krachtig en we zijn gewend om services en dergelijk the _injecteren_ via de constructor:
constructor(private myService: MyService, @Inject(ENVIRONMENT) private env: Environment){}
Dit zorgt zowel voor de initialisatie van de service als het gebruik van het provided `ENVIRONMENT
` token.
Echter is dit nu o.a. door changes in de Typescript Decorator niet meer de juiste manier.
En is dit de aangeraden manier:
inject()
: Voordelen
--experimentalDecorators
are no longer needed. useDefineForClassFields
hoeft niet op false
Router
export const ROUTES: Routes = [
{ path: '', title: 'Ninja Squad | Home', component: HomeComponent },
{ path: 'trainings', title: 'Ninja Squad | Trainings', component: TrainingsComponent }
]
Page Title
CanMatch guard
Vervangt canLoad
(Deprecated)
path
canActive
& canLoad
te definierenconst routes: Routes = [
{
path: 'todos',
canMatch: [() => inject(FeatureFlagsService).hasPermission('todos-v2')],
loadComponent: () => import('./todos-page-v2/todos-page-v2.component')
.then(v => v.TodosPageV2Component)
},
{
path: 'todos',
loadComponent: () => import('./todos-page/todos-page.component')
.then(v => v.TodosPageComponent)
}];
Typed Reactive Forms
Protected class member
protected myValue: string = 'foo';
<h1> {{ myValue }} </h1>
Template
NgOptimizedImage
<img [ngSrc]="imageUrl" width="400" height="300" />
Angular DevTools
v15
Nov 2022
Standalone Components
Functional Guards/Interceptors/Resolvers
Directive Composition API (Host Directives)
@Component({
selector: 'mat-menu',
hostDirectives: [
HasColor,
{
directive: CdkMenu,
inputs: ['cdkMenuDisabled: disabled'],
outputs: ['cdkMenuClosed: closed']
}
]
})
class MatMenu {}
Router
Lazy-load default exports
// feature.routes.ts
export default [
{
path: '',
component: FeaturePage,
},
{
path: 'persoon',
component: PersoonPage,
}
] satisfies Route[];
// main routes
export const MAIN_ROUTES: Rout[] = {
path: '',
loadChildren: () => import('./feature/feature.routes'),
},
}
ESBuild (Experimental)
"architect": {
"build": { /* Add the esbuild suffix */
"builder": "@angular-devkit/build-angular:browser-esbuild",
}
}
self-closing tags
<my-component [data]="myData" />
Removed
environment
ng generate environments
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
N.B.
- environment.ts
is nu de prod
!!
polyfills.ts
{
"polyfills": ["zone.js","zone.js/testing"]
}
v16
May 2023
Signals (Preview)
@Component({
selector: 'my-app',
standalone: true,
template: `
{{ fullName() }} <button (click)="setName('John')">Click</button>
`,
})
export class App {
firstName = signal('Jane');
lastName = signal('Doe');
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
constructor() {
effect(() => console.log('Name changed:', this.fullName()));
}
setName(newName: string) {
this.firstName.set(newName);
}
}
Met RxJs interop
import {toSignal} from '@angular/core/rxjs-interop';
@Component({
template: `
<li *ngFor="let row of data()"> {{ row }} </li>
`
})
export class App {
dataService = inject(DataService);
data = toSignal(this.dataService.data$, []);
}
Router
Required Inputs
@Input({ required: true }) color: 'red' | 'blue';
Transform
provideRouter(routes, withComponentInputBinding())
<my-component disabled></my-component>
<my-component disabled="true"></my-component>
<!-- Before, only the following was properly working -->
<my-component [disabled]="true"></my-component>
@Input voor Route Params
import {Input as RouterInput } from '@angular/router';
export class FeaturePage extends OnInit {
@RouterInput() userId?: string;
ngOnInit(){
// We should check userId here => cannot use `required`
}
}
@Input({ transform: booleanAttribute }) disabled = false;
v17
Nov 2023
New Logo and Site!
https://angular.dev
Control Flow
<div *ngIf="loggedIn; else anonymousUser">
The user is logged in
</div>
<ng-template #anonymousUser>
The user is not logged in
</ng-template>
@if (loggedIn) {
The user is logged in
} @else {
The user is not logged in
}
*ngIf
@if / @else
Control Flow
import { NgFor} from '@angular/common';
@Component({
standalone: true,
imports: [ NgFor ],
template: `
<ul>
<li *ngFor="let ingredient of ingredientList; trackBy: trackByIngredientFn">
{{ ingredient.quantity }} - {{ ingredient.name }}
</li>
</ul>
`
})
@Component({
standalone: true,
template: `
<ul>
@for (ingredient of ingredientList; track ingredient.name) {
<li>{{ ingredient.quantity }} - {{ ingredient.name }}</li>
}
@empty {
<li> There are no items.</li>
}
</ul>
`
})
*ngFor
@for / @empty
Control Flow
<ng-container [ngSwitch]="accessLevel">
<admin-dashboard *ngSwitchCase="admin"/>
<moderator-dashboard *ngSwitchCase="moderator"/>
<user-dashboard *ngSwitchDefault/>
</ng-container>
@switch (accessLevel) {
@case ('admin') { <admin-dashboard/> }
@case ('moderator') { <moderator-dashboard/> }
@default { <user-dashboard/> }
}
*ngSwitch
@switch
Control Flow
ng generate @angular/core:control-flow
Migrate!!
Deferrable Views
@defer (on viewport) {
<comment-list/>
} @loading {
Loading…
} @error {
Loading failed :(
} @placeholder {
<img src="comments-placeholder.png">
}
@defer
Deferrable Views
<button type="button" #searchButton>Search</button>
@defer (on interaction(searchButton)) {
<search-results />
} @loading {
<div> loading items</div>
}
Triggers
Signals
import { Input, numberAttribute, booleanAttribute } from '@angular/core';
@Input({ required: true }) bankName!: string;
@Input({ transform: booleanAttribute }) status: boolean;
// ROUTER PARAM!
@Input({ alias:'account-id', transform: numberAttribute }) id: number;
@Input()
input()
import { input, numberAttribute, booleanAttribute } from '@angular/core';
bankName = input.required<string>();
status = input(false, { transform: booleanAttribute });
// ROUTER PARAM!
id = input<number>({ alias:'account-id', transform: numberAttribute });
Signals
model() inputs
import {Component, model, input} from '@angular/core';
@Component({
selector: 'custom-checkbox',
template: '<div (click)="toggle()"> ... </div>',
})
export class CustomCheckbox {
checked = model(false);
disabled = input(false);
toggle() {
// While standard inputs are read-only, you can write directly to model inputs.
this.checked.set(!this.checked());
}
}
Model inputs are a special type of input that enable a component to propagate new values back to another component.
Signals
import {Component, model, input} from '@angular/core';
@Component({
selector: 'custom-checkbox',
template: '<div (click)="toggle()"> ... </div>',
})
export class CustomCheckbox {
checked = model(false);
disabled = input(false);
toggle() {
// While standard inputs are read-only, you can write directly to model inputs.
this.checked.set(!this.checked());
}
}
Two-way-binding
v18
May 2024
Coalescing reduces unnecessary change detection cycles and significantly improves performance for some applications.
bootstrapApplication(App, {
providers: [
provideZoneChangeDetection({ eventCoalescing: true })
]
});
Zoneless
bootstrapApplication(AppComponent, {
providers: [
// 👇
provideExperimentalZonelessChangeDetection()
]
});
It should work out of the box if all your components are OnPush
and/or rely on signals! 🚀
Coalescing by default
Native await (no more monkey patch to promises)
Angular Material 3 is al zoneless
Stable
@Component({
selector: 'app-profile',
template: `
<ng-content select=".greeting">Hello </ng-content>
<ng-content>Unknown user</ng-content>
`,
})
export class Profile {}
<app-profile>
<span class="greeting">Good morning </span>
</app-profile>
<span class="greeting">Good morning </span>
Unknown user
Usage
Result
Component with ng-content
Stable
@Component({ ... })
export class UserForm {
private fb = inject(NonNullableFormBuilder);
userForm = this.fb.group({
login: ['', Validators.required],
password: ['', Validators.required]
});
constructor() {
this.userForm.events.subscribe(event => {
if (event instanceof TouchedChangeEvent) {
console.log('Touched: ', event.touched);
} else if (event instanceof PristineChangeEvent) {
console.log('Pristine: ', event.pristine);
} else if (event instanceof StatusChangeEvent) {
console.log('Status: ', event.status);
} else if (event instanceof ValueChangeEvent) {
console.log('Value: ', event.value);
} else if (event instanceof FormResetEvent) {
console.log('Form reset');
} else if (event instanceof FormSubmitEvent) {
console.log('Form submit');
}
});
}
}
const routes: Routes = [
{ path: "first-component", component: FirstComponent },
{
path: "old-user-page",
redirectTo: ({ queryParams }) => {
const errorHandler = inject(ErrorHandler);
const userIdParam = queryParams['userId'];
if (userIdParam !== undefined) {
return `/user/${userIdParam}`;
} else {
errorHandler.handleError(
new Error('Attempted navigation to user page without user ID.')
);
return `/not-found`;
}
},
},
{
path: 'users',
component: UsersComponent,
canActivate: [
() => {
const userService = inject(UserService);
return userService.isLoggedIn() || new RedirectCommand(router.parseUrl('/login'), {
state: { requestedUrl: 'users' }
});
}
],
},
{ path: "user/:userId", component: OtherComponent },
];
Stable
@for (user of users; track user.id) {
@let statusMessage = user.isActive ? 'Active' : 'Inactive';
<div>
<p>{{ user.name }} - Status: {{ statusMessage }}</p>
</div>
}
with conditions
<div>
@let data = (data$ | async);
@let processedData = data ? data.map(item => item.value) : [];
<ul>
@for (item of processedData; track item) {
<li>{{ item }}</li>
}
</ul>
</div>
async
The @let
directive allows you to create variables directly in the HTML code. This means you can do simple operations like joining text or calculations without needing to write more complex code elsewhere in your program.
Stable
"compilerOptions": {
...
"isolatedModules": true
}
Enable isolatedModules in tsconfig.json for performance boost
Stable
export class UserComponent {
private userService = inject(UserService);
}
export class UserComponent {
constructor(private userService: UserService) {}
}
ng g @angular/core:inject
From
To
Stable
{
path: 'users',
loadComponent: () => import('./users/users.component').then(m => m.UsersComponent)
}
{
path: 'users',
component: UsersComponent
}
ng g @angular/core:route-lazy-loading
From
To
Resources
v19
Nov 2024
true
{
"angularCompilerOptions": {
"strictStandalone": true
}
}
TS-998113: Imports array contains unused imports [plugin angular-compiler]
src/app/user/users.component.ts:9:27:
9 │ imports: [UserComponent, UnusedComponent],
╵ ~~~~~~~~~~~~~
Angular 19.1 will add auto removal of unused imports
Signals API's
ng generate @angular/core:signal-input-migration
ng generate @angular/core:signal-queries-migration
ng generate @angular/core:output-migration
ng generate @angular/core:signals
of
A linked signal is a writable signal, but it is also a computed signal, as its content can be reset thanks to a computation that depends on another signal (or several ones).
export class ItemListComponent {
items = input.required<ItemModel[]>();
// ✅ This is recommended
selectedItem = linkedSignal(() => this.items()[0]);
}
export class ItemListComponent {
items = input.required<ItemModel[]>();
selectedItem = signal<ItemModel | undefined>(undefined);
constructor() {
// ⚠️ This is not recommended
effect(() => {
this.selectedItem.set(this.items()[0]);
});
}
pickItem(item: ItemModel) {
this.selectedItem.set(item);
}
}
vs
In some cases, the computation for a linkedSignal
needs to account for the previous value of the linkedSignal
.
@Component({/* ... */})
export class ShippingMethodPicker {
shippingOptions: Signal<ShippingMethod[]> = getShippingOptions();
selectedOption = linkedSignal<ShippingMethod[], ShippingMethod>({
// `selectedOption` is set to the `computation` result whenever this `source` changes.
source: this.shippingOptions,
computation: (newOptions, previous) => {
// If the newOptions contain the previously selected option, preserve that selection.
// Otherwise, default to the first option.
return newOptions.find(opt => opt.id === previous?.value?.id) ?? newOptions[0];
}
});
changeShipping(newOptionIndex: number) {
this.selectedOption.set(this.shippingOptions()[newOptionIndex]);
}
}
vs
Signals API's
ng generate @angular/core:signal-input-migration
ng generate @angular/core:signal-queries-migration
ng generate @angular/core:output-migration
ng generate @angular/core:signals
of
v20
May 2025 ??