Skip to content

input_output.ts

文件信息

  • 📄 原文件:01_input_output.ts
  • 🔤 语言:TypeScript (Angular)

Angular 组件通信 Angular 组件间的数据传递通过 @Input 和 @Output 实现。父传子用 @Input,子传父用 @Output + EventEmitter。

完整代码

typescript
/**
 * ============================================================
 *                    Angular 组件通信
 * ============================================================
 * Angular 组件间的数据传递通过 @Input@Output 实现。
 * 父传子用 @Input,子传父用 @Output + EventEmitter。
 * ============================================================
 */

import { Component, Input, Output, EventEmitter, input, output, model } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

// ============================================================
//                    1. @Input - 父传子
// ============================================================

/**
 * 【@Input 装饰器】
 * - 让组件接收来自父组件的数据
 * - 支持默认值
 * - 支持 required 标记(Angular 16+)
 *
 * 【Signal Input (Angular 17+)】
 * - input() 函数替代 @Input 装饰器
 * - input.required<Type>() 必填输入
 * - 返回 Signal,更好的响应式支持
 */

// --- 子组件: 用户卡片 ---
@Component({
    selector: 'app-user-card',
    standalone: true,
    imports: [CommonModule],
    template: `
        <div class="card" [class.vip]="isVip">
            <h3>{{ name }}</h3>
            <p>年龄: {{ age }}</p>
            <p>角色: {{ role }}</p>
            @if (isVip) {
                <span class="badge">VIP</span>
            }
        </div>
    `,
})
export class UserCardComponent {
    @Input() name = '未知用户';
    @Input() age = 0;
    @Input({ required: true }) role!: string;
    @Input() isVip = false;
}

// --- Signal Input (Angular 17+) ---
@Component({
    selector: 'app-product-card',
    standalone: true,
    template: `
        <div class="product">
            <h4>{{ title() }}</h4>
            <p>价格: ¥{{ price() }}</p>
            @if (discount()) {
                <p class="discount">折扣: {{ discount() }}%</p>
            }
        </div>
    `,
})
export class ProductCardComponent {
    title = input.required<string>();
    price = input(0);
    discount = input<number | null>(null);
}


// ============================================================
//                    2. @Output - 子传父
// ============================================================

/**
 * 【@Output 装饰器 + EventEmitter】
 * - 子组件通过事件向父组件发送数据
 * - EventEmitter<T> 定义事件类型
 * - emit() 方法触发事件
 *
 * 【output() 函数 (Angular 17+)】
 * - 替代 @Output 装饰器
 */

// --- 子组件: 计数器 ---
@Component({
    selector: 'app-counter',
    standalone: true,
    template: `
        <div class="counter">
            <button (click)="decrement()">-</button>
            <span>{{ count }}</span>
            <button (click)="increment()">+</button>
            <button (click)="reset()">重置</button>
        </div>
    `,
})
export class CounterComponent {
    @Input() count = 0;
    @Output() countChange = new EventEmitter<number>();
    @Output() onReset = new EventEmitter<void>();

    increment() {
        this.count++;
        this.countChange.emit(this.count);
    }

    decrement() {
        this.count--;
        this.countChange.emit(this.count);
    }

    reset() {
        this.count = 0;
        this.countChange.emit(this.count);
        this.onReset.emit();
    }
}

// --- Angular 17+ output() 函数 ---
@Component({
    selector: 'app-search-box',
    standalone: true,
    imports: [FormsModule],
    template: `
        <div class="search-box">
            <input
                [(ngModel)]="keyword"
                (keyup.enter)="doSearch()"
                placeholder="搜索..."
            >
            <button (click)="doSearch()">搜索</button>
        </div>
    `,
})
export class SearchBoxComponent {
    keyword = '';
    search = output<string>();
    clear = output<void>();

    doSearch() {
        if (this.keyword.trim()) {
            this.search.emit(this.keyword);
        }
    }
}


// ============================================================
//                    3. 双向绑定 - model()
// ============================================================

/**
 * 【组件双向绑定】
 * - 传统方式: @Input() + @Output() xxxChange
 * - 父组件使用 [(xxx)]="value" 语法
 *
 * 【model() 函数 (Angular 17+)】
 * - 替代 @Input + @Output 组合
 * - model<Type>() 创建可写 Signal
 */

// --- 评分组件(传统双向绑定) ---
@Component({
    selector: 'app-rating',
    standalone: true,
    imports: [CommonModule],
    template: `
        <div class="rating">
            @for (star of stars; track star) {
                <span (click)="setRating(star)" [class.filled]="star <= value">
                    {{ star <= value ? '★' : '☆' }}
                </span>
            }
            <span class="label">{{ value }} 分</span>
        </div>
    `,
})
export class RatingComponent {
    @Input() value = 0;
    @Output() valueChange = new EventEmitter<number>();
    stars = [1, 2, 3, 4, 5];

    setRating(star: number) {
        this.value = star;
        this.valueChange.emit(this.value);
    }
}

// --- Angular 17+ model() ---
@Component({
    selector: 'app-toggle',
    standalone: true,
    template: `
        <button
            (click)="checked.set(!checked())"
            [class.on]="checked()"
        >
            {{ checked() ? label() + ': 开' : label() + ': 关' }}
        </button>
    `,
})
export class ToggleComponent {
    checked = model(false);
    label = input('开关');
}


// ============================================================
//                    4. 父组件示例
// ============================================================

@Component({
    selector: 'app-communication-demo',
    standalone: true,
    imports: [
        CommonModule, UserCardComponent, ProductCardComponent,
        CounterComponent, SearchBoxComponent, RatingComponent, ToggleComponent,
    ],
    template: `
        <h2>组件通信演示</h2>

        <h3>1. 父传子 (@Input)</h3>
        <app-user-card name="小明" [age]="25" role="admin" [isVip]="true" />
        <app-user-card name="小红" [age]="22" role="user" />

        <h3>2. 子传父 (@Output)</h3>
        <app-counter
            [count]="myCount"
            (countChange)="onCountChange($event)"
        />
        <p>父组件中的计数: {{ myCount }}</p>

        <h3>3. 双向绑定</h3>
        <app-rating [(value)]="rating" />
        <p>评分: {{ rating }}</p>

        <app-toggle [(checked)]="darkMode" label="深色模式" />
        <p>深色模式: {{ darkMode }}</p>
    `,
})
export class CommunicationDemoComponent {
    myCount = 10;
    rating = 3;
    darkMode = false;

    onCountChange(value: number) {
        this.myCount = value;
    }
}


// ============================================================
//                    5. 最佳实践
// ============================================================

/**
 * 【组件通信最佳实践】
 *
 * ✅ 推荐做法:
 * 1. 父子通信用 @Input/@Output,简单直接
 * 2. Angular 17+ 优先使用 input()/output()/model()
 * 3. 复杂场景用 Service + RxJS 进行跨组件通信
 * 4. @Input 使用不可变数据,避免子组件直接修改
 *
 * ❌ 避免做法:
 * 1. 子组件直接修改 @Input 引用类型的数据
 * 2. 过度嵌套的 @Input/@Output 传递(超过 3 层考虑用 Service)
 */

💬 讨论

使用 GitHub 账号登录后即可参与讨论

基于 MIT 许可发布