Anything New?

Signals

ESBuild

ESLint

Standalone Components

NgImageDirective

No more ViewEngine

Strict (Reactive) Forms

EOL

- TSLint

- Karma
- Protractor

DevTools

Functional

  • Guards
  • Interceptors
  • Resolvers

 

Directive Composition API

Improved Router

  • input() signal
  • model() signal
  • output() - NO Signal

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

  • Reusability
  • Type inference
  • Easier Inheritance
  • NO Typescript patching/hacking needed by Angular
  • --experimentalDecorators are no longer needed.
    Use the new default Typescript Decorators
  • 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)

  • Mogelijk om meerdere routes met hetzelfde path
  • je hoeft niet meer canActive & canLoad te definieren
  • Bijvoorbeeld te gebruiken met feature flags
const 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

  • on idle
  • on viewport
  • on interaction
  • on hover
  • on immediate
  • on timer
  • when cond

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 });
  1. Signal inputs are more type safe:
    • Required inputs do not require initial values, or tricks to tell TypeScript that an input always has a value.
    • Transforms are automatically checked to match the accepted input values.
  2. Signal inputs, when used in templates, will automatically mark onPush components as dirty.
  3. Values can be easily derived whenever an input changes using computed.
  4. Easier and more local monitoring of inputs using effect instead of ngOnChanges or setters.

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

Stable

  • https://angular.dev
  • Built-in control flow
  • Deferrable views
  • (Angular Material 3)

Developer preview

  • Signals API (enhanced API's)
  • @let

Experimental

  • Zoneless

Version insights

(Experimental) Zoneless

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)

(Experimental) Zoneless

Angular Material 3 is al zoneless

Stable

Fallback for <ng-content>

@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

Form events

@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');
      }
    });
  }
}

Route redirects as functions

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

@let (18.1)

@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

isolatedModules

"compilerOptions": {
    ...
    "isolatedModules": true
}

Enable isolatedModules in tsconfig.json for performance boost

Stable

Migrations

export class UserComponent {
  private userService = inject(UserService);
}
export class UserComponent {
  constructor(private userService: UserService) {}
}
ng g @angular/core:inject

From

To

Stable

Migrations

{
  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

Version insights

Highlights

  • Signals API's => Stable
  • LinkedSignal
  • Resource API
  • HMR hot module replacement (HMR) for styles out of the box and enables experimental support for template HMR behind a flag!
  • Automatic CSP Using hash-based CSP, the browser will add the hash of every inline script to the CSP. Each script will have a unique hash associated with it. 
  • Standalone defaults to true
{
  "angularCompilerOptions": {
    "strictStandalone": true
  }
}

Language service

  • Auto refactor to signal API
  • Remove unused imports in Standalone components
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

Migrations

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

LinkedSignal (dev preview)

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

LinkedSignal - prev state

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

Resource

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

Highlights

  • Karma deprecated!!
  • ???
Made with Slides.com