Angular 4: Components & Databinding deep dive



這次紀錄一些額外的databinding與component資料傳遞相關的方法。




1. 從parent傳資料到child component

如果要傳遞某個資料到child component的某個attribute,可以透過attribute binding的方式,但要透過@Input() 來指定component特定attribute為可接受外面傳入資料,才可透過該方式傳遞資料。

以下列程式舉例來說:
app-item.component.html:
<app-item
 *ngFor="let item of items" ></app-item>

app-item.component.ts程式:
export class AppItemComponent implements OnInit {

  item: { type: string, name: string, content: string }
  constructor() {
  }

  ngOnInit() {
  }

}

AppItemComponent中有一item屬性,我們希望在ngFor的item可以傳入到AppItemComponent中的item屬性,就可以透過上述的方式調整程式:

調整app-item.component.html:
<app-item
*ngFor="let item of items"
[srvItem]="item"></app-item>

調整app-item.component.ts程式:

import { ComponentOnInitInput } from '@angular/core';
export class AppItemComponent implements OnInit {

  @Input('srvItem') item: { type: string, name: string, content: string }

}

主要透過Input的方式,將component內item變數宣告為可接受外部傳入資料,或比較像是讓item變成AppItemComponent的attribute,讓使用component的parent可以透過attribute binding的方式將值綁定。使用時要記得將Input import進來。

其中如果是直接使用 @Input() item,在attribute binding時,就要使用[item]="item",即attribute binding的名稱就是變數原始名稱;如果是使用@Input('srvItem') item,則外部看的名稱是srvItem,attribute binding名稱就會是srvItem(如上例程式)。

2. 客製自己的event (可傳送資料)

自己製作的component,可以客製自己的event binding方式,大致方式如下:

app-item.Component.html:
    <button class="btn btn-primary" (click)="onAddItem(ItemNameInput)">Add Item</button>


app-item.component.ts
  export class CockpitComponent implements OnInit {
  
  @Output('itemAdd') itemAdded= new EventEmitter<{ itemName: string, itemContent: string }>();
  
  constructor() { }

  ngOnInit() {
  }

  onAddItem(itemInputHTMLInputElement) {
    this.itemAdded.emit({ itemName: itemInput.value, itemContent: 'content' });
  }
}

Parent  template:
  <app-item (itemAdd)="onItemAdded($event)"></app-item>

Parent script:
onItemAdded(item: { itemName: string, itemContent: string }) {

  }

這個程式最終結果,是parent template的地方,使用了<app-item>,並且用了event binding,目標是AppItemComponent的itemAdd event。

而itemAdd觸發主要條件,就在app-item.component.ts內,可列為下列幾點:

1. parent如何可以成功綁定<app-item>的itemAdd event:

主要是透過EventEmitter,透過EventEmitter可以做到自己定義event事件,關鍵在於透過EventEmitter的emit() function,EventEmitter可用generic型態宣告,所以宣告會像是:

@Output('itemAdd')
itemAdd = new EventEmitter<{ serverName: string, serverContent: string }>();

當中再透過@Output('eventName'), 目的是可以讓parent可以從AppItemComponent取到資料(從AppItemComponent輸出之資料),綁定方式名稱是itemAdd,所以在parent的地方就是綁定AppItemComponent的itemAdd event。

2. 如何觸發itemAdd event到parent

在AppItemComponent觸發click的button事件後,會到到AppItemComponent的onAddItem function中,接著function內透過EventEmitter: itemAdded呼叫emit( data) ,觸發itemAdd event,其中emit所傳入的data,就是parent處 event bindng傳入的內容,呼叫emit後對於parent就是AppItemComponent觸發了itemAdd event。

3. Parent接收itemAdd所傳入之資料

觸發了itemAdd event,會呼叫onItemAdded function,在onItemAdded function內,會帶入一個參數,其參數就是AppItemComponent的EventEmitter所送出之data,parent可以透過這方式取得child component所送出之資料。

3. Local reference

Local Reference是指在template內,我們會利用ngModel來綁定input element,並將綁定的input element在特定事件觸發後加以處理,舉例來說
<input type="text" class="form-control" [(ngModel)]="itemName"> <button class="btn btn-primary" (click)="onAddItem()">Add Item</button>

在上面的template中,我們在Add Item按鈕點下後,因為twoway binding的關係,所以可以在onAddItem function中處理itemName input資料。

不過還有另外可行的方式,就是透過local reference的方式,方法是在input element(或是ngModel綁定的input element),透<tag #variableName>指向某個input element,讓某個input element在template內可以直接傳遞使用,舉例來說:
<input type="text" class="form-control" #itemNameInput>
<button class="btn btn-primary" (click)="onAddItem(itemNameInput)">Add Item</button>

原先使用twoway binding的方式,現在直接用itemNameInput變數指向input element,接著在click event中,就可以直接將itemNameInput作為參數傳入onAddItem中處理。

4. ChildView

我們在angular程式內設定一個reference,直接參考到template上的element,不過一般似乎比較不建議這種方法,還是希望透過data binding的方式做比較好,不過這邊一樣記錄下來可供參考,使用方式與local reference很相似,主要是透過ChildView來做到,方法是在angular內宣告一個變數:
import { Component, OnInit, EventEmitter, Output, ViewChild, ElementRef } from '@angular/core'; @ViewChild('itemNameInput') itemNameInput: ElementRef;
記得要將ViewChild與ElementRef兩個class import到程式內,

在template一樣是透過<tag #variableName>的方式,但variable必須為ViewChild所宣告之名稱才可,不過在ViewChild宣告的地方,是透過ElementRef,可以先透過console看ElementRef內容:
ElementRef內容
在nativeElement屬性下有個value屬性
從上面可以知道,要透過ElementRef取得使用者輸入之資料內容,要透過ElementRef.nativeElement.value,所以在angular程式內,就會變成
this.itemNameInput.nativeElement.value


5. View Encapsulation

每個component內所定義之css,使用後可以發現與預期的運作方式有些差異,舉例來說,如果在<parent-component>內定義
p { color: blue; }
那麼如果這麼使用:
<parent-component> <child-component></child-component> </parent-component>
那麼child-component內的 <p> 內容顏色應該也會是藍色才對,不過實際上會發現,<child-element>內的element css完全不會受到<parent-component>所定義之css影響

打開瀏覽器的develop tool看就會發現,在css的p定義中,變成了p[_ng-content-_e0j]這類的css,表示唯有 <p _ng-content-_e0j>這類的element才會套用css,再仔細查可以發現,在<parent-component>定義的template中,所有p都符合這css規則,但是<child-component>卻不合,所以才不會套用css。

原因跟Angular ViewEncapsulation有關係,預設Angular是使用ViewEncapsulation.Emulated方式,只有component自己的element會受到自己定義的css影響,如同上面看到的,Angular會自動在每個element上加入一個獨特的屬性,藉此區別parent與child不同component。

ViewEncapsulation有三種模式:

  1. ViewEncapsulation.Native: 以瀏覽器原生地Shadow Dom方式產生,有些瀏覽器不支援該模式則無法正常呈現。
  2. ViewEncapsulation.Emulated:Angular模擬Shadow Dom,方法是在css與element上加入隨機的是別屬性。
  3. ViewEncapsulation.None:不使用也不模擬Shadow Dom。
使用方法是在Angular的component宣告內,
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], encapsulation: ViewEncapsulation.Emulated })
可以在這邊設定要採用什麼方式。


6. component tag內放入html

在自己定義了component後,如果希望可以有html當作參數,顯示於component中,Angular預設是會忽略掉的,像是下列template,<p></p>的部分是不會顯示在component中的。
<app-item> <p> test </p> </app-item>

如果希望可以讓component tag接受template內容,並顯示在component中,則只要在<app-item> element的template檔案中,加入

<ng-content></ng-content>

關鍵就是ng-content,<ng-content>擺放的地方,就是傳入的html element顯示的地方,在設計上可以利用這點,結合component的template與輸入之template。

以上例來說,<ng-content></ng-content>的地方,在runtime就會變成<p>test</p>。

沒有留言:

張貼留言

Java Spring Framework 筆記 - Autowiring (2)

這篇記錄透過 Annotation來做到 Spring的 autowiring。