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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2 -0
  3. data/Gemfile +1 -0
  4. data/LICENSE +21 -0
  5. data/Rakefile +41 -0
  6. data/app/channels/live_component_channel.rb +23 -0
  7. data/app/components/live_component/render_component.rb +43 -0
  8. data/app/controllers/live_component/render_controller.rb +9 -0
  9. data/app/helpers/live_component/application_helper.rb +40 -0
  10. data/app/views/live_component/render/show.html.erb +1 -0
  11. data/config/routes.rb +5 -0
  12. data/ext/view_component_patch.rb +22 -0
  13. data/lib/live_component/action.rb +31 -0
  14. data/lib/live_component/base.rb +267 -0
  15. data/lib/live_component/big_decimal_serializer.rb +17 -0
  16. data/lib/live_component/component.html.erb +4 -0
  17. data/lib/live_component/controller_methods.rb +9 -0
  18. data/lib/live_component/date_serializer.rb +15 -0
  19. data/lib/live_component/date_time_serializer.rb +11 -0
  20. data/lib/live_component/duration_serializer.rb +18 -0
  21. data/lib/live_component/engine.rb +21 -0
  22. data/lib/live_component/inline_serializer.rb +34 -0
  23. data/lib/live_component/middleware.rb +25 -0
  24. data/lib/live_component/model_serializer.rb +65 -0
  25. data/lib/live_component/module_serializer.rb +15 -0
  26. data/lib/live_component/object_serializer.rb +29 -0
  27. data/lib/live_component/range_serializer.rb +19 -0
  28. data/lib/live_component/react.rb +24 -0
  29. data/lib/live_component/record_proxy.rb +85 -0
  30. data/lib/live_component/safe_dispatcher.rb +64 -0
  31. data/lib/live_component/serializer.rb +202 -0
  32. data/lib/live_component/state.rb +91 -0
  33. data/lib/live_component/tag_builder.rb +20 -0
  34. data/lib/live_component/target.rb +26 -0
  35. data/lib/live_component/time_object_serializer.rb +13 -0
  36. data/lib/live_component/time_serializer.rb +11 -0
  37. data/lib/live_component/time_with_zone_serializer.rb +20 -0
  38. data/lib/live_component/utils.rb +139 -0
  39. data/lib/live_component/version.rb +5 -0
  40. data/lib/live_component.rb +55 -0
  41. data/lib/tasks/test.rake +1 -0
  42. data/live_component.gemspec +21 -0
  43. 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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LiveComponent
4
+ class TimeObjectSerializer < ObjectSerializer
5
+ NANO_PRECISION = 9
6
+
7
+ private
8
+
9
+ def object_to_hash(time)
10
+ { "value" => time.iso8601(NANO_PRECISION) }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LiveComponent
4
+ class TimeSerializer < TimeObjectSerializer
5
+ private
6
+
7
+ def hash_to_object(hash)
8
+ Time.iso8601(hash["value"])
9
+ end
10
+ end
11
+ end