Angular组件数据绑定

本章目标:深入掌握Angular四种数据绑定方式,理解绑定原理,熟练使用@Input和@Output实现组件通信,掌握最佳实践。

四种数据绑定方式

{{ }}
插值绑定

组件 → 视图

[ ]
属性绑定

组件 → 视图

( )
事件绑定

视图 → 组件

[( )]
双向绑定

视图 ↔ 组件

数据绑定概述

数据绑定是Angular的核心机制,它自动同步组件类(数据)和组件模板(UI)之间的数据。

组件类 (TypeScript)
组件模板 (HTML)
绑定类型 语法 方向 用途 示例
插值绑定 {{ expression }} 组件 → 视图 显示组件数据到模板 {{ userName }}
属性绑定 [property]="expression" 组件 → 视图 设置元素/组件属性 [src]="imageUrl"
事件绑定 (event)="handler()" 视图 → 组件 响应用户交互事件 (click)="onClick()"
双向绑定 [(ngModel)]="property" 视图 ↔ 组件 表单输入的双向同步 [(ngModel)]="userName"

1. 插值绑定 (Interpolation)

使用双花括号{{ }}将组件中的数据插入到模板中,是最简单的单向数据绑定。

组件类
export class InterpolationComponent {
  // 基本类型
  title = 'Angular数据绑定';
  count = 42;
  isActive = true;
  price = 99.99;

  // 对象
  user = {
    name: '张三',
    age: 28,
    email: 'zhangsan@example.com'
  };

  // 数组
  skills = ['TypeScript', 'Angular', 'RxJS'];

  // 方法
  getGreeting(): string {
    return `你好,${this.user.name}!`;
  }

  // 计算属性
  get discountPrice(): number {
    return this.price * 0.8;
  }

  // 当前时间
  currentDate = new Date();
}
模板代码
<h1>{{ title }}</h1>

<!-- 显示基本类型 -->
<p>计数: {{ count }}</p>
<p>状态: {{ isActive ? '活跃' : '不活跃' }}</p>
<p>原价: {{ price }} 元</p>
<p>折后价: {{ discountPrice }} 元</p>

<!-- 显示对象属性 -->
<p>姓名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<p>{{ getGreeting() }}</p>

<!-- 使用管道格式化 -->
<p>当前时间: {{ currentDate | date:'yyyy-MM-dd HH:mm:ss' }}</p>

<!-- 数组长度 -->
<p>技能数量: {{ skills.length }}</p>

<!-- 复杂表达式(应避免) -->
<p>总价: {{ count * price }} 元</p>

<!-- 安全导航操作符 -->
<p>用户城市: {{ user?.address?.city || '未设置' }}</p>
实时演示

2. 属性绑定 (Property Binding)

使用方括号[ ]将组件属性绑定到HTML元素属性、组件输入属性或指令属性。

属性绑定流程
1
定义组件属性

在组件类中创建要绑定的数据

2
创建绑定表达式

在模板中使用[property]="expression"语法

3
Angular处理绑定

Angular计算表达式并将结果设置到目标属性

4
更新DOM

当组件属性变化时,Angular自动更新DOM

绑定到HTML属性
<!-- 绑定到标准HTML属性 -->
<img [src]="imageUrl"
     [alt]="imageAlt"
     [width]="imageWidth">

<!-- 绑定到class属性 -->
<div [class.active]="isActive"
     [class.disabled]="isDisabled">
  内容区域
</div>

<!-- 绑定多个class -->
<div [class]="getClassList()"></div>

<!-- 绑定到style属性 -->
<button [style.color]="isValid ? 'green' : 'red'"
        [style.font-size.px]="fontSize">
  提交
</button>

<!-- 绑定多个样式 -->
<div [style]="getStyles()"></div>

<!-- 绑定到disabled属性 -->
<button [disabled]="isLoading">
  {{ isLoading ? '处理中...' : '提交' }}
</button>

<!-- 绑定到自定义数据属性 -->
<div [attr.data-user-id]="userId"></div>
绑定到组件属性
// 父组件
@Component({
  template: `
    <app-user-card
      [user]="currentUser"
      [showAvatar]="true"
      [maxWidth]="300"
      [theme]="'dark'">
    </app-user-card>
  `
})
export class ParentComponent {
  currentUser = {
    name: '李四',
    role: '管理员'
  };
}

// 子组件
@Component({...})
export class UserCardComponent {
  @Input() user: any;
  @Input() showAvatar: boolean = false;
  @Input() maxWidth?: number;
  @Input() theme: 'light' | 'dark' = 'light';

  // 监听输入变化
  ngOnChanges(changes: SimpleChanges): void {
    if (changes['user']) {
      console.log('用户数据已更新', changes['user'].currentValue);
    }
  }
}
属性绑定 vs 字符串插值:
  • <img src="{{ imageUrl }}"> - 插值方式
  • <img [src]="imageUrl"> - 属性绑定方式
  • 两者效果相同,但属性绑定可以绑定非字符串值,更明确清晰

3. 事件绑定 (Event Binding)

使用圆括号( )监听DOM事件并执行组件中的方法,实现视图到组件的数据流。

事件绑定交互演示
常见DOM事件
<!-- 点击事件 -->
<button (click)="onButtonClick($event)">
  点击我
</button>

<!-- 输入事件 -->
<input (input)="onInputChange($event.target.value)"
       placeholder="输入内容">

<!-- 键盘事件 -->
<input (keydown)="onKeyDown($event)"
       (keyup)="onKeyUp($event)"
       (keyup.enter)="onEnter()"
       placeholder="按Enter键提交">

<!-- 鼠标事件 -->
<div (mouseenter)="onMouseEnter()"
     (mouseleave)="onMouseLeave()"
     (mousemove)="onMouseMove($event)">
  鼠标悬停区域
</div>

<!-- 表单事件 -->
<form (submit)="onFormSubmit($event)"
      (reset)="onFormReset()">
  <button type="submit">提交</button>
</form>

<!-- 聚焦/失焦事件 -->
<input (focus)="onFocus()"
       (blur)="onBlur()">

<!-- 滚动事件 -->
<div (scroll)="onScroll($event)"
     style="height: 200px; overflow: auto;">
  <p>可滚动内容...</p>
</div>
组件事件处理
export class EventBindingComponent {
  clickCount = 0;
  inputValue = '';
  mousePosition = { x: 0, y: 0 };
  keyLog: string[] = [];

  // 处理点击事件
  onButtonClick(event: MouseEvent): void {
    this.clickCount++;
    console.log('按钮被点击', event);
    event.stopPropagation(); // 阻止事件冒泡
  }

  // 处理输入变化
  onInputChange(value: string): void {
    this.inputValue = value;
    console.log('输入内容:', value);
  }

  // 处理键盘事件
  onKeyDown(event: KeyboardEvent): void {
    console.log('按键按下:', event.key);
    this.keyLog.push(`按下: ${event.key}`);
  }

  // 处理鼠标移动
  onMouseMove(event: MouseEvent): void {
    this.mousePosition = {
      x: event.clientX,
      y: event.clientY
    };
  }

  // 带参数的事件处理
  onItemClick(itemId: number): void {
    console.log('项目被点击:', itemId);
  }

  // 防止默认行为
  onLinkClick(event: Event): void {
    event.preventDefault();
    console.log('链接点击被阻止');
  }

  // 使用 $event 对象
  onCustomEvent(event: CustomEvent): void {
    console.log('自定义事件:', event.detail);
  }
}

4. 双向绑定 (Two-Way Binding)

使用[(ngModel)]("香蕉盒"语法)实现视图和组件之间的双向数据同步。

双向绑定原理

[(ngModel)]="property" 是以下两种绑定的语法糖:

[ngModel]="property" (ngModelChange)="property = $event"
使用ngModel
// 1. 导入FormsModule
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [FormsModule],
  // ...
})

// 2. 组件中使用双向绑定
export class TwoWayBindingComponent {
  // 表单数据
  user = {
    name: '',
    email: '',
    age: 25,
    gender: 'male',
    interests: [],
    subscribe: true,
    bio: ''
  };

  // 选项数据
  genderOptions = [
    { value: 'male', label: '男' },
    { value: 'female', label: '女' }
  ];

  interestOptions = [
    { value: 'coding', label: '编程' },
    { value: 'music', label: '音乐' },
    { value: 'sports', label: '运动' }
  ];

  // 数据变化时触发
  onDataChange(): void {
    console.log('数据已更新:', this.user);
  }
}
表单模板
<!-- 文本输入框 -->
<input [(ngModel)]="user.name"
       placeholder="请输入姓名">

<!-- 邮箱输入框 -->
<input type="email"
       [(ngModel)]="user.email"
       placeholder="请输入邮箱">

<!-- 数字输入框 -->
<input type="number"
       [(ngModel)]="user.age"
       min="0" max="100">

<!-- 下拉选择框 -->
<select [(ngModel)]="user.gender">
  <option *ngFor="let option of genderOptions"
          [value]="option.value">
    {{ option.label }}
  </option>
</select>

<!-- 多选框组 -->
<div *ngFor="let option of interestOptions">
  <input type="checkbox"
         [value]="option.value"
         [checked]="user.interests.includes(option.value)"
         (change)="toggleInterest(option.value)">
  {{ option.label }}
</div>

<!-- 单选框 -->
<input type="radio"
       [(ngModel)]="user.gender"
       value="male"> 男
<input type="radio"
       [(ngModel)]="user.gender"
       value="female"> 女

<!-- 复选框 -->
<input type="checkbox"
       [(ngModel)]="user.subscribe"> 订阅通知

<!-- 文本域 -->
<textarea [(ngModel)]="user.bio"
          rows="4"></textarea>
双向绑定实时演示
18 28 60
当前数据
用户名: Angular开发者
年龄: 28
技能等级: 中级
更新次数: 0

组件间数据通信

通过@Input()@Output()实现父子组件之间的数据通信。

父组件向子组件传值 (@Input)
// 子组件:接收数据
@Component({
  selector: 'app-user-info',
  template: `
    <div class="user-card">
      <h3>{{ user.name }}</h3>
      <p>年龄: {{ user.age }}</p>
      <p>邮箱: {{ user.email }}</p>
      <p *ngIf="showDetails">详情: {{ user.details }}</p>
    </div>
  `
})
export class UserInfoComponent {
  @Input() user: any;           // 必填输入
  @Input() showDetails = false; // 可选输入,有默认值

  // 监听输入变化
  ngOnChanges(changes: SimpleChanges): void {
    console.log('输入属性变化:', changes);
  }
}

// 父组件:传递数据
@Component({
  template: `
    <app-user-info
      [user]="currentUser"
      [showDetails]="true">
    </app-user-info>
  `
})
export class ParentComponent {
  currentUser = {
    name: '王五',
    age: 35,
    email: 'wangwu@example.com',
    details: '高级工程师'
  };
}
子组件向父组件传值 (@Output)
// 子组件:发送事件
@Component({
  selector: 'app-product-item',
  template: `
    <div class="product">
      <h4>{{ product.name }}</h4>
      <p>价格: {{ product.price }}元</p>
      <button (click)="addToCart()">
        加入购物车
      </button>
      <button (click)="removeFromCart()">
        移除
      </button>
    </div>
  `
})
export class ProductItemComponent {
  @Input() product: any;

  @Output() added = new EventEmitter<any>();
  @Output() removed = new EventEmitter<any>();
  @Output() quantityChange = new EventEmitter<number>();

  addToCart(): void {
    this.added.emit(this.product);
  }

  removeFromCart(): void {
    this.removed.emit(this.product);
  }

  updateQuantity(quantity: number): void {
    this.quantityChange.emit(quantity);
  }
}

// 父组件:监听事件
@Component({
  template: `
    <app-product-item
      [product]="selectedProduct"
      (added)="onProductAdded($event)"
      (removed)="onProductRemoved($event)">
    </app-product-item>

    <p>购物车数量: {{ cartCount }}</p>
  `
})
export class ParentComponent {
  selectedProduct = { name: 'Angular教程', price: 99 };
  cartCount = 0;

  onProductAdded(product: any): void {
    console.log('产品已添加:', product);
    this.cartCount++;
  }

  onProductRemoved(product: any): void {
    console.log('产品已移除:', product);
    this.cartCount--;
  }
}

数据绑定最佳实践

推荐做法
  • 对非字符串属性使用属性绑定而不是插值
  • 在模板中保持表达式简单,复杂逻辑移到组件方法中
  • 使用安全导航操作符?.避免空值错误
  • @Input()属性提供合适的默认值
  • 在事件处理中传递$event获取事件详情
  • 使用ngOnChanges监听输入属性变化
  • 为双向绑定提供清晰的初始值
应避免的做法
  • 避免在模板表达式中执行复杂计算或有副作用的操作
  • 不要忘记为双向绑定导入FormsModule
  • 不要在事件绑定中直接大量修改组件状态
  • 避免过多的嵌套属性绑定,考虑使用中间变量
  • 不要忘记在ngOnDestroy中清理订阅
  • 避免在Angular表达式使用new、自增/自减
  • 不要混合使用多种绑定方式处理同一数据

本章总结

通过本章学习,你应该掌握了:

  • 插值绑定:使用{{ }}显示组件数据到视图
  • 属性绑定:使用[ ]将组件属性绑定到元素属性
  • 事件绑定:使用( )监听DOM事件并执行组件方法
  • 双向绑定:使用[( )]实现表单输入的双向同步
  • 组件通信:使用@Input()@Output()进行父子组件通信
  • 最佳实践:编写高效、可维护的数据绑定代码