reactive_component 0.1.0 → 0.7.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 +4 -4
- data/CHANGELOG.md +161 -0
- data/app/channels/reactive_component/channel.rb +10 -10
- data/app/controllers/reactive_component/actions_controller.rb +3 -3
- data/app/javascript/reactive_component/controllers/reactive_renderer_controller.js +25 -3
- data/app/javascript/reactive_component/lib/reactive_renderer_utils.js +34 -0
- data/config/importmap.rb +2 -2
- data/config/routes.rb +1 -1
- data/lib/reactive_component/broadcastable.rb +41 -0
- data/lib/reactive_component/compiler.rb +110 -52
- data/lib/reactive_component/data_evaluator.rb +58 -30
- data/lib/reactive_component/engine.rb +10 -4
- data/lib/reactive_component/erubi.rb +30 -0
- data/lib/reactive_component/transpiler.rb +586 -0
- data/lib/reactive_component/version.rb +1 -1
- data/lib/reactive_component/wrapper.rb +7 -12
- data/lib/reactive_component.rb +185 -51
- metadata +35 -25
- data/lib/reactive_component/erb_extractor.rb +0 -610
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a63e206cf0f143eb6c390357606b7de7bd67d8c516867883d3c125b865ff664
|
|
4
|
+
data.tar.gz: ab1e03efda80cff6207b6e7828bc42cdd23cf207953f803a620ccea143719a6c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2476e8ed1dcef187d6abfe7b011cbb8e076ac41ac0cd266b48f4174ec1c4f503ba618d8eb104c19d288f18d694a74d57e2fa2c0e591ded079113d99dc166ee7f
|
|
7
|
+
data.tar.gz: 5f6aae9647f439578c0585d3ba66586c60442a75003531c9a98e8ce0f267c77fd3089830ce9d5c17f3c91638ffd5b3c13f4ba7533d638f4c7d90da92d80f43b5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,166 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.0] - 2026-09-06
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- `live_action` params are filtered with `permit` against the declared
|
|
7
|
+
list, so a nested hash or array sent for a scalar param no longer reaches
|
|
8
|
+
the action method.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `live_action` `params:` accepts the full `permit` spec, so an action can
|
|
12
|
+
declare arrays and nested hashes: `params: [:title, { tags: [] }]`.
|
|
13
|
+
|
|
14
|
+
## [0.6.2] - 2026-09-06
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Gem metadata points at the right places: homepage and documentation
|
|
18
|
+
are the GitHub Pages docs, source/changelog/issues are
|
|
19
|
+
`lluzak/reactive_component`. (First release on RubyGems since 0.1.0.)
|
|
20
|
+
|
|
21
|
+
## [0.6.1] - 2026-09-06
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Docs describe the 0.6 compiler (Prism, prefixed ids, typed conditions,
|
|
25
|
+
compile errors); the live-action examples use the real Stimulus params.
|
|
26
|
+
- RuboCop pinned to `~> 1.90.0` so CI and local lint agree. The 0.6.0 tag
|
|
27
|
+
was never published: its commit failed lint on CI.
|
|
28
|
+
|
|
29
|
+
## [0.6.0] - 2026-09-05
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- **ruby2js is gone; the compiler runs on Prism's own tree.** One pass
|
|
33
|
+
over the Prism AST lifts every Ruby expression to a server-evaluated
|
|
34
|
+
data key and emits the template skeleton as JavaScript — a whitelist
|
|
35
|
+
(literals, data reads, `if`/`unless`/ternaries, boolean and comparison
|
|
36
|
+
operators, `.each`, the `_tag*`/`_render_*` helpers). Anything else raises
|
|
37
|
+
`ReactiveComponent::CompileError` naming the source, where ruby2js used
|
|
38
|
+
to guess (`present?` became a `.present` property read). Dependencies:
|
|
39
|
+
`ruby2js` out, `erubi` in; no `parser` gem. The compiled JS keeps the
|
|
40
|
+
same shape (`function render({ … })`, `_buf +=`, `for..of`,
|
|
41
|
+
`_tag_open(...)`).
|
|
42
|
+
- `"#{@x}!"` — an interpolated string with an ivar or helper — is now
|
|
43
|
+
server-evaluated and escaped like any other expression, instead of being
|
|
44
|
+
emitted as an unescaped template literal.
|
|
45
|
+
- The extractor now lifts a chain rooted at a self call
|
|
46
|
+
(`content.present?`, `current_user.name`) whole, like an ivar chain.
|
|
47
|
+
- **Wrapper ids are now component-prefixed by default** —
|
|
48
|
+
`message_row_message_1` instead of `message_1`. Two components rendering
|
|
49
|
+
the same record previously shared one id, and because broadcasts are
|
|
50
|
+
routed by id each rendered the *other's* payload: a `TypeError` deep in
|
|
51
|
+
the compiled template as soon as the shapes differed, plus duplicate ids
|
|
52
|
+
in the page. `dom_id_prefix` still overrides the default. Anything that
|
|
53
|
+
targeted the old bare ids (CSS, Turbo Stream targets, tests) needs the
|
|
54
|
+
new prefix.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
- **Block variables in conditions.** Inside a `.each`, an item read in a
|
|
58
|
+
non-output position — `<% if item.flag %>`, a ternary, `"x" if item.y` —
|
|
59
|
+
passed through ruby2js as `item.flag`, a property the client never
|
|
60
|
+
receives (items ship as their extracted expressions only, never the raw
|
|
61
|
+
record). Conditions were silently false after every broadcast and
|
|
62
|
+
`item.x.present?` threw `Cannot read properties of undefined`. They are
|
|
63
|
+
now server-evaluated per item like output expressions, but keep their
|
|
64
|
+
type (`"false"` is truthy in JS).
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
- **Compile-time invariant for loops.** After a `.each` body is processed,
|
|
68
|
+
any surviving read of the loop variable that is not an extracted
|
|
69
|
+
`item["vN"]` raises `ReactiveComponent::CompileError` naming the
|
|
70
|
+
expression — an item ships only as its extracted expressions, so such a
|
|
71
|
+
read could never resolve on the client. Guards the block-variable fix
|
|
72
|
+
against new syntactic positions.
|
|
73
|
+
- **Debug-mode strict payloads.** With `ReactiveComponent.debug` on, the
|
|
74
|
+
client renders through a Proxy that throws on reading a key the payload
|
|
75
|
+
does not carry, naming the key, its path, and the keys present — instead
|
|
76
|
+
of an `undefined` that is silently falsy in an `if`.
|
|
77
|
+
- The Stimulus controller checks for other elements sharing its id on
|
|
78
|
+
connect and logs a `console.error` naming the collision and the fix.
|
|
79
|
+
Piggybacks on Stimulus's own MutationObserver, so it also catches
|
|
80
|
+
hand-picked `dom_id_prefix`es that collide.
|
|
81
|
+
|
|
82
|
+
## [0.4.1] - 2026-05-29
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
- Bumped Rails to 8.1.3 (and matching patches across the 8.1.x stack)
|
|
86
|
+
in development and test gemfiles.
|
|
87
|
+
- Upgraded `view_component` to 4.11.0 — full test suite (119 runs,
|
|
88
|
+
308 assertions) green on Rails 7.1, 7.2, and 8.0 appraisals.
|
|
89
|
+
- Refreshed transitive dependencies (rack 3.2.6, nokogiri 1.19.3,
|
|
90
|
+
loofah 2.25.1, zeitwerk 2.8.2, propshaft 1.3.2, sqlite3 2.9.4,
|
|
91
|
+
rubocop 1.86.2, etc.).
|
|
92
|
+
|
|
93
|
+
## [0.4.0] - 2026-04-18
|
|
94
|
+
|
|
95
|
+
### Security
|
|
96
|
+
- **Broadcast payloads now refuse non-primitive values.** Previously
|
|
97
|
+
`build_data` could ship full `ActiveRecord` records over ActionCable,
|
|
98
|
+
leaking every column (including `password_digest`, tokens). The
|
|
99
|
+
sanitizer now raises `ReactiveComponent::UnsafeBroadcastValueError`
|
|
100
|
+
with a context-aware hint pointing at the offending ERB expression.
|
|
101
|
+
|
|
102
|
+
### Added
|
|
103
|
+
- `ReactiveComponent.sanitize_for_broadcast(value, source:)` — the
|
|
104
|
+
strict gatekeeper. Allows primitives (`nil`, booleans, `Integer`,
|
|
105
|
+
`Float`, `String`), `Symbol` (downcast to `String`), and `Array`/
|
|
106
|
+
`Hash` of those; raises on everything else.
|
|
107
|
+
- Node-based compiler regression test: actually executes compiled
|
|
108
|
+
templates so undefined-identifier runtime bugs fail the build.
|
|
109
|
+
- `RichRowComponent` + `WrapperComponent` dummy fixtures exercising
|
|
110
|
+
every shape that has historically broken the extractor.
|
|
111
|
+
|
|
112
|
+
### Fixed
|
|
113
|
+
- `<%= tag.xxx(attrs) do %> … <% end %>` now compiles into
|
|
114
|
+
`_tag_open` + body + `_tag_close`, so inner expressions stay
|
|
115
|
+
per-field reactive instead of collapsing into invalid JS.
|
|
116
|
+
- `<%= raw bare_helper %>` extracts the inner call as a server-
|
|
117
|
+
computed raw field (previously emitted an undefined JS identifier).
|
|
118
|
+
- `**@options` keyword-splat on `tag.xxx` no longer emits `#options`
|
|
119
|
+
(a JS private-field reference, which is a syntax error outside a
|
|
120
|
+
class body).
|
|
121
|
+
- Bare helper calls in conditions and tag attrs (`banner_visible?`,
|
|
122
|
+
`row_classes`, `status_label(@x)`) are extracted as server-computed
|
|
123
|
+
fields instead of surfacing as undefined JS identifiers.
|
|
124
|
+
- Bare `<%= @ivar %>` output alongside `<%= @ivar.chain %>` — both
|
|
125
|
+
destructures are now provided in the broadcast payload.
|
|
126
|
+
- ViewComponent sidecar template layout (`foo_component/foo_component.html.erb`)
|
|
127
|
+
is now supported by `Compiler.read_erb`.
|
|
128
|
+
- `escapeHTML` is aliased in the compiled preamble (ruby2js emits it
|
|
129
|
+
in some nested-component paths).
|
|
130
|
+
- `_render_attrs` now expands `data:`/`aria:` hashes, handles mixed
|
|
131
|
+
`class: [string, {name => cond}]` arrays, and emits bare boolean
|
|
132
|
+
attributes — matching Rails tag-builder semantics.
|
|
133
|
+
- Live-model ivar (e.g. `@message` under `subscribes_to :message`) is
|
|
134
|
+
excluded from broadcast payloads — it's the subscription key, not a
|
|
135
|
+
data field.
|
|
136
|
+
|
|
137
|
+
## [0.3.0] - 2026-04-16
|
|
138
|
+
|
|
139
|
+
### Added
|
|
140
|
+
- `broadcast_reactive_update` public method on models for manual broadcasts without touching the record
|
|
141
|
+
- Client state rendering: `setState` now re-renders components after updating client state
|
|
142
|
+
- Exclusive client state: `setState` with `exclusive` param deselects sibling components
|
|
143
|
+
- Folder navigation (Inbox, Starred, Sent, Archive, Trash) in dummy app
|
|
144
|
+
- Documentation for `client_state` usage (setState, exclusive mode, selectable lists)
|
|
145
|
+
- Documentation for `broadcast_reactive_update` with examples
|
|
146
|
+
- DataEvaluator tests for path helper resolution
|
|
147
|
+
|
|
148
|
+
### Fixed
|
|
149
|
+
- Path helpers (e.g. `message_path`) returning nil in reactive broadcasts — added engine initializer to finalize DataEvaluator at boot
|
|
150
|
+
- `setState` not triggering re-render after updating client state
|
|
151
|
+
- Turbo frame navigation breaking when `setState` morphed the DOM synchronously — deferred with `requestAnimationFrame`
|
|
152
|
+
- `live_action` documentation using outdated Stimulus data attribute conventions
|
|
153
|
+
|
|
154
|
+
## [0.2.0] - 2026-03-25
|
|
155
|
+
|
|
156
|
+
### Added
|
|
157
|
+
- "How It Works" architecture documentation page
|
|
158
|
+
- "Nested Components" guide
|
|
159
|
+
- "Collections & Loops" guide
|
|
160
|
+
- "Troubleshooting" page
|
|
161
|
+
- Enriched README with architecture summary, advanced features, and license section
|
|
162
|
+
- Reorganized docs sidebar for logical learning path
|
|
163
|
+
|
|
3
164
|
## [0.1.0] - 2026-03-15
|
|
4
165
|
|
|
5
166
|
### Added
|
|
@@ -20,23 +20,23 @@ module ReactiveComponent
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def request_update(data)
|
|
23
|
-
component_class = data[
|
|
24
|
-
params = data[
|
|
23
|
+
component_class = data['component'].constantize
|
|
24
|
+
params = data['params'] || {}
|
|
25
25
|
|
|
26
26
|
model_class = component_class.live_model_class
|
|
27
|
-
record_id = data[
|
|
27
|
+
record_id = data['record_id'] || params.delete('record_id')
|
|
28
28
|
record = model_class.find_by(id: record_id)
|
|
29
29
|
return unless record
|
|
30
30
|
|
|
31
|
-
if data[
|
|
31
|
+
if data['record_id'].present?
|
|
32
32
|
if record_matches?(record, params)
|
|
33
|
-
transmit({
|
|
33
|
+
transmit({ 'action' => 'render', 'data' => component_class.build_data(record) })
|
|
34
34
|
else
|
|
35
|
-
transmit({
|
|
35
|
+
transmit({ 'action' => 'remove', 'dom_id' => data['dom_id'] })
|
|
36
36
|
end
|
|
37
37
|
else
|
|
38
38
|
result = component_class.build_data(record, **params.symbolize_keys)
|
|
39
|
-
transmit({
|
|
39
|
+
transmit({ 'action' => 'render', 'data' => result })
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -50,7 +50,7 @@ module ReactiveComponent
|
|
|
50
50
|
|
|
51
51
|
def verified_stream_name
|
|
52
52
|
Turbo::StreamsChannel.verified_stream_name(params[:signed_stream_name])
|
|
53
|
-
rescue
|
|
53
|
+
rescue StandardError
|
|
54
54
|
nil
|
|
55
55
|
end
|
|
56
56
|
|
|
@@ -59,11 +59,11 @@ module ReactiveComponent
|
|
|
59
59
|
signed = Turbo::StreamsChannel.signed_stream_name(streamables)
|
|
60
60
|
stream_name = Turbo::StreamsChannel.verified_stream_name(signed)
|
|
61
61
|
|
|
62
|
-
payload = {action: action, data: data}
|
|
62
|
+
payload = { action: action, data: data }
|
|
63
63
|
|
|
64
64
|
if compress
|
|
65
65
|
json = ActiveSupport::JSON.encode(payload)
|
|
66
|
-
ActionCable.server.broadcast(stream_name, {z: Base64.strict_encode64(ActiveSupport::Gzip.compress(json))})
|
|
66
|
+
ActionCable.server.broadcast(stream_name, { z: Base64.strict_encode64(ActiveSupport::Gzip.compress(json)) })
|
|
67
67
|
else
|
|
68
68
|
ActionCable.server.broadcast(stream_name, payload)
|
|
69
69
|
end
|
|
@@ -10,7 +10,7 @@ module ReactiveComponent
|
|
|
10
10
|
component_class.execute_action(
|
|
11
11
|
params[:action_name],
|
|
12
12
|
record,
|
|
13
|
-
params.fetch(:params, {})
|
|
13
|
+
params.fetch(:params, {})
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
head :ok
|
|
@@ -20,9 +20,9 @@ module ReactiveComponent
|
|
|
20
20
|
|
|
21
21
|
def verify_token!
|
|
22
22
|
Rails.application.message_verifier(:reactive_component_action)
|
|
23
|
-
|
|
23
|
+
.verify(params[:token], purpose: :reactive_component_action)
|
|
24
24
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
25
|
-
raise ActionController::RoutingError,
|
|
25
|
+
raise ActionController::RoutingError, 'Not found'
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
import { createConsumer } from "@rails/actioncable"
|
|
3
|
-
import { compileTemplate, decompress, morphElement, buildActionBody, routeMessage } from "reactive_component/lib/reactive_renderer_utils"
|
|
3
|
+
import { compileTemplate, decompress, morphElement, buildActionBody, routeMessage, duplicateIds, strictData } from "reactive_component/lib/reactive_renderer_utils"
|
|
4
4
|
|
|
5
5
|
const consumer = createConsumer()
|
|
6
6
|
const log = (...args) => {
|
|
@@ -60,6 +60,15 @@ export default class extends Controller {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
connect() {
|
|
63
|
+
const clashes = duplicateIds(document, this.element)
|
|
64
|
+
if (clashes.length) {
|
|
65
|
+
console.error(
|
|
66
|
+
`[reactive-renderer] ${clashes.length + 1} components share id "${this.element.id}" — ` +
|
|
67
|
+
"each will render every other's broadcast. Give the component a distinct dom_id_prefix.",
|
|
68
|
+
this.element, ...clashes
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
this.clientState = { ...this.stateValue }
|
|
64
73
|
this.lastServerData = Object.keys(this.dataValue).length > 0 ? this.dataValue : null
|
|
65
74
|
|
|
@@ -142,7 +151,10 @@ export default class extends Controller {
|
|
|
142
151
|
}
|
|
143
152
|
|
|
144
153
|
render(data) {
|
|
145
|
-
|
|
154
|
+
// ReactiveComponent.debug marks the wrapper; a missing key then throws
|
|
155
|
+
// with its name rather than rendering as a silent falsy/undefined.
|
|
156
|
+
const input = this.element.hasAttribute("data-reactive-debug") ? strictData(data, this.element.id) : data
|
|
157
|
+
const newHtml = this.renderFn(input)
|
|
146
158
|
this.morph(newHtml)
|
|
147
159
|
}
|
|
148
160
|
|
|
@@ -208,14 +220,24 @@ export default class extends Controller {
|
|
|
208
220
|
if (el === this.element) return
|
|
209
221
|
const ctrl = this.application.getControllerForElementAndIdentifier(el, "reactive-renderer")
|
|
210
222
|
if (!ctrl?.clientState) return
|
|
223
|
+
let changed = false
|
|
211
224
|
for (const key of Object.keys(updates)) {
|
|
212
|
-
if (ctrl.clientState[key])
|
|
225
|
+
if (ctrl.clientState[key]) {
|
|
226
|
+
ctrl.clientState[key] = false
|
|
227
|
+
changed = true
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (changed && ctrl.lastServerData && ctrl.renderFn) {
|
|
231
|
+
requestAnimationFrame(() => ctrl.render({ ...ctrl.lastServerData, ...ctrl.clientState }))
|
|
213
232
|
}
|
|
214
233
|
})
|
|
215
234
|
}
|
|
216
235
|
}
|
|
217
236
|
|
|
218
237
|
Object.assign(this.clientState, updates)
|
|
238
|
+
if (this.lastServerData && this.renderFn) {
|
|
239
|
+
requestAnimationFrame(() => this.render({ ...this.lastServerData, ...this.clientState }))
|
|
240
|
+
}
|
|
219
241
|
}
|
|
220
242
|
|
|
221
243
|
morph(newHtml) {
|
|
@@ -97,3 +97,37 @@ export function routeMessage(message, elementId, strategy) {
|
|
|
97
97
|
|
|
98
98
|
return { type: "ignore" }
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
// Every element in `doc` that shares `element`'s id, excluding `element`.
|
|
102
|
+
// Broadcasts are routed to a component by its id, so a duplicate means one
|
|
103
|
+
// payload renders into several components — and a TypeError deep in the
|
|
104
|
+
// compiled template as soon as their shapes differ. Checked on connect, which
|
|
105
|
+
// Stimulus already fires for every component entering the DOM (its own
|
|
106
|
+
// MutationObserver), so nothing else needs to watch the page.
|
|
107
|
+
export function duplicateIds(doc, element) {
|
|
108
|
+
if (!element.id) return []
|
|
109
|
+
const selector = `[id="${element.id.replace(/["\\]/g, "\\$&")}"]`
|
|
110
|
+
return [...doc.querySelectorAll(selector)].filter(other => other !== element)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Debug mode only: `data` (and every item inside it) wrapped so that reading
|
|
114
|
+
// a key the payload does not carry THROWS, naming the key and what is there,
|
|
115
|
+
// instead of yielding undefined — which is silently falsy in an `if` and
|
|
116
|
+
// only ever surfaces as a mystery `.x of undefined` further down. Keys on the
|
|
117
|
+
// prototype chain (map, toString, constructor) and symbols pass through, so
|
|
118
|
+
// only a genuinely missing field trips it.
|
|
119
|
+
export function strictData(data, label) {
|
|
120
|
+
const wrap = (target, path) => new Proxy(target, {
|
|
121
|
+
get(obj, key, receiver) {
|
|
122
|
+
if (typeof key === "symbol" || key in obj) {
|
|
123
|
+
const value = Reflect.get(obj, key, receiver)
|
|
124
|
+
return value && typeof value === "object" ? wrap(value, `${path}${String(key)}.`) : value
|
|
125
|
+
}
|
|
126
|
+
throw new TypeError(
|
|
127
|
+
`[reactive-renderer] ${label}: template read "${path}${String(key)}" but the payload only has: ` +
|
|
128
|
+
Object.keys(obj).join(", ")
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
return wrap(data, "")
|
|
133
|
+
}
|
data/config/importmap.rb
CHANGED
data/config/routes.rb
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module ReactiveComponent
|
|
6
|
+
module Broadcastable
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
class_attribute :reactive_component_classes, instance_writer: false, default: Set.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def register_reactive_component(component_class)
|
|
15
|
+
return if reactive_component_classes.include?(component_class)
|
|
16
|
+
|
|
17
|
+
self.reactive_component_classes = reactive_component_classes | [component_class]
|
|
18
|
+
|
|
19
|
+
return if _commit_callbacks.map(&:filter).include?(:_broadcast_reactive_create)
|
|
20
|
+
|
|
21
|
+
after_create_commit :_broadcast_reactive_create
|
|
22
|
+
after_update_commit :_broadcast_reactive_update
|
|
23
|
+
after_destroy_commit :_broadcast_reactive_destroy
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def broadcast_reactive_update
|
|
28
|
+
_broadcast_reactive(:update)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def _broadcast_reactive_create = _broadcast_reactive(:create)
|
|
34
|
+
def _broadcast_reactive_update = _broadcast_reactive(:update)
|
|
35
|
+
def _broadcast_reactive_destroy = _broadcast_reactive(:destroy)
|
|
36
|
+
|
|
37
|
+
def _broadcast_reactive(action)
|
|
38
|
+
reactive_component_classes.each { |klass| ReactiveComponent.broadcast_for(klass, self, action: action) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|