hibiki_rails 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73beec766683bf4a92c8bebcb19b62e58686f50f68e3b16f0a958c2d0d155de1
4
- data.tar.gz: 8ce7469020b298e4e94b7fa7daef8bafb5443a891fc90163339750f400aa9228
3
+ metadata.gz: 62779990991fb78726bc665b7ad93a93dabee5b49892fa99f6486ad42dc41679
4
+ data.tar.gz: 9cac4fa0143ab6b5f031d9ea0d9d213a1505108e1aa06894773d59f8594b6023
5
5
  SHA512:
6
- metadata.gz: 3f5fc7f1fe7a4f72047d86e054612e9144e5815cc1a96d2fa87737573f9701ce2b2c6d713992822ee6cd90cae7b6cae5b4647bb6001b3f863dee2f38354ec131
7
- data.tar.gz: 67f0275c2e22bd291fc8940f84a99775c9e1acac5e3542ffc68edd9c896e26f9e786270904bfa75029e78c872984560fdeca6499532f4128c4f65a1c420ab7b2
6
+ metadata.gz: bf6340bdef27fd3c320fff3a8d7fb90967d883783a965e61a90ccbae5d536b7bdcdfcba536d827089d3b389af1c07439ff7031ee11792c95a579752c8b235706
7
+ data.tar.gz: a9337451b21ecd955590683648f8a6d824bafa703ce096937387cff0fafaedf5ee5a959089558f7bf02037b00d39e40fd9fcab1bf3176cd976dcd1b957ab6443
data/README.md CHANGED
@@ -1,246 +1,77 @@
1
1
  # hibiki_rails
2
2
 
3
3
  Rails glue for [hibiki](https://github.com/planetaska/hibiki):
4
- connection-scoped signal graphs over ActionCable, pushing re-rendered HTML
5
- to the page — either through Turbo Streams, or over the channel's own
6
- subscription to the gem's packaged client (see "The packaged client").
4
+ connection-scoped signal graphs over ActionCable, pushing re-rendered HTML to the page — either through Turbo Streams, or over the channel's own subscription to the gem's packaged client (see [The JS client](https://planetaska.github.io/hibiki/the-js-client/)).
7
5
 
8
6
  ```
9
7
  cable action arrives → mutate signals → effects render partials →
10
8
  Turbo Streams broadcast → Turbo morphs the DOM
11
9
  ```
12
10
 
13
- A graph lives per cable connection (in practice: per browser tab), built
14
- when the channel subscribes and disposed when it unsubscribes. Effects
15
- subscribe to whatever signals they read; when an action writes a signal,
16
- exactly the affected effects re-render and broadcast.
11
+ A graph lives per cable connection (in practice: per browser tab), built when the channel subscribes and disposed when it unsubscribes. Effects subscribe to whatever signals they read; when an action writes a signal, exactly the affected effects re-render and broadcast.
17
12
 
18
- Incubating inside the hibiki repo while the core gem is pre-release; will
19
- be extracted to its own repository once hibiki 0.1.0 ships and this API
20
- stabilizes. Rails >= 8.0 (what's tested), Ruby >= 3.4.
13
+ Supports Rails >= 7.1, Ruby >= 3.4.
21
14
 
22
- ## Usage
15
+ ## Rails quick start
16
+
17
+ ### Installation
18
+
19
+ **Step 1** - Install the gem (`hibiki_rails` depends on the core `hibiki` gem)
23
20
 
24
21
  ```ruby
25
- class CounterChannel < ApplicationCable::Channel
26
- include Hibiki::Rails::Channel
27
-
28
- # Runs once on the graph's own thread, inside Hibiki.root.
29
- def build_graph
30
- @count = Hibiki::State.new(0)
31
- @step = Hibiki::State.new(1)
32
- doubled = Hibiki::Derived.new { @count.value * 2 }
33
-
34
- Hibiki::Effect.new do
35
- broadcast_replace target: "count", partial: "counter/count",
36
- locals: { count: @count.value, doubled: doubled.value }
37
- end
38
- Hibiki::Effect.new do
39
- broadcast_replace target: "step", partial: "counter/step",
40
- locals: { step: @step.value }
41
- end
42
- end
43
-
44
- # Actions are plain methods that touch signals directly: each one runs
45
- # on the graph thread inside one Hibiki.batch, so N writes still mean
46
- # one re-run per affected effect.
47
- def increment = @count.value += @step.value
48
- def burst = 10.times { @count.value += 1 }
49
- end
22
+ # Gemfile
23
+ gem "hibiki"
24
+ gem "hibiki_rails"
50
25
  ```
51
26
 
52
- The page supplies a per-page-load graph id (`cid`) and listens on the
53
- matching stream:
27
+ Or `gem install hibiki hibiki_rails`.
54
28
 
55
- ```erb
56
- <div data-controller="counter" data-counter-cid-value="<%= @cid %>">
57
- <%= turbo_stream_from "counter", @cid %>
58
- <%= render "count", count: 0, doubled: 0 %> <%# placeholder, see below %>
59
- ...
60
- </div>
61
- ```
29
+ **Step 2** - Run the install generator
62
30
 
63
- with `@cid = SecureRandom.uuid` in the controller action. The channel
64
- broadcasts to `[channel_name, cid]` — override `stream_name` (and/or
65
- `cid`) to derive identity differently.
66
-
67
- ## What the concern does
68
-
69
- - `subscribed` — rejects without a `cid` param, then runs `build_graph`
70
- inside `Hibiki.root` on a dedicated worker thread (a `GraphActor`).
71
- ActionCable dispatches on a thread pool with no per-channel ordering;
72
- hibiki's threading model is confinement — so cable threads only enqueue,
73
- and the graph lives on exactly one thread.
74
- - every action — the whole body is posted to that thread wrapped in one
75
- `Hibiki.batch`. `rescue_from` still applies (it runs on the graph
76
- thread); what it doesn't handle goes to `Rails.error` (source
77
- `"hibiki_rails"`).
78
- - `unsubscribed` — disposes the root (running `on_cleanup` hooks) and
79
- stops the worker, draining what was already queued.
80
- - dev reloading — an Engine hook disposes every live graph before code
81
- reloads (stale effects would run old class versions forever); cable
82
- clients auto-reconnect and rebuild. Graph state resets on reload, like
83
- any remount.
84
-
85
- ## Broadcast helpers
86
-
87
- Available inside effects (all bound to `stream_name`):
88
-
89
- - `broadcast_replace(target:, **rendering)` — `partial:`/`locals:`,
90
- `html:`, or anything Turbo's renderer accepts.
91
- - `broadcast_morph(target:, **rendering)` — replace via Turbo 8 morphing
92
- (keeps focus/scroll).
93
- - `broadcast_refresh` — tell the page to refresh itself.
94
- - `broadcast_refresh_effect(wait: 0.25) { ...read signals... }` — the
95
- morph-everything style: tracks whatever the block reads and answers
96
- changes with a debounced refresh, one per burst of actions rather than
97
- one per action.
98
-
99
- ## The packaged client
100
-
101
- The gem vendors its own JavaScript, turbo-rails-style: the engine puts
102
- `hibiki.js` on the app's asset path and merges the `"hibiki-rails"` pin into the
103
- import map, so importmap-rails apps have no install step beyond
104
- registering the controller — `bin/rails g hibiki:rails:install` does it
105
- (plus the `Helpers` include below, the `ApplicationCable` boilerplate,
106
- and the `@rails/actioncable` pin — a stock app has neither until its
107
- first `rails g channel`), or create the one-line shim yourself:
108
-
109
- ```js
110
- // app/javascript/controllers/hibiki_controller.js
111
- export { default } from "hibiki-rails" // registers as "hibiki" — the helpers hardcode that identifier
31
+ ```sh
32
+ bin/rails g hibiki:rails:install
112
33
  ```
113
34
 
114
- The registration is a file-backed shim on purpose: importmap apps
115
- eager-load it from the controllers directory, jsbundling apps get the
116
- matching import/register pair in `controllers/index.js` (the install
117
- generator appends it), and because it is derived from a real controller
118
- file, `bin/rails stimulus:manifest:update` regenerates it instead of
119
- dropping it.
35
+ The install generator detects whether your app uses an import map:
120
36
 
121
- (jsbundling/vite apps: `npm install hibiki-rails`the [npm
122
- package](https://www.npmjs.com/package/hibiki-rails) is the same module the
123
- engine vendors, and pulls in `@rails/actioncable`; release in lockstep with
124
- the gem.)
37
+ - For importmap apps, installation is fully automatic you are done.
38
+ - For apps with a JS bundler (esbuild, vite, bun, ...), also install the companion JS client (published as an npm package) with **one of**:
39
+ - `npm install hibiki-rails`
40
+ - `yarn add hibiki-rails`
41
+ - `bun add hibiki-rails`
42
+ - or the equivalent for your setup
125
43
 
126
- The client is one generic Stimulus controller that drives any *island*: a
127
- DOM subtree bound to one channel subscription. Islands are stamped with
128
- the opt-in `Hibiki::Rails::Helpers` — include it where you want the bare
129
- names (`ApplicationHelper` for ERB, individual Phlex components); the gem
130
- never includes it for you:
44
+ ### Using the generator
131
45
 
132
- ```erb
133
- <%= tag.div(**hibiki_island(TodosChannel, cid: @cid)) do %>
134
- <%= render TodoList.new %> <%# placeholder; replaced by DOM id %>
135
- <%= tag.form(**on(:add, event: :submit)) do %>
136
- <input type="text" name="title">
137
- <button>add</button>
138
- <% end %>
139
- <% end %>
140
- ```
46
+ You can create reactive components easily with the provided generators.
141
47
 
142
- - `hibiki_island(channel, cid:)` the island root: one subscription,
143
- identified by the page's `cid`.
144
- - `on(action, event:, with:)` — forward a DOM event (`:click` default,
145
- `:change`, `:submit`) as a channel action, with `with:` as its payload.
146
- A changed control also sends `{ name => value }`; a submitted form sends
147
- its FormData and is reset after performing.
48
+ Create your first reactive component by running:
148
49
 
149
- Transport is the channel's own subscription in both directions: render
150
- effects call `transmit({ html: })` and the client swaps each fragment in
151
- by its root DOM id (`Hibiki::Phlex.render_effect` pairs naturally):
50
+ ```sh
51
+ # Replace [your_view_path] with your desired view path,
52
+ # e.g. counters, posts, users/profile...
53
+ bin/rails g hibiki:rails:stimulus counter [your_view_path]
152
54
 
153
- ```ruby
154
- def build_graph
155
- @list = TodoList.new
156
- Hibiki::Phlex.render_effect(@list) { |html| transmit({ html: }) }
157
- end
55
+ # For example, this creates "counter" component partials
56
+ # inside app/views/static_pages
57
+ bin/rails g hibiki:rails:stimulus counter static_pages
158
58
  ```
159
59
 
160
- Because the client registers its `received` callback at subscribe time
161
- before the server ever runs `build_graph` — the effects' first transmits
162
- always land: no Turbo stream, no connected-wait, and the server-rendered
163
- initial HTML is only a paint-avoidance placeholder. One rule carries over
164
- from any replace-fragment design: never transmit a fragment containing
165
- the input the user is currently typing in.
166
-
167
- The `data-hibiki-*` attributes the helpers emit are a private contract
168
- with the vendored JS — they version together; don't hand-write them in
169
- app code. The protocol itself is Stimulus-free (Stimulus only hosts the
170
- controller lifecycle), so a hand-rolled client can drive the same
171
- attributes: `toy-phlex/` in the parent repo does it in ~40 lines. The
172
- helper interface's shape is inspired by
173
- [phlex-reactive](https://phlex-reactive.zoolutions.llc)'s `on(...)`
174
- actions.
175
-
176
- ## Generators
177
-
178
- Each supported shape has a generator that scaffolds it as a *working*
179
- mini-example — one state, one derived, one action, one effect; run it,
180
- render the output from any page, click `+1`, watch it live-update — meant
181
- to be reshaped in place, not filled in from scratch:
60
+ This creates a minimal working reactive component in the given view path.
182
61
 
183
- ```sh
184
- bin/rails g hibiki:rails:install # one-time wiring: register line,
185
- # Helpers include, ApplicationCable
186
- # boilerplate + actioncable pin
187
- # (idempotent)
188
- bin/rails g hibiki:rails:stimulus NAME [VIEW_PATH] # channel + ChannelController
189
- # subclass + view partial
190
- bin/rails g hibiki:rails:island NAME [VIEW_PATH] # channel + helpers-stamped view
191
- # partial, no per-component JS
192
- bin/rails g hibiki:rails:phlex NAME # channel + Phlex component +
193
- # island wrapper (needs hibiki_phlex)
194
- ```
62
+ ### Render the reactive component
195
63
 
196
- `VIEW_PATH` is the views directory under `app/views` (defaults to `NAME`);
197
- the emitted partial is self-contained (`cid` defaults to a per-render
198
- uuid), so `<%= render "counter/counter" %>` — or `<%= render
199
- CounterIsland.new %>` for the Phlex shape — is the only line a page needs.
200
- The `stimulus` shape works with zero wiring; `island` and `phlex` need the
201
- one-time `hibiki:rails:install` (they print a hint when it's missing).
202
- Namespaced names work (`admin/counter` pins `static channel` where the
203
- Stimulus identifier can't infer it). In apps without an importmap
204
- (jsbundling/vite), where `controllers/index.js` has no eager loader, the
205
- `stimulus` generator also appends the controller's import/register pair
206
- to it — the same lines `stimulus:manifest:update` would emit.
207
-
208
- ## The initial-state pattern (Turbo transport)
209
-
210
- Islands on the Turbo-broadcast transport instead (the "Usage" example
211
- above) have an ordering problem the transmit transport doesn't: the
212
- graph's effects do their first run inside `subscribed` — usually before
213
- the page's `turbo_stream_from` subscription has confirmed — so the first
214
- broadcast would be lost. Fix the ordering on the client with the packaged
215
- `streamConnected` helper: wait for Turbo to stamp the `connected`
216
- attribute on the stream source, then subscribe the graph channel.
217
-
218
- ```js
219
- // in the Stimulus controller driving the channel
220
- import { streamConnected } from "hibiki-rails"
221
-
222
- async connect() {
223
- this.consumer = createConsumer()
224
- await streamConnected(this.element.querySelector("turbo-cable-stream-source"))
225
- this.subscription = this.consumer.subscriptions.create(
226
- { channel: "CounterChannel", cid: this.cidValue }, {}
227
- )
228
- }
64
+ The generated components are just Rails partials (or Phlex components, if you used the Phlex generator), so you can render one anywhere like any other partial:
65
+
66
+ ```erb
67
+ <%= render "static_pages/counter" %>
229
68
  ```
230
69
 
231
- With that in place the server-rendered initial HTML is only a
232
- paint-avoidance placeholder — the first broadcast always lands and
233
- replaces it, so it doesn't have to match the graph's initial state.
70
+ Congratulations! Now you have your first reactive component!
234
71
 
235
- ## Error handling layers
72
+ ## Documentation
236
73
 
237
- 1. `rescue_from` on the channel — handles action errors, on the graph
238
- thread.
239
- 2. `Hibiki.error_handler = ->(error, effect) { ... }` — app-level routing
240
- for effect errors raised during a flush (the gem does not set this).
241
- 3. The graph worker's per-job rescue — everything unhandled lands in
242
- `Rails.error.report(..., source: "hibiki_rails")`. Override per channel
243
- via `build_graph_actor` and `GraphActor.new(on_error:)`.
74
+ Documentation site: <https://planetaska.github.io/hibiki/rails-introduction/>
244
75
 
245
76
  ## Development
246
77
 
@@ -248,5 +79,5 @@ replaces it, so it doesn't have to match the graph's initial state.
248
79
  bundle exec rake # specs + rubocop (same as CI)
249
80
  ```
250
81
 
251
- The spec suite boots a minimal inline Rails app (`spec/support/dummy_app.rb`);
252
- the live end-to-end proof app is `spike/` in the parent repo.
82
+ The spec suite boots a minimal inline Rails app (`spec/support/dummy_app.rb`).
83
+
@@ -26,7 +26,9 @@
26
26
  //
27
27
  // Both shapes speak both transports. Transmit: the server's render
28
28
  // effects `transmit({ html: })` fragments that are swapped in by their
29
- // root DOM id; `received` is registered at subscribe time — before the
29
+ // root DOM id, and `transmit_value` messages that update every
30
+ // data-hibiki-value placeholder; `received` is registered at subscribe
31
+ // time — before the
30
32
  // server runs build_graph — so the effects' first transmits always land
31
33
  // (the server-rendered initial HTML is only a paint-avoidance
32
34
  // placeholder). Turbo broadcasts: when the controller's element contains
@@ -44,6 +46,8 @@
44
46
  // data-hibiki-cid-value="<per-page-load id>"
45
47
  // controls data-hibiki-on="<event>-><action>" e.g. "click->increment"
46
48
  // data-hibiki-with='{"index":3}' optional JSON payload
49
+ // value sites data-hibiki-value="<name>" reactive-value placeholder;
50
+ // the server's transmit_value message updates every match
47
51
  //
48
52
  // Register the generic controller under the identifier "hibiki" (the
49
53
  // helpers hardcode it):
@@ -94,10 +98,26 @@ export class ChannelController extends Controller {
94
98
  this.subscription.perform(action, payload)
95
99
  }
96
100
 
97
- // Server → DOM (transmit transport): swap each transmitted fragment in
98
- // by its root id. Broadcast-transport channels never transmit, and a
99
- // non-html transmit is not ours to interpret. Subclasses may override.
100
- received({ html }) {
101
+ // Server → DOM (transmit transport). Two message shapes:
102
+ //
103
+ // { value: { name, text } } a reactive value (transmit_value): write
104
+ // the text into every [data-hibiki-value=name] placeholder, document-
105
+ // wide (a value may render outside its island; names are page-unique).
106
+ // textContent assignment keeps values text-only and preserves each
107
+ // site's own tag/classes, so per-placeholder styling survives updates.
108
+ //
109
+ // { html } — a fragment: swap it in by its root id.
110
+ //
111
+ // Anything else is not ours to interpret. Subclasses may override, but
112
+ // should call super (or handle `value`) to keep reactive values live.
113
+ received({ html, value }) {
114
+ if (value) {
115
+ const selector = `[data-hibiki-value="${CSS.escape(value.name)}"]`
116
+ for (const site of document.querySelectorAll(selector)) {
117
+ site.textContent = value.text
118
+ }
119
+ return
120
+ }
101
121
  if (!html) return
102
122
  const template = document.createElement("template")
103
123
  template.innerHTML = html
@@ -49,7 +49,7 @@ module Hibiki
49
49
 
50
50
  Render it from any page:
51
51
 
52
- <%= render #{class_name}Island.new %>
52
+ <%= render Components::#{class_name}Island.new %>
53
53
 
54
54
  MSG
55
55
  register_hint
@@ -8,7 +8,7 @@ class <%= class_name %>Channel < ApplicationCable::Channel
8
8
  include Hibiki::Rails::Channel
9
9
 
10
10
  def build_graph
11
- @component = <%= class_name %>.new
11
+ @component = Components::<%= class_name %>.new
12
12
 
13
13
  # First run is the dependency-collecting initial render.
14
14
  Hibiki::Phlex.render_effect(@component) { |html| transmit({ html: }) }
@@ -5,7 +5,7 @@
5
5
  # read as ordinary method calls (no .value anywhere), Rerenderable lets
6
6
  # the channel's render effect re-render this same instance, and Helpers
7
7
  # stamps the client's wire protocol.
8
- class <%= class_name %> < Phlex::HTML
8
+ class Components::<%= class_name %> < Phlex::HTML
9
9
  include Hibiki::Reactive
10
10
  include Hibiki::Phlex::Rerenderable
11
11
  include Hibiki::Rails::Helpers
@@ -3,15 +3,15 @@
3
3
  # The island wrapper: one channel subscription per render, identified by
4
4
  # a per-page-load cid. Render from any page:
5
5
  #
6
- # <%%= render <%= class_name %>Island.new %>
7
- class <%= class_name %>Island < Phlex::HTML
6
+ # <%%= render Components::<%= class_name %>Island.new %>
7
+ class Components::<%= class_name %>Island < Phlex::HTML
8
8
  include Hibiki::Rails::Helpers
9
9
 
10
10
  def view_template
11
11
  div(**hibiki_island(<%= class_name %>Channel, cid: SecureRandom.uuid)) do
12
12
  # A throwaway instance as a paint-avoidance placeholder; the
13
13
  # channel's long-lived instance takes over from its first transmit.
14
- render <%= class_name %>.new
14
+ render Components::<%= class_name %>.new
15
15
  end
16
16
  end
17
17
  end
@@ -92,6 +92,22 @@ module Hibiki
92
92
  raise NotImplementedError, "#{self.class} must implement #build_graph"
93
93
  end
94
94
 
95
+ # The channel half of a single reactive value (see Helpers#reactive for
96
+ # the placeholder half): wraps the block in an effect that transmits
97
+ # `{ value: { name:, text: } }` — the packaged client writes the text
98
+ # into every `[data-hibiki-value=NAME]` placeholder on the page (part
99
+ # of the data-hibiki wire contract, versioned with the client). Call
100
+ # from #build_graph; the block tracks whatever signals it reads (state,
101
+ # derived, or an expression over several). Values are text, never
102
+ # markup: the client assigns textContent, so nothing is interpreted as
103
+ # HTML. Returns the effect, owned by the graph root like any other.
104
+ def transmit_value(name, &compute)
105
+ name = Helpers.value_name(name)
106
+ Hibiki::Effect.new do
107
+ transmit({ value: { name:, text: compute.call.to_s } })
108
+ end
109
+ end
110
+
95
111
  # Per-page-load graph identity, supplied by the page's subscription
96
112
  # (each tab is its own graph). Override to derive identity elsewhere.
97
113
  def cid = params[:cid]
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cgi/escape"
3
4
  require "json"
4
5
 
5
6
  module Hibiki
@@ -25,7 +26,30 @@ module Hibiki
25
26
  # The emitted attribute names are a private contract between these
26
27
  # helpers and the gem's JS — they version together; don't hand-write
27
28
  # them in app code.
29
+ #
30
+ # #reactive is the exception to the splat shape: it returns a complete
31
+ # placeholder element (`<%= reactive :doubled, 0 %>` in ERB). Phlex
32
+ # components splat instead: `span(**reactive_attrs(:doubled))`.
28
33
  module Helpers
34
+ # A reactive value's name lands in an attribute selector on both
35
+ # halves, so keep it to safe characters; the placeholder tag name
36
+ # lands in raw markup, so allowlist it too.
37
+ VALUE_NAME = /\A[a-z][a-z0-9_-]*\z/i
38
+ VALUE_TAG = /\A[a-z][a-z0-9-]*\z/i
39
+ private_constant :VALUE_NAME, :VALUE_TAG
40
+
41
+ # The shared name validator for both halves of a reactive value (the
42
+ # view-side data-hibiki-value placeholder and the channel's
43
+ # #transmit_value message).
44
+ def self.value_name(name)
45
+ name = name.to_s
46
+ unless VALUE_NAME.match?(name)
47
+ raise ArgumentError,
48
+ "reactive value name #{name.inspect} must match #{VALUE_NAME.inspect}"
49
+ end
50
+ name
51
+ end
52
+
29
53
  # The island root: one channel subscription per island, identified by
30
54
  # a per-page-load cid (each tab is its own graph). `channel` is the
31
55
  # channel class or its name as a string.
@@ -45,6 +69,29 @@ module Hibiki
45
69
  data[:hibiki_with] = JSON.generate(with) unless with.nil?
46
70
  { data: }
47
71
  end
72
+
73
+ # Placeholder for a single reactive value: `<%= reactive :doubled, 0 %>`
74
+ # paints `<span data-hibiki-value="doubled">0</span>`; the channel's
75
+ # `transmit_value(:doubled) { ... }` keeps it fresh. The same value may
76
+ # be placed any number of times, anywhere on the page — every
77
+ # placeholder updates (the client matches document-wide, so a value can
78
+ # render outside its island too). Names must be page-unique across
79
+ # channels. Only the placeholder text is server-rendered: each site
80
+ # keeps its own tag, classes, and attributes across updates.
81
+ def reactive(name, placeholder = "", tag_name: :span)
82
+ tag_name = tag_name.to_s
83
+ unless VALUE_TAG.match?(tag_name)
84
+ raise ArgumentError,
85
+ "reactive value tag #{tag_name.inspect} must match #{VALUE_TAG.inspect}"
86
+ end
87
+ html = %(<#{tag_name} data-hibiki-value="#{Helpers.value_name(name)}">) +
88
+ %(#{CGI.escapeHTML(placeholder.to_s)}</#{tag_name}>)
89
+ html.respond_to?(:html_safe) ? html.html_safe : html
90
+ end
91
+
92
+ # The value's attributes, for stamping the placeholder yourself — the
93
+ # Phlex form of #reactive: `span(**reactive_attrs(:doubled)) { "0" }`.
94
+ def reactive_attrs(name) = { data: { hibiki_value: Helpers.value_name(name) } }
48
95
  end
49
96
  end
50
97
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Hibiki
4
4
  module Rails
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hibiki_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - planetaska