live_component 0.1.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 +7 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +1 -0
- data/LICENSE +21 -0
- data/Rakefile +41 -0
- data/app/channels/live_component_channel.rb +23 -0
- data/app/components/live_component/render_component.rb +43 -0
- data/app/controllers/live_component/render_controller.rb +9 -0
- data/app/helpers/live_component/application_helper.rb +40 -0
- data/app/views/live_component/render/show.html.erb +1 -0
- data/config/routes.rb +5 -0
- data/ext/view_component_patch.rb +22 -0
- data/lib/live_component/action.rb +31 -0
- data/lib/live_component/base.rb +267 -0
- data/lib/live_component/big_decimal_serializer.rb +17 -0
- data/lib/live_component/component.html.erb +4 -0
- data/lib/live_component/controller_methods.rb +9 -0
- data/lib/live_component/date_serializer.rb +15 -0
- data/lib/live_component/date_time_serializer.rb +11 -0
- data/lib/live_component/duration_serializer.rb +18 -0
- data/lib/live_component/engine.rb +21 -0
- data/lib/live_component/inline_serializer.rb +34 -0
- data/lib/live_component/middleware.rb +25 -0
- data/lib/live_component/model_serializer.rb +65 -0
- data/lib/live_component/module_serializer.rb +15 -0
- data/lib/live_component/object_serializer.rb +29 -0
- data/lib/live_component/range_serializer.rb +19 -0
- data/lib/live_component/react.rb +24 -0
- data/lib/live_component/record_proxy.rb +85 -0
- data/lib/live_component/safe_dispatcher.rb +64 -0
- data/lib/live_component/serializer.rb +202 -0
- data/lib/live_component/state.rb +91 -0
- data/lib/live_component/tag_builder.rb +20 -0
- data/lib/live_component/target.rb +26 -0
- data/lib/live_component/time_object_serializer.rb +13 -0
- data/lib/live_component/time_serializer.rb +11 -0
- data/lib/live_component/time_with_zone_serializer.rb +20 -0
- data/lib/live_component/utils.rb +139 -0
- data/lib/live_component/version.rb +5 -0
- data/lib/live_component.rb +55 -0
- data/lib/tasks/test.rake +1 -0
- data/live_component.gemspec +21 -0
- metadata +136 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveComponent
|
|
4
|
+
class Middleware
|
|
5
|
+
def initialize(app)
|
|
6
|
+
@app = app
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
if env["PATH_INFO"] == "/live_component/render"
|
|
11
|
+
raw_data = env["rack.input"].read
|
|
12
|
+
data = JSON.parse(raw_data)
|
|
13
|
+
payload = JSON.parse(data["payload"])
|
|
14
|
+
|
|
15
|
+
result = LiveComponent::RenderController.renderer.render(
|
|
16
|
+
:show, assigns: { state: payload["state"], reflexes: payload["reflexes"] }, layout: false
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return [200, { "Content-Type" => "text/html" }, [result]]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@app.call(env)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveComponent
|
|
4
|
+
class ModelSerializer
|
|
5
|
+
MODEL_SERIALIZER_KEY = "_lc_ar".freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :sign, :load, :attributes
|
|
8
|
+
|
|
9
|
+
alias sign? sign
|
|
10
|
+
alias load? load
|
|
11
|
+
|
|
12
|
+
def self.make(...)
|
|
13
|
+
new(...)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(sign: true, load: false, attributes: true)
|
|
17
|
+
@sign = sign
|
|
18
|
+
@attributes = attributes.is_a?(Array) ? attributes.map(&:to_s) : attributes
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def serialize(object)
|
|
22
|
+
gid = sign? ? object.to_signed_global_id : object.to_global_id
|
|
23
|
+
|
|
24
|
+
{ MODEL_SERIALIZER_KEY => { "gid" => gid.to_s, "signed" => sign? } }.tap do |result|
|
|
25
|
+
object_attributes = if object.is_a?(RecordProxy)
|
|
26
|
+
object.cached_attributes
|
|
27
|
+
else
|
|
28
|
+
object.attributes
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
attributes_hash = if attributes.is_a?(Array)
|
|
32
|
+
object_attributes.slice(*attributes)
|
|
33
|
+
elsif attributes # true case
|
|
34
|
+
object_attributes
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if attributes_hash
|
|
38
|
+
attributes_hash.each_pair do |k, v|
|
|
39
|
+
result[k] = LiveComponent.serializer.serialize(v)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
rescue URI::GID::MissingModelIdError
|
|
44
|
+
raise SerializationError, "Unable to serialize #{object.class} " \
|
|
45
|
+
"without an id. (Maybe you forgot to call save?)"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def deserialize(hash)
|
|
49
|
+
gid_attrs = hash[MODEL_SERIALIZER_KEY]
|
|
50
|
+
gid = gid_attrs["gid"]
|
|
51
|
+
signed = gid_attrs["signed"]
|
|
52
|
+
|
|
53
|
+
if load?
|
|
54
|
+
if signed
|
|
55
|
+
GlobalID::Locator.locate_signed(gid)
|
|
56
|
+
else
|
|
57
|
+
GlobalID::Locator.locate(gid)
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
parsed_gid = signed ? SignedGlobalID.parse(gid) : GlobalID.parse(gid)
|
|
61
|
+
RecordProxy.for(parsed_gid, hash.except("_lc_ar"))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveComponent
|
|
4
|
+
class ModuleSerializer < ObjectSerializer
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def object_to_hash(constant)
|
|
8
|
+
{ "value" => constant.name }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def hash_to_object(hash)
|
|
12
|
+
hash["value"].constantize
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveComponent
|
|
4
|
+
class ObjectSerializer
|
|
5
|
+
OBJECT_SERIALIZER_KEY = "_lc_ser"
|
|
6
|
+
|
|
7
|
+
def self.make(...)
|
|
8
|
+
new(...)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def serialize(object)
|
|
12
|
+
{ OBJECT_SERIALIZER_KEY => object.class.name }.merge!(object_to_hash(object))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def deserialize(hash)
|
|
16
|
+
hash_to_object(hash)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def object_to_hash(_object)
|
|
22
|
+
raise NotImplementedError, "please define #{__method__} in derived classes"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def hash_to_object(_hash)
|
|
26
|
+
raise NotImplementedError, "please define #{__method__} in derived classes"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveComponent
|
|
4
|
+
class RangeSerializer < ObjectSerializer
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def object_to_hash(range)
|
|
8
|
+
{
|
|
9
|
+
"begin" => LiveComponent.serializer.serialize(range.begin),
|
|
10
|
+
"end" => LiveComponent.serializer.serialize(range.end),
|
|
11
|
+
"exclude_end" => range.exclude_end?, # Always boolean, no need to serialize
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def hash_to_object(hash)
|
|
16
|
+
Range.new(*LiveComponent.serializer.deserialize([hash["begin"], hash["end"]]), hash["exclude_end"])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveComponent
|
|
4
|
+
class React < ViewComponent::Base
|
|
5
|
+
include LiveComponent::Base
|
|
6
|
+
|
|
7
|
+
def initialize(component:, **props)
|
|
8
|
+
@component = component
|
|
9
|
+
@props = props
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
""
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def __lc_tag_name
|
|
17
|
+
"live-component-react"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def __lc_controller
|
|
21
|
+
"livereact"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveComponent
|
|
4
|
+
class RecordProxy
|
|
5
|
+
class << self
|
|
6
|
+
def for(gid, attributes = {})
|
|
7
|
+
proxy_mixins[gid.model_class] ||= Module.new.tap do |mod|
|
|
8
|
+
mtds = (gid.model_class.column_names - ["id"]).map do |column_name|
|
|
9
|
+
<<~RUBY
|
|
10
|
+
def #{column_name}
|
|
11
|
+
return @record.#{column_name} if @record
|
|
12
|
+
|
|
13
|
+
if @attributes.include?("#{column_name}")
|
|
14
|
+
return @attributes["#{column_name}"]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
load
|
|
18
|
+
|
|
19
|
+
@record.#{column_name}
|
|
20
|
+
end
|
|
21
|
+
RUBY
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
mod.class_eval(mtds.join("\n"), __FILE__, __LINE__)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
new(gid, attributes).tap do |proxy|
|
|
28
|
+
proxy.singleton_class.include(proxy_mixins[gid.model_class])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def proxy_mixins
|
|
35
|
+
@proxy_mixins ||= {}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(gid, attributes)
|
|
40
|
+
@gid = gid
|
|
41
|
+
@attributes = attributes
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def cached_attributes
|
|
45
|
+
@attributes
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
|
49
|
+
load unless @record
|
|
50
|
+
@record.send(method_name, *args, **kwargs, &block)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def load
|
|
54
|
+
@record ||= GlobalID::Locator.locate(@gid)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def reload
|
|
58
|
+
@record = @record ? @record.reload : load
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def id
|
|
62
|
+
@id ||= @gid.model_class.type_for_attribute("id").cast(@gid.model_id)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def to_global_id
|
|
66
|
+
GlobalID.new(@gid.uri)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_signed_global_id
|
|
70
|
+
SignedGlobalID.new(@gid.uri)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_model
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def to_param
|
|
78
|
+
id.to_s
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def persisted?
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# this is a gem that conditionally loads a backport of ObjectSpace::WeakKeyMap for
|
|
4
|
+
# ruby 3.2 and older
|
|
5
|
+
require "weak_key_map"
|
|
6
|
+
|
|
7
|
+
module LiveComponent
|
|
8
|
+
class SafeDispatcher
|
|
9
|
+
include Singleton
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@cache = ObjectSpace::WeakKeyMap.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def send_safely(receiver, method_name, **kwargs)
|
|
16
|
+
if receiver_defines_safe_method?(receiver, method_name)
|
|
17
|
+
receiver.send(method_name, **kwargs)
|
|
18
|
+
else
|
|
19
|
+
raise(
|
|
20
|
+
SafeDispatchError,
|
|
21
|
+
"`#{method_name}' could not be called on an object of type '#{receiver.class.name}'. "\
|
|
22
|
+
"Only public methods defined on classes that inherit from ViewComponent::Base "\
|
|
23
|
+
"may be called."
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.send_safely(...)
|
|
29
|
+
instance.send_safely(...)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def receiver_defines_safe_method?(receiver, method_name)
|
|
35
|
+
receiver.class.ancestors.each do |ancestor|
|
|
36
|
+
if ancestor_defines_safe_method?(ancestor, method_name)
|
|
37
|
+
@cache[receiver.class] ||= Set.new
|
|
38
|
+
@cache[receiver.class] << method_name
|
|
39
|
+
|
|
40
|
+
return true
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ancestor_defines_safe_method?(ancestor, method_name)
|
|
48
|
+
return false unless ancestor < ViewComponent::Base
|
|
49
|
+
|
|
50
|
+
public_mtds = @cache[ancestor] ||= Set.new
|
|
51
|
+
|
|
52
|
+
if public_mtds.include?(method_name)
|
|
53
|
+
return true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if ancestor.public_method_defined?(method_name, false)
|
|
57
|
+
public_mtds << method_name
|
|
58
|
+
return true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module LiveComponent
|
|
6
|
+
class Serializer
|
|
7
|
+
GLOBALID_KEY = "_lc_gid"
|
|
8
|
+
SYMBOL_KEY = "_lc_sym"
|
|
9
|
+
SYMBOL_KEYS_KEY = "_lc_symkeys"
|
|
10
|
+
SYMBOL_HASH_KEY = "_lc_symhash"
|
|
11
|
+
WITH_INDIFFERENT_ACCESS_KEY = "_lc_hwia"
|
|
12
|
+
|
|
13
|
+
RESERVED_KEYS = [
|
|
14
|
+
GLOBALID_KEY, GLOBALID_KEY.to_sym,
|
|
15
|
+
SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym,
|
|
16
|
+
SYMBOL_HASH_KEY, SYMBOL_HASH_KEY.to_sym,
|
|
17
|
+
ObjectSerializer::OBJECT_SERIALIZER_KEY, ObjectSerializer::OBJECT_SERIALIZER_KEY.to_sym,
|
|
18
|
+
WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym,
|
|
19
|
+
].to_set
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def make
|
|
23
|
+
new
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
add_serializer(BigDecimal, BigDecimalSerializer)
|
|
29
|
+
add_serializer(Date, DateSerializer)
|
|
30
|
+
add_serializer(DateTime, DateTimeSerializer)
|
|
31
|
+
add_serializer(ActiveSupport::Duration, DurationSerializer)
|
|
32
|
+
add_serializer(Module, ModuleSerializer)
|
|
33
|
+
add_serializer(Range, RangeSerializer)
|
|
34
|
+
add_serializer(Time, TimeSerializer)
|
|
35
|
+
add_serializer(ActiveSupport::TimeWithZone, TimeWithZoneSerializer)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def add_serializer(klass, serializer_klass)
|
|
39
|
+
self.serializers[klass] = serializer_klass.make
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def serializers
|
|
43
|
+
@serializers ||= {}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def serialize(object)
|
|
47
|
+
case object
|
|
48
|
+
when nil, true, false, Integer, Float, String
|
|
49
|
+
object
|
|
50
|
+
when Symbol
|
|
51
|
+
{ SYMBOL_KEY => true, "value" => object.name }
|
|
52
|
+
when ActiveRecord::Base, RecordProxy
|
|
53
|
+
default_model_serializer.serialize(object)
|
|
54
|
+
when GlobalID::Identification
|
|
55
|
+
convert_to_global_id_hash(object)
|
|
56
|
+
when Array
|
|
57
|
+
object.map { |elem| serialize(elem) }
|
|
58
|
+
when ActiveSupport::HashWithIndifferentAccess
|
|
59
|
+
serialize_indifferent_hash(object)
|
|
60
|
+
when Hash
|
|
61
|
+
symbol_keys = object.keys
|
|
62
|
+
symbol_keys.select! { |k| k.is_a?(Symbol) }
|
|
63
|
+
symbol_keys.map!(&:name)
|
|
64
|
+
result = serialize_hash(object)
|
|
65
|
+
|
|
66
|
+
if Hash.ruby2_keywords_hash?(object)
|
|
67
|
+
result[SYMBOL_HASH_KEY] = true
|
|
68
|
+
else
|
|
69
|
+
result[SYMBOL_KEYS_KEY] = symbol_keys
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
result
|
|
73
|
+
else
|
|
74
|
+
if object.respond_to?(:permitted?) && object.respond_to?(:to_h)
|
|
75
|
+
serialize_indifferent_hash(object.to_h)
|
|
76
|
+
elsif serializer = serializers[object.class]
|
|
77
|
+
serializer.serialize(object)
|
|
78
|
+
else
|
|
79
|
+
raise SerializationError, "No serializer found for #{object.class}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def deserialize(object)
|
|
85
|
+
case object
|
|
86
|
+
when nil, true, false, String, Integer, Float
|
|
87
|
+
object
|
|
88
|
+
when Array
|
|
89
|
+
object.map { |elem| deserialize(elem) }
|
|
90
|
+
when Hash
|
|
91
|
+
if object[SYMBOL_KEY]
|
|
92
|
+
object["value"].to_sym
|
|
93
|
+
elsif serialized_model?(object)
|
|
94
|
+
default_model_serializer.deserialize(object)
|
|
95
|
+
elsif serialized_global_id?(object)
|
|
96
|
+
deserialize_global_id(object)
|
|
97
|
+
elsif custom_serialized?(object)
|
|
98
|
+
serializer_name = object[ObjectSerializer::OBJECT_SERIALIZER_KEY]
|
|
99
|
+
raise SerializationError, "Serializer name is not present in the object: #{object.inspect}" unless serializer_name
|
|
100
|
+
|
|
101
|
+
serializer = lookup_serializer(serializer_name)
|
|
102
|
+
raise SerializationError, "Serializer #{serializer_name} is not known" unless serializer
|
|
103
|
+
|
|
104
|
+
serializer.deserialize(object)
|
|
105
|
+
else
|
|
106
|
+
deserialize_hash(object)
|
|
107
|
+
end
|
|
108
|
+
else
|
|
109
|
+
raise SerializationError, "Can only deserialize primitive types, got #{object.inspect}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def lookup_serializer(const_str)
|
|
116
|
+
const = const_str.safe_constantize
|
|
117
|
+
|
|
118
|
+
if serializers.include?(const)
|
|
119
|
+
serializers[const]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def serialized_global_id?(hash)
|
|
124
|
+
hash.include?(GLOBALID_KEY)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def serialized_model?(hash)
|
|
128
|
+
hash.include?(ModelSerializer::MODEL_SERIALIZER_KEY)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def deserialize_global_id(hash)
|
|
132
|
+
GlobalID::Locator.locate(hash[GLOBALID_KEY])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def custom_serialized?(hash)
|
|
136
|
+
hash.include?(ObjectSerializer::OBJECT_SERIALIZER_KEY)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def serialize_hash(object)
|
|
140
|
+
object.each_with_object({}) do |(key, value), hash|
|
|
141
|
+
hash[serialize_hash_key(key)] = serialize(value)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def deserialize_hash(serialized_hash)
|
|
146
|
+
result = serialized_hash.transform_values { |v| deserialize(v) }
|
|
147
|
+
|
|
148
|
+
if result.delete(WITH_INDIFFERENT_ACCESS_KEY)
|
|
149
|
+
result = result.with_indifferent_access
|
|
150
|
+
elsif symbol_keys = result.delete(SYMBOL_KEYS_KEY)
|
|
151
|
+
result = transform_symbol_keys(result, symbol_keys)
|
|
152
|
+
elsif result.delete(SYMBOL_HASH_KEY)
|
|
153
|
+
result.symbolize_keys!
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
result
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def serialize_hash_key(key)
|
|
160
|
+
case key
|
|
161
|
+
when RESERVED_KEYS
|
|
162
|
+
raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}")
|
|
163
|
+
when String
|
|
164
|
+
key
|
|
165
|
+
when Symbol
|
|
166
|
+
key.name
|
|
167
|
+
else
|
|
168
|
+
raise SerializationError.new("Only string and symbol hash keys are supported, but #{key.inspect} is a(n) #{key.class}")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def serialize_indifferent_hash(indifferent_hash)
|
|
173
|
+
result = serialize_hash(indifferent_hash)
|
|
174
|
+
result[WITH_INDIFFERENT_ACCESS_KEY] = true
|
|
175
|
+
result
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def transform_symbol_keys(hash, symbol_keys)
|
|
179
|
+
# NOTE: HashWithIndifferentAccess#transform_keys always
|
|
180
|
+
# returns stringified keys with indifferent access
|
|
181
|
+
# so we call #to_h here to ensure keys are symbolized.
|
|
182
|
+
hash.to_h.transform_keys do |key|
|
|
183
|
+
if symbol_keys.include?(key)
|
|
184
|
+
key.to_sym
|
|
185
|
+
else
|
|
186
|
+
key
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def convert_to_global_id_hash(object)
|
|
192
|
+
{ GLOBALID_KEY => object.to_global_id.to_s }
|
|
193
|
+
rescue URI::GID::MissingModelIdError
|
|
194
|
+
raise SerializationError, "Unable to serialize #{object.class} " \
|
|
195
|
+
"without an id. (Maybe you forgot to call save?)"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def default_model_serializer
|
|
199
|
+
@default_model_serializer ||= ModelSerializer.new
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveComponent
|
|
4
|
+
class State
|
|
5
|
+
class << self
|
|
6
|
+
def build(definition, prop_overrides = {})
|
|
7
|
+
klass = definition["ruby_class"] || definition[:ruby_class]
|
|
8
|
+
klass = LiveComponent::Utils.lookup_component_class(klass) if klass && !klass.is_a?(Class)
|
|
9
|
+
props = definition["props"] || definition[:props] || {}
|
|
10
|
+
|
|
11
|
+
props.symbolize_keys!
|
|
12
|
+
props.except!(*prop_overrides.keys)
|
|
13
|
+
props = klass.deserialize_props(props) if klass
|
|
14
|
+
props.merge!(prop_overrides)
|
|
15
|
+
|
|
16
|
+
slots = build_slots(definition["slots"] || definition[:slots] || {}) || {}
|
|
17
|
+
children = build_children(definition["children"] || definition[:children] || {}) || {}
|
|
18
|
+
|
|
19
|
+
State.new(
|
|
20
|
+
klass: klass,
|
|
21
|
+
props: props,
|
|
22
|
+
slots: slots,
|
|
23
|
+
children: children,
|
|
24
|
+
content: definition["content"] || definition[:content]
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def build_slots(slot_map)
|
|
31
|
+
return unless slot_map
|
|
32
|
+
|
|
33
|
+
{}.tap do |results|
|
|
34
|
+
slot_map.each do |slot_name, slot_entries|
|
|
35
|
+
results[slot_name] = slot_entries.map do |slot_entry|
|
|
36
|
+
build(slot_entry)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build_children(child_map)
|
|
43
|
+
return unless child_map
|
|
44
|
+
|
|
45
|
+
child_map.each_with_object({}) do |(child_id, child_entry), memo|
|
|
46
|
+
memo[child_id] = build(child_entry)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
attr_reader :root, :klass, :props, :slots, :children
|
|
52
|
+
attr_accessor :content
|
|
53
|
+
|
|
54
|
+
alias root? root
|
|
55
|
+
|
|
56
|
+
def initialize(root: false, klass: nil, props: {}, slots: {}, children: {}, content: nil)
|
|
57
|
+
@root = root
|
|
58
|
+
@klass = klass
|
|
59
|
+
@props = props
|
|
60
|
+
@slots = slots
|
|
61
|
+
@children = children
|
|
62
|
+
@content = content
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def root!
|
|
66
|
+
@root = true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_h
|
|
70
|
+
{
|
|
71
|
+
ruby_class: klass ? klass.name : nil,
|
|
72
|
+
|
|
73
|
+
props: klass ? klass.serialize_props(props) : LiveComponent.serializer.serialize(props),
|
|
74
|
+
|
|
75
|
+
slots: slots.each_with_object({}) do |(k, v), h|
|
|
76
|
+
h[k] = v.map(&:to_h)
|
|
77
|
+
end,
|
|
78
|
+
|
|
79
|
+
children: children.each_with_object({}) do |(k, v), h|
|
|
80
|
+
h[k] = v.to_h
|
|
81
|
+
end,
|
|
82
|
+
|
|
83
|
+
content: content,
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def to_json
|
|
88
|
+
to_h.to_json
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveComponent
|
|
4
|
+
class TagBuilder
|
|
5
|
+
def initialize(controller)
|
|
6
|
+
@controller = controller
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def rerender(**kwargs, &block)
|
|
10
|
+
state = JSON.parse(@controller.params[:__lc_rerender_state])
|
|
11
|
+
id = @controller.params[:__lc_rerender_id]
|
|
12
|
+
state["props"]["__lc_attributes"] = { "data-id" => id }
|
|
13
|
+
|
|
14
|
+
component = LiveComponent::RenderComponent.new(state, [], kwargs)
|
|
15
|
+
|
|
16
|
+
# We have to render a turbo stream so Turbo doesn't append this to the <html> tag
|
|
17
|
+
@controller.turbo_stream.update(:this_id_shouldnt_exist, @controller.render(component, &block))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveComponent
|
|
4
|
+
class Target
|
|
5
|
+
def initialize(controller_name, target_name)
|
|
6
|
+
@controller_name = controller_name
|
|
7
|
+
@target_name = target_name
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_attributes
|
|
11
|
+
{
|
|
12
|
+
data: {
|
|
13
|
+
"#{@controller_name}-target": @target_name
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.attr_name
|
|
19
|
+
:targets
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def attr_name
|
|
23
|
+
self.class.attr_name
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|