我們知道 Angular 的系統是透過 Component 的方式堆疊起來的,而 Component 與 Component 的溝通方式有幾種,1. 透過 @Input
和 @Output
,2. 透過 service
的方式,或是 3. 直接將上層 Component
注入到目前的 Component
內使用。
但通常我是不建議使用第 3 種方式,可是,在某些情境下,還是得必須這樣子處理,而且還需要動態的取得上層的 Component
,這篇文章就是分享如何取得上層 Component
取得上層 Component
的方式,我們會透過 Injector
機制來完成,以下介紹兩種方式可以達到一樣的效果
情境描述
當 Input 離開時,需要觸發執行某些動作,但又不想要每一個 Component 都要處理這一類的工作,所以希望能用一個 general 的解法來完成這需求
正規解
根據情境,看起來又是一個可透過 RxJS 來完成的需求,但我要怎麼知道我目前的 Input 離開時,要讓那一個 Component
工作呢? 所以只要能取得目前觸發的 Input 是在哪一個 Component 內,就可以完成這需求了。
初版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { Directive, HostListener } from '@angular/core'; import { ControlService } from './control.service';
@Directive({ selector: 'input' }) export class InputFocusDirective {
@HostListener('blur', ['$event']) inputBlur(event) { const { name, value } = event.target; this.service.inputEvent$.next({ type: 'blur', name, value }) } constructor(private service: ControlService) { } }
|
1 2 3 4 5
| @Injectable() export class ControlService { inputEvent$ = new Subject(); constructor() { } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import { Component, forwardRef } from '@angular/core'; import { ControlService } from './control.service'; import { ParentComponent } from './parent-component'; import { filter } from 'rxjs/operators';
@Component({ selector: 'my-app', template: ` <hello title="{{ name }}"></hello> <form name="test"> <input name="firstName" [(ngModel)]="firstName" /> </form> <p> Start editing to see some magic happen :) </p> `, styleUrls: ['./app.component.css'] }) export class AppComponent { name = 'AppComponent'; firstName;
constructor(private service: ControlService) { service.inputEvent$ .subscribe((x: any) => { console.log(x); }); } }
|
上述的寫法,只要在任何 Component
內的 <input>
離開時都會觸發並廣播訊息到所有註冊者
第二版
接下來就是加入觸發者所處的 Component 資訊就可以做過濾判斷了,但在這之前,先建立一個通用的 ParentComponent
1 2
| export abstract class ParentComponent { }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { Component, forwardRef } from '@angular/core'; import { ParentComponent } from './parent-component';
@Component({ ... providers: [ { provide: ParentComponent, useExisting: forwardRef(() => AppComponent) } ] }) export class AppComponent { ... }
|
-
說明
- 利用 Injector 取 provider 的順序特性,我們就能利用該特性取得目前觸發 directive 事件元件的隸屬 component
forwardRef
: Allows to refer to references which are not yet defined.
useExisting
: 使用已經建立的 instance,這能確保取得的 instance 不是全新的
-
directive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { Directive, HostListener } from '@angular/core'; import { ControlService } from './control.service';
@Directive({ selector: 'input' }) export class InputFocusDirective {
@HostListener('blur', ['$event']) inputBlur(event) { const { name, value } = event.target; this.service.inputEvent$.next({ type: 'blur', comp: this.parent, name, value }) } constructor(private service: ControlService, private parent: ParentComponent) { } }
|
- 程式碼說明
- 將
ParentComponent
注入後,在事件觸發時將 Component
的資訊傳入
- app.component.ts
1 2 3 4 5 6 7 8 9 10
| @Component(...) export class AppComponent { constructor(private service: ControlService) { service.inputEvent$ .pipe(filter((x:any)=> x.comp === this)) .subscribe((x: any) => { console.log(x) }); } }
|
- 程式碼說明
- 因為
inputEvent$
內傳回的資訊已經有包含 Component
的資訊,所以可以透過 filter
的 operators 來過濾廣播訊息
完成需求
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export class AppComponent { constructor(private service: ControlService) { service.inputEvent$ .pipe(filter((x:any)=> x.comp === this)) .subscribe((x: any) => { (x.comp as AppComponent).show(x.value); }); }
show(value) { console.log(value); } }
|
範例程式碼
暗黑解
※ 注意: 此暗黑解法十分黑暗,心臟不夠強的千萬不要用,所以我不會做任何解釋
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { Directive, HostListener, Injector } from '@angular/core'; import { ControlService } from './control.service';
@Directive({ selector: 'input' }) export class InputFocusDirective { @HostListener('blur', ['$event']) inputBlur(event) { const { name, value } = event.target; this.service.inputEvent$.next({ type: 'blur', comp: this.injector['view'].component, name, value }) }
constructor(private service: ControlService, private injector: Injector) { }
}
|