Skip to content

03 - 组件与模板

📖 学习目标

通过本章节学习,您将掌握:

  • 组件的深入理解
  • 模板语法详解
  • 组件生命周期
  • 组件间通信
  • 高级模板功能

🎯 核心概念

1. 组件架构

组件是Angular应用的核心,每个组件包含:

组件 (Component)
├── 类 (Class) - 业务逻辑
├── 模板 (Template) - 视图结构
├── 样式 (Styles) - 外观样式
└── 元数据 (Metadata) - 配置信息

2. 组件生命周期

Angular组件有完整的生命周期钩子:

typescript
ngOnChanges()    // 输入属性变化时
ngOnInit()       // 组件初始化时
ngDoCheck()      // 每次变更检测时
ngAfterContentInit()    // 内容投影初始化后
ngAfterContentChecked() // 内容投影检查后
ngAfterViewInit()       // 视图初始化后
ngAfterViewChecked()    // 视图检查后
ngOnDestroy()    // 组件销毁前

3. 模板语法

Angular模板支持丰富的语法:

  • 插值
  • 属性绑定[property]="expression"
  • 事件绑定(event)="handler()"
  • 双向绑定[(ngModel)]="property"
  • 结构指令*ngIf, *ngFor, *ngSwitch
  • 属性指令[ngClass], [ngStyle]

🏗️ 组件创建

1. 使用CLI创建组件

bash
# 创建组件
ng generate component user-profile
# 或简写
ng g c user-profile

# 创建组件并指定路径
ng g c components/user-profile

# 创建内联模板组件
ng g c user-profile --inline-template

2. 手动创建组件

typescript
// user-profile.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html',
  styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent {
  @Input() user: any;
  @Output() userUpdated = new EventEmitter<any>();
  
  onUpdate() {
    this.userUpdated.emit(this.user);
  }
}

🎨 模板语法详解

1. 插值绑定

html
<!-- 基本插值 -->
<h1>{{ title }}</h1>
<p>用户数量:{{ userCount }}</p>

<!-- 表达式插值 -->
<p>总价:{{ price * quantity }}</p>
<p>状态:{{ isActive ? '活跃' : '非活跃' }}</p>

<!-- 方法调用插值 -->
<p>格式化日期:{{ formatDate(createdAt) }}</p>

2. 属性绑定

html
<!-- 基本属性绑定 -->
<img [src]="imageUrl" [alt]="imageAlt">
<button [disabled]="isDisabled">按钮</button>

<!-- 类绑定 -->
<div [class.active]="isActive">内容</div>
<div [class]="cssClass">内容</div>
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">内容</div>

<!-- 样式绑定 -->
<div [style.color]="textColor">文本</div>
<div [style.font-size.px]="fontSize">文本</div>
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px'}">文本</div>

3. 事件绑定

html
<!-- 基本事件绑定 -->
<button (click)="onClick()">点击</button>
<input (input)="onInput($event)" (blur)="onBlur()">

<!-- 事件对象 -->
<button (click)="onClick($event)">点击</button>

<!-- 键盘事件 -->
<input (keyup.enter)="onEnter()" (keyup.escape)="onEscape()">

4. 双向绑定

html
<!-- 使用ngModel -->
<input [(ngModel)]="name" placeholder="请输入姓名">
<textarea [(ngModel)]="description"></textarea>

<!-- 自定义双向绑定 -->
<app-custom-input [(value)]="data"></app-custom-input>

🔄 结构指令

1. *ngIf

html
<!-- 条件渲染 -->
<div *ngIf="isLoggedIn">欢迎回来!</div>
<div *ngIf="!isLoggedIn">请先登录</div>

<!-- 使用else -->
<div *ngIf="user; else noUser">
  <h2>欢迎,{{ user.name }}!</h2>
</div>
<ng-template #noUser>
  <p>请先登录</p>
</ng-template>

2. *ngFor

html
<!-- 基本循环 -->
<ul>
  <li *ngFor="let item of items">{{ item }}</li>
</ul>

<!-- 带索引的循环 -->
<ul>
  <li *ngFor="let item of items; let i = index">
    {{ i + 1 }}. {{ item }}
  </li>
</ul>

<!-- 带trackBy的循环 -->
<ul>
  <li *ngFor="let user of users; trackBy: trackByUserId">
    {{ user.name }}
  </li>
</ul>

3. *ngSwitch

html
<div [ngSwitch]="status">
  <div *ngSwitchCase="'loading'">加载中...</div>
  <div *ngSwitchCase="'success'">成功!</div>
  <div *ngSwitchCase="'error'">错误!</div>
  <div *ngSwitchDefault>未知状态</div>
</div>

🔗 组件间通信

1. 父子组件通信

typescript
// 父组件
@Component({
  template: `
    <app-child 
      [data]="parentData" 
      (dataChange)="onDataChange($event)">
    </app-child>
  `
})
export class ParentComponent {
  parentData = 'Hello from parent';
  
  onDataChange(newData: string) {
    this.parentData = newData;
  }
}

// 子组件
@Component({
  selector: 'app-child',
  template: `
    <p>接收到的数据:{{ data }}</p>
    <button (click)="updateData()">更新数据</button>
  `
})
export class ChildComponent {
  @Input() data: string = '';
  @Output() dataChange = new EventEmitter<string>();
  
  updateData() {
    this.dataChange.emit('Updated from child');
  }
}

2. 使用服务通信

typescript
// 数据服务
@Injectable()
export class DataService {
  private dataSubject = new BehaviorSubject<string>('');
  data$ = this.dataSubject.asObservable();
  
  updateData(data: string) {
    this.dataSubject.next(data);
  }
}

// 组件A
@Component({...})
export class ComponentA {
  constructor(private dataService: DataService) {}
  
  sendData() {
    this.dataService.updateData('Data from A');
  }
}

// 组件B
@Component({...})
export class ComponentB {
  data: string = '';
  
  constructor(private dataService: DataService) {
    this.dataService.data$.subscribe(data => {
      this.data = data;
    });
  }
}

🎮 实践练习

练习1:创建用户卡片组件

创建一个可重用的用户卡片组件,包含:

  • 用户头像
  • 用户信息
  • 操作按钮
  • 自定义样式

练习2:创建动态列表组件

创建一个动态列表组件,支持:

  • 添加/删除项目
  • 编辑项目
  • 搜索过滤
  • 排序功能

📚 详细示例

完整的用户卡片组件

typescript
// user-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

export interface User {
  id: number;
  name: string;
  email: string;
  avatar: string;
  role: string;
  isActive: boolean;
}

@Component({
  selector: 'app-user-card',
  templateUrl: './user-card.component.html',
  styleUrls: ['./user-card.component.css']
})
export class UserCardComponent {
  @Input() user: User | null = null;
  @Input() showActions: boolean = true;
  @Output() edit = new EventEmitter<User>();
  @Output() delete = new EventEmitter<User>();
  @Output() toggle = new EventEmitter<User>();
  
  onEdit() {
    if (this.user) {
      this.edit.emit(this.user);
    }
  }
  
  onDelete() {
    if (this.user) {
      this.delete.emit(this.user);
    }
  }
  
  onToggle() {
    if (this.user) {
      this.toggle.emit(this.user);
    }
  }
}
html
<!-- user-card.component.html -->
<div class="user-card" [class.inactive]="!user?.isActive">
  <div class="avatar">
    <img [src]="user?.avatar" [alt]="user?.name" *ngIf="user?.avatar">
    <div class="avatar-placeholder" *ngIf="!user?.avatar">
      {{ user?.name?.charAt(0) }}
    </div>
  </div>
  
  <div class="user-info">
    <h3>{{ user?.name }}</h3>
    <p class="email">{{ user?.email }}</p>
    <span class="role" [class]="'role-' + user?.role?.toLowerCase()">
      {{ user?.role }}
    </span>
  </div>
  
  <div class="status" [class.active]="user?.isActive">
    {{ user?.isActive ? '活跃' : '非活跃' }}
  </div>
  
  <div class="actions" *ngIf="showActions">
    <button (click)="onEdit()" class="btn btn-edit">编辑</button>
    <button (click)="onToggle()" class="btn btn-toggle">
      {{ user?.isActive ? '禁用' : '启用' }}
    </button>
    <button (click)="onDelete()" class="btn btn-delete">删除</button>
  </div>
</div>
css
/* user-card.component.css */
.user-card {
  background: white;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: all 0.3s ease;
  border: 2px solid transparent;
}

.user-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}

.user-card.inactive {
  opacity: 0.6;
  border-color: #ffc107;
}

.avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  overflow: hidden;
  margin: 0 auto 15px;
}

.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.avatar-placeholder {
  width: 100%;
  height: 100%;
  background: #007bff;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  font-weight: bold;
}

.user-info {
  text-align: center;
  margin-bottom: 15px;
}

.user-info h3 {
  margin: 0 0 5px 0;
  color: #333;
  font-size: 18px;
}

.email {
  color: #666;
  font-size: 14px;
  margin: 0 0 8px 0;
}

.role {
  display: inline-block;
  padding: 4px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: bold;
  text-transform: uppercase;
}

.role-admin {
  background: #dc3545;
  color: white;
}

.role-user {
  background: #28a745;
  color: white;
}

.role-moderator {
  background: #ffc107;
  color: #333;
}

.status {
  text-align: center;
  margin-bottom: 15px;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  font-weight: bold;
}

.status.active {
  background: #d4edda;
  color: #155724;
}

.status:not(.active) {
  background: #f8d7da;
  color: #721c24;
}

.actions {
  display: flex;
  gap: 8px;
  justify-content: center;
}

.btn {
  padding: 6px 12px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  transition: all 0.2s ease;
}

.btn-edit {
  background: #007bff;
  color: white;
}

.btn-edit:hover {
  background: #0056b3;
}

.btn-toggle {
  background: #ffc107;
  color: #333;
}

.btn-toggle:hover {
  background: #e0a800;
}

.btn-delete {
  background: #dc3545;
  color: white;
}

.btn-delete:hover {
  background: #c82333;
}

🔧 高级功能

1. 内容投影

html
<!-- 父组件 -->
<app-card>
  <h2>卡片标题</h2>
  <p>卡片内容</p>
  <button>操作按钮</button>
</app-card>

<!-- 子组件模板 -->
<div class="card">
  <div class="card-header">
    <ng-content select="h2"></ng-content>
  </div>
  <div class="card-body">
    <ng-content select="p"></ng-content>
  </div>
  <div class="card-footer">
    <ng-content select="button"></ng-content>
  </div>
</div>

2. 视图封装

typescript
@Component({
  selector: 'app-component',
  templateUrl: './component.html',
  styleUrls: ['./component.css'],
  encapsulation: ViewEncapsulation.None // 全局样式
  // encapsulation: ViewEncapsulation.Emulated // 默认,模拟封装
  // encapsulation: ViewEncapsulation.ShadowDom // 原生Shadow DOM
})

✅ 学习检查

完成本章节后,请确认您能够:

  • [ ] 创建和使用组件
  • [ ] 理解组件生命周期
  • [ ] 使用各种模板语法
  • [ ] 实现组件间通信
  • [ ] 使用结构指令
  • [ ] 创建可重用组件

🚀 下一步

完成本章节学习后,请继续学习04-数据绑定与事件处理