AJAX とスニペット
機能がサーバーとブラウザに分かれることの多い現代のウェブアプリケーションにおいて、AJAX は欠かせないつなぎ役です。この領域で Nette Framework は何を提供するのでしょうか。
- テンプレートの一部、いわゆるスニペットの送信
- PHP と JavaScript のあいだの変数の受け渡し
- AJAX リクエストをデバッグするための道具
AJAX リクエスト
AJAX のリクエストは、本質的には通常の HTTP リクエストと変わりません。特定のパラメータでプレゼンターが呼ばれます。リクエストにどう応えるかはプレゼンター次第で、JSON 形式のデータを返すことも、HTML コードの一部や XML のドキュメントを送ることもできます。
ブラウザ側では、fetch() 関数で AJAX のリクエストを始めます。
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
// レスポンスを処理します
});
サーバー側では、HTTP リクエストを包むサービスの
$httpRequest->isAjax() メソッドで AJAX のリクエストを見分けます。検出には
X-Requested-With の HTTP
ヘッダーを使うので、それを送ることが決定的に重要です。プレゼンターの中では
$this->isAjax() メソッドが使えます。
JSON 形式でデータを送りたい場合は sendJson()メソッドを使います。このメソッドはプレゼンターの動作も終わらせます。
public function actionExport(): void
{
$this->sendJson($this->model->getData());
}
AJAX 用に用意した特別なテンプレートで応えるつもりなら、次のようにできます。
public function handleClick($param): void
{
if ($this->isAjax()) {
$this->template->setFile('path/to/ajax.latte');
}
// ...
}
スニペット
サーバーとクライアントをつなぐために Nette が提供する最も強力な道具がスニペットです。これを使えば、ごくわずかな手間と数行のコードで、ふつうのアプリケーションを AJAX のアプリケーションに変えられます。Fifteen の例が全体の動きを示していて、そのコードは GitHubにあります。
スニペットを使うと、ページ全体を読み込み直す代わりに、一部だけを更新できます。速く効率がよいだけでなく、より快適な利用体験も提供します。スニペットは Ruby on Rails の Hotwire や Symfony UX Turbo を思い起こさせるかもしれません。面白いことに、Nette はスニペットをその 14 年前に導入していました。
スニペットはどう動くのでしょうか。ページが最初に読み込まれるとき(AJAX
でないリクエスト)は、すべてのスニペットを含むページ全体が読み込まれます。ユーザーがページを操作すると(ボタンをクリックする、フォームを送信するなど)、ページ全体を読み込み直す代わりに
AJAX
のリクエストが始まります。プレゼンターのコードが処理を行い、どのスニペットを更新すべきかを決めます。Nette
はそのスニペットを描き、スニペットの配列を含む JSON
のペイロードとして送ります。ブラウザ側の処理コードが、受け取ったスニペットをページに戻します。こうして変わったスニペットのコードだけが転送されるので、ページの内容全体を転送する場合と比べて帯域を節約でき、読み込みも速くなります。redrawControl()
でスニペットが無効化されなければ、Nette は AJAX
のリクエストでもページ全体を返します。スニペットが送られるのは、何かが無効化されたときだけです。
Naja
ブラウザ側でスニペットを扱うには Naja ライブラリを使います。Node.js のパッケージとしてインストールしてください(Webpack、Rollup、Vite、Parcel などのバンドラーと使う場合)。
npm install naja
…あるいはページのテンプレートに直接書き込みます。
<script src="https://unpkg.com/naja@3/dist/Naja.min.js"></script>
まずライブラリを初期化する必要があります。
naja.initialize();
ふつうのリンク(シグナル)やフォームの送信を AJAX
のリクエストに変えるには、対象のリンク、フォーム、ボタンに ajax
クラスを付けるだけです。
<a n:href="go!" class="ajax">Go</a>
<form n:name="form" class="ajax">
<input n:name="submit">
</form>
または
<form n:name="form">
<input n:name="submit" class="ajax">
</form>
スニペットの再描画
Controlクラスのオブジェクト(プレゼンター自身も含みます)はすべて、再描画が必要な変更が起きたかどうかを覚えています。そのために
redrawControl() メソッドを使います。
public function handleLogin(string $user): void
{
// ログイン後に該当する部分を再描画する必要があります
$this->redrawControl();
// ...
}
Nette は、何を再描画すべきかをさらに細かく制御できます。このメソッドはスニペットの名前を引数に取れます。つまりテンプレートの部分の水準で無効化(すなわち再描画の強制)ができます。コンポーネント全体が無効化されると、その中のすべてのスニペットも再描画されます。
// 'header' スニペットを無効化します
$this->redrawControl('header');
第 2 パラメータ $redraw
を使えば、保留中の無効化を取り消すこともできます。$this->redrawControl('header', redraw: false)
を呼ぶと、そのスニペットは再描画不要と印を付けられます。完全なシグネチャは
redrawControl(?string $snippet = null, bool $redraw = true) です。
Latte でのスニペット
Latte
でスニペットを使うのはきわめて簡単です。テンプレートの一部をスニペットとして定義するには、{snippet}
と {/snippet} のタグで囲むだけです。
{snippet header}
<h1>Hello ... </h1>
{/snippet}
スニペットは、生成された特別な id を持つ <div> 要素を HTML
のページに作ります。スニペットが再描画されると、この要素の内容が更新されます。ですから、ページが最初に描かれるときには、たとえ最初は空でも、すべてのスニペットが描かれている必要があります。
n:属性を使えば、<div> 以外の要素でスニペットを作ることもできます。
<article n:snippet="header" class="foo bar">
<h1>Hello ... </h1>
</article>
スニペット領域
スニペットの名前は式にもできます。
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
これだけでは動かない途中の段階です。静的な {snippet} や {snippetArea}
の外で描かれた動的スニペットは、*Dynamic snippets are allowed only inside static snippet/snippetArea.*
というメッセージとともに E_USER_WARNING を出します。これは以下で直します。
これは item-0、item-1
といった複数のスニペットを作ります。動的スニペット(たとえば
item-1)を直接無効化しても、何も再描画されません。理由は、スニペットが本当に抜粋として働き、それ自身だけが直接描かれるからです。しかしテンプレートの中には、技術的には
item-1
という名前のスニペットは存在しません。それはスニペットを囲むコード、つまり foreach
ループが実行されてはじめて生まれます。ですから、実行が必要なテンプレートの部分に
{snippetArea} タグで印を付けます。
<ul n:snippetArea="itemsContainer">
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
</ul>
そして個々のスニペットと親の領域の両方の再描画を要求します。
$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');
あわせて、$items
配列には再描画すべき項目だけが入るようにするとよいでしょう。
{include}
タグを使って、スニペットを含む別のテンプレートを主テンプレートに取り込む場合は、そのテンプレートの取り込みをもう一度
snippetArea で包み、スニペットとともに無効化する必要があります。
{snippetArea include}
{include 'included.latte'}
{/snippetArea}
{* included.latte *}
{snippet item}
...
{/snippet}
$this->redrawControl('include');
$this->redrawControl('item');
コンポーネントの中のスニペット
コンポーネントの中にもスニペットを作れ、Nette
が自動的に再描画します。ただし制限があります。スニペットを再描画するために、Nette は
render()
メソッドをパラメータなしで呼びます。ですからテンプレートでパラメータを渡しても働きません。
OK
{control productGrid}
動きません:
{control productGrid $arg, $arg}
{control productGrid:paginator}
独自データの送信
スニペットとあわせて、任意の追加データをクライアントに送れます。payload
オブジェクトに書き込むだけです。
public function actionDelete(int $id): void
{
// ...
if ($this->isAjax()) {
$this->payload->message = 'Success';
}
}
リダイレクト
AJAX のリクエスト中、redirect() と redirectUrl() メソッドは HTTP
のリダイレクトを送りません。代わりに、AJAX
のレスポンスで送られるデータオブジェクトであるペイロードの payload.redirect
プロパティに宛先の URL
を書き込んで送ります。実際のリダイレクトはクライアント側のライブラリ(Naja)が行います。
パラメータの受け渡し
AJAX
のリクエストでコンポーネントにパラメータを送るとき、それがシグナルのパラメータであれ永続パラメータであれ、コンポーネント名を含むグローバルな名前をリクエストで指定しなければなりません。getParameterId()
メソッドが完全なパラメータ名を返します。
let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
そしてコンポーネント側の、対応するパラメータを持つ handle メソッドです。
public function handleFoo(int $bar): void
{
}