single
single可以理解为是 angular 的 响应式状态管理,类比 react 的 hooks
| 特性 | Angular Signal | React Hooks |
|---|---|---|
| 创建 | signal(initialValue) |
useState(initialValue) |
| 读取 | signal() 调用 |
state 变量 |
| 更新 | signal.set(newValue) |
setState(newValue) |
| 更新(基于旧值) | signal.update(old => old + 1) |
setState(old => old + 1) |
| 衍生值 | computed(() => ...) |
useMemo(() => ..., [deps]) |
| 副作用 | effect(() => ...) |
useEffect(() => ..., [deps]) |
computed
类比 react 的 useMemo,自动缓存,single变化时重新计算
app.ts
import { signal, computed } from '@angular/core';
@Component({...})
export class CartComponent {
price = signal(100);
quantity = signal(2);
// 自动缓存,依赖变化时重新计算
total = computed(() => this.price() * this.quantity());
}
上面代码中的 total 是计算Signal,是只读的,当price和quantity变化时重新计算
effect
类比 react 的 useEffect, single 变化时执行
ts
import { effect } from '@angular/core';
@Component({...})
export class LoggerComponent {
count = signal(0);
constructor() {
// 自动追踪依赖,变化时执行
effect(() => {
console.log('Count changed:', this.count());
});
}
}
但是很少会使用effect。避免副作用用来传播状态更改。
组件
组件结构
Angular组件分为
- TypeScript 类
- HTML 模板
- CSS 样式
user-profile.ts
@Component({
// 可以理解为组件的名字
selector: 'user-profile',
// HTML模板
template: `
<h1>User {{ userName }}</h1>
<p>This is the user profile page</p>
`,
// CSS
styles: `h1 { font-size: 3em; } `,
})
// TypeScript类,放数据与逻辑的地方
export class UserProfile {
userName = 'Jolyne'
}
当然,前端开发需要遵循组件化、模块化的开发,所以可以将 html / css 的部分抽离到单独的文件中去。html通过templateUrl 指定HTML模块,css通过styleUrl指定CSS模块。
user-pofile.ts
@Component({
selector: 'user-profile',
templateUrl: 'user-profile.html',
styleUrl: 'user-profile.css',
})
export class UserProfile {
}
user-pofile.html
<h1>User profile</h1>
<p>This is the user profile page</p>
user-profile.css
h1 { font-size: 3em;}
组合组件
实际项目中,一个组件可能由多个子组件构成。那么可以通过 template 和 imports 来组合组件
componentA.ts
@Component({
selector: 'componentA',
template: `
Username: {{ username }}
`,
})
export class ComponentA {
username = 'Jolyne';
}
componentB.ts
@Component({
selector: 'componentB',
template: `
age: {{ age }}
`,
})
export class ComponentB {
age = '26';
}
main.ts
import {ComponentA} from 'ComponentA.ts';
import {ComponentB} from 'ComponentB.ts';
@Component({
selector: 'main',
imports: [ComponentA, ComponentB],
template: `
<div>
<componentA />
<componentB />
</div>
`,
})
export class Main {
}
注意:使用组件时,用的是 selector (名字),imports里面引入的是 Typescript类
组件输入
组件输入类似 React 或者 Vue 的 props。也就是父组件向子组件通信
input()
angular里面通过 input() 这个函数来定义的。input()返回一个InputSignal
app.ts
import {Component} from '@angular/core';
import {User} from './user';
@Component({
selector: 'app-root',
template: `
<app-user name="Simran" />
`,
imports: [User],
})
export class App {}
child.ts
import {Component, input} from '@angular/core';
@Component({
selector: 'app-user',
template: `
<p>The user's name is {{ name() }}</p>
`,
})
export class User {
readonly name = input<string>();
}
必要输入 input.required
比如props是必须的,通过 input.required 来声明
child.ts
export class User {
name = input.required<string>();
}
注意:如果是必要的输入,undefined 不会包含在 InputSignal 泛型参数中
配置输入 transform
有些时候可能需要根据 输入的值进行转换,则用到 transform
child.ts
export class CustomSlider {
label = input('', {transform: trimString});
}
function trimString(value: string | undefined): string {
return value?.trim() ?? '';
}
app.ts
<custom-slider [label]="systemVolume" />
每当 systemVolume 的值更改时,Angular 都会运行 trimString 并将 label 设置为结果。
需要注意的是:transform必须是纯函数
内置转换
Angular 包括两个内置的转换函数 booleanAttribute, numberAttribute
app.ts
import {Component, input, booleanAttribute, numberAttribute} from '@angular/core';
@Component({/*...*/})
export class CustomSlider {
disabled = input(false, {transform: booleanAttribute});
value = input(0, {transform: numberAttribute});
}
注意:booleanAttribute将 "false" 视为 false。numberAttribute如果解析失败是 NaN
输入别名
通过 alias 设置输入的别名
child.ts
@Component({/*...*/})
export class CustomSlider {
value = input(0, {alias: 'sliderValue'});
}
app.ts
<custom-slider [sliderValue]="50" />
@Input装饰器声明输入
child.ts
@Component({...})
export class CustomSlider {
@Input() value = 0;
}
app.ts
<custom-slider [value]="50" />
@Input 的必要输入
app.ts
@Component({...})
export class CustomSlider {
@Input({required: true}) value = 0;
}
@Input 的 transform
app.ts
@Component({
selector: 'custom-slider',
...
})
export class CustomSlider {
@Input({transform: trimString}) label = '';
}
function trimString(value: string | undefined) { return value?.trim() ?? ''; }
@Input 的 alias
child.ts
@Component({...})
export class CustomSlider {
@Input({alias: 'sliderValue'}) value = 0;
}
app.ts
<custom-slider [sliderValue]="50" />
组件输出
output
组件输出通过 output 和 EventEmitter 实现。相当于就是子组件向父组件通信
child.ts
import {Component, output} from '@angular/core';
@Component({
selector: 'app-child',
styles: `.btn { padding: 5px; }`,
template: `
{{count}}
<button class="btn" (click)="addItem()">Add Item</button>
`,
})
export class Child {
count = 0;
readonly addItemEvent = output<number>();
addItem() {
this.count++;
this.addItemEvent.emit(
this.count
);
}
}
app.ts
import {Component} from '@angular/core';
import {Child} from './child';
@Component({
selector: 'app-root',
template: `
<app-child (addItemEvent)="addItem($event)" />
<p>🐢 all the way down {{ items.length }}</p>
@for(item of items; track item) {
<p>{{ item }}</p>
}
`,
imports: [Child],
})
export class App {
items = new Array();
addItem(item: number) {
this.items.push(item);
}
}
@output 装饰器
跟 @Input 类似。
@output装饰器别名
child.ts
@Component({/*...*/})
export class CustomSlider {
@Output('valueChanged') changed = new EventEmitter<number>();
}
app.ts
<custom-slider (valueChanged)="saveVolume()" />
ng-content
ng-content可以类比 React 里面组件封装时的 children,或者 vue 中的 slot。它就是个占位符,不是组件也不是DOM元素
单插槽
custom-card.ts
@Component({
selector: 'custom-card',
template: '<div class="card-shadow">
<ng-content></ng-content>
</div>',
})
export class CustomCard {/* ... */}
app.ts
@Component({
selector: 'app-selector',
template: `
<custom-card>
<p>This is the projected content</p>
</custom-card>
`,
})
上面的代码就是 p 标签挂载到 ng-conent 的位置。渲染后的DOM结构如下
dom
<custom-card>
<div class="card-shadow">
<p>This is the projected content</p>
</div>
</custom-card>
多插槽
ng-content可以通过 select 属性实现多插槽
ts
@Component({
selector: 'card-title',
template: `<ng-content>card-title</ng-content>`,
})
export class CardTitle {}
@Component({
selector: 'card-body',
template: `<ng-content>card-body</ng-content>`,
})
export class CardBody {}
// custom-card
Component({
selector: 'custom-card',
template: `
<div class="card-shadow">
<ng-content select="card-title"></ng-content>
<div class="card-divider"></div>
<ng-content select="card-body"></ng-content>
</div>
`,
})
export class CustomCard {}
app.ts
@Component({
selector: 'app-root',
imports: [CustomCard, CardTitle, CardBody],
template: `
<custom-card>
<card-title>Hello</card-title>
<card-body>Welcome to the example</card-body>
</custom-card>
`,
})
export class App {}
最后渲染的DOM结构如下
ts
<custom-card>
<div class="card-shadow">
<card-title>Hello</card-title>
<div class="card-divider"></div>
<card-body>Welcome to the example</card-body>
</div>
</custom-card>
ng-template
定义了一个模板 ,但默认不会渲染任何内容,需要配合其他指令或机制才能将其渲染到 DOM 中
html
<!-- 这段内容不会显示在页面上 -->
<ng-template>
<p>这段文字不会显示</p>
</ng-template>
配合 *ngIf 使用。比如条件渲染不同的模板
html
<!-- 直接用 *ngIf 的内联写法,实际上 Angular 会自动转换成 ng-template -->
<div *ngIf="isLoggedIn; else loggedOut">
欢迎回来,用户!
</div>
<ng-template #loggedOut>
<p>请先登录</p>
</ng-template>
ng-container
跟React里面的<></>类似。
| 特性 | Angular ng-container |
React <> </> (Fragment) |
|---|---|---|
| 不产生额外 DOM | ✅ 不会创建真实 DOM 元素 | ✅ 不会创建真实 DOM 元素 |
| 分组作用 | ✅ 包裹多个元素但不影响布局 | ✅ 包裹多个元素但不影响布局 |
| 避免破坏样式 | ✅ 不会打乱 Flex/Grid 布局 | ✅ 不会打乱 Flex/Grid 布局 |
ts
<!-- Angular -->
<ng-container>
<div>项目1</div>
<div>项目2</div>
</ng-container>
<!-- React -->
<>
<div>项目1</div>
<div>项目2</div>
</>
只不过 ng-container 可以跟 *ngIf、*ngFor 一起使用,而 React Fragment 不支持
ts
//angular:直接在容器上使用指令
<ng-container *ngIf="isLoggedIn; else guest">
<h1>欢迎回来</h1>
<p>您的订单:{{ orderCount }}</p>
</ng-container>
<ng-template #guest>
<h1>请登录</h1>
<a href="/login">去登录</a>
</ng-template>
// React:需要将条件逻辑写在外部
{isLoggedIn ? (
<>
<h1>欢迎回来</h1>
<p>您的订单:{orderCount}</p>
</>
) : (
<>
<h1>请登录</h1>
<a href="/login">去登录</a>
</>
)}
生命周期
| 方法 | 摘要 | |
|---|---|---|
ngOnInit |
在 Angular 初始化完组件的所有输入之后运行一次。 | |
ngOnChanges |
每次组件的输入发生更改时都会运行。 | |
ngDoCheck |
每次检查此组件是否有更改时都会运行。 | |
ngAfterContentInit |
在组件的内容初始化之后运行一次。 | |
ngAfterContentChecked |
每次检查此组件内容是否有更改时都会运行。 | |
ngAfterViewInit |
在组件的视图初始化之后运行一次。 | |
ngAfterViewChecked |
每次检查组件的视图是否有更改时都会运行。 | |
afterNextRender |
在所有组件下次渲染到 DOM 后运行一次。 | |
afterEveryRender |
每次所有组件渲染到 DOM 后都会运行。 | |
ngOnDestroy |
在组件销毁之前运行一次。 |
ngOnInit
组件自身初始化完成前执行。只执行一次
ngOnChanges
在组件 输入(props)改变后运行
在初始化期间,第一次 ngOnChanges 在 ngOnInit 之前运行。
该生命周期接受一个 SimpleChanges 参数,包含 输入props 的先前值、当前值、标志(表示该输入props是否是第一次更改)
ts
@Component({
/* ... */
})
export class UserProfile {
name = input('');
ngOnChanges(changes: SimpleChanges) {
for (const inputName in changes) {
const inputValues = changes[inputName];
console.log(`Previous ${inputName} == ${inputValues.previousValue}`);
console.log(`Current ${inputName} == ${inputValues.currentValue}`);
console.log(`Is first ${inputName} change == ${inputValues.firstChange}`);
}
}
}
注意:如果输入提供了 alias 别名,SimpleChanges 的 key 依旧是定义时的名字,而不是别名
ngOnDestory
组件销毁前执行一次。
ngDoCheck
组件更改前,ngDoCheck都会运行。
注意:该方法会非常频繁的运行,会存在严重的性能问题。尽量不用
ngAfterContentInit
组件内部的所有子组件都初始化之后运行一次
剩下的生命周期
生命周期接口
Angular 为每个生命周期方法提供了一个 TypeScript 接口。你可以选择导入并 implement 这些接口,以确保你的实现没有任何拼写错误。
每个接口的名称都与相应的方法相同,只是去掉了 ng 前缀。例如,ngOnInit 的接口是 OnInit。
ts
@Component({
/* ... */
})
export class UserProfile implements OnInit {
ngOnInit() {
/* ... */
}
}
生命周期执行顺序
ViewChild
让父组件能获取子组件、子指令或DOM元素的引用。如果找不到目标值是 undefined
app.ts
@Component({
selector: 'custom-card-header',
})
export class CustomCardHeader {
text: string;
}
@Component({
selector: 'custom-card',
template: '<custom-card-header>Visit sunny California!</custom-card-header>',
})
export class CustomCard {
header = viewChild(CustomCardHeader);
headerText = computed(() => this.header()?.text); //能访问到子组件的 text
}
获取DOM元素
ts
@Component({
selector: 'custom-card',
template: '<input #myInput type="text" />',
})
export class CustomCard {
@ViewChild('myInput') inputRef!: ElementRef<HTMLInputElement>;
ngAfterViewInit() {
this.inputRef.nativeElement.focus(); // 操作原生 DOM
}
}
ContentChild
用于在子组件中获取通过 ng-content 投影进来的内容的引用
ts
@Component({
selector: 'app-button',
template: `<button (click)="onClick()">{{ label }}</button>`
})
export class ButtonComponent {
label = '点击我';
onClick() {
console.log('按钮被点击');
}
changeLabel(newLabel: string) {
this.label = newLabel;
}
}
app.ts
import { Component, ContentChild, AfterContentInit } from '@angular/core';
@Component({
selector: 'app-container',
template: `<div class="container"><ng-content></ng-content></div>`
})
export class ContainerComponent implements AfterContentInit {
@ContentChild(ButtonComponent) button!: ButtonComponent;
ngAfterContentInit() {
// 可以直接调用子组件的方法
this.button.changeLabel('新按钮文字');
}
}
使用.ts
<app-container>
<app-button></app-button> <!-- 被投影进去 -->
</app-container>
ViewChild和ContentChild的区别
简单来说,@ViewChild 查自己家(模板内部的元素),@ContentChild 查客人带来的东西(投影进来的内容)。
控制流
就是条件渲染
@if 和 @else if 和 @else
app.ts
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
template: `
@if (isServerRunning) {
<span>Yes, the server is running</span>
} @else if (isLogin) {
<span>No, the server is not running</span>
} @else {
<span>No, there is no anything true</span>
}
`,
})
export class App {
isServerRunning = false
isLogin = true
}
当然,在条件判断时,可以将条件存在一个变量里面复用
ts
@if (user.profile.setting.name; as isUserNameExits) {
{{ isUserNameExits }}
}
@for
就是循环
app.ts
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
template: `
@for(user of users; track user.id) {
<p>{{ user.name }}</p>
}
`,
})
export class App {
users = [
{id: 0, name: 'Sarah'},
{id: 1, name: 'Amy'},
{id: 2, name: 'Rachel'},
{id: 3, name: 'Jessica'},
{id: 4, name: 'Poornima'},
]
注意:必须使用 track,你可以使用 id 或其他唯一标识符。可以理解为是 React/Vue 遍历时指定的key。 指定了 track 后,Angular在重渲染的时候提高性能。
@for内置变量
其实就跟 for 循环里面的 item/index 差不多
| 变量 | 含义 |
|---|---|
$count |
迭代集合中的条目数 |
$index |
当前行的索引 |
$first |
当前行是否为第一行 |
$last |
当前行是否为最后一行 |
$even |
当前行索引是否为偶数 |
$odd |
当前行索引是否为奇数 |
app.ts
@for (item of items; track item.id; let idx = $index, e = $even, o = $odd) {
<p>Item #{{ idx }}: {{ item.name }}。是否是偶数:{{e}}。是否是奇数:{{o}}</p>
}
@empty
@empty为@for兜底。当@for没有数据时,将显示 @empty 控制块的内容:
app.ts
@for (item of items; track item.name) {
<li> {{ item.name }}</li>
} @empty {
<li aria-hidden="true"> There are no items. </li>
}
示例代码表示如果items有数据就展示name,没有就展示 there are no items
@switch
与js的swtich语法类似,只不过不需要指定 break/return
app.ts
@switch (userPermissions) {
@case ('admin') {
<app-admin-dashboard />
}
@case ('reviewer') {
<app-reviewer-dashboard />
}
@case ('editor') {
<app-editor-dashboard />
}
@default {
<app-viewer-dashboard />
}
}
如果前面的 case 表达式均不与 switch 值匹配,则显示 @default 控制块的内容。如果没有 @case 与表达式匹配,并且没有 @default 控制块,则不显示任何内容。
属性绑定
属性绑定需要通过 [] 语法,可以绑定到 DOM、组件、指令实例
原生属性绑定
app.ts
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
styleUrls: ['app.css'],
template: `
<div [contentEditable]="isEditable"></div>
`,
})
export class App {
isEditable = true;
}
这样这个div就是可编辑的了。
组件属性绑定
当元素是 Angular 组件时,你可以使用属性(Property)绑定,使用相同的方括号语法来设置组件的输入属性(Property)
app.ts
<my-listbox [value]="mySelection()" />
每次 mySelection 更改时,Angular 都会自动设置 MyListbox 实例的 value 属性
css类绑定
你也可以直接绑定到 class 属性(Property)。Angular 接受三种类型的值:
class 值的说明 |
TypeScript 类型 |
|---|---|
| 一个包含一个或多个 CSS 类(以空格分隔)的字符串 | string |
| CSS 类字符串的数组 | string[] |
| 一个对象,其中每个属性(Property)名称都是一个 CSS 类名,每个对应的值根据真值与否决定是否将该类应用于元素。 | Record<string, any> |
app.ts
@Component({
template: `
<ul [class]="listClasses"> ... </ul>
<section [class]="sectionClasses()"> ... </section>
<button [class]="buttonClasses()"> ... </button>
`,
...
})
export class UserProfile {
listClasses = 'full-width outlined';
sectionClasses = signal(['expandable', 'elevated']);
buttonClasses = ({
highlighted: true,
embiggened: false,
});
}
DOM渲染后结果如下
html
<ul class="full-width outlined"> ... </ul>
<section class="expandable elevated"> ... </section>
<button class="highlighted"> ... </button>
当然 angular也会智能的将所有类名组合渲染
app.ts
@Component({
template: `<ul class="list" [class]="listType()" [class.expanded]="isExpanded()"> ...`,
...
})
export class Listbox {
listType = signal('box');
isExpanded = signal(true);
}
DOM渲染后
html
<ul class="list box expanded">
css 样式属性绑定
你可以直接绑定 css 的某个样式属性。比如 display: none
app.ts
<section [style.display]="isExpanded() ? 'block' : 'none'">
当然也可以同时绑定多个 style 的值
style 值的描述 |
TypeScript 类型 |
|---|---|
一个包含零个或多个 CSS 声明的字符串,例如 "display: flex; margin: 8px"。 |
string |
| 一个对象,其中每个属性(Property)名称都是一个 CSS 属性(Property)名称,每个对应的值都是该 CSS 属性(Property)的值。 | Record<string, any> |
比如
ts
@Component({
template: `
<ul [style]="listStyles()"> ... </ul>
<section [style]="sectionStyles()"> ... </section>
`,
...
})
export class UserProfile {
listStyles = signal('display: flex; padding: 8px');
sectionStyles = signal({
border: '1px solid black',
'font-weight': 'bold',
});
}
DOM渲染结果:
ts
<ul style="display: flex; padding: 8px"> ... </ul>
<section style="border: 1px solid black; font-weight: bold"> ... </section>
事件处理
绑定事件
通过 () 绑定事件,回调格式是 callback()
app.ts
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
template: `
<section (mouseover)="showSecretMessage()" (mouseleave)="hideMessage()">
There's a secret message for you, hover to reveal 👀
{{ message }}
</section>
`,
})
export class App {
message = '';
showSecretMessage() {
this.message = 'Jolyne'
}
hideMessage() {
this.message = "hide"
}
}
注意:回调函数需要带上()
更多事件查看 元素上所有可用的事件。
读取事件参数
通过 $event 变量访问事件对象
app.ts
@Component({
template: `
<input type="text" (keyup)="updateField($event)" />
`,
...
})
export class AppComponent {
updateField(event: KeyboardEvent): void {
console.log(`The user pressed: ${event.key}`);
}
}
按键修饰符
如果想捕获特定的键盘事件,可以通过修饰符快速实现
app.ts
@Component({
template: `
<input type="text" (keyup.enter)="updateField($event)" />
`,
...
})
export class AppComponent{
updateField(event: KeyboardEvent): void {
console.log('The user pressed enter in the text field.');
}
}
上面的代码就是当按下了 enter 键触发 updateField 回调
当然也可以触发多个按键
app.ts
<input type="text" (keydown.code.alt.shiftleft)="updateField($event)" />
比如这个代码表示 shift + enter 同时按下时触发。
阻止默认事件
通过 preventDefault 阻止默认事件
app.ts
@Component({
template: `
<a href="#overlay" (click)="showOverlay($event)">
`,
})
export class AppComponent{
showOverlay(event: PointerEvent): void {
event.preventDefault();
console.log('Show overlay without updating the URL!');
}
}
可延迟视图
可以理解为懒加载,当满足某一特定条件时,才渲染。通过 @defer 实现。
@defer
默认情况下,当浏览器基于 requestIdleCallback 空闲后,再加载内容
app.ts
<!-- @defer (on idle) -->
@defer {
<large-cmp />
}
@placeholder
在 @defer 渲染前显示的内容,可以指定 minimum 控制 placeholder 显示多久。(单位ms或s)
app.ts
@defer {
<large-component />
} @placeholder (minimum 500ms) {
<p>Placeholder content</p>
}
上面的代码表示,@defer显示前,@placeholder显示至少 500ms
@loading
可以理解为就是 loading 态。当 loading 时,会替代 placeholder 显示。
app.ts
@defer {
<large-component />
} @loading {
<img alt="loading..." src="loading.gif" />
} @placeholder {
<p>Placeholder content</p>
}
同样,@loading 也可以指定 minimum(loading显示的最短时间)、after(@defer加载前需要等待的时间)
app.ts
@defer {
<large-component />
} @loading (after 100ms; minimum 1s) {
<img alt="loading..." src="loading.gif" />
}
上面的代码表示,loading至少显示1秒。在@defer显示前需要等待 100ms时间。
这样的控制可以防止内容快速闪烁
@error
@error是 @defer 加载失败后显示的内容。与@placeholder和@loading类似
app.ts
@defer {
<large-component />
} @error {
<p>Failed to load large component.</p>
}
指定 @defer 何时加载
通过 on 和 when 指定
on
| 触发器 | 描述 |
|---|---|
idle |
当浏览器空闲时触发。 |
viewport |
当指定内容进入视口时触发 |
interaction |
当用户与指定元素交互时触发 |
hover |
当鼠标悬停在指定区域上方时触发 |
immediate |
在非延迟内容完成渲染后立即触发 |
timer |
在特定持续时间后触发 |
viewport
当指定内容使用 Intersection Observer API 进入视口时,viewport 触发器会加载延迟内容。
app.ts
@defer (on viewport) {
<large-cmp />
} @placeholder {
<div>Large component placeholder</div>
}
interaction
当用户通过 click 或 keydown 事件与指定元素交互时,interaction 触发器会加载延迟加载内容。
app.ts
@defer (on interaction) {
<large-cmp />
} @placeholder {
<div>Large component placeholder</div>
}
hover
当鼠标通过 mouseover 和 focusin 事件悬停在触发区域上方时,hover 触发器会加载延迟加载内容
app.ts
@defer (on hover) {
<large-cmp />
} @placeholder {
<div>Large component placeholder</div>
}
immediate
immediate 触发器会立即 @defer。这意味着 @defer 会在所有其他非延迟加载内容完成渲染后立即加载。
app.ts
@defer (on immediate) {
<large-cmp />
} @placeholder {
<div>Large component placeholder</div>
}
timer
timer 触发器会在指定的时长后加载延迟加载内容。单位 ms 或 s
app.ts
@defer (on timer(500ms)) {
<large-cmp />
} @placeholder {
<div>Large component placeholder</div>
}
when
when 触发器接受自定义条件表达式,并在条件变为真值时加载延迟加载内容。
app.ts
@defer (when condition) {
<large-cmp />
} @placeholder {
<div>Large component placeholder</div>
}
prefetch
预获取与 @defer 相关的 JS
app.ts
@defer (on interaction; prefetch on idle) {
<large-cmp />
} @placeholder {
<div>Large component placeholder</div>
}
上面的代码表示:预取在浏览器空闲时开始,并且仅在用户与占位符交互后才渲染块的内容。
指令
属性指令
属性指令不会改变 DOM 结构,只是修改元素本身的属性、样式或行为。
比如自定义一个高亮文本的指令
highlight.directive.ts
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]' // 使用方括号表示属性选择器
})
export class HighlightDirective {
@Input() appHighlight = ''; // 接收传入的颜色值
@Input() defaultColor = 'yellow';
constructor(private el: ElementRef) {}
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight || this.defaultColor);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight('');
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
使用
html
<!-- 基础用法 -->
<p appHighlight>悬停我高亮</p>
<!-- 传入颜色参数 -->
<p [appHighlight]="'red'">悬停变红</p>
<p [appHighlight]="color" [defaultColor]="'blue'">动态颜色</p>
常用的内置属性指令
| 指令 | 作用 |
|---|---|
ngClass |
动态添加或移除 CSS 类 |
ngStyle |
动态设置内联样式 |
ngModel |
双向数据绑定(配合表单使用) |
比如
html
<!-- ngClass 示例 -->
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">状态</div>
<!-- ngStyle 示例 -->
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px'}">样式</div>
结构指令
结构指令改变 DOM 结构,通过添加、移除或替换 DOM 元素来控制渲染
常用的内置结构指令
| 指令 | 作用 |
|---|---|
*ngIf |
条件性渲染元素 |
*ngFor |
循环渲染列表 |
*ngSwitch / *ngSwitchCase / *ngSwitchDefault |
多条件分支渲染 |
html
<!-- *ngIf 示例 -->
<div *ngIf="isLoggedIn; else loginPrompt">欢迎回来</div>
<ng-template #loginPrompt>请先登录</ng-template>
<!-- *ngFor 示例 -->
<ul>
<li *ngFor="let item of items; let i = index">
{{ i + 1 }}. {{ item.name }}
</li>
</ul>
<!-- *ngSwitch 示例 -->
<div [ngSwitch]="status">
<p *ngSwitchCase="'loading'">加载中...</p>
<p *ngSwitchCase="'success'">加载成功</p>
<p *ngSwitchDefault>加载失败</p>
</div>
路由
路由感觉没什么写的,跟 react vue 大同小异。直接看文档即可Angular路由
表单
表单概览
主要是 ngModel 实现双向绑定(表单 --》 数据/数据 --》 表单)
app.ts
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
@Component({
selector: 'app-user',
template: `
<p>Username: {{ username }}</p>
<p>{{ username }}'s favorite framework: {{ favoriteFramework }}</p>
<label for="framework">
Favorite Framework:
<input id="framework" type="text" [(ngModel)]="favoriteFramework" />
</label>
`,
imports: [FormsModule],
})
export class User {
favoriteFramework = '';
username = 'youngTech';
showFramework() {
alert(this.favoriteFramework);
}
}
上面的代码就是 输入框通过 ngModel 双向绑定 favoriteFramework, 输入框输了值后,favoriteFramework更改,界面上也会重新渲染
响应式表单
响应式表单通过
-
- 导入
ReactiveForms模块
- 导入
-
- 使用
ormControl 创建 FormGroup 对象
- 使用
-
- 将
FormGroup 和 FormControl 链接到表单上
- 将
-
- 通过
ngSubmit提交
- 通过
app.ts
import {Component} from '@angular/core';
import {FormGroup, FormControl} from '@angular/forms';
import {ReactiveFormsModule} from '@angular/forms';
@Component({
selector: 'app-root',
template: `
<form [formGroup]="profileForm" (ngSubmit)="handleSubmit()">
<input type="text" formControlName="name" />
<input type="email" formControlName="email" />
<button type="submit">Submit</button>
</form>
<h2>Profile Form</h2>
<p>Name: {{ profileForm.value.name }}</p>
<p>Email: {{ profileForm.value.email }}</p>
`,
imports: [ReactiveFormsModule],
})
export class App {
profileForm = new FormGroup({
name: new FormControl(''),
email: new FormControl(''),
});
handleSubmit() {
alert(this.profileForm.value.name + ' | ' + this.profileForm.value.email);
}
}
验证表单
通过 Validators 验证
app.ts
import {Component} from '@angular/core';
import {FormGroup, FormControl} from '@angular/forms';
import {ReactiveFormsModule, Validators} from '@angular/forms';
@Component({
selector: 'app-root',
template: `
<form [formGroup]="profileForm">
<input type="text" formControlName="name" name="name" />
<input type="email" formControlName="email" name="email" />
<button type="submit" [disabled]="!profileForm.valid">Submit</button>
</form>
`,
imports: [ReactiveFormsModule],
})
export class App {
profileForm = new FormGroup({
name: new FormControl('', Validators.required),
email: new FormControl('', [Validators.required, Validators.email]),
});
}
注入服务
通过 @Injectable 创建可注入的服务,通过 inject 使用对应的服务
car.service.ts
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class CarService {
cars = ['Sunflower GT', 'Flexus Sport', 'Sprout Mach One'];
getCars(): string[] {
return this.cars;
}
getCar(id: number) {
return this.cars[id];
}
}
app.ts
import {Component, inject} from '@angular/core';
import {CarService} from './car.service';
@Component({
selector: 'app-root',
template: '<p> {{ carService.getCars() }} </p>',
})
export class App {
carService = inject(CarService);
}
注意:@Injectable可以通过 providedIn定义程序的哪个部门可以使用这个服务。一般都是 root,即全局可用。
管道
基本使用
管道其实就是转换数据用到的纯函数。Angular内置了许多管道
app.ts
import {Component} from '@angular/core';
import {LowerCasePipe} from '@angular/common';
@Component({
selector: 'app-root',
template: `
{{ username | lowercase }}
`,
imports: [LowerCasePipe],
})
export class App {
username = 'yOunGTECh';
}
首先引入 LowerCasePipe 管道,然后通过 {{ 传递给转换函数的值 | 管道的名称}} 使用
组合多个管道
多个管道组合时,angular从左到右运行管道。
app.ts
<p>The event will occur on {{ scheduledOn | date | uppercase }}.</p>
上面的代码表示:先本地化日期,再以大写展示
向管道传递参数
在管道名字后加一个 :,然后跟数值即可。
app.ts
<p>The event will occur at {{ scheduledOn | date:'hh:mm' }}.</p>
一个管道可能接受多个参数,也是用 : 隔开
app.ts
<p>The event will occur at {{ scheduledOn | date:'hh:mm':'UTC' }}.</p>
管道优先级
管道优先级低于 二元运算符,包括 +、-、*、/、%、&&、|| 和 ??
app.ts
{{ firstName + lastName | uppercase }}
上面代码表示先进行 名字的拼接,把拼接后的值再传给 uppercase 管道
管道优先级 高于 三元运算符
app.ts
{{ isAdmin ? 'Access granted' : 'Access denied' | uppercase }}
上面的代码相当于先执行 'Access denied' | uppercase ,再执行 三元运算符
如果要改变顺序,先执行三元再执行管道,要加小括号
app.ts
{{( isAdmin ? 'Access granted' : 'Access denied') | uppercase }}
加了括号后,先执行三元,再执行管道
自定义 Pipe
通过 @Pipe 装饰器 和 implements PipeTransform 实现
pipe.ts
@Pipe({
name: 'toFixed2point'
})
export class ToFixed2pointPipePipe implements PipeTransform {
transform(value: any, args?: any): any {
return Number(value).toFixed(2);
}
}
自定义的Pipe 内部需要定义 transform 方法,是个纯函数
注意:自定义 Pipe 的命名规则 大驼峰类目 + Pipe。比如 MyCustomerPipe
自定义管道检测数组或者对象
需要加上 pure: false,标记为非纯函数。
app.ts
@Pipe({
name: 'callback',
pure: false
})
export class CallbackPipe implements PipeTransform {
transform(items: any[], callback: (item: any) => boolean): any {
if (!items || !callback) {
return items;
}
return items.filter(item => callback(item));
}
}
注意:除非绝对必要,否则应避免创建非纯管道