Angular学习(二)组件交互的几种方式

2 篇文章 0 订阅
1 篇文章 0 订阅
本文详细介绍了Angular中组件交互的多种方式,包括通过@Input()和setter监听属性变化,使用@Output()和EventEmitter触发事件,模板内通过#变量互动,@ViewChild()注入子组件引用,通过服务实现父子组件双向通信,以及利用单例对象共享数据。每个方法都有具体的代码示例和应用场景解析。
摘要由CSDN通过智能技术生成

通过@Input(),从父组件传数据到子组件

在子组件中通过 @Input() 装饰器修饰的属性,在父组件里可以引用并传递数据

// 子组件
export class Hero {
	name: string;
}

export const HEROES = [
	{name: 'Mr. IQ'},
	{name: 'Magneta'},
	{name: 'Bombasto'}
];

export class HeroChildComponent {
	@Input() hero: Hero;
	@Input('master') masterName: string;
}
<!-- 父组件模板 -->
<app-hero-child *ngFor="let hero of heroes" 
	[hero]="hero" 
	[master]="master">
</app-hero-child>

通过@Input() + setter, 监听父组件属性值变化

使用一个输入属性的 setter,以拦截父组件中值的变化,并采取行动。
子组件 NameChildComponent 的输入属性 name 上的这个 setter,会 trim 掉名字里的空格,并把空值替换成默认字符串

// 子组件
export class NameChildComponent {
  private _name = '';
  
  	@Input()
	set name(name: string) {
		this._name = (name && name.trim()) || 'NULL';
	}
 
	get name(): string { return this._name; }
}
// 父组件
export class NameParentComponent {
	names = ['Mr. IQ', '   ', '  Bombasto  '];
}
<!-- 父组件模板内 -->
<h2>Master controls {{names.length}} names</h2>
<app-name-child *ngFor="let name of names" [name]="name"></app-name-child>

通过@Input() + ngOnChanges(),监听父组件输入属性值的变化

使用 OnChanges 生命周期钩子接口的 ngOnChanges() 方法来监测输入属性值的变化并做处理。
子组件会监测输入属性 major 和 minor 的变化,并把这些变化编写成日志以报告这些变化。
父组件提供 minor 和 major 值,把修改它们值的方法绑定到按钮上。

// 子组件
export class VersionChildComponent implements OnChanges {
  @Input() major: number;
  @Input() minor: number;
  changeLog: string[] = [];
 
  ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
    let log: string[] = [];
    for (let propName in changes) {
      let changedProp = changes[propName];
      let to = JSON.stringify(changedProp.currentValue);
      if (changedProp.isFirstChange()) {
        log.push(`Initial value of ${propName} set to ${to}`);
      } else {
        let from = JSON.stringify(changedProp.previousValue);
        log.push(`${propName} changed from ${from} to ${to}`);
      }
    }
    this.changeLog.push(log.join(', '));
  }
}
// 父组件
export class VersionParentComponent {
  major = 1;
  minor = 23;
 
  newMinor() {
    this.minor++;
  }
 
  newMajor() {
    this.major++;
    this.minor = 0;
  }
}
<!-- 父模板 -->
<h2>Source code version</h2>
<button (click)="newMinor()">New minor version</button>
<button (click)="newMajor()">New major version</button>
<app-version-child [major]="major" [minor]="minor"></app-version-child>

通过@Output() + EventEmitter,父组件监听子组件的事件

子组件暴露一个 EventEmitter 属性,当事件发生时,子组件利用该属性 emits(向上弹射)事件。父组件绑定到这个事件属性,并在事件发生时作出回应。

// 子组件
// 子组件的 EventEmitter 属性是一个输出属性,通常带有@Output 装饰器,点击按钮会触发 true 或 false(布尔型有效载荷)的事件。
import { Component, EventEmitter, Input, Output } from '@angular/core';
 
@Component({
  selector: 'app-voter',
  template: `
    <h4>{{name}}</h4>
    <button (click)="vote(true)"  [disabled]="didVote">Agree</button>
    <button (click)="vote(false)" [disabled]="didVote">Disagree</button>
  `
})
export class VoterComponent {
  @Input()  name: string;
  @Output() voted = new EventEmitter<boolean>();
  didVote = false;
 
  vote(agreed: boolean) {
    this.voted.emit(agreed);
    this.didVote = true;
  }
}
// 父组件
// 父组件绑定了一个事件处理器(onVoted()),用来响应子组件的事件($event)并更新一个计数器。
@Component({
  selector: 'app-vote-taker',
  template: `
    <h2>Should mankind colonize the Universe?</h2>
    <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
    <app-voter *ngFor="let voter of voters"
      [name]="voter"
      (voted)="onVoted($event)">
    </app-voter>
  `
})
export class VoteTakerComponent {
  agreed = 0;
  disagreed = 0;
  voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];
 
  onVoted(agreed: boolean) {
    agreed ? this.agreed++ : this.disagreed++;
  }
}

通过 # ,父子组件在模板内通过变量互动

父组件不能使用数据绑定来读取子组件的属性或调用子组件的方法。但可以在父组件模板里,新建一个本地变量来代表子组件,然后利用这个变量来读取子组件的属性和调用子组件的方法,

父组件不能通过数据绑定使用子组件的 start 和 stop 方法,也不能访问子组件的 seconds 属性。
把本地变量(#timer)放到()标签中,用来代表子组件。这样父组件的模板就得到了子组件的引用,于是可以在父组件的模板中访问子组件的所有属性和方法。

这个例子把父组件的按钮绑定到子组件的 start 和 stop 方法,并用插值表达式来显示子组件的 seconds 属性。

// 子组件
// 子组件进行倒计时,归零时发射一个导弹。start 和 stop 方法负责控制时钟并在模板里显示倒计时的状态信息
@Component({
  selector: 'app-countdown-timer',
  template: '<p>{{message}}</p>'
})
export class CountdownTimerComponent implements OnInit, OnDestroy {
 
  intervalId = 0;
  message = '';
  seconds = 11;
  clearTimer() { clearInterval(this.intervalId); }
  ngOnInit()    { this.start(); }
  ngOnDestroy() { this.clearTimer(); }
 
  start() { this.countDown(); }
  stop()  {
    this.clearTimer();
    this.message = `Holding at T-${this.seconds} seconds`;
  }
 
  private countDown() {
    this.clearTimer();
    this.intervalId = window.setInterval(() => {
      this.seconds -= 1;
      if (this.seconds === 0) {
        this.message = 'Blast off!';
      } else {
        if (this.seconds < 0) { this.seconds = 10; } // reset
        this.message = `T-${this.seconds} seconds and counting`;
      }
    }, 1000);
  }
}
// 父组件
@Component({
  selector: 'app-countdown-parent-lv',
  template: `
  <h3>Countdown to Liftoff (via local variable)</h3>
  <button (click)="timer.start()">Start</button>
  <button (click)="timer.stop()">Stop</button>
  <div class="seconds">{{timer.seconds}}</div>
  <app-countdown-timer #timer></app-countdown-timer>
  `,
  styleUrls: ['../assets/demo.css']
})
export class CountdownLocalVarParentComponent { }

通过 # + @ViewChild(),将模板内的子组件引用注入父组件类中

这个本地变量方法是个简单便利的方法。但是它也有局限性,因为父组件-子组件的连接必须全部在父组件的模板中进行。父组件本身的代码对子组件没有访问权。
如果父组件的类需要读取子组件的属性值或调用子组件的方法,就不能使用本地变量方法。
当父组件类需要这种访问时,可以把子组件作为 ViewChild,注入到父组件里面。

import { AfterViewInit, ViewChild } from '@angular/core';
import { Component }                from '@angular/core';
import { CountdownTimerComponent }  from './countdown-timer.component';
 
@Component({
  selector: 'app-countdown-parent-vc',
  template: `
  <h3>Countdown to Liftoff (via ViewChild)</h3>
  <button (click)="start()">Start</button>
  <button (click)="stop()">Stop</button>
  <div class="seconds">{{ seconds() }}</div>
  <app-countdown-timer></app-countdown-timer>
  `,
  styleUrls: ['../assets/demo.css']
})
export class CountdownViewChildParentComponent implements AfterViewInit {
 
  @ViewChild(CountdownTimerComponent)
  private timerComponent: CountdownTimerComponent;
 
  seconds() { return 0; }
 
  ngAfterViewInit() {
    // Redefine `seconds()` to get from the `CountdownTimerComponent.seconds` ...
    // but wait a tick first to avoid one-time devMode
    // unidirectional-data-flow-violation error
    setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);
  }
 
  start() { this.timerComponent.start(); }
  stop() { this.timerComponent.stop(); }
}

把子组件的视图插入到父组件类需要做一点额外的工作。
首先,你要使用 ViewChild 装饰器导入这个引用,并挂上 AfterViewInit 生命周期钩子。
接着,通过 @ViewChild 属性装饰器,将子组件 CountdownTimerComponent 注入到私有属性 timerComponent 里面。
组件元数据里就不再需要 #timer 本地变量了。而是把按钮绑定到父组件自己的 start 和 stop 方法,使用父组件的 seconds 方法的插值表达式来展示秒数变化。
这些方法可以直接访问被注入的计时器组件。
ngAfterViewInit() 生命周期钩子是非常重要的一步。被注入的计时器组件只有在 Angular 显示了父组件视图之后才能访问,所以它先把秒数显示为 0.
然后 Angular 会调用 ngAfterViewInit 生命周期钩子,但这时候再更新父组件视图的倒计时就已经太晚了。Angular 的单向数据流规则会阻止在同一个周期内更新父组件视图。应用在显示秒数之前会被迫再等一轮。
使用 setTimeout() 来等下一轮,然后改写 seconds() 方法,这样它接下来就会从注入的这个计时器组件里获取秒数的值。

通过服务进行父子组件通信

父组件和它的子组件共享同一个服务,利用该服务在家庭内部实现双向通讯。
该服务实例的作用域被限制在父组件和其子组件内。这个组件子树之外的组件将无法访问该服务或者与它们通讯。
这个 MissionService 把 MissionControlComponent 和多个 AstronautComponent 子组件连接起来。

// 服务
import { Injectable } from '@angular/core';
import { Subject }    from 'rxjs';
 
@Injectable()
export class MissionService {
 
  // Observable string sources
  private missionAnnouncedSource = new Subject<string>();
  private missionConfirmedSource = new Subject<string>();
 
  // Observable string streams
  missionAnnounced$ = this.missionAnnouncedSource.asObservable();
  missionConfirmed$ = this.missionConfirmedSource.asObservable();
 
  // Service message commands
  announceMission(mission: string) {
    this.missionAnnouncedSource.next(mission);
  }
 
  confirmMission(astronaut: string) {
    this.missionConfirmedSource.next(astronaut);
  }
}
// 子组件
// MissionControlComponent 提供服务的实例,并将其共享给它的子组件(通过 providers 元数据数组)
// 子组件可以通过构造函数将该实例注入到自身。
import { Component }          from '@angular/core';
import { MissionService }     from './mission.service';
 
@Component({
  selector: 'app-mission-control',
  template: `
    <h2>Mission Control</h2>
    <button (click)="announce()">Announce mission</button>
    <app-astronaut *ngFor="let astronaut of astronauts"
      [astronaut]="astronaut">
    </app-astronaut>
    <h3>History</h3>
    <ul>
      <li *ngFor="let event of history">{{event}}</li>
    </ul>
  `,
  providers: [MissionService]
})
export class MissionControlComponent {
  astronauts = ['Lovell', 'Swigert', 'Haise'];
  history: string[] = [];
  missions = ['Fly to the moon!',
              'Fly to mars!',
              'Fly to Vegas!'];
  nextMission = 0;
 
  constructor(private missionService: MissionService) {
    missionService.missionConfirmed$.subscribe(
      astronaut => {
        this.history.push(`${astronaut} confirmed the mission`);
      });
  }
 
  announce() {
    let mission = this.missions[this.nextMission++];
    this.missionService.announceMission(mission);
    this.history.push(`Mission "${mission}" announced`);
    if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
  }
}
// AstronautComponent 也通过自己的构造函数注入该服务。
// 由于每个 AstronautComponent 都是 MissionControlComponent 的子组件,所以它们获取到的也是父组件的这个服务实例。
import { Component, Input, OnDestroy } from '@angular/core';
import { MissionService } from './mission.service';
import { Subscription }   from 'rxjs';
 
@Component({
  selector: 'app-astronaut',
  template: `
    <p>
      {{astronaut}}: <strong>{{mission}}</strong>
      <button
        (click)="confirm()"
        [disabled]="!announced || confirmed">
        Confirm
      </button>
    </p>
  `
})
export class AstronautComponent implements OnDestroy {
  @Input() astronaut: string;
  mission = '<no mission announced>';
  confirmed = false;
  announced = false;
  subscription: Subscription;
 
  constructor(private missionService: MissionService) {
    this.subscription = missionService.missionAnnounced$.subscribe(
      mission => {
        this.mission = mission;
        this.announced = true;
        this.confirmed = false;
    });
  }
 
  confirm() {
    this.confirmed = true;
    this.missionService.confirmMission(this.astronaut);
  }
 
  ngOnDestroy() {
    // prevent memory leak when component destroyed
    this.subscription.unsubscribe();
  }
}

注意,这个例子保存了 subscription 变量,并在 AstronautComponent 被销毁时调用 unsubscribe() 退订。 这是一个用于防止内存泄漏的保护措施。实际上,在这个应用程序中并没有这个风险,因为 AstronautComponent 的生命期和应用程序的生命期一样长。但在更复杂的应用程序环境中就不一定了。
不需要在 MissionControlComponent 中添加这个保护措施,因为它作为父组件,控制着 MissionService 的生命期。
History 日志可以证明:在父组件 MissionControlComponent 和子组件 AstronautComponent 之间,信息通过该服务实现了双向传递。

通过导入一个单例对象来共享数据

用ts或js写一个单例的对象,可以共享数据

参考:Angular中文文档

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值