空状態の監視
stimeo--empty-state
リストが空になったら空表示を出し、1 件でも入ったら隠す。
stimeo--empty-state コントローラは MutationObserver でリストを監視し、項目数 0 ↔ 1+ の境界でリストと空プレースホルダを切り替えます。Turbo Stream の行に対して件数監視を手書きする手間を肩代わりします。リストの要素子(または itemSelector に一致する子)を数え、list / empty ターゲットの hidden を切り替え、コントローラ要素に data-empty / data-count を反映し、境界を越えると stimeo--empty-state:change を発火します。announce を付けると(作者が未設定の場合)empty ターゲットを polite なライブリージョンにします。ライブラリは挙動のみで、プレースホルダの見た目や文言は利用側が持ちます。状態は DOM から都度判定する(モジュールスコープ状態なし)ため、connect で Turbo Stream 差し込み後も再同期し、observer は disconnect(Turbo 遷移含む)で切断します。
- 項目 1
- 項目 2
まだ何もありません — 項目を追加してください。
<%# Empty-state demo: add/remove items and the empty placeholder toggles itself. The
controller observes the list with a MutationObserver, toggles hidden on the
list / empty targets at the 0 <-> 1+ boundary, and reflects data-empty / data-count.
demo.js adds/removes <li> (standing in for Turbo Stream rows). This demo only styles
it; the empty placeholder's copy is owned here. %>
<div
class="empty-state-demo"
data-controller="stimeo--empty-state"
data-stimeo--empty-state-announce-value="true">
<div class="empty-state-demo__bar">
<button
type="button"
class="demo-trigger"
data-empty-state-add
data-item-label="<%= t("components.empty_state.demo.item") %>">
<%= t("components.empty_state.demo.add") %>
</button>
<button type="button" class="demo-trigger" data-empty-state-clear>
<%= t("components.empty_state.demo.clear") %>
</button>
</div>
<ul class="empty-state-demo__list" data-stimeo--empty-state-target="list">
<li><%= t("components.empty_state.demo.item") %> 1</li>
<li><%= t("components.empty_state.demo.item") %> 2</li>
</ul>
<p class="empty-state-demo__empty" data-stimeo--empty-state-target="empty" hidden>
<%= t("components.empty_state.demo.empty") %>
</p>
</div>
/*
* Presentation-only styles for the empty-state demo.
* The library toggles `hidden` on the list / empty targets and reflects
* data-empty / data-count; this CSS only lays out the list, items, and placeholder.
*/
.empty-state-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 28rem;
}
.empty-state-demo__bar {
display: flex;
gap: 0.5rem;
}
.empty-state-demo__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.empty-state-demo__list li {
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
background: var(--surface-subtle);
}
.empty-state-demo__empty {
margin: 0;
padding: 1.5rem;
border: 1px dashed var(--border);
border-radius: 0.5rem;
text-align: center;
color: var(--color-text-muted);
}
// Empty-state demo (consumer-side JS).
//
// The controller watches the list with a MutationObserver and toggles the empty
// placeholder on its own. This catalog has no Turbo Stream backend, so here the
// buttons add/remove <li> rows — exactly the kind of mutation Turbo Stream would
// make — and the controller reacts to them.
document.querySelectorAll(".empty-state-demo").forEach((root) => {
const list = root.querySelector('[data-stimeo--empty-state-target="list"]');
const add = root.querySelector("[data-empty-state-add]");
const label = add?.dataset.itemLabel;
if (!list) return;
let count = list.children.length;
add?.addEventListener("click", () => {
const li = document.createElement("li");
li.textContent = `${label ?? "Item"} ${(count += 1)}`;
list.appendChild(li);
});
root.querySelector("[data-empty-state-clear]")?.addEventListener("click", () => {
list.replaceChildren();
});
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--empty-state"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
list
必須
|
子要素を数える監視対象コンテナ。 | data-stimeo--empty-state-target="list" |
empty
|
リストが空のときに表示するプレースホルダ。 | data-stimeo--empty-state-target="empty" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
itemSelector
|
一致する子のみ数えるセレクタ。空なら全要素子を数える。 | data-stimeo--empty-state-item-selector-value |
announce
|
empty ターゲットを polite ライブリージョンにし、表示を読み上げさせるか(既定 false)。 |
data-stimeo--empty-state-announce-value |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
0 ↔ 1+ の境界を越えたときに発火。detail.count / detail.empty を伴う。 |
stimeo--empty-state:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
hidden |
list / empty ターゲット | リスト(1+)か空プレースホルダ(0)を出すために切り替え。 |
data-empty |
コントローラ要素 | 項目数が 0 のときに付与。 |
data-count |
コントローラ要素 | 現在の項目数。 |