0%

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

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...