Laravel 5.4 では Blade の新しい記法として Component が導入されました。
これは、既存のセクションとレイアウトに似てはいるもののより複雑な継承を可能としてい、WebComponentやVue.jsのようなFWのコンポーネントと似た概念を持っています。
Blade のコンポーネント
Blade のコンポーネントは、例えば Laravel 5.4 で導入された Markdown Notification で使用されています。
Markdown Notification については別途記事を書いたので、そちらをご覧ください。
で、「JS FWのコンポーネントとも似たコンポーネント」とは何か。
スロットの概念
たとえば Vue でコンポーネントを呼び出す時、外からコンポーネントに渡すデータとしてプロパティ(バインド)とスロットの 2種類の渡し方が存在します。
一般的に前者は値や状態、オブジェクトを、後者は HTML として渡す場合といった感じでしょうか。
例えば、Vue.js公式ドキュメントに掲載されている単一スロットのサンプルはこんな感じです。
1 2 3 4 5 6 7 |
<div> <h1>I'm the parent title</h1> <my-component> <p>This is some original content</p> <p>This is some more original content</p> </my-component> </div> |
Vue ではコンポーネントにスロットとして渡した中身は <slot />
内でレンダリングされます。
また、複数のスロットを設定できる名前付きスロットもありますね。
1 2 3 4 5 6 |
<app-layout> <h1 slot="header">Here might be a page title</h1> <p>A paragraph for the main content.</p> <p>And another one.</p> <p slot="footer">Here's some contact info</p> </app-layout> |
もちろん忘れてはいけないプロパティも存在します。
1 |
<child my-message="hello!"></child> |
Blade のコンポーネントでは、これら同様の機能が利用できます。
Blade でコンポーネントを呼び出す
Blade ではコンポーネントを呼び出すのに @component
ディレクティブを使用します。(IDEAのLaravelプラグインがまだ未対応?)
@component('component-name', [ vars ])
~ @endcomponent
内に書いたコードは、デフォルトで無名スロットとしてコンポーネントに渡される他、@slot('slotname')
~ @endslot
で囲ってやればその部分は名前付きスロットとして渡されます。
例えば、Vueの無名スロットのサンプルの場合は Blade ではこんな感じ。
1 2 3 4 5 6 7 |
<div> <h1>I'm the parent title</h1> @component('my-component') <p>This is some original content</p> <p>This is some more original content</p> @endcomponent </div> |
1 2 3 4 |
<div> <h2>I'm the child title</h2> {{ $slot }} </div> |
描画される結果はVueと同じ感じですね。
1 2 3 4 5 6 7 8 |
<div> <h1>I'm the parent title</h1> <div> <h2>I'm the child title</h2> <p>This is some original content</p> <p>This is some more original content</p> </div> </div> |
おっと、このblade、slotを {{ }}
で囲ってます。大丈夫なんでしょうか?
……大丈夫です。スロットはレンダリングされた後に HTMLString
としてコンポーネントに渡されるため、{{ }}
はエスケープを行いません。
では名前付きスロットの場合も同様に。
1 2 3 4 5 6 7 8 9 10 |
@component('app.layout') @slot('header') <h1>Here might be a page title</h1> @endslot <p>A paragraph for the main content.</p> <p>And another one.</p> @slot('footer') <p>Here's some contact info</p> @endslot @endcomponent |
1 2 3 4 5 6 7 8 9 10 11 |
<div class="container"> <header> {{ $header }} </header> <main> {{ $slot }} </main> <footer> {{ $footer }} </footer> </div> |
おんなじ感じですね。レンダリングされたものは……
1 2 3 4 5 6 7 8 9 10 11 12 |
<div class="container"> <header> <h1>Here might be a page title</h1> </header> <main> <p>A paragraph for the main content.</p> <p>And another one.</p> </main> <footer> <p>Here's some contact info</p> </footer> </div> |
この通り。
あとVueでいうプロパティもありますね。Notificationで使われているボタンコンポーネントはスロットと併用しています。簡素化してボタンコンポーネントを作ってみます。
1 2 3 |
@component('button', ['url' => 'https://example.com/']) すっごーい! @endcomponent |
1 |
<a class="btn btn-primary" href="{{ $url }}">{{ $slot }}</a> |
なんだか見慣れた書き方ですね。
ちなみに {{ $url }}
の方は通常の文字列として渡しているのでエスケープされます。親次第でスロットとしても文字列としても同じ変数を渡せてしまうので、テンプレート同士の信頼は必要ですね。
ちなみに、そのままだと親でスロットが指定されていないと
ErrorException でコケてしまいます。
スロットやプロパティを省略可能にする場合、
@if
で変数の存在確認をして使う{{ $hoge or "default text" }}
のようにデフォルト・フォールバックテキストを用意しておく
などと言った方法が利用できます。
ページ内で好きな場所で何度でも使えるのはコンポーネントの強みです。
複雑になりうるコンポーネント
おそらく使い方次第なのですが、冒頭でも紹介している Markdown Notification について、わりと関係性が複雑になっているのがわかります。
図にしてみました。
省略しまくってるのにごっちゃごっちゃしすぎてわけがわからないよ……
コンポーネントは何階層にも継承しやすく、何並列にも作りやすい分、使いようによってはメインとなるテンプレートにあちこちに飛ぶものが書けたり、親子の関係性がよくわからなくなってきたりします。便利だからと言って複雑にしすぎてはよくない気が……
レンダリング順は注意
そもそも普通はViewテンプレート上のコードの実行順に依存するコードを書かないと思いますが、多少意識するとややこしいことに気付きます。これは従来のsection & layout でも同様に言えることですが、実質的には「内側から実行される」ことになるのですね。
特定のコンポーネントを呼び出す場合、まずスロット内が実行され、その結果がコンポーネントに渡され、実行される。複数階層あれば繰り返しですね。
といっても結局bladeだって実行可能なPHPにコンパイルされ上から実行されるんです。上から実行されて @endcomponent
のところでコンポーネントがレンダリングされる、ただそれだけです。
まあ詳しくは Illuminate/View/Concerns/ManagesComponents.php あたりをみれば分かると思います。
Laracast みるさ
Component を動画で理解するには Laracast の What’s new がやっぱり分かりやすいです。
英語だけど英語わからなくてもなんとなくわかるでしょう()
……resources
の中だけでもVue (or other) とbladeとコンポーネントが2種類存在するようになったけどまああれだ……コンポーネント指向の時代だししゃあない。