前言

这篇介绍一些基本的 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. Python爬虫(1-4)-基本概念、六个读取方法、下载(源代码、图片、视频 )、user-agent反爬

    Python爬虫 一.爬虫相关概念介绍 1.什么是互联网爬虫 如果我们把互联网比作一张大的蜘蛛网,那一台计算机上的数据便是蜘蛛网上的一个猎物,而爬虫程序就是一只小蜘蛛,沿着蜘蛛网抓取自己想要的数据 解 ...

  2. 使用 useLazyFetch 进行异步数据获取

    title: 使用 useLazyFetch 进行异步数据获取 date: 2024/7/20 updated: 2024/7/20 author: cmdragon excerpt: 摘要:&quo ...

  3. Kubernetes 存储概念之Volumes介绍

    Volumes 默认情况下容器中的磁盘文件是非持久化的,对于运行在容器中的应用来说面临两个问题,第一:当容器挂掉,K8S重启它时,文件将会丢失:第二:当Pod中同时运行多个容器,容器之间需要共享文件时 ...

  4. Django model层之执行原始SQL查询

    Django model层之执行原始SQL查询 by:授客 QQ:1033553122 测试环境 Win7 Django 1.11   执行原始SQL查询 Manager.raw(raw_query, ...

  5. Jmeter函数助手23-intSum

    intSum函数可用于计算两个或多个整数值的和. 要添加的第一个整数:必填,填入整数,不能为小数 要添加的第二个整数:必填,填入整数,不能为小数 存储结果的变量名(可选) 1. intSum函数求多个 ...

  6. 7月22号python 每日一题

    7月22号python 每日一题 LCR 121. 寻找目标值 - 二维数组 难度:中等 m*n 的二维数组 plants 记录了园林景观的植物排布情况,具有以下特性: 每行中,每棵植物的右侧相邻植物 ...

  7. 【Java】JNDI实现

    前提情要: 培训的时候都没讲过,也是书上看到过这么个东西,进公司干外包的第二个项目就用了这个鬼东西 自学也不是没尝试过实现,结果各种失败,还找不到问题所在,索性不写了 JNDI实现参考: 目前参考这篇 ...

  8. 【Docker】09 部署挂载本地目录的Redis

    1.拉取Redis镜像: docker pull redis:6.0.6 2.执行挂载命令: docker run -d \ --name=redis \ --restart=always \ --p ...

  9. 阿里2021年春季实习笔试题(最后一道大题)(2020 China Collegiate Programming Contest, Weihai Site) (C. Rencontre codeforces.com/gym/102798)

    实验室的慕师弟phd马上要毕业了,正准备先找个实习,投了阿里2021年春季实习的招聘,遇到最后一道编程大题没有思路事后找到了该题的最原始出处,即 2020 China Collegiate Progr ...

  10. 【转载】 IEEE Signal Processing Letters(SPL)投稿经验分享

    版权声明:本文为CSDN博主「yellow7-」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明.原文链接:https://blog.csdn.net/weixin_4 ...