Angular 快速入门:模板绑定与指令

上一篇文章我们搭好了项目、创建了组件,还渲染了一个静态的 Todo 列表。但现在的列表只能看,不能点、不能加、不能删——跟一张截图没什么区别。

这一篇我们来解决这个问题。你会学到 Angular 最核心的几个技能:

  • 数据绑定:让数据和页面双向"通气"
  • 事件绑定:点击、输入这些操作怎么触发逻辑
  • 指令*ngIf*ngForngClass 这些带 ng 前缀的魔法

学完之后,你的 Todo 应用从"能看"变成"能用"。

1. 插值 {{ }} — 把数据"塞"进模板

其实你已经用过了。在第一章我们写过:

<h2>{{ title }}</h2>
<li>{{ item.text }} - {{ item.done ? '已完成' : '未完成' }}</li>

{{ }} 就是"插值表达式",花括号里的内容会被 Angular 计算成字符串,然后替换到 HTML 里。

花括号里可以写任何有效的 TypeScript 表达式:

<p>{{ 1 + 1 }}</p>                    <!-- 2 -->
<p>{{ title.toUpperCase() }}</p>      <!-- 调用方法 -->
<p>{{ todos.length > 0 ? '有数据' : '空的' }}</p>

对比一下别的框架:Vue 也是 {{ }},React 是单花括号 {}(JSX 里)。用法几乎一样,区别只在 Angular 的模板里不能写太复杂的逻辑——Angular 的理念是"模板里只放表达式,复杂逻辑放到组件类里"。

2. 事件绑定 () — 点一下,干点啥

现在列表是静态的。我们希望点击一个待办项时,切换它的完成状态

Angular 里监听事件用圆括号,语法是 (事件名)="处理函数()"

先给 Todo 组件加一个切换方法。修改 todo.ts

export class TodoComponent {
  title = '我的待办清单';
  todos = [
    { id: 1, text: '学习 Angular 基础', done: false },
    { id: 2, text: '写一个 Todo 应用', done: true },
    { id: 3, text: '对比 React 和 Vue 的差异', done: false }
  ];

  // 新增:切换完成状态
  toggleDone(id: number) {
    const todo = this.todos.find(item => item.id === id);
    if (todo) {
      todo.done = !todo.done;
    }
  }
}

这里 titletodos 就是普通的 TypeScript 类属性。没有 useState,没有 ref(),没有 data()——就是直接赋值。

Angular 内置了一套变更检测机制,它自动追踪类属性的变化,一旦变了,模板里用到的地方就会自动更新。

对比一下其他框架:

  • React:必须用 useState 定义状态变量,调用 setState 才会触发更新。普通变量改了不会重新渲染。
  • Vue 3:Options API 用 data() 返回对象,Composition API 用 ref()reactive() 包裹,才能变成响应式。
  • Angular:类属性默认就是"可被检测"的。你直接 this.todos.push(...) 或者 this.title = '新标题' 就行,Angular 会自动发现变化。

听起来 Angular 更方便对吧?确实简单场景下是这样。但代价是 Angular 的变更检测机制更"重"——它会定期检查整个组件树,而不是像 React 那样精确到某个状态。应用大了之后,需要一些优化手段(比如 OnPush 策略),这些我们后面再聊。

然后在模板 todo.html 里给每个 <li> 加上点击事件:

<ul>
  <li *ngFor="let item of todos" (click)="toggleDone(item.id)">
    {{ item.text }} - {{ item.done ? '已完成' : '未完成' }}
  </li>
</ul>

保存,刷新浏览器。点击任意一个待办项,它的状态就会在"已完成"/"未完成"之间切换。

事件绑定的对比

  • ReactonClick={handleClick},直接在 JSX 的属性上写。
  • Vue@click="handleClick"@v-on: 的简写。
  • Angular(click)="toggleDone(item.id)",圆括号包裹事件名。调用时可以直接传参数,比如 item.id,这一点跟 Vue 更像。

三种框架都支持事件对象,Angular 里用 $event 获取:

<input (input)="onInput($event)">

3. 属性绑定 [] — 动态控制 HTML 属性

知道了怎么"点",但页面上看不出变化——"已完成"只是文字变了。我们想让它视觉上也不同,比如完成项加一条删除线。

这里需要把 item.done 的状态绑定到元素的样式上

Angular 里绑定属性用方括号,语法是 [HTML属性名]="表达式"

先给 <li> 动态设置一个 CSS 类:

<li *ngFor="let item of todos"
    (click)="toggleDone(item.id)"
    [class.done]="item.done">
  {{ item.text }} - {{ item.done ? '已完成' : '未完成' }}
</li>

[class.done]="item.done" 的意思是:item.done 为 true 时,给这个元素加上 class done

然后在 todo.css 里加上样式:

.done {
  text-decoration: line-through;
  color: #999;
}

现在点击待办项,完成的那条就会有一条删除线,颜色也变灰了。

属性绑定的对比

  • React:JSX 里 className={condition ? 'done' : ''},用 JS 表达式控制。
  • Vue:class="{ done: item.done }":v-bind: 的简写。
  • Angular[class.done]="item.done",方括号是属性绑定的标志。

Angular 这种写法优势是语义明确——看到 [class.xxx] 就知道"这个 class 是动态的"。但写法上比 Vue 的 :class 对象语法稍微啰嗦一点。

除了 [class.xxx],还可以直接绑定样式

<li [style.textDecoration]="item.done ? 'line-through' : 'none'">

或者绑定原生属性

<img [src]="imageUrl">
<a [href]="linkUrl">去百度</a>
<button [disabled]="isLoading">提交</button>

方括号里放的是原生 HTML 属性名,等号右边是组件类里的表达式

等等,这里有个很容易搞混的点:怎么区分等号右边是字符串还是变量?

关键就是看有没有方括号

<!-- 没有方括号 → 纯字符串,照原样显示 -->
<a href="linkUrl">去百度</a>
<!-- 浏览器地址栏会显示 "linkUrl" 这个字符串本身 -->

<!-- 有方括号 → 当做表达式求值 -->
<a [href]="linkUrl">去百度</a>
<!-- 会去组件类里找 linkUrl 变量,比如 'https://www.baidu.com' -->

你可以把方括号理解成一个"开关"——[] 就是表达式模式,没有就是字符串模式。这跟 Vue 的 :(等同于 v-bind:)是一个路数,React 则是因为 JSX 里 {} 无处不在,不存在这个区分。

4. 双向绑定 [(ngModel)] — 输入框和数据的"桥梁"

现在的列表数据是写死的。我们想加一个输入框,让用户自己添加待办。

这就需要"双向绑定"——用户在输入框里打字,组件里的数据跟着变;反过来,数据变了,输入框显示的内容也跟着变。

Angular 里双向绑定用 [(ngModel)],语法是 [(ngModel)]="属性名"

不过要先做一件事:ngModel 不在 CommonModule 里,它来自 @angular/formsFormsModule。需要先导入。

修改 todo.tsimports

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';   // ← 新增

@Component({
  selector: 'app-todo',
  standalone: true,
  imports: [CommonModule, FormsModule],   // ← 加上 FormsModule
  templateUrl: './todo.html',
  styleUrls: ['./todo.css']
})
export class TodoComponent {
  // ...
}

然后在组件类里加一个 newTodoText 属性和添加方法:

export class TodoComponent {
  title = '我的待办清单';
  newTodoText = '';          // ← 新增:绑定输入框的值

  todos = [
    { id: 1, text: '学习 Angular 基础', done: false },
    { id: 2, text: '写一个 Todo 应用', done: true },
    { id: 3, text: '对比 React 和 Vue 的差异', done: false }
  ];

  toggleDone(id: number) {
    const todo = this.todos.find(item => item.id === id);
    if (todo) {
      todo.done = !todo.done;
    }
  }

  // 新增:添加待办
  addTodo() {
    if (!this.newTodoText.trim()) return;
    this.todos.push({
      id: Date.now(),
      text: this.newTodoText,
      done: false
    });
    this.newTodoText = '';   // 添加后清空输入框
  }
}

最后在 todo.html 里加上输入框和添加按钮:

<h2>{{ title }}</h2>

<!-- 新增:输入区域 -->
<div>
  <input [(ngModel)]="newTodoText" placeholder="输入新的待办...">
  <button (click)="addTodo()">添加</button>
</div>

<ul>
  <li *ngFor="let item of todos"
      (click)="toggleDone(item.id)"
      [class.done]="item.done">
    {{ item.text }} - {{ item.done ? '已完成' : '未完成' }}
  </li>
</ul>

刷新浏览器,你可以在输入框里打字,点"添加",新的待办就会出现在列表里。

双向绑定的对比

  • React:没有双向绑定。你需要 value={text} + onChange={e => setText(e.target.value)},两步手动实现。这叫"受控组件"。
  • Vuev-model="text",语法糖。
  • Angular[(ngModel)]="text",语法糖。

其实 Angular 的 [(ngModel)] 就是在背后帮你做了 [value]="text" + (input)="text=$event" 这两件事。方括号里再套圆括号——Angular 社区管这个叫**“香蕉盒子里装了一根香蕉”**([()] 看起来像香蕉盒,里面是圆括号香蕉)。

这个说法很 Angular——喜欢给东西起名字。不深究,记住 [()] 是双向绑定的标志就行。

5. *ngIf — 条件显示

有时候列表是空的。空的列表展示起来很傻。用 *ngIf 来加一个"空状态"提示。

修改 todo.html

<ul *ngIf="todos.length > 0; else emptyState">
  <li *ngFor="let item of todos"
      (click)="toggleDone(item.id)"
      [class.done]="item.done">
    {{ item.text }} - {{ item.done ? '已完成' : '未完成' }}
  </li>
</ul>

<!-- 空状态模板 -->
<ng-template #emptyState>
  <p>还没有待办,输入一条开始吧 🎉</p>
</ng-template>

现在删除所有待办,或者一开始列表就是空的,就显示"还没有待办…"的提示。

*ngIf 可以搭配 else,后面跟一个模板引用(#emptyState 这种写法叫"模板引用变量")。条件为真时显示 <ul>,为假时显示 <ng-template> 里定义的内容。

条件渲染的对比

  • React{todos.length > 0 ? <ul>...</ul> : <p>空的</p>},JS 三元表达式。
  • Vue<ul v-if="todos.length">...<p v-else>空的</p>
  • Angular*ngIf="条件; else 模板名"else 是个额外功能。

三种框架都能实现条件渲染。React 最灵活(JS 想怎么写都行),Vue 和 Angular 的指令语法更声明式——看到 *ngIf 就知道"这块是有条件显示的"。

*ngIf 还有几个常用变体:

<!-- 只显示第一个 true 的条件 -->
<div *ngIf="condition1; else ifBlock">条件1成立</div>
<ng-template #ifBlock><div *ngIf="condition2">条件2成立</div></ng-template>

<!-- 直接隐藏(元素还在 DOM 中) -->
<div [hidden]="!condition">跟 *ngIf 很像,但这个只是添加 HTML hidden 属性</div>

[hidden]*ngIf 的区别:*ngIf从 DOM 里移除/添加元素[hidden] 只是添加了 HTML 原生的 hidden 属性。如果你的元素创建/销毁成本高(比如有大量的子组件),用 [hidden] 性能更好。

6. *ngFor 深入 — 不只是循环

*ngFor 我们已经用过了,但它还有一些有用的"内置变量":

<li *ngFor="let item of todos; let i = index; let first = first; let last = last">
  <span>{{ i + 1 }}.</span>  <!-- 显示序号 -->
  <span>{{ item.text }}</span>
  <span *ngIf="first">🔝 排在第一</span>
  <span *ngIf="last">✅ 最后一个</span>
</li>

这里 index(当前索引)、first(是否是第一个)、last(是否是最后一个)都是 *ngFor 自带的。

这点上 Angular 和 Vue 比较像:*ngFor="let item of items; let i = index" 对比 Vue 的 v-for="(item, index) in items"。React 没有指令,你需要自己 items.map((item, index) => ...)

插一句:为什么有些指令前面有 *,有些没有?

你可能注意到了,ngClassngStyle 前面没有星号,而 *ngIf*ngFor 有。这可不是随便写的。

* 的是结构型指令——它们会添加或移除 DOM 元素。*ngIf 条件为假时直接把元素从 DOM 里删了,*ngFor 根据数组长度创建或销毁元素。

不带 * 的是属性型指令——它们只改变已有元素的样式或行为,不增删 DOM。ngClass 只是给元素加/减 class,ngClass 只是改内联样式。

怎么记呢?看到 * 就想到"动结构"(加/删元素),没有 * 就是"改外观"(改属性/样式)。这个规律对所有 Angular 指令都适用。

7. ngClassngStyle — 更灵活的样式控制

之前我们用 [class.done]="item.done" 控制了一个单独的 class。如果样式条件复杂了,可以用 [ngClass] 一次控制多个 class(对象):

<li [ngClass]="{
  'done': item.done,
  'high-priority': item.priority === 'high',
  'selected': selectedId === item.id
}">

或者传一个 class (数组):

<li [ngClass]="['todo-item', item.done ? 'done' : '']">

同理,[ngStyle] 可以一次控制多个内联样式(对象):

<li [ngStyle]="{
  'text-decoration': item.done ? 'line-through' : 'none',
  'color': item.done ? '#999' : '#333',
  'font-weight': item.done ? 'normal' : 'bold'
}">

但注意:能用 class 解决的,就别用 style。class 更干净、性能更好、也更容易维护。

样式绑定的对比

  • ReactclassName + 条件,或者 style={{ color: ... }} 行内对象。
  • Vue:class="{ done: item.done }",对象语法和数组语法。
  • Angular[ngClass] / [ngClass],也是对象和数组都支持。

其实用法大同小异,换了框架也就是查一下语法怎么写的问题。

8.完整代码一览

把上面的代码拼在一起,todo.ts 现在的全貌:

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-todo',
  standalone: true,
  imports: [CommonModule, FormsModule],
  templateUrl: './todo.html',
  styleUrls: ['./todo.css']
})
export class TodoComponent {
  title = '我的待办清单';
  newTodoText = '';

  todos = [
    { id: 1, text: '学习 Angular 基础', done: false },
    { id: 2, text: '写一个 Todo 应用', done: true },
    { id: 3, text: '对比 React 和 Vue 的差异', done: false }
  ];

  toggleDone(id: number) {
    const todo = this.todos.find(item => item.id === id);
    if (todo) {
      todo.done = !todo.done;
    }
  }

  addTodo() {
    if (!this.newTodoText.trim()) return;
    this.todos.push({
      id: Date.now(),
      text: this.newTodoText,
      done: false
    });
    this.newTodoText = '';
  }
}

todo.html

<h2>{{ title }}</h2>

<div>
  <input [(ngModel)]="newTodoText" placeholder="输入新的待办...">
  <button (click)="addTodo()">添加</button>
</div>

<ul *ngIf="todos.length > 0; else emptyState">
  <li *ngFor="let item of todos"
      (click)="toggleDone(item.id)"
      [class.done]="item.done">
    {{ item.text }} - {{ item.done ? '已完成' : '未完成' }}
  </li>
</ul>

<ng-template #emptyState>
  <p>还没有待办,输入一条开始吧 🎉</p>
</ng-template>

todo.css

.done {
  text-decoration: line-through;
  color: #999;
}

9.本章总结

这一篇我们学会了 Angular 最核心的"三板斧":

  • 数据绑定:插值 {{ }}、属性绑定 []、事件绑定 ()、双向绑定 [()]
  • 常用指令*ngIf 条件渲染、*ngFor 列表渲染、ngClassngStyle 动态样式
  • CommonModule 和 FormsModule:standalone 模式下需要显式导入才能用这些功能

你的 Todo 应用已经从一个"静态截图"变成了一个"可交互的工具"。

接下来可以继续深入:组件之间怎么传数据(@Input / @Output),以及怎么把数据逻辑抽到 Service 里——这两块就是下一篇的内容了。

更多推荐