0%

Overview

Feature Description Example State
new output api a more streamlined output api old: @Output() updateParentName = new EventEmitter<string>();
new: updateParentName = output<string>();
tutorial: output
stable
Deferred view A new mechanism of lazy load
@defer (on viewport) { 
<comment-list/>
} @loading {
Loading…
} @error {
Loading failed :(
} @placeholder {
<img src="comments-placeholder.png">
}
preview(detail)
New control flow a new syntax for control flow
@if(condition) {
Some content
} else {
Other content
}
comments

Future looking documentation

https://www.angular.io -> https://www.angular.dev The new interactive learning experience is powered by WebContainers

Built-in Control flow

@if, @for, @switch

  1. @if
  2. @for
  3. @switch

Benefit of new control flow

  1. More ergonomic syntax that is closer to JavaScript, fewer documentation lookups.
  2. Better type checking thanks to more optimal type narrowing.
  3. Exists in build time which reduces runtime footprint. drop you bundle size by up to 30k.
  4. Automatically available without any imports.
  5. Performance improvements.

Deferrable views

deferrable views

Enable SSR in new project

From Angular 17, when you create a project with ng new, you can enable SSR by adding --ssr flag.

1
ng new my-app --ssr

If you didn’t provide --ssr option, Angular will ask you to choose whether .

Hydration graduate from developer preview

Hydration is now enabled by default for all applications using SSR.

Add hydration to existing project

If you have an existing project and want to add hydration, you can use the following command.

1
ng add @angular/ssr

New lifecycle hooks

The following lifecycle hooks are used to improve the performance of Angular’s SSR and SSG.

  1. afterRender - register a callback to be invoked each time the application finishes rendering.
  2. afterNextRender - register a callback to be invoked the next time the application finishes rendering.

Vite and ESBuild default for new projects

From Angular 17, when you create a new project, the default build tool will be Vite and ESBuild.

Experimental view transitions support

1
2
3
4
5
bootstrapApplication(App, {
providers: [
provideRouter(routes, withViewTransitions()),
]
});

Defer loading of the animation module

1
2
3
4
5
import { provideAnimationsAsync } from '@angular/platform-browser/animations-async';

bootstrapApplication(RootCmp, {
providers: [provideAnimationsAsync()]
});

Input value transforms

1
2
3
4
// child.component.ts
export class ChildComponent {
@Input() flag: boolean = true;
}
1
2
3
4
// parent.component.ts
export class ParentComponent {
flag = true;
}

The following code will cause an error: Type “” is not assignable to type “boolean”.

1
2
<!-- parent.component.html -->
<app-child flag></app-child>

to use flag before Angular 17, you need to use the following code:

1
2
<!-- parent.component.html -->
<app-child [flag]="flag"></app-child>

From Angular 17, you can use the first format with some extra configuration in ChildComponent. In this way Angular will automatically convert the flag attribute to a boolean value.

1
2
3
4
// child.component.ts
export class ChildComponent {
@Input({transform: booleanAttribute}) flag: boolean = true;
}

Style and styleUrls as string

You don’t need to use array for styles and styleUrls anymore.
before:

1
2
3
4
5
6
7
8
9
@Component({
styles: [`
...
`]
})

@Component({
styleUrls: ['styles.css']
})

after:

1
2
3
4
5
6
7
8
9
@Component({
styles: `
...
`
})

@Component({
styleUrls: 'styles.css'
})

Use fetch as backend of HttpClient

1
2
3
4
5
6
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
]
};

Table of content

Feature Description
Self closing tags It’s a small improvement that could save you some typing
Signals
takeUntilDestroyed A more simple version of takeUntil

Self closing tags


In Angular 16, you can use self-closing tags in the template. This is really useful for a components with long names.

Before

1
<app-component></app-component>

Now

1
<app-component />

Angular Signals

takeUntilDestroyed

Detail example

SSR

1
2
3
4
5
6
7
8
9
10
11
// main.ts
import {
bootstrapApplication,
provideClientHydration,
} from '@angular/platform-browser';

...

bootstrapApplication(RootCmp, {
providers: [provideClientHydration()]
});

https://v17.angular.io/guide/hydration

Transition to standalone

You can use the following cli command to transit your app to standalone.

1
ng generate @angular/core:standalone

and you’ll see the following options in terminal, use the arrow key to select the options.

1
2
3
4
? Choose the type of migration: (Use arrow keys)
> Convert all components, directives and pipes to standalone
Remove unnecessary NgModule classes
Bootstrap the application using standalone APIs

Create new project with standalone

1
ng new --standalone

Required Inputs

1
2
3
4
@Component(...)
export class App {
@Input({ required: true }) title: string = '';
}

Passing router data as component input

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes = [
{
path: 'about',
loadComponent: import('./about'),
resolve: { contact: () => getContact() }
}
];

@Component(...)
export class About {
// The value of "contact" is passed to the contact input
@Input() contact?: string;
}

Flexible ngOnDestroy

现在还不懂,需要继续研究。

Reference

https://blog.angular.dev/angular-v16-is-here-4d7a28ec680d

Introduction

Tree-shakable router

You can build multiple routes applications with the new router standalone API.

Define the root routes in app.routes.ts, and use loadChildren to load the lazy routes.

1
2
3
4
5
6
// app.routes.ts
export const appRoutes: Routes = [{
path: 'lazy',
loadChildren: () => import('./lazy/lazy.routes')
.then(routes => routes.lazyRoutes)
}];

Define the lazy routes in lazy.routes.ts.

1
2
3
4
5
// lazy.routes.ts
import {Routes} from '@angular/router';
import {LazyComponent} from './lazy.component';

export const lazyRoutes: Routes = [{path: '', component: LazyComponent}];

Provide the router in file main.ts.

1
2
3
4
5
6
// main.ts
bootstrapApplication(AppComponent, {
providers: [
provideRouter(appRoutes) // tree-shakable
]
});

The benefit of provideRouter is that it is tree-shakable.

Directive composition API

这个我没太看懂,需要进一步研究它的来历。

1
2
3
4
5
6
7
8
9
@Component({
selector: 'mat-menu',
hostDirectives: [HasColor, {
directive: CdkMenu,
inputs: ['cdkMenuDisabled: disabled'],
outputs: ['cdkMenuClosed: closed']
}]
})
class MatMenu {}

NgOptimizedImage

NgOptimizedImage is a new directive that optimizes images for the web.
First, import NgOptimizedImage in your component.

1
2
3
4
5
import { NgOptimizedImage } from '@angular/common';
@Component({
imports: [NgOptimizedImage],
...
})

Then, replace src in img tag with ngSrc.

1
<img [ngSrc]="url" alt="image">

Functional router guards.

You can now use functional router guards.

Before:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Injectable({ providedIn: 'root' })
export class MyGuardWithDependency implements CanActivate {
constructor(private loginService: LoginService) {}

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

const route = {
path: 'somePath',
canActivate: [MyGuardWithDependency]
};

After:

1
2
3
4
const route = {
path: 'admin',
canActivate: [() => inject(LoginService).isLoggedIn()]
};

Route unwraps default imports

Before:
You need .then to unwrap the default import.

1
2
3
4
{
path: 'lazy',
loadComponent: () => import('./lazy-file').then(m => m.LazyComponent),
}

Now: with the default export enabled, you can directly use the default import.

1
2
3
4
{
path: 'lazy',
loadComponent: () => import('./lazy-file'), // <--- no need to unwrap manually.
}

You must use default export in the lazy file.

1
2
3
4
5
@Component({
standalone: true,
template: '...'
})
export default class LazyComponent { ... } // default export.

Automatic imports in language service.

When you type a component selector in the template, the language service will prompt to import the component for you.

CLI improvements

Generate standalone components with the following command

1
ng g component --standalone

Simplify the output of the ng serve command.

  1. Remove File test.ts, polyfills.ts, and environments
  2. Specify polyfills in angular.json
    1
    2
    3
    "polyfills": [
    "zone.js"
    ]

Global format for Date pipe

1
2
3
4
5
6
7
8
bootstrapApplication(AppComponent, {
providers: [
{
provide: DATE_PIPE_DEFAULT_OPTIONS,
useValue: { dateFormat: 'shortDate' }
}
]
});

Deprecations

  1. providedIn: 'any' is deprecated. Use providedIn: 'root' instead.
  2. providedIn: NgModule is deprecated. Use providedIn: 'root' or providedIn: NgModule instead.

Reference

  1. https://blog.angular.dev/angular-v15-is-now-available-df7be7f2f4c8

Table of content

Feature Description
Standalone components
Typed Angular Forms
inject a new function to inject dependencies

Standalone Directives/Components/Pipes

Typed Angular Forms

Define title in router

You can specify the title of a page in the router configuration now.

1
2
3
4
5
6
7
const routes: Routes = [
{
path: 'home',
component: HomeComponent,
title: 'Home Page'
}
];

Use TitleStrategy to set the title in the browser tab for complex scenarios.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Injectable()
export class TemplatePageTitleStrategy extends TitleStrategy {
override updateTitle(routerState: RouterStateSnapshot) {
const title = this.buildTitle(routerState);
if (title !== undefined) {
document.title = `My App - ${title}`;
} else {
document.title = `My App - Home`;
};
};

@NgModule({
// ...
providers: [{provide: TitleStrategy, useClass: TemplatePageTitleStrategy}]
})
class MainModule {}

Target TypeScript 4.7 and ES2020

Bind to protected component members.

Start form Angular 14, you can bind protected properties in component class to your template.

1
2
3
4
5
6
7
@Component({
selector: 'my-component',
template: '{{ message }}', // <--- Now compiles!
})
export class MyComponent {
protected message: string = 'Hello world';
}

Optional injectors in Embedded Views

What’s Embedded views? Need further research.

1
2
3
viewContainer.createEmbeddedView(templateRef, context, {
injector: injector,
})

New CLI commands

1
2
3
4
ng completion
ng analytics
ng cache
ng cache info

Experimental ESM Application build

Update the build config in angular.json to use the new esbuild builder.

1
2
"builder": "@angular-devkit/build-angular:browser"
"builder": "@angular-devkit/build-angular:browser-esbuild" // <-- new

inject functions

The new inject function in Angular 14 is a new way to inject dependencies into your components.

Reference

https://blog.angular.dev/angular-v14-is-now-available-391a6db736af

Dost it make sense to return undefined in else branch?

Today, I noticed a code snippet during code review, it looks like this:

1
2
3
4
5
6
7
const getUserInfo(user: User) {
if (user.isActive) {
return user.info;
} else {
return undefined;
}
}

The code is trying to get the user info, if the user is active, it will return the user info, otherwise, it will return undefined. But does it make sense to return undefined in the else branch? In JavaScript, if a function does not return anything, it implicitly returns undefined. So, the above code can be refactored to:

1
2
3
4
5
const getUserInfo(user: User) {
if (user.isActive) {
return user.info;
}
}

Similarly to the following code.

1
2
3
4
5
6
7
8
9
const fetchData = async () => {
try {
const data = await fetch('https://api.example.com/data');
return data.json();
} catch (error) {
console.error(error);
return undefined;
}
};

Which can be simplified to:

1
2
3
4
5
6
7
8
const fetchData = async () => {
try {
const data = await fetch('https://api.example.com/data');
return data.json();
} catch (error) {
console.error(error);
}
};

What’s the difference of the following two code snippets?
first code snippet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const handler = () => {
console.log("handler executed...");
};

const fetchData = async () =>
new Promise((resolve) => {
setTimeout(() => {
resolve("success in fetchData");
}, 100);
});

fetchData()
.then(handler)
.catch((error) => {
console.log(error);
});

second code snippet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const handler = () => {
console.log("handler executed...");
};

const fetchData = async () =>
new Promise((resolve, reject) => {
setTimeout(() => {
reject("error in fetchData");
}, 100);
});

fetchData()
.catch((error) => {
console.log(error);
})
.then(handler);

The first code snippet is xxx.then().catch() while the second code snippet is xxx.catch().then(), since promise.catch also return a promise, so handler in the second code snippet will always be executed. But in the first code snippet, the handler will not be executed.

outputs:

first code snippet.

1
error in fetchData

second code snippet.

1
2
error in fetchData
handler executed...

点击清除(Clear)按钮时,表单竟然提交了!

今天在编写 Angular + Material Design 表单时,遇到一个奇怪的问题:点击清除按钮时,表单竟然被提交了!

表单结构非常简单:

  • 用户名和密码输入框
  • 提交按钮(Submit)
  • 清除按钮(Clear)

但点击清除按钮时,触发了意外的表单提交行为。
button-default-submit

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<form onsubmit="onSubmit()">
<label for="username">Username:</label>
<input id="username" name="username" type="text" />
<label for="password">Password:</label>
<input id="password" name="password" type="password" />
<button type="submit">Submit</button> <!-- submit -->
<button onclick="clear()">Clear</button> <!-- clear input -->
</form>

<script>
function onSubmit() {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
console.log("username:", username);
console.log("password:", password);
}

function clear() {
document.getElementById("username").value = "";
document.getElementById("password").value = "";
}
</script>
</body>
</html>

原因分析

后来仔细查阅MDN文档发现,form表单中button的默认type是submit,而我的代码中没有为清除按钮指定button的type,所以点击清除按钮时,同时触发了清除和提交两个操作。

form中button的三种类型

  • submit:默认值,点击按钮时提交表单。
  • reset:点击按钮时重置表单中的所有字段。
  • button:点击按钮时不执行任何操作,这种类型的button需要用户自己添加事件处理函数。

解决办法

方法一

为清除按钮指定type为button,这样点击清除按钮时就不会触发表单提交了。

1
<button type="button" onclick="clear()">Clear</button>

方法二

为清除按钮指定type为reset,这样做的好处是:1. 语义更清晰,2. 不需添加额外的事件处理函数。

1
<button type="reset">Clear</button>

参考

关于form中button的更多细节,请参考MDN

Introduction

In this article, we will learn how to dynamically load a component in Angular, In addition to load a component in html template directly, you can also load a component dynamically with NgComponentOutlet or ViewContainerRef

NgComponentOutlet

NgComponentOutlet is a directive that provide a declarative way for dynamic component creation, it requires a component type as input and create an instance of the component in the view.

1
*ngComponentOutlet="componentType"

Here are the code snippets to dynamically load a component with NgComponentOutlet:

1
2
<!-- app.component.html -->
<ng-container *ngComponentOutlet="getComponent()"></ng-container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// app.component.ts
import { Component } from '@angular/core';
import {NgComponentOutlet} from "@angular/common";
import {ProductComponent} from "./product/product.component";
import {CustomerComponent} from "./customer/customer.component";

@Component({
selector: 'app-root',
standalone: true,
imports: [NgComponentOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
showProduct = true;

// We return the component type by condition.
getComponent() {
return this.showProduct? ProductComponent: CustomerComponent;
}
}

Question:
What’s the difference between *ngComponentOutlet and *ngIf* in this case? *ngIf can also load the component by condition.

ViewContainerRef

ViewContainerRef is a class that represents a container where one or more views can be attached to a component. It provides methods to create components and attach them to the container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {Component, OnInit, ViewContainerRef} from '@angular/core';
import {ProductComponent} from "./product/product.component";
import {NgComponentOutlet} from "@angular/common";
import {CustomerComponent} from "./customer/customer.component";

@Component({
selector: 'app-root',
standalone: true,
imports: [NgComponentOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent implements OnInit {
showProduct = true;
constructor(private viewContainerRef: ViewContainerRef) {
}

ngOnInit() {
this.viewContainerRef.createComponent(this.getComponent());
}

getComponent() {
return this.showProduct? ProductComponent: CustomerComponent;
}
}

The above code will create the component instance as the sibling node of the host element(the component where ViewContainerRef was declared).

Lazy load component

You can also lazy load a component with ViewContainerRef.

1
2
3
<!-- app.component.html -->
<p>app works</p>
<button (click)="lazyLoadComponent()">Lazy load component</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// app.component.ts
import {Component, ViewContainerRef} from '@angular/core';
import {NgComponentOutlet} from "@angular/common";

@Component({
selector: 'app-root',
standalone: true,
imports: [NgComponentOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
showProduct = false;
constructor(private viewContainerRef: ViewContainerRef) {
}

async lazyLoadComponent() {
this.viewContainerRef.clear(); // avoid loading multiple components
const component = await this.getComponent();
this.viewContainerRef.createComponent(component);
}

async getComponent() {
if (this.showProduct) {
const {ProductComponent} = await import('./product/product.component');
return ProductComponent;
} else {
const {CustomerComponent} = await import('./customer/customer.component');
return CustomerComponent;
}
}
}

Explanation: The above code decides which component should be loaded when the user clicks the button, based on the showProduct variable. After running the program, press F12 to open the developer tools, and observe the Network tab. You can see that after clicking the Lazy load component button, the app initiates a request to dynamically load the Product component.

angular-lazy-load-component

The component was bundled separately with name chunk-VFVWUJZL.js to support lazy load, if you want to see the real name of the chunk file, you can add namedChunk to your angular.json file.

1
2
3
4
5
6
7
8
9
10
11
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/my-app",
"index": "src/index.html",
// ...
"namedChunks": true // <-- here
},
}
}

In this way the chunk file will got a name like product.component-RGAR2EGQ.js

Finally, the getComponent method can be optimized to avoid duplicate code, but it seems that the readability is not very good after optimization. What do you think?

优化后的代码:

1
2
3
4
5
async getComponent() {
const component = this.showProduct ? 'ProductComponent' : 'CustomerComponent';
const result = await import(`./${component.toLowerCase()}/${component}.component`);
return result[component];
}

References

[1] programmatic-rendering

Introduction

In this post, we’ll discuss how to upgrade Angular to the latest version.

Steps

Suppose the current Angular version is 16.0.0, and the latest version is 17.0.0. Here are the steps to upgrade Angular to the latest version:

Update Node.js

First, make sure you have the latest version of Node.js installed on your machine. You can download the latest version from the official Node.js website.

Update Angular core/cli

Navigate to your Angular project root directory and run the following command to update Angular CLI to the latest version:

1
ng update @angular/core@16 @angular/cli@16

Update Angular Material/CDK

If your project use Angular Material or Angular CDK, you can update them to the latest version by running the following command:

1
ng update @angular/material@16 @angular/cdk@16

Update 3rd-party libraries

If your project uses any 3rd-party libraries, make sure to update them to the latest compatible version with Angular 17.

  • Jest
  • Cypress

References

[1] Angular Update Guide

Introduction

In this article, we will learn how to use ChangeDetectionStrategy.OnPush in Angular to improve the performance of your application. By default, Angular uses ChangeDetectionStrategy.Default, which means that the change detection runs every time an event is triggered in the application. This can be inefficient if your application has a lot of components that don’t need to be updated every time an event is triggered. By using ChangeDetectionStrategy.OnPush, you can tell Angular to only run change detection on a component when its input properties change. with onPush strategy, Angular will only trigger change detection in the following cases:

  • When component’s input property changes
  • When an event is triggered in the component
  • When you manually trigger change detection
  • 注意:这里好像还有一条是async pipe,但是我找不到文档了,以后找到再补充吧。

Example

Take a look at the product component below, it has an input property name, and an event handler which changes the price of the product. We have set the changeDetection property to ChangeDetectionStrategy.OnPush in the component’s metadata.

1
2
3
4
<!-- product.component.html -->
<button (click)="changePrice(10)">Change price</button>
<p>Product Name: {{name}}</p>
<p>Product Price: {{price}}</p>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Component({
selector: 'app-product',
standalone: true,
imports: [],
templateUrl: './product.component.html',
styleUrl: './product.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductComponent implements OnInit, DoCheck, OnChanges {
@Input() name: string;
price: number;

constructor() {
this.name = '';
this.price = 0;
}

// `ngDoCheck()` will called each time change detection run
ngDoCheck() {
console.log('change detection triggered...')
}

// This will not trigger change detection
ngOnInit() {
setTimeout(() => {
this.price = 20;
}, 1000);
}

// click handler, will trigger change detection.
changePrice(price: number) {
this.price = price;
}
}
  1. When parent component change the input property name, Angular will trigger change detection in the product component. and console will output change detection triggered...
  2. When user click button to change the price, Angular will trigger change detection in the product component. and console will output change detection triggered...
  3. setTimeout function will not trigger change detection in the product component.

Change Detection with OnPush Diagram

See here for details.

Do I need OnPush strategy if component has no @Input bindings?

Yes, OnPush still make sense even if the component has no @Input bindings.

  1. With OnPush, change detection still triggers when an event is triggered in the component.
  2. With OnPush, you prevent the change detection running automatically when the parent component changes.
  3. With OnPush, you can manually trigger change detection when needed with ChangeDetectorRef.detectChanges(), or ChangeDetectorRef.markForCheck().

OnPush + signal is the future of Angular.

References:

  1. https://angular.dev/best-practices/skipping-subtrees
  2. https://v17.angular.io/guide/change-detection-skipping-subtrees