Angular 18+ 高级教程 – Component 组件 の Template Binding Syntax
前言
这篇介绍一些基本的 Angular 模板语法。
参考
Render、Event Listening and DOM Manipulation
Angular 作为一个 MVVM 框架,有两个任务是一定要处理好的
1. First Render
2. Event Listening & DOM Manipulation
Render Engine
First Render 的工作是把 data 和 template 做 binding and render, 这世上有太多种模板语法了。
EJS, mustache, handlebars, JSX, Liquid, Razor
只能说各花入各眼吧。Angular 也整了一套自己独一无二的语法。不像 JSX 和 Razor 那样直接把 JS / C# 结合 HTML。
Angular 的思想是尽可能不去破坏 Native HTML 的规则,尽可能少的去加入新语法(虽说尽可能少, 但其实并不少...)
Event Listening & DOM Manipulation
除了 first render,用户交互也是避不开的,监听事件、修改 DOM 也是一个 MVVM 框架的任务。
常用的 Template Binding Syntax
我先介绍一些简单和常用的。先大概过一遍, 体会一下。下一章节我们再 focus 一些比较复杂的。
常见的 DOM Manipulation 有
const element = document.querySelector<HTMLElement>('.selector')!; // query element element.textContent = 'value'; // update text element.title = 'title'; // update property element.setAttribute('data-value', 'value'); // set attribute (note: attribute and property are not the same thing) element.style.padding = '16px'; // change style element.classList.add('new-class'); // add class const headline = document.createElement('h1'); // create element headline.textContent = 'Hello World'; element.appendChild(headline); // append a element element.innerHTML = `<h1>Hello World</h1>`; // write raw HTML element.addEventListener('click', () => console.log('clicked')); // listen and handle a event
Text Binding
<h1>{{ value }}</h1>
value 来自组件的 property
Property Binding
<h1 [title]="value">Hello World</h1> <h1 [title]="'Hello World'">Hello World</h1> <h1 [title]="(100 - 10 === 90) ? 'Hello World' : 'Hello Kitty'">Hello World</h1>
当左边是 [放括弧],右边的输入就变成了 JavaScript expression(不是所有 expression 都支持)
第一行 value 是 binding with component property
第二行是 hardcode 一个 string。注意它有 quote,因为是 JavaScript expression。
第三行是一个复杂的 expression(注: 虽然 Angular 允许写 expression,但请尽量不要把逻辑放到 HTML,这会让 HTML 看上去很乱)
Attribute Binding
<h1 [attr.data-value]="value">Hello World</h1>
和 property 写法一样,只是前面加了 prefix attr。
attribute 和 property 是不同的东西哦,如果你不知道,请看这篇 Attribute 和 Property 的区别.
Style Binding
<!-- single style + declare unit in property side --> <h1 [style.padding.px]="value">Hello World</h1> <!-- declare unit in value side --> <h1 [style.padding]="'16px'">Hello World</h1> <!-- multiple style property declare --> <h1 [style.padding.px]="16" [style.width]="'100px'">Hello World</h1> <!-- multiple style by passing object --> <h1 [style]="{ padding : '16px', width: '100px' }"></h1> <!-- Error: declare unit in property side won't work when using passing object mode --> <h1 [style]="{ 'padding.px' : 16, width: '100px' }"></h1> <!-- use value null or undefined to remove the specify style --> <h1 [style]="{ padding : null, width: '100px' }"></h1>
上面给的例子很多是 hardcode,这个是为了我方便写,全部都可以改成 link with component property 的。
style 写法蛮多的,但常用的是第一种而已。
[style]="object" 这个写法不好, 它不支持 .px suffix 而且修改 property 是不会 re-render 的, 只有给予整个全新的 object 才会 re-render(简单说, 就是需要 immutable object)
所以 best practice 是用 ngStyle 指令来做 multiple style(这个以后会教)。
Class Binding
<!-- if value true, then class will be added --> <h1 [class.my-class]="true">Hello World</h1> <!-- use object for multiple --> <h1 [class]="{ 'my-class' : true, 'second-class': true }">Hello World</h1> <!-- use array --> <!-- note: value should not be null or undefined, empty string is ok --> <h1 [class]="['my-class', 'second-class']">Hello World</h1> <!-- use string --> <h1 [class]="'my-class second-class'">Hello World</h1>
和 style binding 一样,[class]="object | array" 同样要求 immutable for re-render
for multiple class,best practice 是使用 ngClass 指令(这个以后会教)。
Event Listening
<button (click)="doSomething($event)">Click Me</button> <input (keydown)="doSomething($event)"> <!-- 还可以指定只监听某个 key 哦 --> <input (keydown.space)="doSomething($event)"> <input (keydown.enter)="doSomething($event)"> <input (keydown.shift.a)="doSomething($event)"> <!-- conditional execute --> <input (keydown)="(100 - 10 === 90) ? doSomething($event) : doSomethingElse($event)"> <input (keydown)="(100 - 10 === 90) && doSomething($event)">
监听事件用的 (元括弧),右边依然是 JavaScript expression
doSomething 是 component method,$event 是一个特殊关键字,它代表了这个事件监听 emit / dispatch 的 event。
for click 的话是 MouseEvent,keydown 则是 KeyboardEvent
export class AppComponent { doSomething(event: MouseEvent | KeyboardEvent) { console.log(event); } }
效果
preventDefault
handler 如果返回 false,Angular 会执行 preventDefault 方法。
<input (keydown)="doSomething()"> export class AppComponent { doSomething() { return false; } }
相关源码在 dom_renderer.ts 的 DefaultDomRenderer2.listen 方法
当然我们也可以直接 $event.preventDefault()。
Host Binding and Listening
<app-test (click)="handleClick()" [title]="'test only'"></app-test>
上面这样是在组件外的 html 上对组件做 binding 和 listening.
试想, 如果我们想把这些逻辑也封装到组件里. 我该如何在组件内对组件本身 binding 和 listening 呢?
你可能会想, 我们可以在组件内的 html wrap 一个 container element. 然后监听它. 但这样就太间接了,Angular 不喜欢这样 (Thinking in Angular Way)
component metadata host
在组件 metadata 里声明 binding 和 listening
@Component({ selector: 'app-test', standalone: true, imports: [CommonModule], templateUrl: './test.component.html', styleUrl: './test.component.scss', // 这里声明对 host 的 binding 和 listening host: { '(click)': 'handleClick($event)', '[title]': 'title', }, }) export class TestComponent { title = 'Hello World'; handleClick(event: MouseEvent) { console.log(event); } }
HostBinding can't use as @Input
host binding 是用来设置 HTML attribute 的,比如上面的 title。
我们不可以把它用于组件的 @Input
上面这样是不正确的。
@HostBinding 和 @HostListener
另一种方法是通过 decorator
export class TestComponent { @HostBinding('title') title = 'Hello World'; @HostListener('click', ['$event']) handleClick(event: MouseEvent) { console.log(event); } }
2 个方案选其中一个用就可以了哦.
component metadata vs decorator
metadata 比较 readable
decorator 是 Angular 正在远离的方式,所以推荐用 metadata 就好。
Two-way Binding 双向绑定 [()]
看例子
App 组件有一个 value
export class AppComponent { value = 'Hello World'; }
它被显示在 App,同时传入 Hello World 组件。
<p>outside value: {{ value }}</p> <app-hello-world [value]="value"></app-hello-world> <button (click)="value = 'new outside value'">change value from outside</button>
并且 App 有一个 button,点击后可以修改这个 App.value。
HelloWorld 组件 @Input 接收这个 value
export class HelloWorldComponent { @Input({ required: true }) value!: string; }
然后显示
<p>inside value : {{ value }}</p>
效果
当外部(App)更新值,内部(HelloWorld)也会更新到。
好,我们试试反过来,内部做更新,看看外部是否也会更新。
把 button 移到 HelloWorld 组件内
<p>inside value : {{ value }}</p> <button (click)="value = 'new inside value'">change value from inside</button>
效果
外部 App 的值没有被更新。那如果我们希望它被更新,那就叫双向绑定。
怎么实现呢?答案是用 @Output
export class HelloWorldComponent { @Input({ required: true }) value!: string; @Output() valueChange = new EventEmitter<string>(); }
定义一个 @Output valueChange EventEmitter
hello-world.component.html 点击后 emit
<button (click)="valueChange.emit('new value')">change value from inside</button>
app.component.html
监听 valueChange event 然后更新 value
<app-hello-world [value]="value" (valueChange)="value = $event"></app-hello-world>
效果
关键就在 HelloWorld 组件只负责监听修改 value 的事件,正真修改 value 是由 App 负责的。
这样双方读取的始终是同一个源。
Angular 做了一个语法糖给下面这句代码
[value]="value" (valueChange)="value = $event"
加糖后变成
[(value)]="value"
Signal-based Two-way Binding (a.k.a Signal Models)
Angular v16.0.0 发布了 Signals,请先确保你已经掌握 Signals 才继续看。
Angular v17.1.0 发布了 Signal-based Input,请先确保你已经掌握 Signal-based Input 才继续看。
Angular v17.2.0 发布了 Signal-based Two-way Binding,它便是用于双向绑定的。
同样上面的例子,我们把 HelloWorld 组件的 @Input 和 @Output 改成 Signal-based Two-way Binding。
export class HelloWorldComponent { // before // @Input({ required: true }) // value!: string; // @Output() // valueChange = new EventEmitter<string>(); // after value = model.required<string>(); // 这就是 Signal-based Two-way Binding 写法,和 Signal-based Input 如出一辙 }
HelloWorld Template
<!-- before --> <!-- <p>inside value : {{ value }}</p> <button (click)="valueChange.emit('new value')">change value from inside</button> --> <!-- after --> <p>inside value : {{ value() }}</p> <button (click)="value.set('new value')">change value from inside</button>
把 valueChange.emit 换成 value.set,把 {{ value }} 换成 {{ value() }}
App 组件
export class AppComponent { // before // value = 'Hello World'; // after value = signal('Hello World'); }
一样使用 signal
App Template
<!-- before --> <!-- <p>outside value: {{ value }}</p> --> <!-- <app-hello-world [value]="value" (valueChange)="value = $event"></app-hello-world> --> <!-- after --> <p>outside value: {{ value() }}</p> <app-hello-world [(value)]="value"></app-hello-world>
把 {{ value }} 换成 {{ value() }},把 [value]="value" (valueChange)="value = $event" 换成 [(value)]="value" (注:这里没有括弧,binding 的是 Signal 对象而不是它的值)。
新旧搭配使用
上面给的是 all in Signal 的写法。其实也不一定要 all in,它是支持一半一半的,我们继续看例子
App 组件
export class AppComponent { readonly person = { name: 'old value' } }
person 是一个普通对象,不是 Signal。
<app-hello-world [(value)]="person.name" /> <p>outside value: {{ person.name }}</p>
当内部更新 value 时,外部一样会更新。
其原理是
compile 以后,内部更新值时会执行 appInstance.person.name = newValue 这句代码,所以外部就更新了。
另外,想拆开 binding 也是可以的。
<app-hello-world [value]="person.name" />
虽然里面是 model,但外部只使用了它 input 的功能,所以当内部更新时,外部不会更新。
再来
<app-hello-world [value]="person.name" (valueChange)="doSomethingWithNewValue($event)" />
当内部修改 value 时,person.name 不会更新,同时 valueChange 会被调用,我们可以做对新值做任何处理。
再来
<app-hello-world [(value)]="valueSignal" /> <app-hello-world [value]="valueSignal()" (valueChange)="valueSignal.set($event)" />
这两个写法是等价的
model 不支持 transform
相关 Github Issue – Add "transform" to model()
严格来讲,model !== input + output
它们还是有一点点区别的,比如说 model 不支持 transform,而 input 是支持 transform 的。
我们看个简单例子
Checkbox 组件
export class CheckboxComponent { readonly checked = input(false, { transform: booleanAttribute }); readonly checkedChange = output<boolean>(); }
使用 input + output
Checkbox Template
<input id="my-checkbox-1" type="checkbox" #checkbox [checked]="checked()" (change)="checkedChange.emit(checkbox.checked)"> <label for="my-checkbox-1"><ng-content /></label>
App Template
<app-checkbox checked>check me</app-checkbox>
我们可以直接写一个 checked attribute 来表达 checked,因为内部会 transform string to boolean
效果
假如我们要 two-way bindding with Signal 可以这样写
App 组件
export class AppComponent { readonly checked = signal(true); }
App Template
<app-checkbox [(checked)]="checked">check me</app-checkbox> <pre>{{ checked() }}</pre>
效果
好,现在我们把它从 input + output 改成 model
Checkbox 组件
export class CheckboxComponent { // readonly checked = input(false, { transform: booleanAttribute }); // readonly checkedChange = output<boolean>(); readonly checked = model(false); // model 不支持 transform: booleanAttribute }
Checkbox Template
<input id="my-checkbox-1" type="checkbox" #checkbox [checked]="checked()" (change)="checked.set(checkbox.checked)">
App Template
<app-checkbox [(checked)]="checked">check me</app-checkbox>
到这里都没有问题,但是由于 model 不支持 transform,所以下面这样写就不行了
我们只能老老实实传 boolean 类型进去了。
<app-checkbox [checked]="true">check me</app-checkbox>
Angular 之所以不让 model 支持 transform 是因为它们怕乱,如果使用 model,那动机就是以同步为主,而不是 input + output 这种分开的形式。
目录
上一篇 Angular 18+ 高级教程 – Component 组件 の Angular Component vs Shadow DOM (CSS Isolation & slot)
下一篇 Angular 18+ 高级教程 – Component 组件 の Attribute Directives 属性型指令
想查看目录,请移步 Angular 18+ 高级教程 – 目录
喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻