reactive_component 0.1.0 → 0.7.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 +161 -0
- data/app/channels/reactive_component/channel.rb +10 -10
- data/app/controllers/reactive_component/actions_controller.rb +3 -3
- data/app/javascript/reactive_component/controllers/reactive_renderer_controller.js +25 -3
- data/app/javascript/reactive_component/lib/reactive_renderer_utils.js +34 -0
- data/config/importmap.rb +2 -2
- data/config/routes.rb +1 -1
- data/lib/reactive_component/broadcastable.rb +41 -0
- data/lib/reactive_component/compiler.rb +110 -52
- data/lib/reactive_component/data_evaluator.rb +58 -30
- data/lib/reactive_component/engine.rb +10 -4
- data/lib/reactive_component/erubi.rb +30 -0
- data/lib/reactive_component/transpiler.rb +586 -0
- data/lib/reactive_component/version.rb +1 -1
- data/lib/reactive_component/wrapper.rb +7 -12
- data/lib/reactive_component.rb +185 -51
- metadata +35 -25
- data/lib/reactive_component/erb_extractor.rb +0 -610
|
@@ -1,31 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
require "ruby2js/filter/erb"
|
|
7
|
-
require "ruby2js/filter/functions"
|
|
8
|
-
require_relative "erb_extractor"
|
|
3
|
+
require 'prism'
|
|
4
|
+
require_relative 'erubi'
|
|
5
|
+
require_relative 'transpiler'
|
|
9
6
|
|
|
10
7
|
module ReactiveComponent
|
|
11
8
|
module Compiler
|
|
12
|
-
ESCAPE_FN_JS = <<~JS
|
|
9
|
+
ESCAPE_FN_JS = <<~JS
|
|
13
10
|
function _escape(s) {
|
|
14
11
|
return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
15
12
|
}
|
|
13
|
+
// A variable or property read in `<%= %>` is emitted as escapeHTML(value);
|
|
14
|
+
// every other output is String(value), which `add_html_escaping` wraps.
|
|
15
|
+
function escapeHTML(s) { return _escape(String(s)); }
|
|
16
16
|
JS
|
|
17
17
|
|
|
18
|
-
TAG_FN_JS = <<~JS
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
TAG_FN_JS = <<~JS
|
|
19
|
+
function _render_class(v) {
|
|
20
|
+
if (v == null || v === false) return '';
|
|
21
|
+
if (Array.isArray(v)) return v.map(_render_class).filter(Boolean).join(' ');
|
|
22
|
+
if (typeof v === 'object') {
|
|
23
|
+
return Object.entries(v).filter(([, on]) => on).map(([name]) => name).join(' ');
|
|
24
|
+
}
|
|
25
|
+
return String(v);
|
|
26
|
+
}
|
|
27
|
+
function _render_attrs(attrs) {
|
|
28
|
+
if (!attrs) return '';
|
|
29
|
+
let html = '';
|
|
30
|
+
for (let [k, v] of Object.entries(attrs)) {
|
|
31
|
+
if (v == null || v === false) continue;
|
|
32
|
+
if (k === 'class') {
|
|
33
|
+
const cls = _render_class(v);
|
|
34
|
+
if (cls) html += ' class="' + _escape(cls) + '"';
|
|
35
|
+
continue;
|
|
26
36
|
}
|
|
37
|
+
if (v === true) { html += ' ' + k; continue; }
|
|
38
|
+
if (typeof v === 'object' && !Array.isArray(v)) {
|
|
39
|
+
for (let [dk, dv] of Object.entries(v)) {
|
|
40
|
+
if (dv == null || dv === false) continue;
|
|
41
|
+
const dashKey = String(dk).replace(/_/g, '-');
|
|
42
|
+
if (dv === true) { html += ' ' + k + '-' + dashKey; continue; }
|
|
43
|
+
html += ' ' + k + '-' + dashKey + '="' + _escape(String(dv)) + '"';
|
|
44
|
+
}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(v)) v = v.filter(Boolean).join(' ');
|
|
48
|
+
html += ' ' + k + '="' + _escape(String(v)) + '"';
|
|
27
49
|
}
|
|
28
|
-
return html
|
|
50
|
+
return html;
|
|
51
|
+
}
|
|
52
|
+
function _tag(name, content, attrs) {
|
|
53
|
+
return '<' + name + _render_attrs(attrs) + '>' +
|
|
54
|
+
(content != null ? _escape(String(content)) : '') +
|
|
55
|
+
'</' + name + '>';
|
|
56
|
+
}
|
|
57
|
+
function _tag_open(name, attrs) {
|
|
58
|
+
return '<' + name + _render_attrs(attrs) + '>';
|
|
59
|
+
}
|
|
60
|
+
function _tag_close(name) {
|
|
61
|
+
return '</' + name + '>';
|
|
29
62
|
}
|
|
30
63
|
JS
|
|
31
64
|
|
|
@@ -33,7 +66,7 @@ module ReactiveComponent
|
|
|
33
66
|
|
|
34
67
|
def compile(component_class)
|
|
35
68
|
erb_source = read_erb(component_class)
|
|
36
|
-
erb_ruby =
|
|
69
|
+
erb_ruby = ReactiveComponent::Erubi.new(erb_source).src
|
|
37
70
|
|
|
38
71
|
extraction = { expressions: {}, raw_fields: Set.new }
|
|
39
72
|
|
|
@@ -43,30 +76,36 @@ module ReactiveComponent
|
|
|
43
76
|
# Components with their own model attr are only nestable inside collection loops
|
|
44
77
|
# (where we can call build_data per item), not as standalone nested components
|
|
45
78
|
return nil if !inside_block && klass.respond_to?(:_live_model_attr) && klass._live_model_attr
|
|
46
|
-
|
|
79
|
+
|
|
80
|
+
begin
|
|
81
|
+
read_erb(klass)
|
|
82
|
+
klass
|
|
83
|
+
rescue StandardError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
47
86
|
end
|
|
48
87
|
|
|
49
|
-
js_function =
|
|
50
|
-
erb_ruby,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
nestable_checker: nestable_checker
|
|
55
|
-
).to_s
|
|
88
|
+
js_function = begin
|
|
89
|
+
ReactiveComponent::Transpiler.call(erb_ruby, extraction: extraction, nestable_checker: nestable_checker)
|
|
90
|
+
rescue ReactiveComponent::CompileError => e
|
|
91
|
+
raise ReactiveComponent::CompileError, "#{component_class.name}: #{e.message}"
|
|
92
|
+
end
|
|
56
93
|
|
|
57
94
|
expressions = extraction[:expressions] || {}
|
|
58
95
|
raw_fields = extraction[:raw_fields] || Set.new
|
|
59
96
|
collection_computed = extraction[:collection_computed] || {}
|
|
60
97
|
nested_components = extraction[:nested_components] || {}
|
|
61
98
|
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
99
|
+
# Every @ivar the template mentions needs its stripped form (`initials`
|
|
100
|
+
# for `@initials`) in the JS destructure, because the emitter reads those
|
|
101
|
+
# names directly alongside any extracted-expression vars. Don't subtract
|
|
102
|
+
# ivars that also appear inside extracted expressions — a template that
|
|
103
|
+
# uses both `<%= @initials %>` and `<%= @initials.present? %>` needs
|
|
104
|
+
# both `initials` and `v0` on the data object.
|
|
105
|
+
simple_ivars = extract_ivar_names(erb_ruby).to_a.sort
|
|
67
106
|
|
|
68
107
|
# Compile nested component templates and embed as JS functions
|
|
69
|
-
nested_functions_js =
|
|
108
|
+
nested_functions_js = ''
|
|
70
109
|
embedded_classes = Set.new
|
|
71
110
|
|
|
72
111
|
nested_components.each do |key, info|
|
|
@@ -84,7 +123,7 @@ module ReactiveComponent
|
|
|
84
123
|
child_body = wrap_debug_return(child_body, debug_label)
|
|
85
124
|
end
|
|
86
125
|
nested_functions_js += "function _render_#{key}(data) {\n"
|
|
87
|
-
nested_functions_js += child_body.gsub(/^/,
|
|
126
|
+
nested_functions_js += "#{child_body.gsub(/^/, ' ')}\n"
|
|
88
127
|
nested_functions_js += "}\n"
|
|
89
128
|
end
|
|
90
129
|
|
|
@@ -92,8 +131,10 @@ module ReactiveComponent
|
|
|
92
131
|
collection_computed.each_value do |cc_info|
|
|
93
132
|
(cc_info[:expressions] || {}).each_value do |expr_info|
|
|
94
133
|
next unless expr_info[:nested_component]
|
|
134
|
+
|
|
95
135
|
nc_class_name = expr_info[:nested_component][:class_name]
|
|
96
136
|
next if embedded_classes.include?(nc_class_name)
|
|
137
|
+
|
|
97
138
|
embedded_classes << nc_class_name
|
|
98
139
|
|
|
99
140
|
child_class = nc_class_name.constantize
|
|
@@ -110,7 +151,7 @@ module ReactiveComponent
|
|
|
110
151
|
child_body = wrap_debug_return(child_body, debug_label)
|
|
111
152
|
end
|
|
112
153
|
nested_functions_js += "function _render_#{fn_name}(data) {\n"
|
|
113
|
-
nested_functions_js += child_body.gsub(/^/,
|
|
154
|
+
nested_functions_js += "#{child_body.gsub(/^/, ' ')}\n"
|
|
114
155
|
nested_functions_js += "}\n"
|
|
115
156
|
end
|
|
116
157
|
end
|
|
@@ -118,7 +159,7 @@ module ReactiveComponent
|
|
|
118
159
|
fields = (expressions.keys + simple_ivars + nested_components.keys).uniq.sort
|
|
119
160
|
parent_raw_body = strip_function_wrapper(js_function)
|
|
120
161
|
js_body = "#{ESCAPE_FN_JS}#{TAG_FN_JS}#{nested_functions_js}"
|
|
121
|
-
js_body += "let { #{fields.join(
|
|
162
|
+
js_body += "let { #{fields.join(', ')} } = data;\n"
|
|
122
163
|
js_body += add_html_escaping(parent_raw_body, raw_fields)
|
|
123
164
|
|
|
124
165
|
{
|
|
@@ -149,15 +190,17 @@ module ReactiveComponent
|
|
|
149
190
|
collection_computed = compiled[:collection_computed] || {}
|
|
150
191
|
|
|
151
192
|
compiled[:expressions].each do |var_name, ruby_source|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
193
|
+
value = if collection_computed.key?(var_name)
|
|
194
|
+
evaluator.evaluate_collection(ruby_source, collection_computed[var_name])
|
|
195
|
+
else
|
|
196
|
+
evaluator.evaluate(ruby_source)
|
|
197
|
+
end
|
|
198
|
+
data[var_name] = ReactiveComponent.sanitize_for_broadcast(value, source: ruby_source)
|
|
157
199
|
end
|
|
158
200
|
|
|
159
201
|
compiled[:simple_ivars].each do |ivar_name|
|
|
160
|
-
|
|
202
|
+
value = kwargs.key?(ivar_name.to_sym) ? kwargs[ivar_name.to_sym] : evaluator.evaluate("@#{ivar_name}")
|
|
203
|
+
data[ivar_name] = ReactiveComponent.sanitize_for_broadcast(value, source: "@#{ivar_name}")
|
|
161
204
|
end
|
|
162
205
|
|
|
163
206
|
data
|
|
@@ -167,44 +210,59 @@ module ReactiveComponent
|
|
|
167
210
|
result = Prism.parse(erb_ruby)
|
|
168
211
|
ivars = Set.new
|
|
169
212
|
walk(result.value) do |node|
|
|
170
|
-
ivars << node.name.to_s.delete_prefix(
|
|
213
|
+
ivars << node.name.to_s.delete_prefix('@') if node.is_a?(Prism::InstanceVariableReadNode)
|
|
171
214
|
end
|
|
172
215
|
ivars
|
|
173
216
|
end
|
|
174
217
|
|
|
175
218
|
def read_erb(component_class)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
&.sub(/\.rb$/, ".html.erb")
|
|
219
|
+
rb_path = component_class.instance_method(:initialize).source_location&.first
|
|
220
|
+
raise ArgumentError, "Cannot find source file for #{component_class}" unless rb_path
|
|
179
221
|
|
|
180
|
-
|
|
222
|
+
erb_path = erb_path_for(rb_path)
|
|
223
|
+
raise ArgumentError, "Cannot find ERB template for #{component_class}" unless erb_path
|
|
181
224
|
|
|
182
225
|
File.read(erb_path)
|
|
183
226
|
end
|
|
184
227
|
|
|
228
|
+
# ViewComponent supports both flat (`foo_component.html.erb`) and sidecar
|
|
229
|
+
# (`foo_component/foo_component.html.erb`) template layouts. Try both.
|
|
230
|
+
def erb_path_for(rb_path)
|
|
231
|
+
flat = rb_path.sub(/\.rb\z/, '.html.erb')
|
|
232
|
+
return flat if File.exist?(flat)
|
|
233
|
+
|
|
234
|
+
base = File.basename(rb_path, '.rb')
|
|
235
|
+
sidecar = File.join(File.dirname(rb_path), base, "#{base}.html.erb")
|
|
236
|
+
return sidecar if File.exist?(sidecar)
|
|
237
|
+
|
|
238
|
+
nil
|
|
239
|
+
end
|
|
240
|
+
|
|
185
241
|
def strip_function_wrapper(js_function)
|
|
186
242
|
js_function
|
|
187
|
-
.sub(/\Afunction render\(\{[^}]*\}\) \{\n?/,
|
|
188
|
-
.sub(/\}\s*\z/,
|
|
189
|
-
.gsub(/^ /,
|
|
243
|
+
.sub(/\Afunction render\(\{[^}]*\}\) \{\n?/, '')
|
|
244
|
+
.sub(/\}\s*\z/, '')
|
|
245
|
+
.gsub(/^ /, '')
|
|
190
246
|
end
|
|
191
247
|
|
|
192
248
|
def unwrap_function(js_function, fields, raw_fields, include_helpers: true)
|
|
193
249
|
body = strip_function_wrapper(js_function)
|
|
194
|
-
destructure = "let { #{fields.join(
|
|
250
|
+
destructure = "let { #{fields.join(', ')} } = data;\n"
|
|
195
251
|
escaped_body = add_html_escaping(body, raw_fields)
|
|
196
|
-
helpers = include_helpers ? "#{ESCAPE_FN_JS}#{TAG_FN_JS}" :
|
|
252
|
+
helpers = include_helpers ? "#{ESCAPE_FN_JS}#{TAG_FN_JS}" : ''
|
|
197
253
|
"#{helpers}#{destructure}#{escaped_body}"
|
|
198
254
|
end
|
|
199
255
|
|
|
200
256
|
def wrap_debug_return(body, label)
|
|
201
|
-
wrapper = "return '<div data-reactive-debug=\"#{label}'
|
|
257
|
+
wrapper = "return '<div data-reactive-debug=\"#{label}'" \
|
|
258
|
+
"+ (data.dom_id ? ' #' + data.dom_id : '')" \
|
|
259
|
+
"+ '\" class=\"reactive-debug-wrapper\">' + _buf + '</div>';"
|
|
202
260
|
body.sub(/return _buf\s*\z/, wrapper)
|
|
203
261
|
end
|
|
204
262
|
|
|
205
263
|
def add_html_escaping(body, raw_fields)
|
|
206
264
|
body.gsub(/\+= String\((.+?)\);/) do
|
|
207
|
-
expr =
|
|
265
|
+
expr = ::Regexp.last_match(1)
|
|
208
266
|
if raw_fields.include?(expr)
|
|
209
267
|
"+= #{expr};"
|
|
210
268
|
else
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'action_view'
|
|
4
|
+
require 'action_view/record_identifier'
|
|
5
5
|
|
|
6
6
|
module ReactiveComponent
|
|
7
7
|
class DataEvaluator
|
|
@@ -10,14 +10,15 @@ module ReactiveComponent
|
|
|
10
10
|
include ActionView::Helpers::NumberHelper
|
|
11
11
|
include ActionView::Helpers::TagHelper
|
|
12
12
|
include ActionView::Helpers::OutputSafetyHelper
|
|
13
|
+
include ActionView::Helpers::TranslationHelper
|
|
13
14
|
include ActionView::RecordIdentifier
|
|
14
15
|
include ActionView::Helpers::UrlHelper
|
|
15
16
|
|
|
16
17
|
def self.inherited(subclass)
|
|
17
18
|
super
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
return unless defined?(Rails) && Rails.application
|
|
20
|
+
|
|
21
|
+
subclass.include Rails.application.routes.url_helpers
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def self.finalize!
|
|
@@ -28,32 +29,42 @@ module ReactiveComponent
|
|
|
28
29
|
instance_variable_set(:"@#{model_attr}", record) if model_attr
|
|
29
30
|
kwargs.each { |k, v| instance_variable_set(:"@#{k}", v) }
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@component_delegate = component_class.allocate
|
|
32
|
+
return unless component_class
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
constructor_args = model_attr ? { model_attr => record }.merge(kwargs) : kwargs
|
|
36
|
+
instance = component_class.new(**constructor_args)
|
|
37
|
+
@component_delegate = instance
|
|
38
|
+
instance.instance_variables.each do |ivar|
|
|
39
|
+
next if (model_attr && ivar == :"@#{model_attr}") || instance_variable_defined?(ivar)
|
|
40
|
+
|
|
41
|
+
instance_variable_set(ivar, instance.instance_variable_get(ivar))
|
|
42
42
|
end
|
|
43
|
+
rescue StandardError
|
|
44
|
+
@component_delegate = component_class.allocate
|
|
43
45
|
end
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
def evaluate(ruby_source)
|
|
47
49
|
instance_eval(ruby_source)
|
|
48
50
|
rescue NameError
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
begin
|
|
52
|
+
@component_delegate&.instance_eval(ruby_source)
|
|
53
|
+
rescue StandardError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
rescue StandardError => e
|
|
51
57
|
Rails.logger.error "[ReactiveComponent::DataEvaluator] Error evaluating '#{ruby_source}': #{e.message}"
|
|
52
58
|
nil
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
def render(renderable, &block)
|
|
56
62
|
renderer = ReactiveComponent.renderer || ActionController::Base
|
|
63
|
+
if block
|
|
64
|
+
# ViewComponent needs block content set via with_content
|
|
65
|
+
block_result = yield
|
|
66
|
+
renderable.with_content(block_result) if renderable.respond_to?(:with_content)
|
|
67
|
+
end
|
|
57
68
|
renderer.render(renderable, layout: false)
|
|
58
69
|
end
|
|
59
70
|
|
|
@@ -66,9 +77,9 @@ module ReactiveComponent
|
|
|
66
77
|
return [] unless collection
|
|
67
78
|
|
|
68
79
|
block_var = computed[:block_var]
|
|
69
|
-
eval_context = self
|
|
70
80
|
|
|
71
81
|
lambdas = {}
|
|
82
|
+
typed = {}
|
|
72
83
|
nested = {}
|
|
73
84
|
(computed[:expressions] || {}).each do |var_name, info|
|
|
74
85
|
if info[:nested_component]
|
|
@@ -81,13 +92,23 @@ module ReactiveComponent
|
|
|
81
92
|
nested[var_name] = { klass: klass, kwargs: kwarg_lambdas }
|
|
82
93
|
else
|
|
83
94
|
lambdas[var_name] = eval_lambda(block_var, info[:source])
|
|
95
|
+
typed[var_name] = info[:source] if info[:typed]
|
|
84
96
|
end
|
|
85
97
|
end
|
|
86
98
|
|
|
87
99
|
collection.map do |item|
|
|
88
100
|
result = {}
|
|
89
101
|
lambdas.each do |var_name, fn|
|
|
90
|
-
|
|
102
|
+
value = fn.call(item)
|
|
103
|
+
# Condition fields keep their type — the client tests them for
|
|
104
|
+
# truthiness and "false" is truthy in JS. Output fields stay
|
|
105
|
+
# stringified so nil renders as "" rather than "null". Sanitized
|
|
106
|
+
# here, not only in build_data: the nested-component path skips it.
|
|
107
|
+
result[var_name] = if typed.key?(var_name)
|
|
108
|
+
ReactiveComponent.sanitize_for_broadcast(value, source: typed[var_name])
|
|
109
|
+
else
|
|
110
|
+
value.to_s
|
|
111
|
+
end
|
|
91
112
|
end
|
|
92
113
|
nested.each do |var_name, nc_info|
|
|
93
114
|
kwargs_values = nc_info[:kwargs].transform_values { |fn| fn.call(item) }
|
|
@@ -97,10 +118,10 @@ module ReactiveComponent
|
|
|
97
118
|
result[var_name] = klass.build_data(record, **kwargs_values)
|
|
98
119
|
else
|
|
99
120
|
result[var_name] = if klass.respond_to?(:build_data_for_nested)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
klass.build_data_for_nested(**kwargs_values)
|
|
122
|
+
else
|
|
123
|
+
ReactiveComponent::Compiler.build_data_for_nested(klass, **kwargs_values)
|
|
124
|
+
end
|
|
104
125
|
end
|
|
105
126
|
end
|
|
106
127
|
result
|
|
@@ -111,6 +132,10 @@ module ReactiveComponent
|
|
|
111
132
|
Rails.application.routes.default_url_options
|
|
112
133
|
end
|
|
113
134
|
|
|
135
|
+
def controller
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
114
139
|
def optimize_routes_generation?
|
|
115
140
|
false
|
|
116
141
|
end
|
|
@@ -118,14 +143,16 @@ module ReactiveComponent
|
|
|
118
143
|
private
|
|
119
144
|
|
|
120
145
|
def eval_lambda(block_var, source)
|
|
121
|
-
|
|
146
|
+
# lambda { |<block_var>| <source> }
|
|
147
|
+
instance_eval("lambda { |#{block_var}| #{source} }", __FILE__, __LINE__)
|
|
122
148
|
rescue NameError
|
|
123
|
-
|
|
149
|
+
# lambda { |<block_var>| <source> }
|
|
150
|
+
@component_delegate.instance_eval("lambda { |#{block_var}| #{source} }", __FILE__, __LINE__)
|
|
124
151
|
end
|
|
125
152
|
|
|
126
|
-
def method_missing(method,
|
|
153
|
+
def method_missing(method, ...)
|
|
127
154
|
if component_own_method?(method)
|
|
128
|
-
@component_delegate.send(method,
|
|
155
|
+
@component_delegate.send(method, ...)
|
|
129
156
|
else
|
|
130
157
|
super
|
|
131
158
|
end
|
|
@@ -137,9 +164,10 @@ module ReactiveComponent
|
|
|
137
164
|
|
|
138
165
|
def component_own_method?(method)
|
|
139
166
|
return false unless @component_delegate
|
|
167
|
+
|
|
140
168
|
klass = @component_delegate.class
|
|
141
|
-
klass.
|
|
142
|
-
klass.
|
|
169
|
+
klass.method_defined?(method, false) ||
|
|
170
|
+
klass.private_method_defined?(method, false)
|
|
143
171
|
end
|
|
144
172
|
end
|
|
145
173
|
end
|
|
@@ -6,13 +6,19 @@ module ReactiveComponent
|
|
|
6
6
|
class Engine < ::Rails::Engine
|
|
7
7
|
isolate_namespace ReactiveComponent
|
|
8
8
|
|
|
9
|
-
initializer
|
|
9
|
+
initializer 'reactive_component.data_evaluator' do
|
|
10
|
+
ReactiveComponent::DataEvaluator.finalize!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer 'reactive_component.importmap', before: 'importmap' do |app|
|
|
10
14
|
if defined?(Importmap)
|
|
11
15
|
app.config.importmap.paths <<
|
|
12
|
-
Engine.root.join(
|
|
16
|
+
Engine.root.join('config/importmap.rb')
|
|
13
17
|
|
|
14
|
-
app.config.assets
|
|
15
|
-
|
|
18
|
+
if app.config.respond_to?(:assets)
|
|
19
|
+
app.config.assets.paths <<
|
|
20
|
+
Engine.root.join('app/javascript')
|
|
21
|
+
end
|
|
16
22
|
end
|
|
17
23
|
end
|
|
18
24
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erubi'
|
|
4
|
+
|
|
5
|
+
module ReactiveComponent
|
|
6
|
+
# ERB → Ruby. Erubi with a `_buf` buffer, plus one rule the stock engine
|
|
7
|
+
# lacks: an expression that opens a block (`<%= render X do %>`) is emitted
|
|
8
|
+
# as `_buf.append= expr do` so the block attaches to the call instead of to
|
|
9
|
+
# a parenthesised `.to_s`. The transpiler recognises both `<<` and `append=`.
|
|
10
|
+
class Erubi < ::Erubi::Engine
|
|
11
|
+
BLOCK_EXPR = /((\s|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
|
|
12
|
+
|
|
13
|
+
def initialize(input, properties = {})
|
|
14
|
+
properties[:bufvar] ||= '_buf'
|
|
15
|
+
properties[:preamble] ||= "#{properties[:bufvar]} = ::String.new;"
|
|
16
|
+
properties[:postamble] ||= "#{properties[:bufvar]}.to_s"
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def add_expression(_indicator, code)
|
|
23
|
+
if BLOCK_EXPR.match?(code)
|
|
24
|
+
src << " #{@bufvar}.append= " << code
|
|
25
|
+
else
|
|
26
|
+
src << " #{@bufvar} << (" << code << ').to_s;'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|