ライブカウンタ
stimeo--live-counter
開いている全タブで同時に動くカウンタ — HTML と broadcast 1 本だけ。
stimeo--live-counter は、いいね・閲覧数・残席などの数値を Action Cable ストリームに束ね、購読中の全クライアントで同時に動かします。ローカルの increment アクションは表示を楽観的に即時加算しつつ、チャンネルへ increment を perform します。数を所有するのはサーバで、永続化して結果を broadcast します。照合するワイヤ契約は 2 形あります。推奨の絶対値 { count }(自然に冪等 — このカタログの LikesChannel が実演)と、差分 { delta, by } (by が自分の id と一致する受信は自分のエコーとしてスキップ。楽観加算で適用済みのため)です。DOM のテキストが唯一の真実で、初期値はサーバレンダリング — Turbo のキャッシュ復元や Morph 後も照合処理は不要です。1 回あたりのステップはアクションパラメータ data-stimeo--live-counter-delta-param で指定します(既定 1)。数値の書式や永続化はスコープ外(ロケール書式が要る場合は change を購読)。opt-in サブパス stimeo-ui/cable に同梱で、コアはゼロ依存のままです。
このページをもう 1 つのタブで開き、どちらかで「いいね」を押してください。サーバが所有するカウントが両方のタブで収束します。自分のクリックは楽観的に即時加算され、 broadcast の絶対値がそれを確定します。
0 件
<%# live-counter: the count's single source of truth is the DOM text
(server-rendered below from the channel's demo counter) plus the
LikesChannel stream. Clicking Like bumps the number optimistically and
performs increment; the server adds it to its counter and broadcasts the
absolute { count }, so every subscribed tab converges — open a second tab
and click in either. The trigger target keeps the button disabled while an
increment would be dropped (before the subscription confirms, during an
outage, after a rejection). The client id is randomized per render purely
for the demo (a real app passes current_user.id). The room comes from the
channel's allowlisted constant so the subscription params and the
server-rendered initial value below can never drift (a real app passes its
record id, e.g. post.id). %>
<% room = LikesChannel::ROOM %>
<div class="live-counter-demo"
data-controller="stimeo--live-counter"
data-stimeo--live-counter-channel-value="LikesChannel"
data-stimeo--live-counter-params-value='{"room":"<%= room %>"}'
data-stimeo--live-counter-id-value="<%= SecureRandom.hex(4) %>">
<p class="live-counter-demo__hint"><%= t("components.live_counter.demo.hint") %></p>
<div class="live-counter-demo__row">
<button type="button" class="demo-trigger live-counter-demo__button"
data-stimeo--live-counter-target="trigger"
data-action="stimeo--live-counter#increment">
<svg class="demo-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2
-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7Z"/>
</svg>
<%= t("components.live_counter.demo.like") %>
</button>
<p class="live-counter-demo__count">
<%# Server-rendered initial value: reading the channel's in-memory demo
counter keeps the DOM truth aligned with the server (a real app
renders the persisted count, e.g. post.likes_count). %>
<span class="live-counter-demo__value" data-stimeo--live-counter-target="value"><%=
LikesChannel.count(room) %></span>
<span class="live-counter-demo__unit"><%= t("components.live_counter.demo.unit") %></span>
</p>
</div>
</div>
/*
* Presentation-only styles for the live-counter demo. The library only rewrites
* the value target's text (optimistic bump + broadcast reconciliation); this
* CSS lays out the like button and the count readout.
*/
.live-counter-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 28rem;
}
.live-counter-demo__hint {
margin: 0;
font-size: 0.9rem;
color: var(--muted);
}
.live-counter-demo__row {
display: flex;
align-items: center;
gap: 1rem;
}
.live-counter-demo__button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.live-counter-demo__button .demo-icon {
width: 1rem;
height: 1rem;
}
.live-counter-demo__count {
margin: 0;
}
.live-counter-demo__value {
font-size: 1.25rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.live-counter-demo__unit {
margin-left: 0.25rem;
font-size: 0.9rem;
color: var(--muted);
}
このデモに固有の消費側 JS はありません(挙動はコントローラが担います)。
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--live-counter"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
value
|
カウントの表示要素(任意)。無ければコントローラ要素自身が表示面になる。 | data-stimeo--live-counter-target="value" |
trigger
|
data-stimeo--live-counter-target="trigger" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
channel
|
購読する Action Cable チャンネル名。空(既定)なら購読せず、楽観加算だけが効く。 | data-stimeo--live-counter-channel-value |
params
|
購読識別子に混ぜる追加パラメータ(例 {"room":"post_42"})。 |
data-stimeo--live-counter-params-value |
id
|
このクライアントの識別子。差分 { delta, by } broadcast のエコー抑制に使う。 |
data-stimeo--live-counter-id-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
increment
|
表示を楽観的に加算し、チャンネルへ increment を perform する。ステップは data-stimeo--live-counter-delta-param(既定 1)。 |
stimeo--live-counter#increment |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
表示値が実際に変わるたび(楽観・照合の両方)に { count } と共に発火。 |
stimeo--live-counter:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
value target のテキスト |
value target(無ければコントローラ要素) | カウントそのもの — DOM が唯一の真実(サーバレンダリングの初期値)。 |