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

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

Test teardown by default

No more `entryComponents: []`

getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting(),
  { teardown: { destroyAfterEach: true } }
);

v14

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

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

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

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

Made with Slides.com