0%

What’s Fixture?

先解释一下fixture这个单词,它的意思是固定设施,比如室内的浴缸或抽水马桶。当然这里指的是测试中的固定设施,也就是我们要测试的组件。在Angular中,我们可以通过TestBed来创建一个组件的fixture,然后对这个fixture进行测试。

通常来说,fixture都是在beforeEach中创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
describe('ProductComponent', () => {
let component: ProductComponent;
let fixture: ComponentFixture<ProductComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductComponent] // import standalone component
})
.compileComponents();

fixture = TestBed.createComponent(ProductComponent); // create fixture
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

fixture.detectChanges()

fixture.detectChanges()是用来触发组件的变更检测的,也就是说,当我们对组件的状态进行了修改之后,我们需要调用fixture.detectChanges()来通知Angular进行变更检测,以便更新视图。
如果你的测试中包含对UI的检测,那么你就需要调用fixture.detectChanges()。否则不需要。
来,举个例子!

We have a product component which just display the product name as ‘Computer’, and when user click the Change product name button, we’ll update the product name to ‘Phone’.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Component } from '@angular/core';

@Component({
selector: 'app-product',
standalone: true,
imports: [],
templateUrl: './product.component.html',
styleUrl: './product.component.css'
})
export class ProductComponent {
name = 'Computer';

changeName() {
this.name = 'Phone';
}
}
1
2
<p id="product-name">Product name: {{name}}</p>
<button (click)="changeName()">Change product name</button>

Does this test case work?

1
2
3
4
5
it('should have name as Computer', () => {
component.changeName();
fixture.detectChanges();
expect(component.name).toEqual('Phone');
});

Yes, it works, but, we don’t need to call fixture.detectChanges() here, because we are testing the component’s property name change directly, not the UI changes.

Does the following test case work?

1
2
3
4
5
it('should change name', () => {
component.changeName();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('#product-name').textContent).toContain('Phone');
});

No, it doesn’t work, because, you call component.changeName() to change the product name, but you didn’t call fixture.detectChanges() to trigger the change detection and update the view. so the product name on page is still ‘Computer’.

We can call fixture.detectChanges() after component.changeName() to fix this issue.

1
2
3
4
5
6
it('should change name', () => {
component.changeName();
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('#product-name').textContent).toContain('Phone');
});

fixture.whenStable()

fixture.whenStable()是用来等待异步任务完成的,为了使用这个函数,我们给product component添加一个异步任务,比如通过setTimeout来模拟一个异步任务。

1
2
3
4
5
6
7
8
9
10
11
getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Keyboard');
}, 1000);
});
}

async updateName() {
this.name = await this.getData() as unknown as string;
}
1
<button (click)="updateName()">Update name</button>

接下来,我们测试一下updateName方法。下面这个test case,没有调用fixture.detectChanges(),但是仍然可以通过测试,为什么呢?

1
2
3
4
5
6
7
8
it('should update name', async () => {
component.updateName();
fixture.whenStable().then(() => {
expect(component.name).toEqual('Keyboard');
const compiled = fixture.nativeElement;
expect(compiled.querySelector('#product-name').textContent).toContain('Keyboard');
});
});

其实,这个test case是有问题的,它根本没有通过测试,虽然我们使用async标记了测试方法,但是async要配合await使用,而不是then,实际上,这里的then根本没有执行!

为了修复这个test case,我们可以将async删除。(这里有待验证!!!

1
2
3
4
5
6
7
8
9
it('should update name', async () => {
await component.updateName();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(component.name).toEqual('Keyboard');
const compiled = fixture.nativeElement;
expect(compiled.querySelector('#product-name').textContent).toContain('Keyboard');
});
});

其实,这里根本就不需要fixture.whenStable(),因为我们使用了await,它会等待异步任务完成,所以我们可以直接这样写:

1
2
3
4
5
6
7
it('should update name', async () => {
await component.updateName();
fixture.detectChanges();
expect(component.name).toEqual('Keyboard');
const compiled = fixture.nativeElement;
expect(compiled.querySelector('#product-name').textContent).toContain('Keyboard');
});

那到底什么时候需要使用fixture.whenStable()呢?当你测试的方法里面包含异步操作,但是这个方法又不返回Promise的时候,你就需要使用fixture.whenStable()来等待异步任务完成。

思考题:
下面的代码能通过测试吗?

1
2
3
4
5
it('should update name', () => {
fixture.whenStable().then(() => {
expect(false).toBe(true);
});
});

答案是:能!因为fixture.whenStable()里面的代码根本就没有执行,如何修复?

1
2
3
4
it('should update name', async () => {
await fixture.whenStable();
expect(false).toBe(true);
});

还可以在fixture.whenStable()前面加上return,因为Jest中如果返回一个Promise,Jest会等待这个Promise执行完成。详情看这里

1
2
3
4
5
it('should update name', async () => {
return fixture.whenStable().then(() => {
expect(false).toBe(true);
});
});

Conclusion

fixture.detectChanges()

  1. This function will trigger change detection.
  2. Change detection will updates the view.
  3. If you test UI changes, you need to call fixture.detectChanges().

fixture.whenStable()

  1. This function will wait for the asynchronous tasks to complete.
  2. You should use this function in async function.

1. TypeError: configSet.processWithEsbuild is not a function

Solution: Update jest.config.js to use jest-angular-preset instead of ts-preset.

1
2
3
4
5
module.exports = {
transform: {
'^.+\\.(ts|js|html)$': 'ts-jest', // <-- update this line
},
};
1
2
3
4
5
module.exports = {
transform: {
'^.+\\.(ts|js|html)$': 'jest-preset-angular', // <-- update this line
},
};

2. SyntaxError: xxx.spec.ts: Missing semicolon. (26:42)

Look into the error message, the error occurs on this line.

1
const compiled = fixture.nativeElement as HTMLElement;

The reason is because as, it’s a keyword in TypeScript, but it’s not a keyword in JavaScript. So the root cause is Jest cannot understand the TypeScript syntax.
We need a preset to help Jest to understand TypeScript syntax. The ts-jest is the most popular one.

  • If you project is a pure TypeScript project, see here on how to config ts-jest.
  • If you project is an Angular project, Please use jest-preset-angular, see here for details.

3. jest: failed to cache transform results in: xxx/jest/jest-transform-cache.map, Failure message: ENOSPC: no space left on device, write

This is because Jest is trying to transform itself, so add the following to your jest.config.js file will resolve this issue. see here for details.

1
2
3
4
transformIgnorePatterns: [
'<rootDir>/node_modules/@babel',
'<rootDir>/node_modules/@jest',
],

Firstly, node can’t run typescript files directly. You need to compile the typescript files to javascript files and then run them with node.

The following command won’t work!

1
node ./src/main.ts

You’ll got error like this:

1
2
3
(node:13596) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.

SyntaxError: Cannot use import statement outside a module

How to solve this problem?

  1. Use ts-node to run typescript files directly.
    1. npm install -g ts-node
    2. ts-node ./src/main.ts
  2. Use tsc to compile typescript files to javascript files and then run them with node.
    1. npm install -g typescript
    2. tsc ./src/main.ts
    3. node ./src/main.js

Happy coding!

This article discuss how to debug Angular/Node.js application in WebStorm and VSCode.

Debug Angular app

WebStorm

  1. Start angular application
    1
    npm start // or ng serve
  2. Set breakpoints in the source code in WebStorm Editor.
  3. Press Ctrl + Shift + Click on the URL in the terminal to open the browser.
    debug angular WebStorm
  4. See here for more details.

VSCode

Launch with task file

  1. Create file launch.json under .vscode folder in project root.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
    {
    "name": "debug angular app",
    "type": "chrome",
    "request": "launch",
    "preLaunchTask": "npm: start",
    "url": "http://localhost:4200/"
    },
    ]
    }
  2. Create file task.json under .vscode folder in 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
    {
    // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
    "version": "2.0.0",
    "tasks": [
    {
    "type": "npm",
    "script": "start",
    "isBackground": true,
    "problemMatcher": {
    "owner": "typescript",
    "pattern": "$tsc",
    "background": {
    "activeOnStart": true,
    "beginsPattern": {
    "regexp": "(.*?)"
    },
    "endsPattern": {
    "regexp": "bundle generation complete"
    }
    }
    }
    },
    ]
    }
  3. Click Run and Debug in VSCode’s left side menu. // number 1 on picture below
  4. Select debug angular app in the dropdown list. // number 2 on picture below
  5. Click the Debug icon or Press F5 to start debugging. // number 3 on picture below
    debug angular VSCode
    In this way, VSCode will open a new Chrome window and you can debug the Angular app in VSCode.

Launch without task file

If you don’t want to use task.json file, you can remove it and delete preLaunchTask in launch.json file. Then

  1. Manually start the Angular app by npm start in terminal.
  2. Click the Debug icon or Press F5 to start debugging in VSCode.
    In this way you need to manually open the browser and navigate to http://localhost:4200/ to debug the Angular app.

Browser

No matter you use WebStorm or VSCode, you can also debug Angular app in browser.

  1. Run your angular app by npm run start or ng serve
  2. Open the browser and navigate to http://localhost:4200/.
  3. Press F12 to open the developer tools and switch to Source tab.
  4. Set breakpoint in your source code in browser. see here for more details.
    1. If you use webpack, the source file was in webpack://src folder.
    2. If you use ESBuild + Vite, the source files were in src folder.
  5. Refresh the page to trigger the breakpoints.

This is a very common error when you are working with Angular, today, I will give a special case of this error.

I have the following code in my app.

  1. ProductHomeModule -> ProductHomeComponent
  2. ProductDetailModule -> ProductDetailComponent
  3. App router contains the following routes:
    1
    2
    3
    4
    // app-routing.module.ts
    const routes: Routes = [
    { path: 'product', component: ProductHomeComponent },
    ]
    When I build my app, I got the following error:
1
ERROR in src/app/product-home/product-home.component.html:1:1 - error NG8001: 'app-product-detail' is not a known element:

But If I remove the router for product, the error is gone, why?
Because, I use ProductHomeComponent in app-routing.module.ts which in turn was used in file app.module.ts, But, when Angular compile template for ProductHomeComponent, it didn’t know where to find ProductDetailComponent. to solve this, import ProductHomeModule in app.module.ts like this:

1
2
3
imports: [
ProductHomeModule
]

Then the error is gone. Because, ProductHomeModule has imports ProductDetailModule, so Angular knows where to find ProductDetailComponent.

Conclusion:
If you wan to use a component from another module, you need to import the module which contains the component in the module where you use the component. You may use the component by selector in templates or in the router, both need to import the module which contains the component.

Angular Server Side Rendering For New Project

Step by step guide

Start from Angular 17, SSR was enabled by default when you create a new Angular project. To create a new Angular project, run the following command.

1
ng new angular-ssr

Press Y on keyboard when prompted with the following question, then Angular will generate the boilerplate code for you.

1
Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? (y/N)

Compared with the traditional pure client-side rendering project, the Angular SSR project has the following differences:

Project structure changes for SSR

The following new files were added to the project:

  1. The server.ts file is added to the project root directory.
  2. The src/main.server.ts file is added to the project source directory.
  3. The src/app/app.config.server.ts file is added to the project source directory.

The following changes were made to the existing files:

  1. The package.json file has a new server script.
    1
    2
    3
    4
    "scripts": {
    // ...
    "serve:ssr:angular-ssr": "node dist/angular-ssr/server/server.mjs" // <--- new
    },
  2. The angular.json file has a new server configuration under the architect | build | options section.
    1
    2
    3
    4
    5
    6
    7
    8
    "options": {
    // ...
    "server": "src/main.server.ts",
    "prerender": true,
    "ssr": {
    "entry": "server.ts"
    }
    },
  3. The tsconfig.app.json file file section is updated to include the src/main.server.ts file.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    // ...
    "files": [
    "src/main.ts",
    "src/main.server.ts", // <--- new
    "server.ts" // <--- new
    ],
    // ...
    }
  4. The src/app/app.config.ts file add provideClientHydration in providers.
    1
    2
    3
    export const appConfig: ApplicationConfig = {
    providers: [provideRouter(routes), provideClientHydration()]
    };

Run app

Deployment

How to check if page is rendered by server or client

  1. Open the page in Chrome browser.
  2. Right-click on the page and select View page source.
  3. Check the body tag, if the body tag is empty, it means the page is rendered in client side, the following page is a CSR page, since the body tag only contains <app-root></app-root>. that’s for the client side rendering.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!doctype html>
    <html lang="en">
    <head>
    <script type="module" src="/@vite/client"></script>

    <meta charset="utf-8">
    <title>Angular SSR In Depth</title>
    <base href="/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">
    <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&amp; display=swap" rel="stylesheet">
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <link rel="stylesheet" href="styles.css"></head>
    <body class="mat-typography">
    <app-root></app-root>
    <script src="polyfills.js" type="module"></script><script src="main.js" type="module"></ script></body>
    </html>

Angular SSR for existing project

If you have an existing project and want to enable SSR, you can follow the steps below.

1
ng add @angular/ssr

This command will add the necessary files and configurations to your project to enable SSR.

  1. Create src/main.server.ts file.

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

    const bootstrap = () => bootstrapApplication(AppComponent, config);

    export default bootstrap;
  2. Create src/app/app.config.server.ts file.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
    import { provideServerRendering } from '@angular/platform-server';
    import { appConfig } from './app.config';

    const serverConfig: ApplicationConfig = {
    providers: [
    provideServerRendering()
    ]
    };

    export const config = mergeApplicationConfig(appConfig, serverConfig);
  3. Create server.ts file.

    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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    import { APP_BASE_HREF } from '@angular/common';
    import { CommonEngine } from '@angular/ssr';
    import express from 'express';
    import { fileURLToPath } from 'node:url';
    import { dirname, join, resolve } from 'node:path';
    import bootstrap from './src/main.server';

    // 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');
    const commonEngine = new CommonEngine();

    server.set('view engine', 'html');
    server.set('views', browserDistFolder);

    // Example Express Rest API endpoints
    // server.get('/api/**', (req, res) => { });
    // Serve static files from /browser
    server.get('**', express.static(browserDistFolder, {
    maxAge: '1y',
    index: 'index.html',
    }));

    // All regular routes use the Angular engine
    server.get('**', (req, res, next) => {
    const { protocol, originalUrl, baseUrl, headers } = req;
    commonEngine
    .render({
    bootstrap,
    documentFilePath: indexHtml,
    url: `${protocol}://${headers.host}${originalUrl}`,
    publicPath: browserDistFolder,
    providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
    })
    .then((html) => res.send(html))
    .catch((err) => next(err));
    });

    return server;
    }

    function run(): void {
    const port = process.env['PORT'] || 4000;
    // Start up the Node server
    const server = app();
    server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
    });
    }

    run();
  4. Update angular.json file.

    Add the following in architect | build | options section.

    1
    2
    3
    4
    5
    "server": "src/main.server.ts",
    "prerender": true,
    "ssr": {
    "entry": "server.ts"
    }
  5. Update tsconfig.app.json file.

  6. Update src/app/app.config.ts file.

  7. Update package.json file.

References

  1. https://www.youtube.com/watch?v=U1MP4uCuUVI

There are many ways to define a dependency provider in Angular. In this article, we will discuss the following ways:

useClass

useClass是依赖注入中提供provider最常见的方式,我们项目中90%用的都是这种方式,下面来看看怎么使用这种方式,假设我们有一个ProductComponent组件,它依赖一个OrderService服务,我们可以像下面这样提供OrderService:

1
2
3
4
5
6
7
8
9
@Component({
selector: 'app-product',
standalone: true,
providers: [OrderService], // <-- Provide OrderService
templateUrl: './product.component.html',
styleUrl: './product.component.scss'
})
export class ProductComponent {
}

上面的例子其实是简写形式,它等价于下面的完整形式:也就是当provide对应的值和useClass对应的值相同时,我们可以采用这种简写形式。

1
providers: [{ provide: OrderService, useClass: OrderService }],

还有一点需要注意的是:如果OrderServiceprovidedIn: 'root'的话,那么我们就不需要在providers中再次提供OrderService了,直接在构造函数中注入即可。

1
2
3
export class ProductComponent {
constructor(private orderService: OrderService) {}
}

useClass在单元测试中特别有用,如果要测试的Service很复杂,我们可以提供一个mockService来代替他,这个mockService只要满足最小测试范围内即可,这样就可以保证测试的独立性。

1
providers: [{ provide: UserService, useClass: mockUserService}]

useExisting

useExisting通常用于提供一个别名,也就是说,useExisting提供了多种访问同一Service的方式,下面来看一个例子:
假设我们的Order分为线下订单和线上订单,那么线上订单我们可以单独定义一个Service,叫做OnlineOrderService,这个Service需要的功能OrderService中都有,所以我们可以如下使用它。

1
2
3
4
5
6
7
8
9
@Component({
selector: 'app-product',
standalone: true,
providers: [OrderService, {provide: OnlineOrderService, useExisting: OrderService}],
templateUrl: './product.component.html',
styleUrl: './product.component.scss'
})
export class ProductComponent {
}

这里:OnlineOrderServiceOrderService的别名,我们可以通过OnlineOrderService来访问OrderService的实例。

这种方式通常发生在旧系统到新系统的迁移时,比如迁移的时候,我们可能会更改一些Service的名字,但是为了兼容旧系统,我们可以通过useExisting来提供别名。

以Logger为例,假设旧系统使用的是OldLoggerService, 新系统使用的是NewLoggerService,那么我们可以这样定义:

1
providers: [{provide: OldLoggerService, useExisting: NewLoggerService}]

useFactory

useFactory,顾名思义,这种方式使用一个工厂方法来提供依赖的实例,通常用在需要参数的Service中,上面的例子中,我们提供的Service都是无参数的,下面我们来看一个需要参数的Service。

下面这个HeroService需要一个Logger和一个isAuthorized参数,无论是useClass,还是useExisting都无法满足这个需求,这时我们就可以使用useFactory来提供这个Service。

1
2
3
4
5
6
7
8
9
10
11
class HeroService {
constructor(
private logger: Logger,
private isAuthorized: boolean) { }

getHeroes() {
const auth = this.isAuthorized ? 'authorized' : 'unauthorized';
this.logger.log(`Getting heroes for ${auth} user.`);
return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}
}

useFactory后面跟的是一个函数,所以我们先定义这个函数。(注意Factory函数是一个injection context,在这个函数内部可以使用inject()方法来注入其他依赖。)

1
2
const heroServiceFactory = (logger: Logger, userService: UserService) =>
new HeroService(logger, userService.user.isAuthorized);

然后使用useFactory来提供HeroService

1
2
3
4
5
6
7
8
9
providers: [
Logger,
UserService,
{
provide: HeroService,
useFactory: heroServiceFactory,
deps: [Logger, UserService] // deps是用来给工厂方法传递参数的
}
]

注意观察,上面的工厂函数heroServiceFactory中的参数loggeruserService是通过deps传递进来的。

useValue

useValue允许你绑定一个静态值到一个token上,这种方式通常用于提供一些常量,比如配置信息等。如果我们需要运行时动态取值,就可以使用这种方式了。

useValue的使用步骤如下:

  1. 定义一个token
  2. 使用useValue来提供这个token
  3. 在构造函数中注入这个token

token是简单值

定义一个token

首先新建一个文件src/app/config/AppConfig.ts用来存储token。这里我们创建一个token用来存储主题信息。

1
2
3
import {InjectionToken} from "@angular/core";

export const AppTheme = new InjectionToken<string>('appTheme');

使用useValue来提供这个token

由于我们这个token比较简单,就是字符串类型的,所以直接用字符串’light’来提供这个token。

1
providers: [{provide: AppTheme, useValue: 'light'}],

在构造函数中注入这个token

1
2
3
constructor(@Inject(AppTheme) private theme: string) {
console.log(theme);
}

token是对象

对于复杂的token,我们将其封装到一个JS对象中,如果一个app中有多个配置项,我们可以将这些配置项封装到一个对象AppConfig中,然后使用useValue来提供这个对象。

定义一个token

首先定义token对应的对象类型,这里使用typescript中的interface来定义。

1
2
3
4
5
6
7
8
// src/app/config/AppConfig.ts
export interface AppConfig {
theme: string;
version: string;
production: boolean;
baseUrl: string;
apiEndpoint: string;
}

然后定义该类型对应的token实例。

1
2
// src/app/config/AppConfig.ts
export const APP_CONFIG = new InjectionToken<AppConfig>('app config');

使用useValue来提供这个token

先创建一个AppConfig对象,然后使用useValue来提供这个对象。

1
2
3
4
5
6
7
8
9
10
// app.component.ts
const currentAppConfig: AppConfig = {
theme: 'light',
version: '1.0.0',
production: true,
baseUrl: 'https://example.com',
apiEndpoint: 'https://example.com/api',
}

providers: [{provide: APP_CONFIG, useValue: currentAppConfig}],

在构造函数中注入这个token

1
2
3
constructor(@Inject(APP_CONFIG) private config: AppConfig) {
console.log(config);
}

这样在Component中,就可以使用this.config来访问这个配置对象了。

References

  1. https://angular.dev/guide/di/dependency-injection-providers
  2. https://medium.com/@matsal.dev/angular-usevalue-useclass-useexisting-and-usefactory-in-a-nutshell-97db8d206084

:last-child

:last-child是一个CSS伪类,它表示一组元素中的最后一个。

:last-of-type

:last-of-type也是一个CSS伪类,它表示一组同类元素中的最后一个。注意这里多了一个限制:同类

那么这两者有什么区别呢?我们来看一个例子。
给定如下html代码,现在要求你写一段CSS,设置最后一个li元素的背景颜色为绿色。

1
2
3
4
5
<ul>
<li>Angular</li>
<li>React</li>
<li>Vue</li>
</ul>

以下两种写法都可以实现这个要求。

1
2
3
li:last-child {
background-color: green;
}
1
2
3
li:last-of-type {
background-color: green;
}

现在我们稍稍改动一下html代码,增加一个span元素作为最后一个子元素。

1
2
3
4
5
6
<ul>
<li>Angular</li>
<li>React</li>
<li>Vue</li>
<span>JavaScript</span>
</ul>

这时候下面的方法就不好使了,为什么呢?实际上li:last-child不会选中任何元素,因为span是这组元素的最后一个元素,所以要选中这组元素的最后一个li元素是找不到的。如果改成span: last-child,那么就可以选中span元素了。

1
2
3
li:last-child {
background-color: green;
}

但是下面这种方法仍然好用,因为我们指定了type为li,所以它会选中最后一个li元素。

1
2
3
li:last-of-type {
background-color: green;
}

参考

Table of content

Feature Description
provideIn A new way to declare service scope

provideIn


In Angular 6, you can use the provideIn property in the @Injectable decorator to declare the scope of the service.

Introduction

The get syntax binds an object property to a function that will be called when that property is looked up. It can also be used in classes.

Here is an example:

1

Case

When you use getter, you would better provide setter for the property too. If you only provide getter, you might got errors, here is a real case.

We have a child component with a name property, and we want to format the name to uppercase in the child component. the name property is passed from the parent component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// child.component.ts
@Component({
standalone: true,
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent {
private _name = '';

@Input() set name(name: string) {
this._name = name
}

formatName(name: string) {
return name.toUpperCase();
}
}
1
2
<!-- child.component.html -->
<div>Uppercase name: {{ formatName(name) }}</div>

And in the parent component’s template, we pass the name property to the child component.

1
2
<!-- parent.component.html -->
<app-child name="Philip"></app-child>

When you run the code, you will get the following error:

1
TypeError: Cannot read properties of undefined (reading 'toUpperCase')

The error is caused by the formatName function in the child component, here name is undefined, we can’t call toUpperCase on undefined, but why?

1
return name.toUpperCase();

When we read name in the formatName function, it will trigger the getter of the name property, but we only provide the setter for the name property, so name is undefined.

To fix this error, we need to provide the getter for the name property.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// child.component.ts
@Component({
standalone: true,
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent {
private _name = '';

@Input() set name(name: string) {
this._name = name
}

get name() {
return this._name;
}

formatName(name: string) {
return name.toUpperCase();
}
}

References:

  1. get in object