solid_objects 0.4.3 → 0.5.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.
@@ -37,10 +37,20 @@ module SolidObjects
37
37
  )
38
38
  end
39
39
 
40
- # @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?) -> untyped
41
- def component(name, observes: nil, partial: nil)
40
+ # @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol) -> untyped
41
+ def component(
42
+ name,
43
+ observes: nil,
44
+ partial: nil,
45
+ key: nil,
46
+ locals: {},
47
+ refresh: :replace
48
+ )
42
49
  component_name = normalized_component_name(name)
43
- return static_component(component_name, partial:) unless observes
50
+ unless observes
51
+ validate_static_options!(key:, locals:, refresh:)
52
+ return static_component(component_name, partial:)
53
+ end
44
54
  if partial
45
55
  raise ArgumentError,
46
56
  "reactive components resolve partials by actor and component name"
@@ -48,19 +58,21 @@ module SolidObjects
48
58
 
49
59
  dependencies = normalized_dependencies(observes)
50
60
  validate_dependencies!(dependencies)
51
- ensure_unique_component!(component_name)
52
61
  refresh_path = component_path_resolver.call(view_context:)
53
62
  registration = ComponentRegistration.issue(
54
63
  reference:,
55
64
  component_name:,
65
+ component_key: key,
56
66
  dependencies:,
67
+ locals:,
68
+ refresh_method: refresh,
57
69
  snapshot:,
58
70
  refresh_path:
59
71
  )
72
+ ensure_unique_component!(registration)
60
73
  rendered = ComponentRenderer.new(
61
74
  snapshot:,
62
- component_name:,
63
- dependencies:,
75
+ registration:,
64
76
  view_context:,
65
77
  authorization_context:
66
78
  ).call
@@ -68,9 +80,10 @@ module SolidObjects
68
80
  view_context.content_tag(
69
81
  :"turbo-frame",
70
82
  rendered,
71
- id: DomIdentity.component(reference, component_name),
83
+ id: registration.dom_id,
72
84
  data: {
73
- solid_objects_revision: "#{snapshot.instance_id}:#{snapshot.revision}"
85
+ solid_objects_revision: "#{snapshot.instance_id}:#{snapshot.revision}",
86
+ solid_objects_refresh: registration.refresh_method
74
87
  }
75
88
  )
76
89
  end
@@ -85,6 +98,11 @@ module SolidObjects
85
98
  observable_names.dup
86
99
  end
87
100
 
101
+ # @rbs () -> bool
102
+ def morph_components?
103
+ component_registrations.any?(&:morph?)
104
+ end
105
+
88
106
  # @rbs () -> State
89
107
  def state
90
108
  snapshot.actor.state
@@ -183,14 +201,23 @@ module SolidObjects
183
201
  "unknown observable dependency #{unknown.inspect} for #{reference.actor_type}"
184
202
  end
185
203
 
186
- # @rbs (String) -> void
187
- def ensure_unique_component!(component_name)
188
- return unless component_registrations.any? do |registration|
189
- registration.component_name == component_name
204
+ # @rbs (ComponentRegistration) -> void
205
+ def ensure_unique_component!(registration)
206
+ return unless component_registrations.any? do |existing_registration|
207
+ existing_registration.dom_id == registration.dom_id
190
208
  end
191
209
 
192
210
  raise ArgumentError,
193
- "component #{component_name.inspect} is already rendered in this solid_object scope"
211
+ "component #{registration.component_name.inspect} with key " \
212
+ "#{registration.component_key.inspect} is already rendered in this solid_object scope"
213
+ end
214
+
215
+ # @rbs (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol) -> void
216
+ def validate_static_options!(key:, locals:, refresh:)
217
+ return if key.nil? && locals.empty? && refresh.to_s == "replace"
218
+
219
+ raise ArgumentError,
220
+ "key, locals, and refresh require an observable component"
194
221
  end
195
222
 
196
223
  # @rbs () -> Proc | ComponentPathResolver
@@ -6,7 +6,10 @@ module SolidObjects
6
6
  class ComponentRegistration
7
7
  # @rbs @reference: Reference
8
8
  # @rbs @component_name: String
9
+ # @rbs @component_key: String | Integer?
9
10
  # @rbs @dependencies: Array[String]
11
+ # @rbs @locals: Hash[String, untyped]
12
+ # @rbs @refresh_method: String
10
13
  # @rbs @instance_id: Integer
11
14
  # @rbs @revision: Integer
12
15
  # @rbs @refresh_path: String
@@ -14,17 +17,23 @@ module SolidObjects
14
17
 
15
18
  attr_reader :reference,
16
19
  :component_name,
20
+ :component_key,
17
21
  :dependencies,
22
+ :locals,
23
+ :refresh_method,
18
24
  :instance_id,
19
25
  :revision,
20
26
  :refresh_path,
21
27
  :token
22
28
 
23
- # @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, token: String) -> void
29
+ # @rbs (reference: Reference, component_name: String, component_key: String | Integer?, dependencies: Array[String], locals: Hash[String, untyped], refresh_method: String, instance_id: Integer, revision: Integer, refresh_path: String, token: String) -> void
24
30
  def initialize(
25
31
  reference:,
26
32
  component_name:,
33
+ component_key:,
27
34
  dependencies:,
35
+ locals:,
36
+ refresh_method:,
28
37
  instance_id:,
29
38
  revision:,
30
39
  refresh_path:,
@@ -32,7 +41,10 @@ module SolidObjects
32
41
  )
33
42
  @reference = reference
34
43
  @component_name = component_name
44
+ @component_key = component_key
35
45
  @dependencies = dependencies.freeze
46
+ @locals = Serialization.readonly_copy(locals)
47
+ @refresh_method = refresh_method
36
48
  @instance_id = instance_id
37
49
  @revision = revision
38
50
  @refresh_path = refresh_path
@@ -40,30 +52,43 @@ module SolidObjects
40
52
  end
41
53
 
42
54
  class << self
43
- # @rbs (reference: Reference, component_name: String, dependencies: Array[String], snapshot: ActorSnapshot, refresh_path: String) -> ComponentRegistration
44
- def issue(reference:, component_name:, dependencies:, snapshot:, refresh_path:)
55
+ # @rbs (reference: Reference, component_name: String, component_key: untyped, dependencies: Array[String], locals: Hash[untyped, untyped], refresh_method: String | Symbol, snapshot: ActorSnapshot, refresh_path: String) -> ComponentRegistration
56
+ def issue(
57
+ reference:,
58
+ component_name:,
59
+ component_key:,
60
+ dependencies:,
61
+ locals:,
62
+ refresh_method:,
63
+ snapshot:,
64
+ refresh_path:
65
+ )
45
66
  token = ComponentToken.generate(
46
67
  reference:,
47
68
  component_name:,
69
+ component_key:,
48
70
  dependencies:,
71
+ locals:,
72
+ refresh_method:,
49
73
  instance_id: snapshot.instance_id,
50
74
  revision: snapshot.revision,
51
75
  refresh_path:
52
76
  )
53
- new(
54
- reference:,
55
- component_name:,
56
- dependencies:,
57
- instance_id: snapshot.instance_id,
58
- revision: snapshot.revision,
59
- refresh_path:,
60
- token:
61
- )
77
+ build(ComponentToken.verify(token), token:)
62
78
  end
63
79
 
64
80
  # @rbs (String) -> ComponentRegistration
65
81
  def from_token(token)
66
82
  payload = ComponentToken.verify(token)
83
+ build(payload, token:)
84
+ rescue UnknownActorType, UnknownComponentDependency => error
85
+ raise InvalidComponentToken, error.message
86
+ end
87
+
88
+ private
89
+
90
+ # @rbs (Hash[String, untyped], token: String) -> ComponentRegistration
91
+ def build(payload, token:)
67
92
  reference = Reference.new(
68
93
  actor_type: payload.fetch("actor_type"),
69
94
  actor_id: payload.fetch("actor_id")
@@ -73,18 +98,17 @@ module SolidObjects
73
98
  new(
74
99
  reference:,
75
100
  component_name: payload.fetch("component_name"),
101
+ component_key: payload["component_key"],
76
102
  dependencies:,
103
+ locals: payload.fetch("locals"),
104
+ refresh_method: payload.fetch("refresh_method"),
77
105
  instance_id: payload.fetch("instance_id"),
78
106
  revision: payload.fetch("revision"),
79
107
  refresh_path: payload.fetch("refresh_path"),
80
108
  token:
81
109
  )
82
- rescue UnknownActorType, UnknownComponentDependency => error
83
- raise InvalidComponentToken, error.message
84
110
  end
85
111
 
86
- private
87
-
88
112
  # @rbs (Reference, Array[String]) -> void
89
113
  def validate_dependencies!(reference, dependencies)
90
114
  actor_class = SolidObjects.registry.fetch(reference.actor_type)
@@ -104,6 +128,27 @@ module SolidObjects
104
128
  [ instance_id, revision ]
105
129
  end
106
130
 
131
+ # @rbs () -> String
132
+ def dom_id
133
+ DomIdentity.component(
134
+ reference,
135
+ component_name,
136
+ key: component_key
137
+ )
138
+ end
139
+
140
+ # @rbs () -> bool
141
+ def morph?
142
+ refresh_method == "morph"
143
+ end
144
+
145
+ # @rbs () -> Hash[String, untyped]
146
+ def authorization_arguments
147
+ return locals unless component_key
148
+
149
+ locals.merge("component_key" => component_key).freeze
150
+ end
151
+
107
152
  # @rbs (Integer, Integer) -> String
108
153
  def refresh_url(instance_id, revision)
109
154
  query = URI.encode_www_form(
@@ -3,46 +3,44 @@
3
3
  module SolidObjects
4
4
  class ComponentRenderer
5
5
  # @rbs @snapshot: ActorSnapshot
6
- # @rbs @component_name: String
7
- # @rbs @dependencies: Array[String]
6
+ # @rbs @registration: ComponentRegistration
8
7
  # @rbs @view_context: untyped
9
8
  # @rbs @authorization_context: untyped
10
9
 
11
- # @rbs (snapshot: ActorSnapshot, component_name: String, dependencies: Array[String], view_context: untyped, authorization_context: untyped) -> void
10
+ # @rbs (snapshot: ActorSnapshot, registration: ComponentRegistration, view_context: untyped, authorization_context: untyped) -> void
12
11
  def initialize(
13
12
  snapshot:,
14
- component_name:,
15
- dependencies:,
13
+ registration:,
16
14
  view_context:,
17
15
  authorization_context:
18
16
  )
19
17
  @snapshot = snapshot
20
- @component_name = component_name
21
- @dependencies = dependencies
18
+ @registration = registration
22
19
  @view_context = view_context
23
20
  @authorization_context = authorization_context
24
21
  end
25
22
 
26
23
  # @rbs () -> untyped
27
24
  def call
28
- [ component_name, *dependencies ].uniq.each do |authorization_name|
25
+ [ registration.component_name, *registration.dependencies ].uniq.each do |authorization_name|
29
26
  authorize_read!(authorization_name)
30
27
  end
31
28
  actor = ComponentView.new(
32
29
  snapshot:,
33
- dependencies:,
30
+ dependencies: registration.dependencies,
34
31
  authorization_context:
35
32
  )
36
33
  view_context.render(
37
34
  partial: default_partial,
38
- locals: {
35
+ locals: registration.locals.transform_keys(&:to_sym).merge(
39
36
  actor:,
40
- authorization_context:
41
- }
37
+ authorization_context:,
38
+ component_key: registration.component_key
39
+ )
42
40
  )
43
41
  rescue ActionView::MissingTemplate
44
42
  raise UnknownComponent,
45
- "unknown component #{component_name.inspect} for #{snapshot.reference.actor_type}"
43
+ "unknown component #{registration.component_name.inspect} for #{snapshot.reference.actor_type}"
46
44
  rescue ActionView::Template::Error => error
47
45
  raise error.cause if error.cause.is_a?(SolidObjects::Error)
48
46
 
@@ -52,8 +50,7 @@ module SolidObjects
52
50
  private
53
51
 
54
52
  attr_reader :snapshot,
55
- :component_name,
56
- :dependencies,
53
+ :registration,
57
54
  :view_context,
58
55
  :authorization_context
59
56
 
@@ -63,7 +60,7 @@ module SolidObjects
63
60
  actor_type: snapshot.reference.actor_type,
64
61
  actor_id: snapshot.reference.actor_id,
65
62
  message_name: observable_name,
66
- arguments: {},
63
+ arguments: registration.authorization_arguments,
67
64
  authorization_context:
68
65
  )
69
66
  return if authorized
@@ -76,7 +73,7 @@ module SolidObjects
76
73
  actor_name = snapshot.actor_class.name
77
74
  raise InvalidActor, "anonymous actors cannot render reactive components" unless actor_name
78
75
 
79
- "actors/#{actor_name.underscore}/#{component_name}"
76
+ "actors/#{actor_name.underscore}/#{registration.component_name}"
80
77
  end
81
78
  end
82
79
  end
@@ -28,7 +28,7 @@ module SolidObjects
28
28
  validate_identity!(registration, reference)
29
29
  end
30
30
  end
31
- if registrations.map(&:component_name).uniq.length != registrations.length
31
+ if registrations.map(&:dom_id).uniq.length != registrations.length
32
32
  raise InvalidComponentToken, "duplicate actor component registration"
33
33
  end
34
34
 
@@ -39,7 +39,7 @@ module SolidObjects
39
39
  def initialize(registrations)
40
40
  @registrations = registrations
41
41
  @revisions = registrations.to_h do |registration|
42
- [ registration.component_name, registration.revision_key ]
42
+ [ registration.dom_id, registration.revision_key ]
43
43
  end
44
44
  end
45
45
 
@@ -51,7 +51,7 @@ module SolidObjects
51
51
  registrations.filter_map do |registration|
52
52
  next unless registration.dependencies.include?(observable_name)
53
53
  next unless newer_revision?(
54
- registration.component_name,
54
+ registration.dom_id,
55
55
  instance_id,
56
56
  revision
57
57
  )
@@ -64,7 +64,7 @@ module SolidObjects
64
64
  def reconnect_refreshes(snapshot)
65
65
  registrations.filter_map do |registration|
66
66
  next unless newer_revision?(
67
- registration.component_name,
67
+ registration.dom_id,
68
68
  snapshot.instance_id,
69
69
  snapshot.revision
70
70
  )
@@ -92,7 +92,7 @@ module SolidObjects
92
92
 
93
93
  # @rbs (ComponentRegistration, Integer, Integer) -> String
94
94
  def refresh(registration, instance_id, revision)
95
- revisions[registration.component_name] = [ instance_id, revision ]
95
+ revisions[registration.dom_id] = [ instance_id, revision ]
96
96
  TurboStreamRenderer.component_refresh(
97
97
  registration,
98
98
  instance_id,
@@ -101,8 +101,8 @@ module SolidObjects
101
101
  end
102
102
 
103
103
  # @rbs (String, Integer, Integer) -> bool
104
- def newer_revision?(component_name, instance_id, revision)
105
- current = revisions.fetch(component_name)
104
+ def newer_revision?(dom_id, instance_id, revision)
105
+ current = revisions.fetch(dom_id)
106
106
  (current <=> [ instance_id, revision ]) == -1
107
107
  end
108
108
  end
@@ -7,23 +7,38 @@ module SolidObjects
7
7
  PURPOSE = "solid_objects.actor_component"
8
8
  MAXIMUM_TOKEN_BYTES = 16_384
9
9
  MAXIMUM_DEPENDENCIES = 50
10
+ MAXIMUM_LOCALS = 50
11
+ MAXIMUM_COMPONENT_KEY_BYTES = 512
12
+ REFRESH_METHODS = %w[replace morph].freeze
13
+ RESERVED_LOCALS = %w[actor authorization_context component_key].freeze
14
+ RUBY_KEYWORDS = %w[
15
+ alias and begin break case class def defined do else elsif end ensure
16
+ false for if in module next nil not or redo rescue retry return self
17
+ super then true undef unless until when while yield
18
+ ].freeze
10
19
 
11
20
  module_function
12
21
 
13
- # @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String) -> String
22
+ # @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, ?component_key: untyped, ?locals: Hash[untyped, untyped], ?refresh_method: String | Symbol) -> String
14
23
  def generate(
15
24
  reference:,
16
25
  component_name:,
17
26
  dependencies:,
18
27
  instance_id:,
19
28
  revision:,
20
- refresh_path:
29
+ refresh_path:,
30
+ component_key: nil,
31
+ locals: {},
32
+ refresh_method: "replace"
21
33
  )
22
34
  payload = {
23
35
  "actor_type" => reference.actor_type,
24
36
  "actor_id" => reference.actor_id,
25
37
  "component_name" => component_name,
38
+ "component_key" => Serialization.dump(component_key),
26
39
  "dependencies" => dependencies,
40
+ "locals" => Serialization.dump(locals),
41
+ "refresh_method" => refresh_method.to_s,
27
42
  "instance_id" => instance_id,
28
43
  "revision" => revision,
29
44
  "refresh_path" => refresh_path
@@ -41,6 +56,7 @@ module SolidObjects
41
56
  end
42
57
 
43
58
  payload = verifier.verified(token, purpose: PURPOSE)
59
+ apply_defaults!(payload)
44
60
  validate_payload!(payload)
45
61
  payload
46
62
  rescue ActiveSupport::MessageVerifier::InvalidSignature
@@ -54,6 +70,8 @@ module SolidObjects
54
70
  payload["actor_id"].is_a?(String) &&
55
71
  payload["component_name"].is_a?(String) &&
56
72
  payload["dependencies"].is_a?(Array) &&
73
+ payload["locals"].is_a?(Hash) &&
74
+ payload["refresh_method"].is_a?(String) &&
57
75
  payload["instance_id"].is_a?(Integer) &&
58
76
  payload["revision"].is_a?(Integer) &&
59
77
  payload["refresh_path"].is_a?(String)
@@ -61,7 +79,10 @@ module SolidObjects
61
79
  end
62
80
 
63
81
  validate_component_name!(payload.fetch("component_name"))
82
+ validate_component_key!(payload["component_key"])
64
83
  validate_dependencies!(payload.fetch("dependencies"))
84
+ validate_locals!(payload.fetch("locals"))
85
+ validate_refresh_method!(payload.fetch("refresh_method"))
65
86
  validate_revision!(
66
87
  payload.fetch("instance_id"),
67
88
  payload.fetch("revision")
@@ -71,6 +92,16 @@ module SolidObjects
71
92
  end
72
93
  private_class_method :validate_payload!
73
94
 
95
+ # @rbs (Hash[String, untyped]?) -> void
96
+ def apply_defaults!(payload)
97
+ return unless payload.is_a?(Hash)
98
+
99
+ payload["component_key"] = nil unless payload.key?("component_key")
100
+ payload["locals"] = {} unless payload.key?("locals")
101
+ payload["refresh_method"] = "replace" unless payload.key?("refresh_method")
102
+ end
103
+ private_class_method :apply_defaults!
104
+
74
105
  # @rbs (String) -> void
75
106
  def validate_component_name!(component_name)
76
107
  return if component_name.match?(/\A[a-zA-Z0-9_]+\z/)
@@ -79,6 +110,20 @@ module SolidObjects
79
110
  end
80
111
  private_class_method :validate_component_name!
81
112
 
113
+ # @rbs (untyped) -> void
114
+ def validate_component_key!(component_key)
115
+ return if component_key.nil?
116
+ return if component_key.is_a?(Integer)
117
+ if component_key.is_a?(String) &&
118
+ component_key.bytesize.positive? &&
119
+ component_key.bytesize <= MAXIMUM_COMPONENT_KEY_BYTES
120
+ return
121
+ end
122
+
123
+ raise InvalidComponentToken, "invalid actor component key"
124
+ end
125
+ private_class_method :validate_component_key!
126
+
82
127
  # @rbs (Array[untyped]) -> void
83
128
  def validate_dependencies!(dependencies)
84
129
  valid = dependencies.any? &&
@@ -91,6 +136,29 @@ module SolidObjects
91
136
  end
92
137
  private_class_method :validate_dependencies!
93
138
 
139
+ # @rbs (Hash[untyped, untyped]) -> void
140
+ def validate_locals!(locals)
141
+ valid = locals.length <= MAXIMUM_LOCALS &&
142
+ locals.keys.all? do |name|
143
+ name.is_a?(String) &&
144
+ name.match?(/\A[a-z_][a-zA-Z0-9_]*\z/) &&
145
+ !RESERVED_LOCALS.include?(name) &&
146
+ !RUBY_KEYWORDS.include?(name)
147
+ end
148
+ return if valid
149
+
150
+ raise InvalidComponentToken, "invalid actor component locals"
151
+ end
152
+ private_class_method :validate_locals!
153
+
154
+ # @rbs (String) -> void
155
+ def validate_refresh_method!(refresh_method)
156
+ return if REFRESH_METHODS.include?(refresh_method)
157
+
158
+ raise InvalidComponentToken, "invalid actor component refresh method"
159
+ end
160
+ private_class_method :validate_refresh_method!
161
+
94
162
  # @rbs (Integer, Integer) -> void
95
163
  def validate_revision!(instance_id, revision)
96
164
  return unless instance_id.negative? || revision.negative?
@@ -16,9 +16,12 @@ module SolidObjects
16
16
  "#{scope(reference)}_observable_#{normalized_name(name)}"
17
17
  end
18
18
 
19
- # @rbs (Reference, Symbol | String) -> String
20
- def component(reference, name)
21
- "#{scope(reference)}_component_#{normalized_name(name)}"
19
+ # @rbs (Reference, Symbol | String, ?key: String | Integer?) -> String
20
+ def component(reference, name, key: nil)
21
+ identity = "#{scope(reference)}_component_#{normalized_name(name)}"
22
+ return identity unless key
23
+
24
+ "#{identity}_#{component_key_digest(key)}"
22
25
  end
23
26
 
24
27
  # @rbs (Reference) -> String
@@ -34,5 +37,11 @@ module SolidObjects
34
37
  name.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")
35
38
  end
36
39
  private_class_method :normalized_name
40
+
41
+ # @rbs (String | Integer) -> String
42
+ def component_key_digest(key)
43
+ Digest::SHA256.hexdigest(JSON.generate(key)).first(24)
44
+ end
45
+ private_class_method :component_key_digest
37
46
  end
38
47
  end
@@ -26,6 +26,13 @@ module SolidObjects
26
26
  end
27
27
  end
28
28
 
29
+ initializer "solid_objects.assets" do |application|
30
+ next unless application.config.respond_to?(:assets)
31
+ next unless application.config.assets.respond_to?(:precompile)
32
+
33
+ application.config.assets.precompile << "solid_objects/component_refresh.js"
34
+ end
35
+
29
36
  rake_tasks do
30
37
  tasks_path = File.expand_path("../tasks", __dir__)
31
38
  Dir[File.join(tasks_path, "**/*.rake")].sort.each { |task| load task }
@@ -40,15 +40,14 @@ module SolidObjects
40
40
 
41
41
  # @rbs (ComponentRegistration, Integer, Integer) -> String
42
42
  def component_refresh(registration, instance_id, revision)
43
- target = DomIdentity.component(
44
- registration.reference,
45
- registration.component_name
46
- )
43
+ return morph_component_refresh(registration, instance_id, revision) if registration.morph?
44
+
45
+ target = registration.dom_id
47
46
  source = ERB::Util.html_escape(
48
47
  registration.refresh_url(instance_id, revision)
49
48
  )
50
49
  revision_value = "#{instance_id}:#{revision}"
51
- %(<turbo-stream action="replace" target="#{target}"><template><turbo-frame id="#{target}" src="#{source}" data-solid-objects-revision="#{revision_value}"></turbo-frame></template></turbo-stream>)
50
+ %(<turbo-stream action="replace" target="#{target}"><template><turbo-frame id="#{target}" src="#{source}" data-solid-objects-revision="#{revision_value}" data-solid-objects-refresh="replace"></turbo-frame></template></turbo-stream>)
52
51
  end
53
52
 
54
53
  # @rbs (String) -> Hash[String, untyped]?
@@ -75,5 +74,15 @@ module SolidObjects
75
74
  JSON.generate(value)
76
75
  end
77
76
  private_class_method :display_value
77
+
78
+ # @rbs (ComponentRegistration, Integer, Integer) -> String
79
+ def morph_component_refresh(registration, instance_id, revision)
80
+ source = ERB::Util.html_escape(
81
+ registration.refresh_url(instance_id, revision)
82
+ )
83
+ scope = DomIdentity.scope(registration.reference)
84
+ %(<turbo-stream action="append" target="#{scope}"><template><solid-objects-refresh data-target="#{registration.dom_id}" data-source="#{source}" data-refresh-method="morph"></solid-objects-refresh></template></turbo-stream>)
85
+ end
86
+ private_class_method :morph_component_refresh
78
87
  end
79
88
  end
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.4.3"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -22,8 +22,8 @@ module SolidObjects
22
22
  # @rbs (Symbol | String) -> untyped
23
23
  def value: (Symbol | String) -> untyped
24
24
 
25
- # @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?) -> untyped
26
- def component: (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?) -> untyped
25
+ # @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol) -> untyped
26
+ def component: (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol) -> untyped
27
27
 
28
28
  # @rbs () -> Array[String]
29
29
  def component_tokens: () -> Array[String]
@@ -31,6 +31,9 @@ module SolidObjects
31
31
  # @rbs () -> Array[String]
32
32
  def scalar_observable_names: () -> Array[String]
33
33
 
34
+ # @rbs () -> bool
35
+ def morph_components?: () -> bool
36
+
34
37
  # @rbs () -> State
35
38
  def state: () -> State
36
39
 
@@ -73,8 +76,11 @@ module SolidObjects
73
76
  # @rbs (Array[String]) -> void
74
77
  def validate_dependencies!: (Array[String]) -> void
75
78
 
76
- # @rbs (String) -> void
77
- def ensure_unique_component!: (String) -> void
79
+ # @rbs (ComponentRegistration) -> void
80
+ def ensure_unique_component!: (ComponentRegistration) -> void
81
+
82
+ # @rbs (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol) -> void
83
+ def validate_static_options!: (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol) -> void
78
84
 
79
85
  # @rbs () -> Proc | ComponentPathResolver
80
86
  def component_path_resolver: () -> Proc