コンテンツにスキップ

保存ビュー

現在の検索のクエリ文字列に名前を付けて、サーバー側に保存します。 保存はフィルタフォームのペアと名前を POST し(data-hx-include)、 ビューの適用はただの GET リンク — ブックマーク可能・共有可能・ クライアント状態ゼロ — で、削除は再レンダリングされたストリップで 応答します。ビューを適用すると、サーバーがフィルタフォームを値入りで 再レンダリングします — ビューが展開するクエリ文字列は常に見えるので、 ビューが不透明になることはありません。新規 JavaScript ゼロ。 バージョニングポリシーの 対象です。

別名: 保存した検索、保存フィルター。

フィルタして、結果に名前を付けて Save view — 新しいチップが current 印付きでストリップに入ります。チップをクリックすると適用 (フィルタコントロールが埋まるのを見てください)、× で削除、同じ 名前を 2 回保存すると 422 ブランチです。このデモは何も保存しません: 各チップのリンクがビューの完全なクエリ文字列を運びます — 実アプリは ユーザーごとにビューを保存します。

Who can see it

No saved views yet — filter, then save the result under a name.

<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 の 中身であり、クライアント側でチップを継ぎ足すことはありません。 各チップはただの適用リンクと、ビュー名を含むアクセシブルネームを 持つ削除ボタンです。

ケース応答
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 エンコード)。ビューは 定義からしてユーザーごとです — 保存方法や上限はサーバーの領分で、 ワイヤ契約は同じです。適用したクエリ文字列がコントロールに戻るため、 ビューは編集可能なままです: 適用して、フィールドを直して、別名で 保存。

ビューを適用してから条件を 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 を指し、適用レスポンスにフォームを埋めさせてください。

呼び出しは画面側に置きます。フィルタのエディタの中ではありません。 ビューは名前の付いた 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" を持つかもサーバーが決めます — 帯のときと同じ 判断です。

実際に置いた例は データグリッドページのテンプレート にあります。

名前だけを訊くダイアログは、残り 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 をクリップボードに載せます。共有オブジェクトは要りません。「あの一覧 送って」の大半はリンクで足ります。共有ビューは、会話より長生きする 標準のために取っておきましょう。

ビューは「また尋ねたい問い」であって「立っていた場所」ではありません。

入れる入れない
フィルタ条件ページ番号
ソート(順序つき複数列)行の選択
列構成・列順(ビューが固定する場合)スクロール位置
ページサイズ、グルーピング展開中の行

危険なのはページ番号です。「昨日のデータの 7 ページ目」に意味はなく、 共有リンクを踏んだ相手は別の場所に着きます。

列はまず設定です。ユーザーの列選択は画面をまたいで付いてくるもの なので、すべての URL に押し込むのではなくユーザー単位で保存します。 ビューは列構成を固定できます — 「出荷チェック」はたいてい条件と その仕事に必要な列の両方を意味します — その場合、適用時に列が変わる ことを見える形で示し、戻る手段を用意します。解決順序は常に URL → ユーザー設定 → アプリ既定で、これが共有リンクを保存済み レイアウトより優先させます。

  • **スコープ。**ビューは必ずしも個人のものではありません。部門標準が 普通のケースです。スコープはサーバーが持ち、共有ビューにはラベルを 付け、その編集は明示的な操作にしてください。同僚のビューを黙って 分岐させるのではなく「自分のビューにコピー」を提供します。
  • **既定。**素の一覧 URL は既定ビューの URL へ 303 して構いません。 アドレスバーに実条件が出るので、既定が隠れフィルタになりません。
  • **再認可。**保存ビューには、後から実行権限を失った条件が残りえます。 適用のたびに再検査し、fail closed403 か再入力)にしてください。 黙って条件を落とすと結果が広がります。業務画面が最も許容できない 失敗です。
  • **並び順。**5 件までは快適、30 件で機能不全です。ピン留めと最近使用を 先頭に、あふれはメニューへ、打ち切ったならそう述べてください。

プログレッシブエンハンスメント

Section titled “プログレッシブエンハンスメント”

適用リンクは本物の href です — ブックマーク可能・共有可能で、 JavaScript なしではフルページ遷移になります。保存フォームは method="post" + action="/views" を保つので、ネイティブ送信も 機能します(フルページか、古典的な 303 post/redirect/get で応答)。 削除ボタンは htmx 専用です。JavaScript なしでも削除が必要なら、 小さな POST フォームで包んでください。

  • ストリップは本物のリストです — 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 — ビューで選んだ結果リストを新鮮に保つ方法。