reactive_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.
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ require_relative "reactive_component/version"
6
+ require_relative "reactive_component/compiler"
7
+ require_relative "reactive_component/erb_extractor"
8
+ require_relative "reactive_component/data_evaluator"
9
+ require_relative "reactive_component/wrapper"
10
+ require_relative "reactive_component/engine" if defined?(Rails::Engine)
11
+
12
+ module ReactiveComponent
13
+ extend ActiveSupport::Concern
14
+
15
+ mattr_accessor :debug, default: false
16
+ mattr_accessor :renderer, default: nil
17
+
18
+ class Error < StandardError; end
19
+
20
+ included do
21
+ class_attribute :_live_model_attr, instance_writer: false
22
+ class_attribute :_live_model_class_name, instance_writer: false
23
+ class_attribute :_live_actions, instance_writer: false, default: {}
24
+ class_attribute :_broadcast_config, instance_writer: false
25
+ class_attribute :_client_state_fields, instance_writer: false, default: {}
26
+ end
27
+
28
+ def render_in(view_context, &block)
29
+ inner_html = super
30
+ return inner_html unless self.class._live_model_attr
31
+ return inner_html if @_skip_live_wrapper
32
+
33
+ record = instance_variable_get(:"@#{self.class.live_model_attr}")
34
+ return inner_html unless record
35
+
36
+ stream = ReactiveComponent::Wrapper.find_stream_for(self.class, record)
37
+
38
+ client_state = if self.class._client_state_fields.any?
39
+ kwargs = {}
40
+ self.class._client_state_fields.each_key do |name|
41
+ val = instance_variable_get(:"@#{name}")
42
+ kwargs[name] = val unless val.nil?
43
+ end
44
+ self.class.client_state_values(**kwargs)
45
+ end
46
+
47
+ extra_opts = respond_to?(:live_wrapper_options, true) ? live_wrapper_options : {}
48
+
49
+ wrapped = ReactiveComponent::Wrapper.wrap(self.class, record, inner_html, stream: stream, client_state: client_state, **extra_opts)
50
+
51
+ template_script = self.class.template_script_tag(view_context)
52
+ template_script ? (template_script + wrapped).html_safe : wrapped
53
+ end
54
+
55
+ class_methods do
56
+ def subscribes_to(attr_name, class_name: nil)
57
+ self._live_model_attr = attr_name.to_sym
58
+ self._live_model_class_name = class_name || attr_name.to_s.classify
59
+ end
60
+
61
+ def live_model_class
62
+ _live_model_class_name&.constantize
63
+ end
64
+
65
+ def broadcasts(stream:, prepend_target: nil)
66
+ self._broadcast_config = {
67
+ stream: stream,
68
+ prepend_target: prepend_target
69
+ }
70
+ end
71
+
72
+ def live_model_attr
73
+ _live_model_attr
74
+ end
75
+
76
+ def client_state(name, default: nil)
77
+ self._client_state_fields = _client_state_fields.merge(
78
+ name.to_sym => { default: default }
79
+ )
80
+ end
81
+
82
+ def client_state_values(**kwargs)
83
+ _client_state_fields.each_with_object({}) do |(name, config), hash|
84
+ hash[name.to_s] = kwargs.key?(name) ? kwargs[name] : config[:default]
85
+ end
86
+ end
87
+
88
+ def live_action(action_name, params: [])
89
+ self._live_actions = _live_actions.merge(
90
+ action_name.to_sym => { params: Array(params).map(&:to_sym) }
91
+ )
92
+ end
93
+
94
+ def live_action_token(record)
95
+ live_action_verifier.generate(
96
+ { c: name, m: record.class.name, r: record.id },
97
+ purpose: :reactive_component_action
98
+ )
99
+ end
100
+
101
+ def execute_action(action_name, record, action_params = {})
102
+ action_name = action_name.to_sym
103
+ action_config = _live_actions[action_name]
104
+ raise ArgumentError, "Unknown live action: #{action_name}" unless action_config
105
+
106
+ instance = allocate
107
+ instance.instance_variable_set(:"@#{live_model_attr}", record)
108
+
109
+ allowed = action_config[:params]
110
+ if allowed.any?
111
+ filtered = action_params.symbolize_keys.slice(*allowed)
112
+ instance.send(action_name, **filtered)
113
+ else
114
+ instance.send(action_name)
115
+ end
116
+ end
117
+
118
+ def compiled_data
119
+ @compiled_data ||= ReactiveComponent::Compiler.compile(self)
120
+ end
121
+
122
+ def compiled_template_js
123
+ compiled_data[:js_body]
124
+ end
125
+
126
+ def encoded_template
127
+ @encoded_template ||= if ReactiveComponent.debug
128
+ compiled_template_js
129
+ else
130
+ Base64.strict_encode64(compiled_template_js)
131
+ end
132
+ end
133
+
134
+ def template_element_id
135
+ @template_element_id ||= "#{name.underscore}_template"
136
+ end
137
+
138
+ def template_script_tag(view_context)
139
+ emitted = (view_context.instance_variable_get(:@_reactive_component_templates) || Set.new)
140
+ return nil if emitted.include?(name)
141
+
142
+ emitted.add(name)
143
+ view_context.instance_variable_set(:@_reactive_component_templates, emitted)
144
+ %(<script type="text/x-template" id="#{template_element_id}">#{encoded_template}</script>).html_safe
145
+ end
146
+
147
+ def dom_id_for(record)
148
+ if respond_to?(:dom_id_prefix) && dom_id_prefix.present?
149
+ ActionView::RecordIdentifier.dom_id(record, dom_id_prefix)
150
+ else
151
+ ActionView::RecordIdentifier.dom_id(record)
152
+ end
153
+ end
154
+
155
+ def expression_field_map
156
+ compiled_data[:expressions].invert
157
+ end
158
+
159
+ def build_data_for_nested(**kwargs)
160
+ evaluator = ReactiveComponent::DataEvaluator.new(nil, nil, component_class: self, **kwargs)
161
+ data = {}
162
+ collection_computed = compiled_data[:collection_computed] || {}
163
+
164
+ compiled_data[:expressions].each do |var_name, ruby_source|
165
+ data[var_name] = if collection_computed.key?(var_name)
166
+ evaluator.evaluate_collection(ruby_source, collection_computed[var_name])
167
+ else
168
+ evaluator.evaluate(ruby_source)
169
+ end
170
+ end
171
+ compiled_data[:simple_ivars].each do |ivar_name|
172
+ data[ivar_name] = kwargs[ivar_name.to_sym] if kwargs.key?(ivar_name.to_sym)
173
+ end
174
+ data
175
+ end
176
+
177
+ def build_data(record, **kwargs)
178
+ evaluator = ReactiveComponent::DataEvaluator.new(live_model_attr, record, component_class: self, **kwargs)
179
+ data = {}
180
+ collection_computed = compiled_data[:collection_computed] || {}
181
+
182
+ compiled_data[:expressions].each do |var_name, ruby_source|
183
+ data[var_name] = if collection_computed.key?(var_name)
184
+ evaluator.evaluate_collection(ruby_source, collection_computed[var_name])
185
+ else
186
+ evaluator.evaluate(ruby_source)
187
+ end
188
+ end
189
+
190
+ compiled_data[:simple_ivars].each do |ivar_name|
191
+ data[ivar_name] = kwargs[ivar_name.to_sym] if kwargs.key?(ivar_name.to_sym)
192
+ end
193
+
194
+ (compiled_data[:nested_components] || {}).each do |key, info|
195
+ klass = info[:class_name].constantize
196
+ kwargs_values = {}
197
+ info[:kwargs].each do |kwarg_name, ruby_source|
198
+ kwargs_values[kwarg_name.to_sym] = evaluator.evaluate(ruby_source)
199
+ end
200
+ data[key] = if klass.respond_to?(:build_data_for_nested)
201
+ klass.build_data_for_nested(**kwargs_values)
202
+ else
203
+ ReactiveComponent::Compiler.build_data_for_nested(klass, **kwargs_values)
204
+ end
205
+ end
206
+
207
+ data["id"] = record.id
208
+ data["dom_id"] = dom_id_for(record)
209
+ data
210
+ end
211
+
212
+ private
213
+
214
+ def live_action_verifier
215
+ Rails.application.message_verifier(:reactive_component_action)
216
+ end
217
+ end
218
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: reactive_component
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Przemyslaw Lusar
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: view_component
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: turbo-rails
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: ruby2js
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '5.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '5.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: prism
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rails
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '7.1'
75
+ - - "<"
76
+ - !ruby/object:Gem::Version
77
+ version: '9'
78
+ type: :runtime
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '7.1'
85
+ - - "<"
86
+ - !ruby/object:Gem::Version
87
+ version: '9'
88
+ description: Build reactive, real-time UI components that automatically re-render
89
+ server-side when subscribed models change. Uses ViewComponent, Turbo Streams, and
90
+ ActionCable to keep your UI in sync without writing custom JavaScript.
91
+ email:
92
+ - lluzak@gmail.com
93
+ executables: []
94
+ extensions: []
95
+ extra_rdoc_files: []
96
+ files:
97
+ - CHANGELOG.md
98
+ - LICENSE.txt
99
+ - app/channels/reactive_component/channel.rb
100
+ - app/controllers/reactive_component/actions_controller.rb
101
+ - app/javascript/reactive_component/controllers/reactive_renderer_controller.js
102
+ - app/javascript/reactive_component/lib/reactive_renderer_utils.js
103
+ - config/importmap.rb
104
+ - config/routes.rb
105
+ - lib/reactive_component.rb
106
+ - lib/reactive_component/compiler.rb
107
+ - lib/reactive_component/data_evaluator.rb
108
+ - lib/reactive_component/engine.rb
109
+ - lib/reactive_component/erb_extractor.rb
110
+ - lib/reactive_component/version.rb
111
+ - lib/reactive_component/wrapper.rb
112
+ homepage: https://github.com/przymusiala/reactive_component
113
+ licenses:
114
+ - MIT
115
+ metadata:
116
+ homepage_uri: https://github.com/przymusiala/reactive_component
117
+ source_code_uri: https://github.com/przymusiala/reactive_component
118
+ changelog_uri: https://github.com/przymusiala/reactive_component/blob/main/CHANGELOG.md
119
+ rubygems_mfa_required: 'true'
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: 3.1.0
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubygems_version: 3.6.7
135
+ specification_version: 4
136
+ summary: Reactive server-rendered components for Rails via ActionCable
137
+ test_files: []