保存ビュー
現在の検索のクエリ文字列に名前を付けて、サーバー側に保存します。
保存はフィルタフォームのペアと名前を POST し(data-hx-include)、
ビューの適用はただの GET リンク — ブックマーク可能・共有可能・
クライアント状態ゼロ — で、削除は再レンダリングされたストリップで
応答します。ビューを適用すると、サーバーがフィルタフォームを値入りで
再レンダリングします — ビューが展開するクエリ文字列は常に見えるので、
ビューが不透明になることはありません。新規 JavaScript ゼロ。
バージョニングポリシーの
対象です。
別名: 保存した検索、保存フィルター。
フィルタして、結果に名前を付けて Save view — 新しいチップが
current 印付きでストリップに入ります。チップをクリックすると適用
(フィルタコントロールが埋まるのを見てください)、× で削除、同じ
名前を 2 回保存すると 422 ブランチです。このデモは何も保存しません:
各チップのリンクがビューの完全なクエリ文字列を運びます — 実アプリは
ユーザーごとにビューを保存します。
マークアップ
Section titled “マークアップ”<form id="filters" action="/items" method="get" data-hx-get="/items" data-hx-target="#results"> <!-- …the filter fields: q, status, …, and an Apply button… --></form>
<form method="post" action="/views" data-hx-post="/views" data-hx-include="#filters" data-hx-target="#views" data-hx-disabled-elt="find button[type=submit]"> <div class="hc-field"> <label class="hc-field__label" for="view-name">View name</label> <input class="hc-input" id="view-name" name="name" required> </div> <button class="hc-button" data-variant="primary" type="submit">Save view</button></form>
<div id="views"> <ul class="hc-chips"> <li class="hc-chip"> <a href="/items?view=quarterly" data-hx-get="/items?view=quarterly" data-hx-target="#results">quarterly</a> <button class="hc-button" data-size="sm" type="button" aria-label="Delete view quarterly" data-hx-delete="/views/quarterly" data-hx-target="#views">×</button> </li> </ul></div>
<div id="results" aria-live="polite"></div>保存フォームは data-hx-include="#filters" でフィルタのペアを
取り込み、ストリップ領域を target にします — 応答は #views の
中身であり、クライアント側でチップを継ぎ足すことはありません。
各チップはただの適用リンクと、ビュー名を含むアクセシブルネームを
持つ削除ボタンです。
サーバー応答契約
Section titled “サーバー応答契約”| ケース | 応答 |
|---|---|
POST /views(name + フィルタのペア) | 200 + ストリップフラグメント。新しいチップに current 印(aria-current="true") |
| 名前が重複 | 422 + インラインフィールドエラー付きのストリップ(field-errors の形、data-field="name")— 標準の 422 許可でスワップ |
GET /items?view=<name> | そのビューのフィルタを適用した #results 用リストフラグメント + 値入りフィルタフォームの OOB outerHTML 再レンダリング — ビューは不透明になりません |
DELETE /views/<name> | 200 + ストリップ |
ビューの名前がキーです(DELETE パスでは URL エンコード)。ビューは
定義からしてユーザーごとです — 保存方法や上限はサーバーの領分で、
ワイヤ契約は同じです。適用したクエリ文字列がコントロールに戻るため、
ビューは編集可能なままです: 適用して、フィールドを直して、別名で
保存。
変更済み状態
Section titled “変更済み状態”ビューを適用してから条件を 1 つ変える — 保存ビューで最もよく行われ、 最も手当てされていない操作です。いま見ているものがまだそのビューなのか、 何も教えてくれません。ユーザーは調整を失うか、画面にない保存内容を 信じることになります。
適用リンクは自分の出所を名乗ります(&from-view=<name>)。これにより
サーバーは受信した条件と保存済みの条件を比較でき、違えばチップに印を
付けられます。
<li class="hc-chip" data-modified> <a href="…" aria-current="true">Overdue shipments</a> <span class="hc-badge" data-variant="warning">Modified</span> <button type="button" data-hx-put="/views/Overdue%20shipments" data-hx-include="#filters" data-hx-target="#views">更新</button> <a href="/items?view=Overdue%20shipments">元に戻す</a></li>比較は正規化したクエリストリングで行ってください(同じパラメータ、 ソート済み)。そうしないと、フォーム経由か リンク経由かで同じ問いが 別物に見えます。
PUT /views/<name> はその場で更新します。ビューを直しても名前が
残るので、既に共有されたリンクも生き続けます。422 duplicate は残り
ますが、本当に新規の名前が衝突した場合だけです。
「元に戻す」はリンクであって <button type="reset"> ではありません。
ネイティブのリセットが戻すのはサーバーがコントロールに描画した値 —
つまり適用後に手を入れた変更済みの状態そのものです。ビュー自身の URL
を指し、適用レスポンスにフォームを埋めさせてください。
呼び出しはどこに置くか
Section titled “呼び出しはどこに置くか”呼び出しは画面側に置きます。フィルタのエディタの中ではありません。 ビューは名前の付いた URL なので、適用はナビゲーションです。ダイアログの 奥に置くと画面で最も頻度の高い操作が 4 手になり、「そこへ行く」動詞が 「条件を組み立てる」面に混ざります。組み立てたものに名前を付けるのは 組み立ての終端なので、保存はエディタに残し、呼び出しは画面 タイトルの隣に置きます。
上のチップの帯は少数向けの形です。ビューが数個を超えたら、同じリンクを
hc-menu として描画し、
ボタンのラベルを適用中のビュー名そのものにします。
<button class="hc-button" id="view-trigger" popovertarget="views" type="button"> <span>Overdue shipments</span> <span class="hc-badge" data-variant="warning">Modified</span></button>
<div class="hc-menu" id="views" popover role="menu" aria-labelledby="view-trigger"> <a class="hc-menu__item" role="menuitemradio" aria-checked="true" href="/items?view=overdue">Overdue shipments</a> <a class="hc-menu__item" role="menuitemradio" aria-checked="false" href="/items">Show everything</a></div>menuitemradioです。適用されているビューは常に 1 つで、 Show everything がその「どれでもない」選択肢 — 既定ビューが素の URL をリダイレクトしているときの戻り道になります。- 中身は本物の
<a href>のままです。ブックマークでき、中クリックで開け、 JavaScript なしでも動きます。 - メニューを描画するのはサーバーなので、ピン留めや最近使った順、どの項目
が
aria-checked="true"を持つかもサーバーが決めます — 帯のときと同じ 判断です。
実際に置いた例は データグリッドページのテンプレート にあります。
保存時に訊くことは 3 つ
Section titled “保存時に訊くことは 3 つ”名前だけを訊くダイアログは、残り 2 つの決定を「後で気付いた人」に押し 付けます。
| 項目 | ここで訊く理由 |
|---|---|
| 名前 | ビューのキー。view= と DELETE のパスに URL エンコードして載る |
| スコープ(個人 / 共有) | 部門標準は業務ソフトでは普通のケースで、同僚のビューを黙って分岐させるのは事故 |
| 既定 | 画面が違う問いで開くと毎日 1 手無駄になる。素の一覧 URL はここへ 303 するので、アドレスバーには実際の条件が出る |
<fieldset class="hc-field"> <legend class="hc-field__label">Who can see it</legend> <label class="hc-radio-label"> <input class="hc-radio" type="radio" name="scope" value="personal" checked> Only me </label> <label class="hc-radio-label"> <input class="hc-radio" type="radio" name="scope" value="shared"> My team </label></fieldset>
<label class="hc-checkbox-label"> <input class="hc-checkbox" type="checkbox" name="default" value="1"> Open this screen on it</label>- **既定は多くても 1 つ。**2 つの問いで開く画面に既定はありません。新しい 既定を保存したら古い方は外れます。
- スコープと既定は条件ではありません。
PUT /views/<name>が直すのは ビューが訊いていることであって、黙って持ち主を変えたり既定を移したり してはいけません。 - **共有ビューには印を付けます。**他人の標準を編集するのは目に見える別の 操作にして、分岐ではなく「自分のビューにコピー」を提供します。
**Copy link は Save の隣に置きます。**ビューは URL そのものだからです。
data-hc-copy-text が適用 URL
をクリップボードに載せます。共有オブジェクトは要りません。「あの一覧
送って」の大半はリンクで足ります。共有ビューは、会話より長生きする
標準のために取っておきましょう。
ビューが捉えるもの
Section titled “ビューが捉えるもの”ビューは「また尋ねたい問い」であって「立っていた場所」ではありません。
| 入れる | 入れない |
|---|---|
| フィルタ条件 | ページ番号 |
| ソート(順序つき複数列) | 行の選択 |
| 列構成・列順(ビューが固定する場合) | スクロール位置 |
| ページサイズ、グルーピング | 展開中の行 |
危険なのはページ番号です。「昨日のデータの 7 ページ目」に意味はなく、 共有リンクを踏んだ相手は別の場所に着きます。
列はまず設定です。ユーザーの列選択は画面をまたいで付いてくるもの なので、すべての URL に押し込むのではなくユーザー単位で保存します。 ビューは列構成を固定できます — 「出荷チェック」はたいてい条件と その仕事に必要な列の両方を意味します — その場合、適用時に列が変わる ことを見える形で示し、戻る手段を用意します。解決順序は常に URL → ユーザー設定 → アプリ既定で、これが共有リンクを保存済み レイアウトより優先させます。
スコープ・既定・再認可
Section titled “スコープ・既定・再認可”- **スコープ。**ビューは必ずしも個人のものではありません。部門標準が 普通のケースです。スコープはサーバーが持ち、共有ビューにはラベルを 付け、その編集は明示的な操作にしてください。同僚のビューを黙って 分岐させるのではなく「自分のビューにコピー」を提供します。
- **既定。**素の一覧 URL は既定ビューの URL へ
303して構いません。 アドレスバーに実条件が出るので、既定が隠れフィルタになりません。 - **再認可。**保存ビューには、後から実行権限を失った条件が残りえます。
適用のたびに再検査し、fail closed(
403か再入力)にしてください。 黙って条件を落とすと結果が広がります。業務画面が最も許容できない 失敗です。 - **並び順。**5 件までは快適、30 件で機能不全です。ピン留めと最近使用を 先頭に、あふれはメニューへ、打ち切ったならそう述べてください。
プログレッシブエンハンスメント
Section titled “プログレッシブエンハンスメント”適用リンクは本物の href です — ブックマーク可能・共有可能で、
JavaScript なしではフルページ遷移になります。保存フォームは
method="post" + action="/views" を保つので、ネイティブ送信も
機能します(フルページか、古典的な 303 post/redirect/get で応答)。
削除ボタンは htmx 専用です。JavaScript なしでも削除が必要なら、
小さな POST フォームで包んでください。
アクセシビリティ
Section titled “アクセシビリティ”- ストリップは本物のリストです —
ul.hc-chipsは「リスト、N 項目」と 読み上げられ、aria-current="true"が現在のビューを示します。 - どの × も、素のグリフではなくビュー名を含むアクセシブルネーム (「Delete view quarterly」)を持ちます。
- 結果領域は
aria-live="polite"なので、ビューの適用はフォーカスを 奪わずに新しいリストを読み上げます。名前重複のエラーはrole="alert"の field-errors フラグメントです。 - フィルタの OOB 再レンダリングは id を安定させたままなので、
<label for>の関連付けが保たれます。
- filter-popover — フィルタフォーム自体がよく住む場所。
- live-search —
同じ
q入力の逐次検索版。 - field-errors — 名前重複ブランチが応答する 422 の形。
- data-region — ビューで選んだ結果リストを新鮮に保つ方法。