Skip to content

05 - 指令与管道

📖 学习目标

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

  • Angular内置指令的使用
  • 结构指令详解
  • 属性指令详解
  • 自定义指令创建
  • 内置管道的使用
  • 自定义管道创建

🎯 核心概念

1. 指令类型

Angular指令分为三种类型:

指令 (Directives)
├── 组件指令 (Component Directives)
├── 结构指令 (Structural Directives) - 改变DOM结构
└── 属性指令 (Attribute Directives) - 改变元素属性

2. 管道类型

管道 (Pipes)
├── 内置管道 (Built-in Pipes)
└── 自定义管道 (Custom Pipes)

🔧 结构指令

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>

<!-- 复杂条件 -->
<div *ngIf="user && user.role === 'admin'">
  管理员面板
</div>

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>

<!-- 获取循环状态 -->
<div *ngFor="let item of items; let first = first; let last = last; let even = even; let odd = odd">
  <span [class.first]="first" [class.last]="last" [class.even]="even" [class.odd]="odd">
    {{ item }}
  </span>
</div>

3. *ngSwitch

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

<!-- 数字switch -->
<div [ngSwitch]="dayOfWeek">
  <div *ngSwitchCase="1">星期一</div>
  <div *ngSwitchCase="2">星期二</div>
  <div *ngSwitchCase="3">星期三</div>
  <div *ngSwitchCase="4">星期四</div>
  <div *ngSwitchCase="5">星期五</div>
  <div *ngSwitchDefault>周末</div>
</div>

🎨 属性指令

1. [ngClass]

html
<!-- 条件类 -->
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">内容</div>

<!-- 动态类 -->
<div [ngClass]="cssClass">内容</div>

<!-- 复杂条件 -->
<div [ngClass]="{
  'btn': true,
  'btn-primary': type === 'primary',
  'btn-secondary': type === 'secondary',
  'btn-large': size === 'large',
  'btn-disabled': isDisabled
}">按钮</div>

2. [ngStyle]

html
<!-- 动态样式 -->
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px'}">文本</div>

<!-- 条件样式 -->
<div [ngStyle]="{
  'background-color': isActive ? 'green' : 'red',
  'color': 'white',
  'padding': '10px'
}">内容</div>

<!-- 复杂样式对象 -->
<div [ngStyle]="dynamicStyles">内容</div>

3. [ngModel]

html
<!-- 双向绑定 -->
<input [(ngModel)]="name" placeholder="请输入姓名">
<textarea [(ngModel)]="description"></textarea>

<!-- 带验证 -->
<input 
  [(ngModel)]="email" 
  type="email" 
  required 
  email
  #emailInput="ngModel">

<!-- 选择框 -->
<select [(ngModel)]="selectedOption">
  <option value="">请选择</option>
  <option value="option1">选项1</option>
  <option value="option2">选项2</option>
</select>

🔨 自定义指令

1. 属性指令

typescript
// highlight.directive.ts
import { Directive, ElementRef, Input, OnInit, OnDestroy, Renderer2 } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective implements OnInit, OnDestroy {
  @Input() appHighlight: string = 'yellow';
  @Input() highlightColor: string = 'yellow';
  @Input() defaultColor: string = '';

  private originalColor: string = '';

  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}

  ngOnInit() {
    this.originalColor = this.el.nativeElement.style.backgroundColor;
    this.setHighlightColor(this.highlightColor || this.appHighlight);
  }

  ngOnDestroy() {
    this.setHighlightColor(this.originalColor);
  }

  private setHighlightColor(color: string) {
    this.renderer.setStyle(this.el.nativeElement, 'background-color', color);
  }
}
html
<!-- 使用自定义指令 -->
<div appHighlight="lightblue">高亮内容</div>
<div appHighlight highlightColor="lightgreen">另一种高亮</div>

2. 结构指令

typescript
// unless.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]'
})
export class UnlessDirective {
  private hasView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}

  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}
html
<!-- 使用自定义结构指令 -->
<div *appUnless="isHidden">这个内容在isHidden为false时显示</div>

🔧 内置管道

1. 常用内置管道

html
<!-- 日期管道 -->
<p>当前时间:{{ currentDate | date:'yyyy-MM-dd HH:mm:ss' }}</p>
<p>短日期:{{ currentDate | date:'short' }}</p>
<p>长日期:{{ currentDate | date:'full' }}</p>

<!-- 货币管道 -->
<p>价格:{{ price | currency:'CNY':'symbol':'1.2-2' }}</p>
<p>美元:{{ price | currency:'USD' }}</p>

<!-- 数字管道 -->
<p>小数:{{ number | number:'1.2-2' }}</p>
<p>百分比:{{ percentage | percent:'1.2-2' }}</p>

<!-- 文本管道 -->
<p>大写:{{ text | uppercase }}</p>
<p>小写:{{ text | lowercase }}</p>
<p>标题:{{ text | titlecase }}</p>

<!-- JSON管道 -->
<pre>{{ object | json }}</pre>

<!-- 切片管道 -->
<p>前5个字符:{{ text | slice:0:5 }}</p>
<p>数组切片:{{ items | slice:0:3 }}</p>

<!-- 键值对管道 -->
<div *ngFor="let item of object | keyvalue">
  {{ item.key }}: {{ item.value }}
</div>

2. 管道链式调用

html
<!-- 链式管道 -->
<p>格式化价格:{{ price | currency:'CNY' | lowercase }}</p>
<p>格式化日期:{{ date | date:'short' | uppercase }}</p>
<p>数组处理:{{ items | slice:0:5 | json }}</p>

🎯 自定义管道

1. 基本管道

typescript
// capitalize.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'capitalize'
})
export class CapitalizePipe implements PipeTransform {
  transform(value: string): string {
    if (!value) return '';
    return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
  }
}

2. 带参数的管道

typescript
// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate'
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit: number = 50, suffix: string = '...'): string {
    if (!value) return '';
    if (value.length <= limit) return value;
    return value.substring(0, limit) + suffix;
  }
}

3. 纯管道 vs 非纯管道

typescript
// 纯管道(默认)
@Pipe({
  name: 'purePipe',
  pure: true // 默认值
})
export class PurePipe implements PipeTransform {
  transform(value: any): any {
    // 纯管道:输入相同,输出相同
    return value.toUpperCase();
  }
}

// 非纯管道
@Pipe({
  name: 'impurePipe',
  pure: false
})
export class ImpurePipe implements PipeTransform {
  transform(value: any): any {
    // 非纯管道:每次变更检测都会执行
    return new Date().toISOString();
  }
}

4. 复杂管道示例

typescript
// filter.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

export interface FilterOptions {
  field: string;
  value: any;
  operator?: 'equals' | 'contains' | 'startsWith' | 'endsWith';
}

@Pipe({
  name: 'filter'
})
export class FilterPipe implements PipeTransform {
  transform(items: any[], options: FilterOptions): any[] {
    if (!items || !options) return items;

    return items.filter(item => {
      const fieldValue = this.getNestedValue(item, options.field);
      
      switch (options.operator) {
        case 'contains':
          return fieldValue && fieldValue.toString().toLowerCase().includes(options.value.toLowerCase());
        case 'startsWith':
          return fieldValue && fieldValue.toString().toLowerCase().startsWith(options.value.toLowerCase());
        case 'endsWith':
          return fieldValue && fieldValue.toString().toLowerCase().endsWith(options.value.toLowerCase());
        case 'equals':
        default:
          return fieldValue === options.value;
      }
    });
  }

  private getNestedValue(obj: any, path: string): any {
    return path.split('.').reduce((current, key) => current && current[key], obj);
  }
}

🎮 实践练习

练习1:创建自定义指令

创建一个自定义指令,实现以下功能:

  • 鼠标悬停时高亮显示
  • 点击时改变背景色
  • 支持自定义颜色配置

练习2:创建自定义管道

创建一个自定义管道,实现以下功能:

  • 格式化文件大小(字节转换为KB、MB、GB)
  • 格式化时间差(显示相对时间,如"2小时前")
  • 过滤数组(支持多条件过滤)

📚 详细示例

完整的指令和管道示例

typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { HighlightDirective } from './directives/highlight.directive';
import { UnlessDirective } from './directives/unless.directive';
import { CapitalizePipe } from './pipes/capitalize.pipe';
import { TruncatePipe } from './pipes/truncate.pipe';
import { FilterPipe } from './pipes/filter.pipe';

@NgModule({
  declarations: [
    AppComponent,
    HighlightDirective,
    UnlessDirective,
    CapitalizePipe,
    TruncatePipe,
    FilterPipe
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
typescript
// app.component.ts
import { Component } from '@angular/core';

export interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  status: string;
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = '指令与管道示例';
  
  // 用户数据
  users: User[] = [
    { id: 1, name: 'john doe', email: 'john@example.com', role: 'admin', status: 'active' },
    { id: 2, name: 'jane smith', email: 'jane@example.com', role: 'user', status: 'inactive' },
    { id: 3, name: 'bob wilson', email: 'bob@example.com', role: 'moderator', status: 'active' }
  ];

  // 状态
  isHighlighted = false;
  selectedUser: User | null = null;
  searchTerm = '';
  filterOptions = {
    field: 'name',
    value: '',
    operator: 'contains' as const
  };

  // 方法
  toggleHighlight() {
    this.isHighlighted = !this.isHighlighted;
  }

  selectUser(user: User) {
    this.selectedUser = user;
  }

  onSearchChange(event: Event) {
    const target = event.target as HTMLInputElement;
    this.searchTerm = target.value;
    this.filterOptions.value = target.value;
  }

  getStatusClass(status: string): string {
    return status === 'active' ? 'status-active' : 'status-inactive';
  }
}
html
<!-- app.component.html -->
<div class="container">
  <h1>{{ title }}</h1>

  <!-- 指令示例 -->
  <section class="directives-section">
    <h2>指令示例</h2>
    
    <!-- 结构指令 -->
    <div class="structural-directives">
      <h3>结构指令</h3>
      
      <!-- ngIf -->
      <div *ngIf="selectedUser; else noUserSelected">
        <h4>选中的用户:{{ selectedUser.name | capitalize }}</h4>
        <p>邮箱:{{ selectedUser.email }}</p>
        <p>角色:{{ selectedUser.role | capitalize }}</p>
        <p>状态:<span [ngClass]="getStatusClass(selectedUser.status)">{{ selectedUser.status | capitalize }}</span></p>
      </div>
      <ng-template #noUserSelected>
        <p>请选择一个用户</p>
      </ng-template>

      <!-- ngFor -->
      <div class="user-list">
        <h4>用户列表</h4>
        <ul>
          <li *ngFor="let user of users; let i = index; trackBy: trackByUserId" 
              [class.selected]="selectedUser?.id === user.id"
              (click)="selectUser(user)">
            {{ i + 1 }}. {{ user.name | capitalize }} - {{ user.email }}
          </li>
        </ul>
      </div>

      <!-- ngSwitch -->
      <div class="status-display">
        <h4>状态显示</h4>
        <div [ngSwitch]="selectedUser?.status">
          <div *ngSwitchCase="'active'" class="status-active">用户活跃</div>
          <div *ngSwitchCase="'inactive'" class="status-inactive">用户非活跃</div>
          <div *ngSwitchDefault class="status-unknown">状态未知</div>
        </div>
      </div>
    </div>

    <!-- 属性指令 -->
    <div class="attribute-directives">
      <h3>属性指令</h3>
      
      <!-- ngClass -->
      <div [ngClass]="{'highlighted': isHighlighted, 'selected': selectedUser}">
        动态类绑定示例
      </div>
      
      <!-- ngStyle -->
      <div [ngStyle]="{
        'background-color': isHighlighted ? 'lightblue' : 'lightgray',
        'padding': '10px',
        'border-radius': '5px'
      }">
        动态样式绑定示例
      </div>

      <!-- 自定义指令 -->
      <div appHighlight="lightgreen" class="custom-directive">
        自定义高亮指令
      </div>
    </div>
  </section>

  <!-- 管道示例 -->
  <section class="pipes-section">
    <h2>管道示例</h2>
    
    <!-- 内置管道 -->
    <div class="built-in-pipes">
      <h3>内置管道</h3>
      
      <div class="pipe-examples">
        <p><strong>日期管道:</strong>{{ currentDate | date:'yyyy-MM-dd HH:mm:ss' }}</p>
        <p><strong>货币管道:</strong>{{ price | currency:'CNY':'symbol':'1.2-2' }}</p>
        <p><strong>数字管道:</strong>{{ number | number:'1.2-2' }}</p>
        <p><strong>文本管道:</strong>{{ text | uppercase }}</p>
        <p><strong>JSON管道:</strong><pre>{{ selectedUser | json }}</pre></p>
      </div>
    </div>

    <!-- 自定义管道 -->
    <div class="custom-pipes">
      <h3>自定义管道</h3>
      
      <div class="pipe-examples">
        <p><strong>首字母大写:</strong>{{ 'hello world' | capitalize }}</p>
        <p><strong>文本截断:</strong>{{ longText | truncate:20:'...' }}</p>
        <p><strong>链式管道:</strong>{{ 'hello world' | capitalize | truncate:5 }}</p>
      </div>
    </div>

    <!-- 过滤示例 -->
    <div class="filter-example">
      <h3>过滤示例</h3>
      
      <input 
        type="text" 
        placeholder="搜索用户..." 
        (input)="onSearchChange($event)"
        class="search-input">
      
      <div class="filtered-users">
        <h4>过滤结果:</h4>
        <ul>
          <li *ngFor="let user of users | filter:filterOptions">
            {{ user.name | capitalize }} - {{ user.email }}
          </li>
        </ul>
      </div>
    </div>
  </section>

  <!-- 控制按钮 -->
  <div class="controls">
    <button (click)="toggleHighlight()" class="btn btn-primary">
      {{ isHighlighted ? '取消高亮' : '高亮显示' }}
    </button>
  </div>
</div>
css
/* app.component.css */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

section {
  margin-bottom: 30px;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
}

h1, h2, h3, h4 {
  color: #333;
  margin-bottom: 15px;
}

/* 结构指令样式 */
.user-list ul {
  list-style: none;
  padding: 0;
}

.user-list li {
  padding: 10px;
  margin: 5px 0;
  background: #f8f9fa;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.user-list li:hover {
  background: #e9ecef;
}

.user-list li.selected {
  background: #007bff;
  color: white;
}

.status-active {
  color: #28a745;
  font-weight: bold;
}

.status-inactive {
  color: #dc3545;
  font-weight: bold;
}

.status-unknown {
  color: #6c757d;
  font-weight: bold;
}

/* 属性指令样式 */
.highlighted {
  background-color: #fff3cd;
  border: 2px solid #ffc107;
}

.selected {
  border: 2px solid #007bff;
}

.custom-directive {
  padding: 15px;
  margin: 10px 0;
  border-radius: 5px;
  font-weight: bold;
}

/* 管道示例样式 */
.pipe-examples {
  background: #f8f9fa;
  padding: 15px;
  border-radius: 5px;
  margin: 10px 0;
}

.pipe-examples p {
  margin: 8px 0;
  font-family: monospace;
}

.pipe-examples pre {
  background: #e9ecef;
  padding: 10px;
  border-radius: 4px;
  overflow-x: auto;
}

/* 过滤示例样式 */
.search-input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
  margin-bottom: 15px;
}

.filtered-users ul {
  list-style: none;
  padding: 0;
}

.filtered-users li {
  padding: 8px;
  margin: 5px 0;
  background: #e3f2fd;
  border-radius: 4px;
}

/* 控制按钮 */
.controls {
  text-align: center;
  margin-top: 20px;
}

.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s ease;
}

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

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

/* 响应式设计 */
@media (max-width: 768px) {
  .container {
    padding: 10px;
  }
  
  section {
    padding: 15px;
  }
}

✅ 学习检查

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

  • [ ] 使用所有内置结构指令
  • [ ] 使用所有内置属性指令
  • [ ] 创建自定义属性指令
  • [ ] 创建自定义结构指令
  • [ ] 使用所有内置管道
  • [ ] 创建自定义管道
  • [ ] 理解纯管道和非纯管道的区别

🚀 下一步

完成本章节学习后,请继续学习06-服务与依赖注入