前言

这篇介绍一些基本的 Angular 模板语法。

参考

Docs – Understanding binding

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, 这世上有太多种模板语法了。

EJSmustachehandlebarsJSXLiquidRazor

只能说各花入各眼吧。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

Angular 18+ 高级教程 – Component 组件 の Template Binding Syntax的更多相关文章

  1. Vue教程:组件Component详解(六)

    一.什么是组件? 组件 (Component) 是 Vue.js 最强大的功能之一.组件可以扩展 HTML 元素,封装可重用的代码.在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功 ...

  2. 微信小程序template模板与component组件的区别和使用

    前言: 除了component,微信小程序中还有另一种组件化你的方式template模板,这两者之间的区别是,template主要是展示,方法则需要在调用的页面中定义.而component组件则有自己 ...

  3. vue 基础-->进阶 教程(3):组件嵌套、组件之间的通信、路由机制

    前面的nodejs教程并没有停止更新,因为node项目需要用vue来实现界面部分,所以先插入一个vue教程,以免不会的同学不能很好的完成项目. 本教程,将从零开始,教给大家vue的基础.高级操作.组件 ...

  4. 一篇文章看懂angularjs component组件

     壹 ❀ 引 我在 angularjs 一篇文章看懂自定义指令directive 一文中详细介绍了directive基本用法与完整属性介绍.directive是个很神奇的存在,你可以不设置templa ...

  5. Angular CLI 使用教程指南参考

    Angular CLI 使用教程指南参考 Angular CLI 现在虽然可以正常使用但仍然处于测试阶段. Angular CLI 依赖 Node 4 和 NPM 3 或更高版本. 安装 要安装Ang ...

  6. angular里使用vue/vue组件怎么在angular里用

    欢迎加入前端交流群交流知识&&获取视频资料:749539640 如何在angularjs(1)中使用vue参考: https://medium.com/@graphicbeacon/h ...

  7. angularjs中directive指令与component组件有什么区别?

     壹 ❀ 引 我在前面花了两篇博客分别系统化介绍了angularjs中的directive指令与component组件,当然directive也能实现组件这点毋庸置疑.在了解完两者后,即便我们知道co ...

  8. vue高级进阶( 三 ) 组件高级用法及最佳实践

      vue高级进阶( 三 ) 组件高级用法及最佳实践 世界上有太多孤独的人害怕先踏出第一步. ---绿皮书 书接上回,上篇介绍了vue组件通信比较有代表性的几种方法,本篇主要讲述一下组件的高级用法和最 ...

  9. 分享25个新鲜出炉的 Photoshop 高级教程

    网络上众多优秀的 Photoshop 实例教程是提高 Photoshop 技能的最佳学习途径.今天,我向大家分享25个新鲜出炉的 Photoshop 高级教程,提高你的设计技巧,制作时尚的图片效果.这 ...

  10. avalon2学习教程13组件使用

    avalon2最引以为豪的东西是,终于有一套强大的类Web Component的组件系统.这个组件系统媲美于React的JSX,并且能更好地控制子组件的传参. avalon自诞生以来,就一直探索如何优 ...

随机推荐

  1. 深入理解 JavaScript 闭包:前端开发中的重要概念

    闭包是 JavaScript 中一个非常重要的概念,对于理解和编写高效.灵活的代码至关重要.尽管它看似复杂,但一旦掌握了闭包,你将能够更好地理解 JavaScript 的函数作用域和变量生命周期.本文 ...

  2. SQL查询语句汇总

    SQL查询语句汇总 students表 id class_id name gender score 1 1 小明 M 90 2 1 小红 F 95 class表 id name 1 一班 2 二班 3 ...

  3. RedisTemplate使用rightPushAll时的注意事项

    问题:第一次使用时rightPushAll,我以为这个方法就是直接把我们集合中的数据全部添加到redis的list里面,但是如果直接使用ArrayList类型添加,发现事情并不是我们想的这样,他并没有 ...

  4. app接口测试

    app接口测试 一,app请求服务器端接口和web页面请求服务器端接口有什么区别? 1,大多数项目如果有app的话,而且web端和app端的页面显示结构已经功能都相似,调用的后台接口也是一样的. 2, ...

  5. 【Java】MuliThread 多线程

    程序Program 是完成特定人,用某种语言编写的一组指令集合,即一段静态代码,静态对象 进程Process 是程序的一次执行过程,可以是一个正在执行的程序 - 程序是静态的,进程是动态的 - 进程是 ...

  6. AI领域的国产显卡如何在现有技术下吸引用户 —— 廉价增加显存 —— 大显存

    先给出一个不大准确的但相差不差的背景介绍: 同样性能级别的显卡,NVIDA的24G的要3W,32G的要5W,48G的要7W, 80G的要10W. 国产同同性能的显卡32G的要10W,48G的要15W, ...

  7. 台式机,华硕主板z390ws,cpu为i7-9700k 安装Ubuntu18.04系统 使用独立显卡工作 (但是显示器HDMI线缆插在主板的HDMI插槽)开机进入系统运行几分钟后自动重启,此时主板显示错误码为AMI错误

    如题: 手上有这样一台新的工作站,配置为华硕主板z390ws,cpu为i7-9700k ,独立显卡为技嘉2060super, 安装Ubuntu18.04系统 . 在主板bios中进行设置(设置使用 P ...

  8. 破局SAP实施难题、降低开发难度,定制化需求怎样快速上线?

    前言 SAP 是全球领先的业务流程管理软件供应商之一,其提供广泛的模块化解决方案和套件,所开发的软件解决方案面向各种规模的企业,帮助客户规划和设计业务流程.分析并高效设计整个价值链,以更好的了解和响应 ...

  9. Leetcode: 1484. Groups Sold Products By The Date

    题目要求如下: 输入的数据为 要求按照日期查询出每日销售数量及相应产品的名称,并按照字符顺序进行排序. 下面是实现的代码: import pandas as pd def categorize_pro ...

  10. SMU Autumn 2023 Round 2(Div.1+2)

    SMU Autumn 2023 Round 2(Div.1+2) C. Chaotic Construction 把环展开的话就是\(1 \sim 2n\),若\(D\)的位置放上路障的话,在这个展开 ...