動的フィールド行
stimeo--nested-form
明細行を足したり消したりします。Rails の入れ子フォームの作法どおりに送信されます。
フォームの明細行を、テンプレートから複製して足します。連番は自動で振られるので、名前がぶつかりません。消すときは、まだ保存していない行はその場で取り除き、すでに保存されている行は削除の印を立てて隠します。送信すれば Rails 側が消してくれます。行の状態は画面の中だけにあるので、Turbo でページが差し替わってもそのまま数え直せます。足したときは新しい行の先頭へ、消したときは隣の行へフォーカスが移るので、キーボードの居場所を見失いません。今の行数は読み上げにも知らせられます。最小と最大の行数も決められます。マークアップも CSS も持ちません。
キーボード操作
このコンポーネント自体はキーボード操作を持ちません。
<%# Markup for the nested / dynamic fields demo (invoice-style line items).
The controller clones the <template> row (renumbering __INDEX__), removes rows by
dropping unsaved ones / flagging _destroy on persisted ones, enforces min/max, and
hands the row count to the shared stimeo--announcer your app seats once, in its
layout (count changes are read out without moving focus away; the row list owns no
live region of its own). Remove buttons are handled by delegation, so cloned rows
need no per-row wiring. %>
<%
item_label = t("components.nested_form.demo.item_label")
remove_label = t("components.nested_form.demo.remove")
count_message = t("components.nested_form.demo.count_message")
persisted_value = t("components.nested_form.demo.persisted_value")
%>
<div class="nested-demo" data-controller="stimeo--nested-form"
data-stimeo--nested-form-min-value="1"
data-stimeo--nested-form-max-value="5"
data-stimeo--nested-form-count-message-value="<%= count_message %>">
<div class="nested-demo__list" data-stimeo--nested-form-target="list">
<fieldset class="nested-demo__row">
<label class="nested-demo__field">
<span><%= item_label %></span>
<input type="text" class="demo-input" name="order[items_attributes][0][name]">
</label>
<button type="button" class="demo-trigger"
data-stimeo--nested-form-target="remove"><%= remove_label %></button>
</fieldset>
<%# A persisted row: its _destroy flag makes Remove hide it instead of deleting it. %>
<fieldset class="nested-demo__row">
<input type="hidden" name="order[items_attributes][1][_destroy]" value="0"
data-stimeo--nested-form-target="destroyFlag">
<label class="nested-demo__field">
<span><%= item_label %></span>
<input type="text" class="demo-input" value="<%= persisted_value %>"
name="order[items_attributes][1][name]">
</label>
<button type="button" class="demo-trigger"
data-stimeo--nested-form-target="remove"><%= remove_label %></button>
</fieldset>
</div>
<template data-stimeo--nested-form-target="template">
<fieldset class="nested-demo__row">
<label class="nested-demo__field">
<span><%= item_label %></span>
<input type="text" class="demo-input" name="order[items_attributes][__INDEX__][name]">
</label>
<button type="button" class="demo-trigger"
data-stimeo--nested-form-target="remove"><%= remove_label %></button>
</fieldset>
</template>
<button type="button" class="demo-trigger" data-stimeo--nested-form-target="add"
data-action="click->stimeo--nested-form#add">
<%= t("components.nested_form.demo.add") %>
</button>
</div>
/*
* Presentation-only styles for the nested / dynamic fields demo.
* The library clones rows, flags/hides destroyed ones, and reflects
* data-nested-count / data-nested-at-min / data-nested-at-max; this CSS owns the
* row layout and the disabled add-button affordance.
*/
.nested-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 32rem;
}
.nested-demo__list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.nested-demo__row {
display: flex;
align-items: flex-end;
gap: 0.5rem;
margin: 0;
padding: 0.6rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
}
.nested-demo__field {
display: flex;
flex-direction: column;
gap: 0.25rem;
flex: 1;
font-size: 0.85rem;
}
/* The library disables the add button once data-nested-at-max is reached. */
.nested-demo .demo-trigger[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
// Consumer-side JS for the nested-form demo (demo-only).
// The controller handles add/remove, focus, and the count announcement on its own;
// this only logs the add/remove events to show the event contract consumers can
// hook for analytics, totals, or enabling a submit button.
document.addEventListener("stimeo--nested-form:add", (event) => {
console.log(`[nested-form] added row index=${event.detail.index}`);
});
document.addEventListener("stimeo--nested-form:remove", (event) => {
console.log(`[nested-form] removed row persisted=${event.detail.persisted}`);
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--nested-form"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
list
必須
|
新規行を追加し、件数を数える対象のコンテナ。 | data-stimeo--nested-form-target="list" |
template
必須
|
各新規行の複製元となる <template>。 |
data-stimeo--nested-form-target="template" |
add
|
追加ボタン。max 指定時のみ上限到達で自動無効化し、authored の disabled は teardown で復元する。 | data-stimeo--nested-form-target="add" |
remove
|
行ごとの削除ボタン(委譲で処理)。 | data-stimeo--nested-form-target="remove" |
destroyFlag
|
既存行の hidden な _destroy。削除時に 1 を設定する。 |
data-stimeo--nested-form-target="destroyFlag" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
min
|
最小行数。ここで削除が止まる(既定 0)。 | data-stimeo--nested-form-min-value |
max
|
最大行数。0 は無制限(既定 0)。 | data-stimeo--nested-form-max-value |
indexPlaceholder
|
テンプレ内で行ごとに置換するプレースホルダ(既定 __INDEX__)。 |
data-stimeo--nested-form-index-placeholder-value |
announce
|
件数変化を告知するか(既定 true)。 |
data-stimeo--nested-form-announce-value |
countMessage
|
{count} トークンを含む読み上げテンプレート(既定は空)。 |
data-stimeo--nested-form-count-message-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
add
|
テンプレートを複製・採番して挿入し、新規行へフォーカスする。 | stimeo--nested-form#add |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
add
|
行追加後に発火。detail に index と element を伴う。 |
stimeo--nested-form:add |
remove
|
行削除後に発火。detail に element と persisted を伴う。 |
stimeo--nested-form:remove |
reconcile
|
外部由来の行増減や実行時の min/max 変更で公開状態が動いたとき発火。detail に count / atMin / atMax を伴う。 |
stimeo--nested-form:reconcile |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-nested-count |
ルート要素 | 現在の有効(破棄されていない)行数。 |
data-nested-at-min / data-nested-at-max |
ルート要素 | 行数が下限 / 上限に達したとき "true"。 |
hidden |
既存行 | _destroy を立てたとき付与され、送信時に削除される。 |