reactive_component 0.7.2 → 0.8.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 +14 -0
- data/app/javascript/reactive_component/lib/reactive_renderer_utils.js +7 -1
- data/lib/reactive_component/broadcastable.rb +11 -9
- data/lib/reactive_component/entity.rb +85 -0
- data/lib/reactive_component/version.rb +1 -1
- data/lib/reactive_component.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6b20beff63665f50b6910b02f241982c53bf7112145d2db4158b08738d2b3c4
|
|
4
|
+
data.tar.gz: 43c738cda0f1d36bb20a42f41a16ba9c55585858487d363cb37cebd4aa26de0d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b1011b2b0c3d08ad80ecc86586d7c0061c77bc762c0bbafbbf031c2420d711e69c1c63b34ea67ef0f3bfd0824f7f48ce2737e59b26e61a3ad1def298a6b9ed3
|
|
7
|
+
data.tar.gz: dd3c3c6ad4e6d900bfd37ad95450c20344fda96060e0cb1d8f8695918b9e7a6678c89a2d8f1ff673a129cd850c6f07e8c556249bd4410eeaef549c29c9be49d2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-09-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `ReactiveComponent::Entity`: subscribe a component to an object built from
|
|
7
|
+
several models. The entity declares its `root` record and `rebuilds_on`
|
|
8
|
+
each source model, optionally narrowed to `fields:`. See the Derived
|
|
9
|
+
Entities guide.
|
|
10
|
+
- `broadcast_reactive(action)` and `broadcast_reactive_destroy` are public on
|
|
11
|
+
wired models, alongside `broadcast_reactive_update`.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Multibyte characters in a template (★, é, emoji) rendered as mojibake after
|
|
15
|
+
a live re-render. The client now decodes the base64 template as UTF-8.
|
|
16
|
+
|
|
3
17
|
## [0.7.2] - 2026-09-07
|
|
4
18
|
|
|
5
19
|
### Security
|
|
@@ -8,7 +8,7 @@ export function compileTemplate(source) {
|
|
|
8
8
|
if (templateCache.has(source)) return templateCache.get(source)
|
|
9
9
|
|
|
10
10
|
try {
|
|
11
|
-
const body = isBase64(source) ?
|
|
11
|
+
const body = isBase64(source) ? decodeBase64(source) : source
|
|
12
12
|
const fn = new Function("data", body)
|
|
13
13
|
templateCache.set(source, fn)
|
|
14
14
|
return fn
|
|
@@ -18,6 +18,12 @@ export function compileTemplate(source) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// atob yields one char per byte, so multibyte UTF-8 (★, é, emoji) in a
|
|
22
|
+
// template literal came out as mojibake once the client re-rendered.
|
|
23
|
+
function decodeBase64(base64) {
|
|
24
|
+
return new TextDecoder().decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0)))
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
export function clearTemplateCache() {
|
|
22
28
|
templateCache.clear()
|
|
23
29
|
}
|
|
@@ -16,6 +16,9 @@ module ReactiveComponent
|
|
|
16
16
|
|
|
17
17
|
self.reactive_component_classes = reactive_component_classes | [component_class]
|
|
18
18
|
|
|
19
|
+
# A derived entity (ReactiveComponent::Entity) is not an ActiveRecord
|
|
20
|
+
# model: it has no commit callbacks and broadcasts itself.
|
|
21
|
+
return unless respond_to?(:after_create_commit)
|
|
19
22
|
return if _commit_callbacks.map(&:filter).include?(:_broadcast_reactive_create)
|
|
20
23
|
|
|
21
24
|
after_create_commit :_broadcast_reactive_create
|
|
@@ -24,18 +27,17 @@ module ReactiveComponent
|
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
|
|
27
|
-
def
|
|
28
|
-
|
|
30
|
+
def broadcast_reactive(action)
|
|
31
|
+
reactive_component_classes.each { |klass| ReactiveComponent.broadcast_for(klass, self, action: action) }
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
def broadcast_reactive_update = broadcast_reactive(:update)
|
|
35
|
+
def broadcast_reactive_destroy = broadcast_reactive(:destroy)
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
def _broadcast_reactive_update = _broadcast_reactive(:update)
|
|
35
|
-
def _broadcast_reactive_destroy = _broadcast_reactive(:destroy)
|
|
37
|
+
private
|
|
36
38
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
def _broadcast_reactive_create = broadcast_reactive(:create)
|
|
40
|
+
def _broadcast_reactive_update = broadcast_reactive(:update)
|
|
41
|
+
def _broadcast_reactive_destroy = broadcast_reactive(:destroy)
|
|
40
42
|
end
|
|
41
43
|
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_model'
|
|
4
|
+
|
|
5
|
+
module ReactiveComponent
|
|
6
|
+
# A derived entity: a plain object built on top of several ActiveRecord
|
|
7
|
+
# models that a component can `subscribes_to` like a model. The entity owns
|
|
8
|
+
# the whole declaration — which record it is keyed on, and which changes to
|
|
9
|
+
# which models rebuild it:
|
|
10
|
+
#
|
|
11
|
+
# class OrderSummary
|
|
12
|
+
# include ReactiveComponent::Entity
|
|
13
|
+
#
|
|
14
|
+
# root :order
|
|
15
|
+
# rebuilds_on Order, fields: %i[status total_cents]
|
|
16
|
+
# rebuilds_on Payment, via: :order_id, fields: %i[amount]
|
|
17
|
+
# rebuilds_on Shipment, via: :order_id, fields: %i[delivered_at]
|
|
18
|
+
#
|
|
19
|
+
# def total = order.payments.sum(:amount)
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# class OrderSummaryComponent < ApplicationComponent
|
|
23
|
+
# include ReactiveComponent
|
|
24
|
+
# subscribes_to :summary, class_name: "OrderSummary"
|
|
25
|
+
# end
|
|
26
|
+
module Entity
|
|
27
|
+
extend ActiveSupport::Concern
|
|
28
|
+
include ActiveModel::Model
|
|
29
|
+
include Broadcastable
|
|
30
|
+
|
|
31
|
+
included do
|
|
32
|
+
class_attribute :root_name, instance_writer: false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def persisted? = true
|
|
36
|
+
|
|
37
|
+
# Default stream when the component declares no `broadcasts stream:`.
|
|
38
|
+
# A bare id would collide with every other entity sharing it.
|
|
39
|
+
def to_param = "#{self.class.model_name.param_key}/#{id}"
|
|
40
|
+
|
|
41
|
+
class_methods do
|
|
42
|
+
# The record the entity is keyed on. Defines `initialize(<name>:)`, the
|
|
43
|
+
# reader, `id`, and the `find` / `find_by(id:)` the channel and actions
|
|
44
|
+
# controller need.
|
|
45
|
+
def root(name, class_name: name.to_s.classify)
|
|
46
|
+
self.root_name = name.to_sym
|
|
47
|
+
attr_reader name
|
|
48
|
+
|
|
49
|
+
define_method(:initialize) { |**kwargs| instance_variable_set(:"@#{name}", kwargs.fetch(name)) }
|
|
50
|
+
delegate :id, to: name
|
|
51
|
+
|
|
52
|
+
define_singleton_method(:find) { |id| new(name => class_name.constantize.find(id)) }
|
|
53
|
+
define_singleton_method(:find_by) { |id:| (record = class_name.constantize.find_by(id: id)) && new(name => record) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Rebroadcast the entity after `model` commits. `via:` is the foreign key
|
|
57
|
+
# on `model` pointing at the root; omit it when `model` is the root.
|
|
58
|
+
# `fields:` narrows updates to the listed columns; create and destroy
|
|
59
|
+
# always count.
|
|
60
|
+
def rebuilds_on(model, via: nil, fields: nil)
|
|
61
|
+
entity = self
|
|
62
|
+
fields = fields&.map(&:to_s)
|
|
63
|
+
|
|
64
|
+
model.after_commit { entity.rebuild_from(self, via: via, fields: fields) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def rebuild_from(record, via:, fields:)
|
|
68
|
+
action = commit_action(record)
|
|
69
|
+
return if action == :update && fields && !record.saved_changes.keys.intersect?(fields)
|
|
70
|
+
return find_by(id: record.public_send(via))&.broadcast_reactive(:update) if via
|
|
71
|
+
|
|
72
|
+
new(root_name => record).broadcast_reactive(action)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def commit_action(record)
|
|
78
|
+
return :destroy if record.destroyed?
|
|
79
|
+
return :create if record.previously_new_record?
|
|
80
|
+
|
|
81
|
+
:update
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/reactive_component.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative 'reactive_component/compiler'
|
|
|
8
8
|
require_relative 'reactive_component/data_evaluator'
|
|
9
9
|
require_relative 'reactive_component/wrapper'
|
|
10
10
|
require_relative 'reactive_component/broadcastable'
|
|
11
|
+
require_relative 'reactive_component/entity'
|
|
11
12
|
require_relative 'reactive_component/engine' if defined?(Rails::Engine)
|
|
12
13
|
|
|
13
14
|
module ReactiveComponent
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: reactive_component
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Przemyslaw Lusar
|
|
@@ -113,6 +113,7 @@ files:
|
|
|
113
113
|
- lib/reactive_component/compiler.rb
|
|
114
114
|
- lib/reactive_component/data_evaluator.rb
|
|
115
115
|
- lib/reactive_component/engine.rb
|
|
116
|
+
- lib/reactive_component/entity.rb
|
|
116
117
|
- lib/reactive_component/erubi.rb
|
|
117
118
|
- lib/reactive_component/transpiler.rb
|
|
118
119
|
- lib/reactive_component/version.rb
|