Generators
hibiki_rails comes with batteries included. Each supported shape has a generator that scaffolds it as a working mini-example — one state, one derived, one action, one effect. Run a generator, render the output from any page, click +1, watch it live-update. The generated files are meant to be reshaped in place.
Stimulus shape
Stock Stimulus vocabulary (data-controller / data-action) over the Turbo-broadcast transport — you own a small Stimulus controller per component.
bin/rails g hibiki:rails:stimulus NAME [VIEW_PATH]
Generates:
- A minimal channel in
app/channels - A minimal Stimulus
ChannelControllersubclass inapp/javascript/controllers - A minimal view partial set (two files) in
app/views/[VIEW_PATH]
Partial example:
<% cid = local_assigns.fetch(:cid) { SecureRandom.uuid } %>
<div data-controller="counter" data-counter-cid-value="<%= cid %>">
<%= turbo_stream_from "counter", cid %>
<%= render "counter/counter_display", count: 0, doubled: 0 %>
<p><button data-action="counter#increment">+1</button></p>
</div>
Island shape
The same Turbo-broadcast transport, but the gem’s packaged generic controller drives the island — no per-component JS at all. The view is stamped through the hibiki_island / on helpers (see The JS client).
bin/rails g hibiki:rails:island NAME [VIEW_PATH]
Generates:
- A minimal channel in
app/channels - A minimal view partial set (two files) in
app/views/[VIEW_PATH]
Partial example:
<% cid = local_assigns.fetch(:cid) { SecureRandom.uuid } %>
<%= tag.div(**hibiki_island(CounterChannel, cid:)) do %>
<%= turbo_stream_from "counter", cid %>
<%= render "counter/counter_display", count: 0, doubled: 0 %>
<p><%= tag.button("+1", **on(:increment)) %></p>
<% end %>
Phlex shape
The component owns the state, the channel owns the transport: a render effect re-renders the component and transmits its HTML over the channel’s own subscription — no Turbo Streams involved (see Phlex support).
# requires the hibiki_phlex gem
# (and phlex-rails, to render components from views)
bin/rails g hibiki:rails:phlex NAME
Generates:
- A minimal channel in
app/channels - A minimal Phlex component set (two files) in
app/components
Phlex component example:
class Components::Counter < Phlex::HTML
state :count, 0
derived(:doubled) { count * 2 }
def view_template
div(id: "counter") do
p { "count: #{count} · doubled: #{doubled}" }
button(**on(:increment)) { "+1" }
end
end
def increment
self.count += 1
end
end
Rendering the generated component
VIEW_PATH is the views directory under app/views (it defaults to NAME). The emitted partial is self-contained, so rendering it — <%= render "counter/counter" %>, or <%= render Components::CounterIsland.new %> for the Phlex shape — is the only line a page needs.
The stimulus shape works with zero extra wiring; island and phlex need the one-time hibiki:rails:install (they print a hint when it’s missing). Namespaced names work too: admin/counter pins the channel class via static channel in the generated controller, since the Stimulus identifier can’t infer it. And in apps without an importmap (jsbundling/vite), where controllers/index.js has no eager loader, the stimulus generator appends the controller’s import/register pair to it — in the exact format stimulus:manifest:update emits, so a later manifest update keeps it.