[Angular學習紀錄] @ViewChild 和 @ContentChild

在前面的學習過程中,理解了Angular的Layout布局方式可以像是ASP.NET的MasterPage方式來規劃好UI的布局,使用的是Template內嵌另外一個子Template,如下圖紅框處就是一個Child的Template,也學習到 Parent & Child Template 之間資料的傳遞方式(參考先前文章)。


今天又再次接觸到幾個新的裝飾器 @ViewChild & @ContentChild,在開始說明之前,需要先理解<ng-content>這個嵌入標籤。

先回到UI布局的部分,如果想要在一個 Parent 下方放置另外一個 Child Template,最簡單的使用方式就是直接在要顯示的地方加上類似上圖的操作方法。

從上面的範例來看,該標籤的InnerHtml是空白的,因此就大膽的假設有機會把資料寫在InnerHtml的部分,讓HTML標籤可以從 Parent Template 傳入Child Template當中,但經過實際測試的結果(如下圖),果然失敗了。


實際上Angular是允許讓HTML標籤由Parent Template傳入Child Template,ng-content就是要用來處理此問題的。


ng-Content


上面的Parent Template先不用改變,打開Child Template,在希望呈現【Hello 有資料嗎?】這個字串的位置處,放置ng-content這個標籤,確認下圖的範例,因此可以知道<ng-content></ng-content>就是Parent傳入的HTML要嵌入位置
<fieldset>
  <legend>Ng Conent 範例</legend>
  <!-- 單一嵌入 -->
  <ng-content></ng-content>
</fieldset>
除了單一嵌入的方式外,還可定義多個傳入的嵌入點,並且可以在Child端指定(透過select的方式,不多說自行參考底下的code)哪個嵌入點要放哪個位置,當然這就得另外宣告嵌入點的名稱(Parent端宣告)才行,底下的範例有幾種常見的宣告方式。

  • 透過屬性。
  • 透過Classs。
  • 透過Html Attribute & Value。
  • 透過Html Tag(這個需要額外的設定,不建議,暫不說明)。

Parent Template語法如下參考
<app-ng-content>
  <!--定義class child-header -->
  <div class="child-header">
    <strong>This is Parent Templage Define Header</strong>
  </div>

  <!--定義屬性 child-body -->
  <div child-body>
    這個是寫在Parent的Div,需在Child端透過宣告 &lt;ng-content&gt;&lt;/ng-content&gt; 的方式傳入
  </div>

  <!--自定義Html Attribute detail-type & value = child-footer  -->
  <div detail-type="child-footer">
    <strong>This is Parent Templage Define Footer</strong>
  </div>
</app-ng-content>

Child Template語法如下參考
<fieldset>
  <legend>Ng Conent 範例</legend>
  <!-- 多點嵌入 透過class選擇 -->
  <ng-content select=".child-header"></ng-content>

  <!-- 多點嵌入 透過屬性選擇 -->
  <ng-content select="[child-body]"></ng-content>

  <!-- 多點嵌入 透過自定義Html Attribute & value 選擇 -->
  <ng-content select="[detail-type=child-footer]"></ng-content>
</fieldset>
執行結果如下圖


@ViewChild


Parent Template語法如下參考
<fieldset>
  <legend>Parent current view child count ({{ parentCount }})</legend>
  <button (click)="onParentClick()">累加View Child Count</button>
  <button (click)="onViewChildReset()">重置View Child Count</button>
</fieldset>

<app-view-child>
</app-view-child>
什麼是ViewChild?  上面的代碼<app-view-child></app-view-child>其實就是一個ViewChild,也就是Child Component的概念,你或許或覺得這跟之前有什麼差異?

說穿了它其實架構上是一樣的,但有個好處就是透過ViewChild的實作,可以讓Parent Component直接呼叫Child內提供的公開屬性和方法,講白話就是不需要透過@Input() 和 @Output()的方式來溝通,至於什麼時候要使用什麼方式,這個個人倒是還沒研究徹底,因此這裡就暫不討論優缺點了。

現在來實作一個範例,在Parent端建立兩個按鈕,一個是呼叫Child內的計數器+1,另外一個是清空Child計數器。

Child Template語法如下參考
<fieldset>
    <legend>View Child</legend>
    <p>View Child Count : {{ childCount }}</p>
</fieldset>

Parent Component語法如下參考
import { ViewChildComponent } from './view-child/view-child.component';
import { Component, ViewChild } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  //ViewChild 好處就是可以直接調用Child View公開的屬性 & 方法,可以不須透過 @Input @Output的方式
  @ViewChild(ViewChildComponent) viewChildControl: ViewChildComponent;
  parentCount: number = 1;

  onParentClick() {
    this.parentCount = this.viewChildControl.accumulate();
  }

  onViewChildReset() {
    this.viewChildControl.childCount = 0;
    this.parentCount = this.viewChildControl.childCount;
  }
}

執行結果如下圖
※ 取得@ViewChild的方式有兩種,#id 和 className,本範例示範取得className的方式。

特別說明一下使用方式,由於 ViewChild 的概念是要讓 Parent可以呼叫Child,因此重點就會放在Parent Component的部分,由上面的程式碼可以看見,透過@ViewChild(類別名稱)的方式可以將子類別的Component載入,後續因為該類別已經被 Parent 所宣告,因此理所當然地可以正常呼叫 Child Component 內所公開的方法屬性了。

同場加印,@ViewChild允許有多個相同的Component存在(複數),只是複數的裝飾詞為@ViewChildren(類別名稱) views: QueryList<類別名稱>,並且資料必須在ngAfterViewInit事件之後才可以使用,相關語法可自行參考範例檔案。

@ContentChild


不同於@ViewChild,@ContentChild如期命名方式,元素必須要嵌入(<ng-content>)在其Template結構內才能存取,下面來實作一個範例,在 Parent Template 內的 Child 嵌入了兩個 h1 標題,其中一個標題給予#變數,然後再由 Child Component 端來改變該變數所在標題的顏色。

Parent Template語法如下參考
<app-content-child>
  <h1>標題一</h1>
  <h1 #lastHead>標題二</h1>
</app-content-child>

Child Template語法如下參考
<fieldset>
    <legend>Content Child</legend>
    <button (click)="onChangeColor()">改變最後一個H1的顏色</button>
    <ng-content></ng-content>
</fieldset>

Child Component語法如下參考
import { ViewChildComponent } from './../view-child/view-child.component';
import { Component, OnInit, ContentChild, ElementRef } from '@angular/core';

@Component({
  selector: 'app-content-child',
  templateUrl: './content-child.component.html',
  styleUrls: ['./content-child.component.css']
})
export class ContentChildComponent implements OnInit {
  @ContentChild('lastHead') lastHead : ElementRef;
  constructor() { }

  ngOnInit() {
  }

  onChangeColor() {
    console.dir(this.lastHead);
    this.lastHead.nativeElement.style.color = "Red";
  }
}
執行結果如下圖

※ 取得@ContentChild的方式一樣有兩種,#id 和 className,本範例示範取得#id的方式。

原則上 ContentChild 的操作方式跟 ViewChild很像,只有兩個部分有些許的差異,這裡就不詳述,可自行參考範例檔。
  • 透過@ContentChild(類別名稱)的方式可以將子類別的Component載入。
  • 一樣有複數@ContentChildren,資料必須在ngAfterContentInit()事件之後才可以使用

重點回顧


ng-content

  • Child Template 內的<ng-content></ng-content>,就是 Parent Template 傳入的HTML要嵌入的地方。
  • 允許單一多點嵌入。

@ViewChild

  • 從目前元件中找出 View 裡面的特定元件,白話文就是說,讓 Parent Component 可以呼叫Child Component的公開屬性和方法
  • @ViewChild 有複數型態 @ViewChildren。
  • 需要再ngAfterViewInit()生命週期後才能調用。

@ContentChild

  • 從元件中找出內容元件中的特定元件,白話文就是說,讓Child Component 可以回到 Parent Template 找出嵌入的元素來操作
  • @ContentChild 有複數型態 @ContentChildren。
  • 需要再ngAfterContentInit()生命週期後才能調用。



本文撰寫時Angular版本為v4.0。

本文範例下載 : Github,使用Angular CLI。

參考網站
[Day 14] Angular 2 + <ng-content> 嵌入式設計靈活組織 HTML
[Day 26] Angular 2 裝飾器 @ContentChild
[Day 27] Angular 2 裝飾器 @ViewChild
ViewChildren and ContentChildren in Angular

留言