0%

Introduction

noscript is used in following senarios:

  1. When the browser does not support JavaScript.
  2. When the browser has disabled JavaScript.

How to disable JavaScript in Chrome

  1. Open your page with Chrome.
  2. Press F12 to open the developer tools.
  3. Click on the three dots on the top right corner.(Or press Ctrl + Shift + P)
  4. Type disable JavaScript in the search bar.
  5. Click on the Disable JavaScript option.

When JavaScript was disabled, there will be a warning sign on Source tab. mouse hove on that warning sign, it will show the message JavaScript is disabled.

disable-javascript

Example

The following code will print Hello, world! in the console if JavaScript is enabled. If JavaScript is disabled, the message This page requires JavaScript. will be displayed.

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<body>
<script>
console.log('Hello, world!');
</script>
</body>
<noscript> This page requires JavaScript.</noscript>
</html>

As I know, currently, React framework will generate a noscript tag in the body tag when you create your project with create-react-app.

But Angular does not have a noscript tag in the body tag. there is no browser does not support JavaScript or disable JavaScript, so it is not necessary to add a noscript tag in the body tag.

Static import

The static import declaration is used to import read-only live bindings which are exported by another module.

Static import is syntactic.

Named imports

Each import has a name, which must be the same as the corresponding export name in the imported module.

1
2
3
4
5
6
7
8
// math.mjs
export function add(a, b) {
return a + b;
}

export function subtract(a, b) {
return a - b;
}
1
2
// main.mjs
import { add, subtract } from './math.mjs';

Default imports

You can import the default export of a module using the following syntax:

1
2
3
4
// math.mjs
export default function add(a, b) {
return a + b;
}

You can use any name here, not limited to add. But you can only have one default export per module.

1
2
// main.mjs
import add from './math.mjs';

Namespace imports

You can also import all exports from a module using the following syntax:

1
2
3
4
5
6
7
8
// math.mjs
export function add(a, b) {
return a + b;
}

export function subtract(a, b) {
return a - b;
}
1
2
// main.mjs
import * as math from './math.mjs';

Side effects imports

You can also import a module for its side effects only, without importing any bindings. This is useful when you just want to run the code in the module, but don’t need to import any of its bindings.

1
import './math.mjs';

This is very common in Angular code, especially with Module Federation, where you import a module for its side effects only.

1
2
3
4
5
6
7
// bootstrap.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

main.ts use side effect import here which will run all global code in bootstrap.ts but not import any bindings. (Note, the following code is dynamic import, not static import)

1
2
3
// main.ts
import('./bootstrap')
.catch(err => console.error(err));

Static import only execute once

No matter how many times you import a module, it will only be executed once. Suppose you have a math module with the following code:

1
2
3
4
5
6
7
8
9
10
11
// math.mjs
export function addFunc(a, b) {
return a + b;
}

export function subtractFunc(a, b) {
return a - b;
}

// Print a random number to identify different instances
console.log(Math.random());

And another two module add and subtract with the following code:

1
2
3
4
5
6
// add.mjs
import { addFunc } from './math.mjs';

export function add(a, b) {
return addFunc(a, b);
}
1
2
3
4
5
6
// subtract.mjs
import { subtractFunc } from './math.mjs';

export function subtract(a, b) {
return subtractFunc(a, b);
}

and finlay, you import add and subtract in your main module:

1
2
3
4
5
6
// main.mjs
import { add } from './add.mjs';
import { subtract } from './subtract.mjs';

console.log(add(1, 2)); // 3
console.log(subtract(2, 1)); // 1

When you run main.mjs, you will see only one random number printed in the console, which means the math module is only executed once.

1
2
3
0.2534933886729216
3
1

Dynamic import

Introduction

import() is dynamic import, it returns a promise. The import() syntax, commonly called dynamic import, is a function-like expression that allows loading an ECMAScript module asynchronously and dynamically into a potentially non-module environment.

Basic example

1
2
3
4
5
6
7
8
9
10
import("ramda").then(module => {
const moduleDefault = module.default;
console.log(moduleDefault);
});

import("./utility.js").then(module => {
const DefaultFunction = module.default;
const exportFunction = module.exportFunction;
console.log(DefaultFunction, exportFunction);
});

With dynamic import, you can import modules conditionally. The following code load the chat-box when user click on the contact button.

1
2
3
4
5
6
const contactBtn = document.querySelector("#button");
contactBtn.addEventListener("click", () => {
import("chat-box").then(module => {
module.load(); // or perform any desired action
});
});

Async/Await

Since import() returns a promise, you can use async/await to import modules.

1
2
3
4
5
const contactBtn = document.querySelector("#button");
contactBtn.addEventListener("click", async () => {
const module = await import("chat-box");
module.load();
});

Destruction

You can also use object destruction to import modules.

1
2
3
4
5
const contactBtn = document.querySelector("#button");
contactBtn.addEventListener("click", async () => {
const { load } = await import("chat-box");
load();
});

Error handling

And last, don’t forget to handle error when importing modules.

1
2
3
4
5
try {
const module = await import("chat-box");
} catch(e) {
console.error(e)
}

Use import() in non-module environment

import() can be used in non-module environment, such as in a .js file or script tag without type="module".

1
2
3
4
// test.js, you don't need test.mjs here.
import("chat-box").then(module => {
module.load();
});
1
2
3
4
5
6
<!-- You don't need type="module" in script tag-->
<script>
import("chat-box").then(module => {
module.load();
});
</script>

But static import can’t be used in non-module environment, you will got Error: SyntaxError: Cannot use import statement outside a module, to make static import work, you must use file end with .mjs or add type="module" in script tag.

1
2
import {load} from "./chat-box.mjs";
load();
1
2
3
4
5
<!-- You need type="module" in script tag-->
<script type="module">
import {load} from "./chat-box.mjs";
load();
</script>

pass by reference

There is no real pass by reference in JavaScript like in C/C++ language, but you can use import to achieve similar effect.

First, create a module module.mjs, in this file, we export an object person.

1
2
3
4
5
// module.mjs
export const person = {
name: 'zdd',
age: 18
};

Create another module module1.mjs, in this file, we import person from module.mjs and change the age to 40.

1
2
3
// module1.mjs
import { person } from './module.mjs';
person.age = 40;

Finally, import module1.mjs and module.mjs in main.mjs, and print the person object, you will found the age is 40 now.

1
2
3
4
5
// main.mjs
import './module1.mjs'; // This will execute the code in module1.mjs
import { person } from './module.mjs';

console.log(person);

输出如下:

1
{ name: 'zdd', age: 40 }

import.meta.url

import.meta.url The full URL to the module, includes query parameters and/or hash (following the ? or #). In browsers, this is either the URL from which the script was obtained (for external scripts), or the URL of the containing document (for inline scripts). In Node.js, this is the file path (including the file:// protocol).

In Node.js, import.meta.url is the file path of the current module. It is similar to __filename in CommonJS modules.

This is server.ts file generated by angular SSR, you can see import.meta.url is used to get the current file path.

1
2
3
4
5
6
7
8
9
10
11
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';

// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');
}

References

What’s ngTemplateOutlet?

ngTemplateOutlet is a directive that allows you to render a template dynamically.

Use Cases

一个大的页面,可能有一部分会多次被渲染,这种情况下,我们可以使用 ngTemplateOutlet 来减少重复的代码。

考虑如下需求:我们需要在一个页面上多次渲染某个产品信息,首先可以将产品信息的模板放在一个 ng-template中,并给它起一个名字叫做product, 然后通过 ngTemplateOutlet 来渲染。ngTemplateOutlet接受一个模板变量作为参数,然后将这个模板变量所指向的模板渲染到当前的位置。

1
2
3
4
5
6
7
8
9
<p>Product 1</p>
<ng-container *ngTemplateOutlet="product"/>
<p>Product 2</p>
<ng-container *ngTemplateOutlet="product"/>

<ng-template #product>
<p>Product name: computer</p>
<p>Product price: 100$</p>
</ng-template>

ngTemplateOutlet 还可以接受一个上下文参数,用来传递数据到模板中。比如上例中,我们可以把Product name和price进行参数化。

1
2
3
4
5
6
7
8
9
<p>Product 1</p>
<ng-container *ngTemplateOutlet="product; context: {name: 'computer', price: 100}"/>
<p>Product 2</p>
<ng-container *ngTemplateOutlet="product; context: {name: 'phone', price: 200}"/>

<ng-template #product let-name="name" let-price="price">
<p>Product name: {{ name }}</p>
<p>Product price: {{ price }}$</p>
</ng-template>

更进一步的,我们可以把name和price封装到一个对象中,然后传递这个对象。

1
2
3
4
5
6
7
8
9
<p>Product 1</p>
<ng-container *ngTemplateOutlet="product; context: {product: {name: 'computer', price: 100}}"/>
<p>Product 2</p>
<ng-container *ngTemplateOutlet="product; context: {product: {name: 'phone', price: 200}}"/>

<ng-template #product let-product="product">
<p>Product name: {{ product.name }}</p>
<p>Product price: {{ product.price }}$</p>
</ng-template>

渲染结果如下:

1
2
3
4
5
6
7
Product 1
Product name: computer
Product price: 100$

Product 2
Product name: phone
Product price: 200$

再进一步,我们可以将product信息放到组件中,然后通过ngTemplateOutlet来渲染。

1
2
3
4
5
6
// app.component.ts
products = [
{name: 'Book 1', price: 100},
{name: 'Book 2', price: 200},
{name: 'Book 3', price: 300},
]
1
2
3
4
5
6
7
8
9
10
<!-- app.component.html -->
<p>Product 1</p>
<ng-container *ngTemplateOutlet="product; context: {product: products[0]}"/>
<p>Product 2</p>
<ng-container *ngTemplateOutlet="product; context: {product: products[1]}"/>

<ng-template #product let-product="product">
<p>Product name: {{ product.name }}</p>
<p>Product price: {{ product.price }}$</p>
</ng-template>

再再进一步,我们可以用ngFor来循环渲染。

1
2
3
4
5
6
7
8
<ng-container *ngFor="let product of products">
<ng-container *ngTemplateOutlet="productTemplate; context: {$implicit: product}"></ng-container>
</ng-container>

<ng-template #productTemplate let-product>
<p>Product name: {{ product.name }}</p>
<p>Product price: {{ product.price }}$</p>
</ng-template>

注意,这里使用了$implicit, 这是一个特殊的关键字,表示默认的上下文参数。如果不指定上下文参数,$implicit会被用作默认的上下文参数。也就是说,let-product="product"可以简写为let-product。而$implicit后面的参数就是product. (注意,这句我已经看不懂了,有空重新整理一下。)

References

https://angular.dev/api/common/NgTemplateOutlet?tab=usage-notes

What is ng-content in Angular?

Angular’s ng-content element is used to project content into a component.

通常我们定义一个组件时,都会在某个html模板中使用组件对应的selector来引用组件。例如:

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

注意观察以上组件的html结构,app-my-component标签中间是没有内容的。这种组件的特点是静态的,无法动态插入内容。如果我们需要动态的更改组件中的某个部分,那就需要用到ng-content了。

考虑下面的例子,假设我们有一个Card组件,我们希望Card组件的内容是动态的,可以根据不同的需求插入不同的内容。但是我们又希望Card的Header和Footer是固定的,不会变化。这个需求就可以用ng-content来实现。

1
2
3
<header>Card Header</header>
<ng-content></ng-content>
<footer>Card Footer</footer>

假设我们要创建一个内容为Hello, world!的Card组件,我们可以这样写:

1
2
3
<app-card>
<p>Hello, world!</p>
</app-card>

Angular最终会将<p>Hello, world!</p>插入到<ng-content></ng-content>标签的位置,渲染结果如下:

1
2
3
<header>Card Header</header>
<p>Hello, world!</p>
<footer>Card Footer</footer>

这就是ng-content的作用。

总结

使用ng-content可以实现动态插入内容到组件中,使得组件更加灵活。

  1. 固定不变的内容直接书写到组件模板中。
  2. 动态的内容使用ng-content来插入。

What is ng-template in Angular?

Angular’s ng-template element defines a template that is not rendered by default.

ng-template必须配合Angular指令使用,例如ngIfngForngSwitch等。或者定义模板变量以做他用(详情看这里),单纯使用ng-template是不会渲染的。

以下内容不会渲染。

1
2
3
<ng-template>
Hello, world!
</ng-template>

以下内容也不会渲染,为什么?

1
2
3
<ng-template *ngIf="true">
Hello, world!
</ng-template>

以下内容可以渲染:

1
2
3
<ng-template [ngIf]="true">
Hello, world!
</ng-template>

指令的简写形式

Angular会将指令的简写形式转换成ng-template的完整形式。
以下代码

1
<div *ngIf="true">Hello, world!</div>

会被Angular内部转换为

1
<ng-template [ngIf]="true"><div>Hello, world!</div></ng-template>

但是渲染的时候,<ng-template>不会被渲染,只会渲染<div>ng-containerng-template一样,都不会向DOM中插入元素,而只会渲染他们所包裹的元素。

What’s ng-container?

来看一下官方定义:
ng-container is a special element that can hold structural directives like *ngIf, *ngFor, etc. without adding an extra element to the DOM.

翻译过来就是:ng-container 是一个特殊的元素,可以包含结构性指令,比如 *ngIf, *ngFor 等等,而不会在 DOM 中添加额外的元素。

从名字来看,ng-container 是一个容器,而且是一个隐形的容器,渲染后ng-container本身不会在DOM中生成任何元素。

比如下面的代码:

1
2
3
<ng-container *ngIf="true">
<div>Content</div>
</ng-container>

渲染后的结果是:

1
<div>Content</div>

需要注意的是,上面仅仅是一个例子,现实中没人会如此这般写代码,我们一般都这么写。

1
<div *ngIf="true">Content</div>

Use Cases

1. 使用 ng-container 来包裹多个元素,而不需要额外的 div 或者 span 等元素。

1
2
3
4
5
<!-- This way will generate an extra div element -->
<div *ngIf="true">
<div>Content 1</div>
<div>Content 2</div>
</div>
1
2
3
4
5
<!-- This way will not generate any extra div element -->
<ng-container *ngIf="true">
<div>Content 1</div>
<div>Content 2</div>
</ng-container>

2. 使用 ng-container来分隔多个结构化指令。

在Angular中,一个元素只能有一个结构性指令,比如 *ngIf, *ngFor 等等。如果需要在一个元素上使用多个结构性指令,可以使用 ng-container 来分隔。
比如下面的代码我们在li元素上同时使用了ngForngIf,这会导致编译错误:Only one structural directive can be applied

1
2
3
4
<ul>
<li *ngFor="let item of items" *ngIf="item.isValid"> {{ item.name }}
</li>
</ul>

可以使用 ng-container 来分隔这两个指令。

1
2
3
4
5
<ul>
<ng-container *ngFor="let item of items">
<li *ngIf="item.isValid"> {{ item.name }} />
</ng-container>
</ul>

注意下面的代码虽然也能工作,但是会生成多个ul元素(每个ul元素包含一个li元素),这可能不是我们想要的结果。

1
2
3
4
<ul *ngFor="let item of items">
<li *ngIf="item.isValid"> {{ item.name }}
</li>
</ul>

3. 配合 ng-template 来实现 else 逻辑。

注意:#elseBlock 是一个模板引用变量,只能在ng-template中使用。在ng-container中使用#elseBlock是不会生效的。

1
2
3
4
5
6
<ng-container *ngIf="true; else elseBlock">
<div>Content 2</div>
</ng-container>
<ng-template #elseBlock>
<div>Content 3</div>
</ng-template>

4. 配合 ngTemplateOutlet 来实现动态模板。

详情看这里

References:

  1. https://angular.dev/api/core/ng-container
  2. ng-template: https://zdd.github.io/2024/07/09/angular-ng-template/

How to apply custom theme to Angular project.

  1. Create a custom file under project root
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
35
// Custom Theming for Angular Material
// For more information: https://material.angular.io/guide/theming
@use '@angular/material' as mat;
// Plus imports for other components in your app.

// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Be sure that you only ever include this mixin once!
@include mat.core();

// Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/
$my-app-primary: mat.define-palette(mat.$teal-palette);
$my-app-accent: mat.define-palette(mat.$teal-palette, A200, A100, A400);

// The warn palette is optional (defaults to red).
$my-app-warn: mat.define-palette(mat.$red-palette);

// Create the theme object. A theme consists of configurations for individual
// theming systems such as "color" or "typography".
$my-app-theme: mat.define-light-theme((
color: (
primary: $my-app-primary,
accent: $my-app-accent,
warn: $my-app-warn,
),
typography: mat.define-typography-config(),
density: 0
));

// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component
// that you are using.
@include mat.all-component-themes($my-app-theme);
  1. Put the theme file in angular.json or project.json(for Nx monorepo), Architect -> build -> options -> styles
1
2
3
4
"styles": [
"src/styles.scss",
"src/custom-theme.scss"
],

await后面的代码是同步执行吗?

asyncpromise并无本质区别,async只是看起来更加同步,本质还是异步执行。来看一段代码。

1
2
3
4
5
6
7
8
async function foo() {
console.log(1);
await Promise.resolve();
console.log(2);
}

foo();
console.log(3);

这段代码的输出结果是:1 3 2。而不是1 2 3。await会等待右侧的Promise执行完毕,并将后续代码 - console.log(2)加入微任务队列。所以上述代码的执行过程是:

  1. 执行console.log(1),输出1
  2. 执行await Promise.resolve(),将后续代码console.log(2)加入微任务队列
  3. 执行console.log(3),输出3
  4. 执行微任务队列中的代码,输出2

我们可以将foo改写一下,它实际上等价于下面的代码。

1
2
3
4
function foo() {
console.log(1);
Promise.resolve().then(() => console.log(2));
}

由此,我们可以得出如下结论:

  • await右侧的Promise同步执行。
  • await后续的代码会在Promise resolve后背加入微任务队列等待后续执行。

再看一个例子:

1
2
3
4
5
6
7
8
9
10
11
async function test() {
console.log("A");
await new Promise(resolve => {
console.log("B");
setTimeout(resolve, 0); // 宏任务触发 resolve
});
console.log("C");
}

test();
console.log("D");

这段代码的输出结果是:A B D C。执行过程如下:

  1. 首先执行test函数中的console.log("A"),输出A
  2. 然后执行await右侧的Promise,由于console.log("B")是同步代码,所以输出B
  3. 执行setTimeout(resolve, 0),将resolve加入宏任务队列
  4. 执行console.log("D"),输出D
  5. 注意此时微任务队列为空,所以执行宏任务队列中的resolve,此时await右侧的Promise执行完毕,将后续代码console.log("C")加入微任务队列
  6. 执行微任务队列中的代码,输出C

再看一个例子,你知道以下代码输出什么结果吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function async1() {
console.log(1);
await Promise.resolve().then(() => console.log(4));
console.log(2);
}

const async3 = async () => {
Promise.resolve().then(() => {
console.log(6);
});
};

async1();
console.log(7);
async3();

答案是:1 7 4 6 2。来分析一下执行过程:

  • 执行async1(),输出1
  • 执行await Promise.resolve().then(() => console.log(4)),此时将console.log(4)加入微任务队列。
  • 执行console.log(7),输出7
  • 执行async3(),将console.log(6)加入微任务队列
  • 执行微任务队列中的代码console.log(4),输出4,此时async1中的await右侧的Promise执行完毕,将后续代码console.log(2)加入微任务队列
  • 执行微任务队列中的代码console.log(6),输出6
  • 执行微任务队列中的代码console.log(2),输出2

总结一下:

  • 遇到await时,await会暂停当前async函数的执行,并等待右侧的Promise执行完毕,然后async函数后续代码加入微任务队列。
  • await只是暂停当前async函数的执行,并不会阻塞主线程的执行。当前async函数外的代码会继续执行。
  • 从影响范围来说,await只影响当前async函数,不影响其他函数。

await与Promise链

In JavaScript, await can works with promise chain, it will get the resolved value of the last promise in the chain. For example:

1
2
3
4
5
6
7
8
9
10
11
getData = () =>
Promise.resolve(1)
.then(() => 2)
.then(() => 3);

async function test() {
const res = await getData(); // output: 3
console.log(res);
}

test();

In above code, getData() returns a promise chain, the resolved value of the last promise in the chain is 3, so the output of the code is 3.

You can even append more promise chain when calling getData()

1
2
3
4
5
6
async function test() {
const res = await getData()
.then(() => 4)
.then(() => 5);
console.log(res); // output: 5
}

The @Output decorator is used to define an output property in a component. The output property is used to emit events from the component to the parent component. The @Output decorator is used in conjunction with the EventEmitter class to emit events.

How to use the @Output decorator

  1. Create a Child component
1
2
<!--child.component.html-->
<button (click)="onClick()">Update parent name</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
// child.component.t
@Component({
selector: 'app-child',
standalone: true,
// ...
})
export class ChildComponent {
@Output() updateParentName = new EventEmitter<string>();

onClick() {
this.updateParentName.emit('new name');
}
}
  1. Create a parent component
1
2
3
<!--parent.component.html-->
<p>Parent name: {{ name }}</p>
<app-child (updateParentName)="updateName($event)"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// parent.component.ts
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
// ...
})
export class ParentComponent {
name = 'parent';

updateName(name: string) {
this.name = name;
}
}

The connection between child component and parent component is established by binding the updateParentName output property of the child component to the updateName method of the parent component.

1
2
// child.component.ts
@Output() updateParentName = new EventEmitter<string>();
1
2
<!--parent.component.html-->
<app-child (updateParentName)="updateName($event)"/>

New output api in Angular 17.3

Angular 17.3 provide a new output api to simplify the usage of @Output decorator.

1
2
3
4
5
6
7
8
9
import {output} from '@angular/core';
// ...
export class ChildComponent {
updateParentName = output<string>(); // <--- new output api

onClick() {
this.updateParentName.emit('new name');
}
}

Naming conventions

Always use camelCase output names. Avoid prefixing output names with “on”.

References:

https://angular.dev/guide/components/outputs
https://blog.angular.dev/meet-angulars-new-output-api-253a41ffa13c

Over the past three releases, Angular team introduced a lot of new features and improvements, Angular 18 focused on polishing the previous work and graduating many of the new APIs from experimental to stable. another exciting feature is zoneless change detection - still in experimental phase. Here is the content table:

  1. Zoneless Change Detection
  2. angular.dev is the new home for angular developers
  3. Stable features: Material 3, deferrable views, built-in control flows
  4. SSR improvements: i18n hydration support, better debugging, hydration support in Angular Material.
  5. Native await for zoneless apps.
  6. Fallback content for ng-content.

Zoneless Change Detection

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

Then remove zone.js from file angular.json in your project.

Testing

HttpClientTestingModule is deprecated, use `` instead.

Before:

1
2
3
4
5
6
7
8
9
import { HttpClientTestingModule } from '@angular/common/http/testing';

//...

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule], // <-- Here
}).compileComponents();
});

Now

1
2
3
4
5
6
7
8
9
import { provideHttpClientTesting } from '@angular/common/http/testing';

beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
provideHttpClientTesting(),
]
}).compileComponents();
});

Native await for zoneless apps

因为Angular一直使用zone.js实现更新检测,但是async/await语法zone.js一直无法做monkey patch,导致在zone.js中使用promise来模拟,现在如果你的app不是用zone.js,那么Angular就不会再使用promise来模拟async/await,而是直接使用原生的async/await。

Specifying a fallback content for ng-content

ng-content现在可以指定一个fallback content,当没有传递内容时,会显示这个fallback content。

1
2
3
4
5
6
7
8
9
@Component({
selector: 'app-profile',
template: `
<ng-content select=".greeting">Hello </ng-content>

<ng-content>Unknown user</ng-content>
`,
})
export class Profile {}

调用代码:

1
2
3
<app-profile>
<span class="greeting">Good morning </span>
</app-profile>

which will result in:

1
2
<span class="greeting">Good morning </span>
Unknown user
  1. We specified Good morning for the first ng-content, so the fallback Hello won’t be displayed.
  2. We didn’t specify any content for the second ng-content, so the fallback Unknown user will be displayed.

Unified control state change events

1
2
3
4
const nameControl = new FormControl<string|null>('name', Validators.required);
nameControl.events.subscribe(event => {
// process the individual events
});

Router redirect as function

The redirectTo property in the route configuration can now be a function that returns a URL string or an array of URL segments.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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: "user/:userId", component: OtherComponent },
];

Support typescript 5.4

References:

  1. https://blog.angular.dev/angular-v18-is-now-available-e79d5ac0affe