0%

Have you ever seen this error: “TypeError: xxx is not a function”? This error occurs when you try to call a function that does not exist.

1
2
3
4
5
6
const person = {
name: 'Philip',
age: 18,
};

person.getAge(); // TypeError: person.getAge is not a function

You can solve this by the optional chaining operator ?.:

1
person.getAge?.(); // nothing output

However, if there is a property in the object has the same name by accident, you will still get that error

1
2
3
4
5
6
7
const person = {
name: 'Philip',
age: 18,
getAge: 18,
};

person.getAge?.(); // TypeError: person.getAge is not a function

Test promise

How many ways to test an async function which has a promise with jest in an angular app? suppose you have the following function

1
2
3
4
5
6
7
getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(10);
}, 0);
});
}

To test this function, you can use the following ways:

1. Using async/await

1
2
3
4
it('should return 10', async () => {
const result = await getData();
expect(result).toBe(10);
});

2. By returning promise

In jest, when you return a promise in your test, jest will wait for the promise to resolve. see here for details.

1
2
3
4
5
it('should return 10', () => {
return getData().then(result => {
expect(result).toBe(10);
});
});

3. By a done callback

This one works fine when promise is resolved. but if the promise is rejected, the test will wait for a maximum of 5 seconds timeout, and then fail. while others will fail immediately.

1
2
3
4
5
6
it('should return 10', (done) => {
getData().then(result => {
expect(result).toBe(10);
done();
});
});

4. By waitForAsync in angular

In angular, you can use waitForAsync to test async functions. Note that you don’t need await in this case.

1
2
3
4
5
it('should return 10', waitForAsync(() => {
getData().then(result => {
expect(result).toBe(10);
});
}));

5. Using fakeAsync in angular

In angular, you can use fakeAsync to test async functions. Note that you don’t need flush or tick in this case.

1
2
3
4
5
6
it('should return 10', fakeAsync(() => {
getData().then(res => {
expect(result).toBe(10);
});
flush();
}));

6. This one not work!

Since we have a setTimeout in the getData function, the following test will fail.

1
2
3
4
5
it('should return 10', () => {
getData().then(result => {
expect(result).toBe(10);
});
});

if getData() doesn’t have a setTimeout, the above test will work.

1
2
3
4
5
getData() {
return new Promise((resolve, reject) => {
resolve(10);
});
}

Test observable

How many ways to test an async function which returns an observable with jest in an angular app? suppose you have the following function

1
2
3
4
5
6
7
getData() {
return new Observable((observer) => {
setTimeout(() => {
observer.next(10);
}, 1000);
});
}

1. Test with done callback

1
2
3
4
5
6
it('should complete with value 10', (done) => {
component.getData().subscribe((result) => {
expect(result).toBe(10);
done();
});
});

2. Test by convert observable to promise

1
2
3
it('should complete with value 10', async () => {
await expect(firstValueFrom(component.getData())).resolves.toBe(10);
});

3. Convert to promise manually

This one is not recommended, Please use firstValueFrom or lastValueFrom from rxjs to convert observable to promise.

1
2
3
4
5
6
7
8
it('should complete with value 10', () => {
return new Promise((resolve, reject) => {
component.getData().subscribe((result) => {
expect(result).toBe(10);
resolve(true);
});
});
});

In this post, I will show you how to set up a Jest project from scratch. We won’t use any front-end frameworks like React, Angular, or Vue, just a simple vanilla JavaScript project.

CommonJs

Step 1: Create a new project

Run the following command to init a new project directory, it will simply create a package.json file in the current directory.

1
npm init -y # with -y, you don't need to answer the questions

Step 2: Install Jest

Run the following command to install Jest as a dev dependency.

1
npm install --save-dev jest

Step 3: Create a javascript file

We’ll first focus on CommonJs, create a new file named sum.js in the root directory of your project, and add the following code:

1
2
3
4
function sum(a, b) {
return a + b;
}
module.exports = sum;

Step 4: Create a test file

Create a new file named sum.test.js in the root directory of your project, and add the following code:

1
2
3
4
5
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});

Step 5: Update package.json

Add the following line to the scripts property in the package.json file:

1
2
3
"scripts": {
"test": "jest"
}

Step 6: Run the test

Run the following command to run the test:

1
npm test

Run test with coverage

Run the following command to run the test with coverage:

1
npx jest -- --coverage

ESModule

To run Jest with ESModule, first rewrite your file and test file to use ESModule syntax.

1
2
3
4
// sum.js
export default function sum(a, b) {
return a + b;
}
1
2
3
4
5
6
// sum.test.js
import sum from './sum.js';

test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});

Run the test, you will got following error:

1
2
Jest encountered an unexpected token
SyntaxError: Cannot use import statement outside a module

This is because you are trying to use ESModule syntax in a CommonJs environment, to fix this, you can update your file name to sum.test.mjs or you need to add the following line to the package.json file:

1
2
3
4
"jest": {
"moduleFileExtensions": ["js", "mjs"],
"transform": {}
}

Then run the test again, it still not work, got the following error, it seems that Jest didn’t find any test file with the default pattern.

1
2
3
4
5
6
7
8
No tests found, exiting with code 1
Run with `--passWithNoTests` to exit with code 0
In D:\personal\codes\jest
5 files checked.
testMatch: **/__tests__/**/*.[jt]s?(x), **/?(*.)+(spec|test).[tj]s?(x) - 0 matches
testPathIgnorePatterns: \\node_modules\\ - 5 matches
testRegex: - 0 matches
Pattern: - 0 matches

Update jest config in package.json file to include mjs file extension in testMatch, see micromatch for detail matching rules. Note: this is not regex.

1
2
3
4
5
"jest": {
"testMatch": [
"**/?(*.)test.?js"
]
},

Then run the test again, got the previous error again

1
SyntaxError: Cannot use import statement outside a module

Then search jest official documentation, change jest command in package.json file to:

1
2
3
4
5
"scripts": {
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
},
}

Then run the test again, it works now.

Using config file

You can move the jest config in package.json to a separate file named jest.config.js, and update the package.json file to use the config file.

1
2
3
4
"scripts": {
"test": "jest --config jest.config.js"
"test-esm": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.js"
}

Then create a new file named jest.config.js in the root directory of your project, and add the following code:

1
2
3
4
5
module.exports = {
moduleFileExtensions: ['js', 'mjs'],
transform: {},
testMatch: ['**/?(*.)test.?js'],
};

Then run the test again, it works now.

You don’t need to mock localStorage with latest jsdom

Suppose you have a localStorage utility function like this:

1
2
3
4
5
6
7
8
9
10
11
12
// storage-utils.ts
export function saveItem(item: any) {
localStorage.setItem('item', JSON.stringify(item));
}

export function getItem(name: string) {
const jsonString = localStorage.getItem(name);
if (jsonString) {
return JSON.parse(jsonString);
}
return null;
}

To test above code, you have the following test file:

1
2
3
4
5
6
7
8
9
10
11
// storage-utils.spec.ts
import { getItem, saveItem } from './storage-utils';

describe('local storage should work', () => {
it('should set and get item', () => {
const item = { name: 'John Doe', age: 30 };
saveItem(item);
const storedItem = getItem('item');
expect(storedItem).toEqual(item);
});
});

When you run this test with Jest

1
2
# first, navigate to the directory where the test file is located.
jest storage-utils.spec.ts

Everything works fine, but localStorage is only available in browser environment, and you didn’t mock it, why?

This is because Jest now use jsdom as the default environment, and jsdom has implemented localStorage and sessionStorage APIs, so you don’t need to mock them anymore.

To test this, you can find the jest.config.js file in the root directory of your project, and add the following line to the testEnvironment property:

1
2
3
4
5
module.exports = {
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
testEnvironment: 'node', // <-- change this to 'node'
};

Then run the test again, you will see the test failed because localStorage is not available in the node environment, and you will get the following error:

1
2
localStorage is not defined
ReferenceError: localStorage is not defined

Use jsdom by in test file

Suppose you global jest config is node(in file jest.config.js under project root), but you want to use jsdom in some test cases, you can use the following code in the test file, add @jest-environment jsdom at the top of the file.(Don’t use // comment, it will not work)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @jest-environment jsdom
*/

import { getItem, saveItem } from './storage-utils';

describe('local storage should work', () => {
it('should set and get item', () => {
const item = { name: 'John Doe', age: 30 };
saveItem(item);
const storedItem = getItem('item');
expect(storedItem).toEqual(item);
});
});

Mock localStorage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// mockLocalStorage.ts
const mockLocalStorage = (() => {
let store = {} as Storage;

return {
getItem(key: string) {
return store[key];
},

setItem(key: string, value: string) {
store[key] = value;
},

removeItem(key: string) {
delete store[key];
},

clear() {
store = {} as Storage;
},
};
})();

export default mockLocalStorage;

Test file, note that we use globalThis here, it’s environment independently, and works both in node and browser environment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// storage-utils.spec.ts
import { getItem, saveItem } from './storage-utils';
import mockLocalStorage from './mockLocalStorage';

describe('local storage should work', () => {
beforeEach(() => {
Object.defineProperty(globalThis, 'localStorage', {
value: mockLocalStorage,
});
});

it('should set and get item', () => {
const item = { name: 'John Doe', age: 30 };
saveItem(item);
const storedItem = getItem('item');
expect(storedItem).toEqual(item);
});
});

References:

  1. https://jestjs.io/docs/configuration#testenvironment-string - the official document still said that node is the default environment, but in my case, it’s jsdom, not sure why.
  2. https://github.com/jsdom/jsdom/blob/main/Changelog.md#11120

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/