Module: Hibiki::Rails::Helpers
- Defined in:
- lib/hibiki/rails/helpers.rb
Overview
Opt-in helpers that stamp the packaged client's attribute protocol (interpreted by the vendored hibiki.js Stimulus controller). Strictly opt-in, mirroring Hibiki::DSL: include it where you want the bare names — ApplicationHelper for ERB views, individual Phlex components — the gem never includes it for you.
div(**hibiki_island(CounterChannel, cid:)) do
button(**on(:increment)) { "+" }
button(**on(:toggle, with: { index: 3 })) { "toggle" }
input(name: "step", **on(:set_step, event: :change))
input(name: "q", **on(:search, event: :input))
button(**on(:destroy, confirm: "Are you sure?")) { "delete" }
button(**on(:load_more, event: %i[click visible])) { "more" }
form(**on(:save, event: :submit, reset: false)) { ... }
end
Both helpers return a { data: { ... } } hash: splat it into Phlex
element methods or Rails tag helpers (tag.div(**hibiki_island(...))).
When the element needs other attributes on the same data: key, merge
the hashes yourself (Phlex's mix does this).
The emitted attribute names are a private contract between these helpers and the gem's JS — they version together; don't hand-write them in app code.
#reactive is the exception to the splat shape: it returns a complete
placeholder element (<%= reactive :doubled, 0 %> in ERB). Phlex
components splat instead: span(**reactive_attrs(:doubled)).
Constant Summary collapse
- DEFAULT_INPUT_DEBOUNCE =
Per-keystroke round trips are the failure mode #on exists to avoid, so :input carries a debounce unless the caller says otherwise. It is applied here rather than in the client, so the number is visible in the emitted markup instead of being an invisible default.
250
Class Method Summary collapse
-
.event_name(name) ⇒ Object
The shared validator for both halves of an
event->actiontoken. -
.value_name(name) ⇒ Object
The shared name validator for both halves of a reactive value (the view-side data-hibiki-value placeholder and the channel's #transmit_value message).
Instance Method Summary collapse
-
#hibiki_island(channel, cid:, params: nil) ⇒ Object
The island root: one channel subscription per island, identified by a per-page-load cid (each tab is its own graph).
-
#on(action, event: :click, with: nil, debounce: nil, confirm: nil, reset: nil) ⇒ Object
Forward an event on this element as a channel action.
-
#reactive(name, placeholder = "", tag_name: :span) ⇒ Object
Placeholder for a single reactive value:
<%= reactive :doubled, 0 %>paints<span data-hibiki-value="doubled">0</span>; the channel'stransmit_value(:doubled) { ... }keeps it fresh. -
#reactive_attrs(name) ⇒ Object
The value's attributes, for stamping the placeholder yourself — the Phlex form of #reactive:
span(**reactive_attrs(:doubled)) { "0" }.
Class Method Details
.event_name(name) ⇒ Object
The shared validator for both halves of an event->action token.
71 72 73 74 75 76 77 78 |
# File 'lib/hibiki/rails/helpers.rb', line 71 def self.event_name(name) name = name.to_s unless EVENT_NAME.match?(name) raise ArgumentError, "event or action name #{name.inspect} must match #{EVENT_NAME.inspect}" end name end |
.value_name(name) ⇒ Object
The shared name validator for both halves of a reactive value (the view-side data-hibiki-value placeholder and the channel's #transmit_value message).
61 62 63 64 65 66 67 68 |
# File 'lib/hibiki/rails/helpers.rb', line 61 def self.value_name(name) name = name.to_s unless VALUE_NAME.match?(name) raise ArgumentError, "reactive value name #{name.inspect} must match #{VALUE_NAME.inspect}" end name end |
Instance Method Details
#hibiki_island(channel, cid:, params: nil) ⇒ Object
The island root: one channel subscription per island, identified by
a per-page-load cid (each tab is its own graph). channel is the
channel class or its name as a string.
params: is a hash of extra subscribe params, reaching the channel
as params[:key] beside cid. It is how a channel learns WHICH
record its page is about (a show page's record_id), which is
otherwise unexpressible — the subscription is the only server-side
hook that runs before the graph is built.
THE TRUST RULE. Subscribe params are client-supplied and untrusted,
exactly like query params on a request: anyone can open a socket and
send whatever they like. A channel may use one only to LOOK UP A
RECORD INSIDE A SCOPE IT CHOOSES ITSELF —
current_user.books.find(params[:record_id]) — and must reject
when the lookup fails. It must never interpolate a param into a
streamable name, a class name, a column name, or a scope. The
streamable a channel streams from is always derived server-side from
the record it has already loaded and authorized. The client cannot
override channel or cid through this hash.
100 101 102 103 104 105 106 |
# File 'lib/hibiki/rails/helpers.rb', line 100 def hibiki_island(channel, cid:, params: nil) channel_name = channel.is_a?(Class) ? channel.name : channel.to_s data = { controller: "hibiki", hibiki_channel_value: channel_name, hibiki_cid_value: cid } data[:hibiki_params_value] = JSON.generate(params) unless params.nil? { data: } end |
#on(action, event: :click, with: nil, debounce: nil, confirm: nil, reset: nil) ⇒ Object
Forward an event on this element as a channel action.
event: names the event, or a list of them — the left side of the
-> is a hibiki event name, of which DOM events are a subset:
:click, :change, :input, :submit, plus the :visible pseudo-event
(an IntersectionObserver sentinel — the element entering the
viewport). A list makes one element answer several, which is how a
load-more button doubles as an infinite-scroll sentinel:
on(:load_more, event: %i[click visible], with: { shown: rows.size })
with: is a hash sent as the action's payload. The client adds
event-derived data on top: a changed control contributes
{ name => value } — a checkbox its checked state, a multi-select
its selected values — and a submitted form contributes its FormData.
Everything else is a per-control modifier, kept out of the token
grammar so the -> left side stays purely "which event":
debounce: ms wait for the gesture to settle before performing.
:input defaults to DEFAULT_INPUT_DEBOUNCE; pass 0 to
send every keystroke.
confirm: msg window.confirm before performing; declining performs
nothing (and does not submit the form).
reset: false keep a submitted form's inputs. The default resets
them, which is right for an "add" form and wrong for
an edit one — a failed commit would otherwise discard
what the user typed, synchronously, before the server
has even replied.
137 138 139 140 141 142 143 144 |
# File 'lib/hibiki/rails/helpers.rb', line 137 def on(action, event: :click, with: nil, debounce: nil, confirm: nil, reset: nil) action = Helpers.event_name(action) events = Array(event).map { Helpers.event_name(it) } debounce = DEFAULT_INPUT_DEBOUNCE if debounce.nil? && events.include?("input") tokens = events.map { "#{it}->#{action}" }.join(" ") { data: { hibiki_on: tokens } .merge(on_modifiers(with:, debounce:, confirm:, reset:)) } end |
#reactive(name, placeholder = "", tag_name: :span) ⇒ Object
Placeholder for a single reactive value: <%= reactive :doubled, 0 %>
paints <span data-hibiki-value="doubled">0</span>; the channel's
transmit_value(:doubled) { ... } keeps it fresh. The same value may
be placed any number of times, anywhere on the page — every
placeholder updates (the client matches document-wide, so a value can
render outside its island too). Names must be page-unique across
channels. Only the placeholder text is server-rendered: each site
keeps its own tag, classes, and attributes across updates.
154 155 156 157 158 159 160 161 162 163 |
# File 'lib/hibiki/rails/helpers.rb', line 154 def reactive(name, placeholder = "", tag_name: :span) tag_name = tag_name.to_s unless VALUE_TAG.match?(tag_name) raise ArgumentError, "reactive value tag #{tag_name.inspect} must match #{VALUE_TAG.inspect}" end html = %(<#{tag_name} data-hibiki-value="#{Helpers.value_name(name)}">) + %(#{CGI.escapeHTML(placeholder.to_s)}</#{tag_name}>) html.respond_to?(:html_safe) ? html.html_safe : html end |
#reactive_attrs(name) ⇒ Object
The value's attributes, for stamping the placeholder yourself — the
Phlex form of #reactive: span(**reactive_attrs(:doubled)) { "0" }.
167 |
# File 'lib/hibiki/rails/helpers.rb', line 167 def reactive_attrs(name) = { data: { hibiki_value: Helpers.value_name(name) } } |