Angularアドベントカレンダー2日目です。昨日は @kasaharu さんでした。
ReactやMithrilのテンプレートは基本的に、render()/view()メソッド内のJavaScriptの関数呼び出しですので、JavaScriptの文法でいろいろコードをいじることができます。それに対して、VueとAngularはテンプレート言語を持っていて、それを実行時に評価して(パースは事前に行うが)HTMLを生成します。
で、Angularの方には、テンプレートを構造化するためのもろもろの便利タグがあります。
<ng-container>
ReactでいうところのFragmentです。ちょっとタグのようにみえるけど、タグではない、でも少しタグっっぽいタグです。表示するときには何も表示されません。Angularではタグに*ngIf
とか*ngFor
構造化ディレクティブをつけて、タグのON/OFFや、ループを行いますが、ng-container
を使えば、余計なタグが生成されません。
例えば、divタグに*ngFor
をつけると、
<div *ngFor="let station of stations">
<span>{{station.name}}</span>
</div>
このdivタグも繰り返されます。
<div><span>渋谷</span></div>
<div><span>新宿</span></div>
<div><span>池袋</span></div>
ng-containerにつけると、タグ自体の出力がされなくなります。
<ng-container *ngFor="let station of stations">
<span>{{station.name}}</span>
</ng-container>
<span>渋谷</span>
<span>新宿</span>
<span>池袋</span>
<ng-content>
アダプティブなGUIコンポーネントの考察のエントリーで紹介したのが<ng-content>
です。
コンポーネントが利用させるときに、コンポーネントのタグに挟まっている子要素を取得してきてテンプレート内部に展開します。例えば、blinkを再現するコンポーネントapp-blinkを作ったとして、次のようにテンプレートを作成します(実際にはCSSを付与するだけならディレクティブの方が良いです)。
<div class="blinking">
<ng-content></ng-content>
</div>
利用側のコードでは次のようになりますが、このタグの中の子供要素(ここではテキスト)が<ng-content>
のところに展開されます。
<app-blink>Hello World</app-blink>
ng-contentを何回も使うことができます。select属性をつけると、それにマッチしたものだけを展開します。例えば次のようなテンプレートの場合、imgタグだけを前に並べて、それ以外の要素を後ろに並べます。同じタグがなんども出力されることはありません。イメージとしては、子タグは最初にすべて見えないリストに積まれます。最初のng-contentにヒットしたタグは、子供タグリストから削られて、最後にselectなしの時に残りのリストの要素が全部表示されます。同じselectをなんども使うと、2つ目移行はヒットしなくなるので、何も出力されません。
<ng-content select="img"></ng-content>
<ng-content></ng-content>
3/14追記
Angularでi18nを行うときに、公式以上に使われているというngx-translateを使う場合、<ng-content>
を使ったライブラリの翻訳はそのままではうまくいきません。
<p translate>翻訳前のテキスト</p>
通常はこのように書いておくと、抽出ツールで 翻訳前のテキスト
をキーにした項目が辞書ファイルに追加され、翻訳文を入れておくと、実行時にリプレースされて翻訳が実行されます。ですが、内部で<ng-content>
を使って子要素を表示する自作のタグコンポーネントでは翻訳されません。
<!-- 抽出はうまくいくが実行時の置き換えがされない -->
<my-component translate>翻訳前のテキスト</my-component>
次のように書く必要があります。
<!-- こうかけばOK -->
<my-component>{{ '翻訳前のテキスト' | translate }}</my-component>
<ng-template>
<ng-template>
はデフォルトでは表示されない(コメント化される)テンプレートを作ります。HTMLの<template>
に似ていますが、<template>
の方は、中身のタグも実態として生成される点が少し違います。#
で名前をつけます。
例えば、ロード済みでなければ、loadingの方を出してあげる、みたいに、単純なifのON/OFFではなくて、完全に別の要素を表示するときに使います。Angular 4.0からの機能みたいですね。
<div *ngIf="loaded; else loading">
ロード済みの時に表示される
</div>
<ng-template #loading>
<div>Loading...</div>
</ng-template>
thenも書けば両方を外だしできます。
<div *ngIf="loaded; then loaded; else loading"></div>
<ng-template #loaded>
<div>ロード済みの時に表示される</div>
</ng-template>
<ng-template #loading>
<div>Loading...</div>
</ng-template>
ViewChildでアクセスもできます。
@ViewChild('loading') template: TemplateRef<any>;
<ng-template>
と <ng-container>
<ng-template>
と<ng-container>
は一緒に使うと便利です。
先ほどのloadingのテンプレートは、if文がマッチしなかったときのフォールバックとしてテンプレートを表示していましたが、テンプレート自体のインスタンス化は<ng-container>
の*ngTemplateOutlet
でテンプレート名を指定すると、いつでもテンプレートの展開ができます。
<ng-container *ngTemplateOutlet="loading"></ng-container>
<ng-template>
と <ng-container>
と <ng-content>
さて、条件によってまったく違う見た目をさせたいテンプレートがあったとします。どちらの条件時もng-contentで子要素を表示させたいとします。例えば、次の例では、モバイルではないときはサイドバーを表示しています(この例であれば*ngIfをsidebarにつけるだけで十分ですが多目にみてください)。
<ng-container *ngIf="mobile else desktop">
<ng-content></ng-content>
</ng-container>
<ng-template #desktop>
<sidebar>サイドバー</sidebar>
<ng-content></ng-content>
</ng-template>
はい。これはうまくいきません。<ng-content>
のときに、子供の要素は内部の見えないリストに入れてから取り出されると説明しました。この場合、if文によって<ng-content>
は一個ずつしか表示されないのですが、デスクトップの時も、モバイル側の<ng-content>
側に要素が入ってしまい、表示されないのです。if文の評価とか関係なく、上から<ng-content>
が処理されるようです。
この場合は、<ng-template>
を使って、<ng-content>
をまとめてあげると大丈夫になります。
<ng-template #child>
<ng-content></ng-content>
</ng-template>
<ng-container *ngIf="mobile else desktop">
<ng-container *ngTemplateOutlet="child"></ng-container>
</ng-container>
<ng-template #desktop>
<sidebar>サイドバー</sidebar>
<ng-container *ngTemplateOutlet="child"></ng-container>
</ng-template>
おまけ: タグの中のテキストをプログラム的に利用したい (<template>
と<ng-content>
)
Angularのテンプレートは、中身の要素が空でも、閉じタグを書かなければなりません。Angular MaterialでSVG Iconを使うには次のように書きます。
<mat-icon svgIcon="home"></mat-icon>
ReactのMaterial UIのタグは次のように書きます。Angularはタグのプリフィックス必須なのでタグ名が長いのをおいといても長いです。
<Icon>home</Icon>
せめてこうしたいですよね?
<mat-icon>home</mat-icon>
ですが、前回紹介した@ContentChild
ではテキストタグにヒットさせる方法がなく、一度HTMLに書き出してから読み出す、という方法しかうまくいきませんでした。らこらこさんに教えていただいた方法を参考に試行錯誤した結果の方法が次の結果です。
まずは、<template>
タグを使ってテキストの中身を書き出します。<ng-content>
ではコメントになってしまうので、ふつうのHTMLの方のタグを使います。
<template #iconName><ng-content></ng-content></template>
<mat-icon [svgIcon]="name"></mat-icon>
次にコンポーネント側のコードです。まずは、 @ViewChild
を使ってこのタグを取得します。名前を使ってセレクトしています。その後実行されるのがngAfterViewInit()メソッドで、この中でこのテンプレート内のテキストを取り出してthis._name
変数に設定します。これはテンプレートの中でsvgIcon
に渡されるようになっているため、正しくAngular Materialのアイコンが利用できます。
import {
Component,
ElementRef,
ViewChild,
AfterViewInit,
Input
} from '@angular/core';
@Component({
selector: 'app-icon',
templateUrl: './icon.component.html',
styleUrls: ['./icon.component.scss']
})
export class IconComponent implements AfterViewInit {
private _name: string;
get name() {
return this._name;
}
@ViewChild('iconName')
private iconName: ElementRef<HTMLTemplateElement>;
constructor() {}
ngAfterViewInit() {
this._name = this.iconName.nativeElement.innerText;
}
}
なお、一度表示してから評価して、その結果をまた評価しているのでパフォーマンス上はよくない気がしています。
明日は @puku0x さんです。