funicular 0.3.0 → 0.4.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 +57 -0
- data/demo/test_chartjs.html +9 -9
- data/demo/test_component.html +8 -8
- data/demo/test_error_boundary.html +44 -41
- data/demo/test_router.html +48 -48
- data/demo/tic-tac-toe.html +25 -25
- data/docs/architecture.md +46 -9
- data/lib/funicular/ssr/runtime.rb +1 -0
- data/lib/funicular/vendor/mrbc/VERSION +1 -1
- data/lib/funicular/vendor/mrbc/mrbc.js +613 -486
- data/lib/funicular/vendor/mrbc/mrbc.wasm +0 -0
- data/lib/funicular/vendor/picoruby/VERSION +1 -1
- data/lib/funicular/vendor/picoruby/debug/picoruby.js +669 -449
- data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby/dist/picoruby.js +2 -2
- data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby-test-node/VERSION +1 -1
- data/lib/funicular/vendor/picoruby-test-node/picoruby.js +921 -629
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +1 -1
- data/lib/funicular/version.rb +1 -1
- data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +37 -38
- data/minitest/dsl_test.rb +237 -0
- data/minitest/fixtures/funicular_app/components/greeting_component.rb +5 -5
- data/minitest/form_for_test.rb +2 -2
- data/minitest/hydration_test.rb +2 -2
- data/minitest/sig_tags_test.rb +30 -0
- data/minitest/view_context_test.rb +15 -15
- data/mrblib/0_tags.rb +62 -0
- data/mrblib/component.rb +113 -23
- data/mrblib/error_boundary.rb +25 -19
- data/mrblib/form_builder.rb +10 -10
- data/mrblib/funicular.rb +1 -1
- data/mrblib/styles.rb +102 -12
- data/mrblib/view_context.rb +3 -32
- data/sig/component.rbs +18 -4
- data/sig/error_boundary.rbs +4 -4
- data/sig/styles.rbs +18 -5
- data/sig/tags.rbs +54 -0
- data/sig/view_context.rbs +47 -34
- metadata +5 -1
data/mrblib/component.rb
CHANGED
|
@@ -52,9 +52,60 @@ module Funicular
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
include Tags
|
|
56
|
+
|
|
55
57
|
attr_accessor :props, :vdom, :dom_element, :mounted, :runtime, :children, :current_children
|
|
56
58
|
attr_reader :refs
|
|
57
59
|
|
|
60
|
+
# Opt out of DSL collision detection for the given method names. The
|
|
61
|
+
# shadowed tag remains reachable through tag(:name, ...).
|
|
62
|
+
def self.allow_dsl_override(*names)
|
|
63
|
+
@dsl_overrides ||= [] #: Array[Symbol]
|
|
64
|
+
@dsl_overrides.concat(names)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.dsl_overrides
|
|
68
|
+
@dsl_overrides ||= [] #: Array[Symbol]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Layer 1: catches `def`, `define_method`, and `alias` in subclasses at
|
|
72
|
+
# class-definition time. attr_* does not fire this hook on mruby, and
|
|
73
|
+
# included modules never do; validate_dsl_conflicts! covers those.
|
|
74
|
+
def self.method_added(name)
|
|
75
|
+
if self != Funicular::Component && Funicular::Tags::RESERVED_DSL[name] && !dsl_overrides.include?(name)
|
|
76
|
+
kind = Funicular::Tags::RESERVED_DSL[name] == :tag ? "<#{name}> tag helper" : "##{name} helper"
|
|
77
|
+
raise Funicular::DSLCollisionError,
|
|
78
|
+
"#{self}##{name} collides with the Funicular DSL (#{kind}). " \
|
|
79
|
+
"Rename it (e.g. `#{name}_value`), or declare `allow_dsl_override :#{name}` " \
|
|
80
|
+
"and use `tag(:#{name}, ...)` to emit the element."
|
|
81
|
+
end
|
|
82
|
+
super
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Layer 2: once per class, sweep methods the method_added hook cannot
|
|
86
|
+
# see (attr_* on mruby, user-included modules).
|
|
87
|
+
def self.validate_dsl_conflicts!
|
|
88
|
+
return if @dsl_validated
|
|
89
|
+
if instance_method(:render).arity != 0
|
|
90
|
+
raise Funicular::DSLCollisionError,
|
|
91
|
+
"#{self}#render must not take parameters as of Funicular 0.4.0: " \
|
|
92
|
+
"delete the parameter (`def render`) and drop the `h.` receivers " \
|
|
93
|
+
"inside; tags are bareword methods on the component now."
|
|
94
|
+
end
|
|
95
|
+
ancestors.each do |mod|
|
|
96
|
+
break if mod == Funicular::Component || mod == Funicular::Tags
|
|
97
|
+
next unless mod.is_a?(Module)
|
|
98
|
+
methods = mod.instance_methods(false) + mod.private_instance_methods(false)
|
|
99
|
+
offenders = methods.select { |m| Funicular::Tags::RESERVED_DSL[m] } - dsl_overrides
|
|
100
|
+
next if offenders.empty?
|
|
101
|
+
raise Funicular::DSLCollisionError,
|
|
102
|
+
"#{mod} defines methods that collide with the Funicular DSL: " \
|
|
103
|
+
"#{offenders.map { |m| m.to_s }.join(', ')}. Rename them, or declare " \
|
|
104
|
+
"`allow_dsl_override` and use `tag(...)` to emit the element."
|
|
105
|
+
end
|
|
106
|
+
@dsl_validated = true
|
|
107
|
+
end
|
|
108
|
+
|
|
58
109
|
def initialize(props = {})
|
|
59
110
|
@props = props
|
|
60
111
|
@state = initialize_state || {}
|
|
@@ -93,7 +144,7 @@ module Funicular
|
|
|
93
144
|
end
|
|
94
145
|
|
|
95
146
|
def styles
|
|
96
|
-
@style_accessor ||=
|
|
147
|
+
@style_accessor ||= self.class.style_accessor_class.new(self.class.styles_definitions)
|
|
97
148
|
end
|
|
98
149
|
|
|
99
150
|
# Override this method in subclasses to define initial state
|
|
@@ -230,21 +281,21 @@ module Funicular
|
|
|
230
281
|
# ) do
|
|
231
282
|
# div { user.name }
|
|
232
283
|
# end
|
|
233
|
-
def render_suspense(
|
|
284
|
+
def render_suspense(name, fallback:, error: nil, &block)
|
|
234
285
|
current_children = @current_children
|
|
235
286
|
child_count_before = current_children&.size
|
|
236
287
|
result = nil
|
|
237
288
|
|
|
238
289
|
if @suspense_states[name] == :rejected
|
|
239
290
|
result = if error
|
|
240
|
-
error.call(
|
|
291
|
+
error.call(@suspense_errors[name])
|
|
241
292
|
else
|
|
242
|
-
fallback.call
|
|
293
|
+
fallback.call
|
|
243
294
|
end
|
|
244
295
|
elsif suspense_loading?(name)
|
|
245
|
-
result = fallback.call
|
|
296
|
+
result = fallback.call
|
|
246
297
|
else
|
|
247
|
-
result = block.call(
|
|
298
|
+
result = block.call(resources)
|
|
248
299
|
end
|
|
249
300
|
|
|
250
301
|
if current_children && current_children.size == child_count_before
|
|
@@ -254,17 +305,25 @@ module Funicular
|
|
|
254
305
|
result
|
|
255
306
|
end
|
|
256
307
|
|
|
257
|
-
# Class methods for styles DSL
|
|
308
|
+
# Class methods for styles DSL. The block is instance_exec'd on a
|
|
309
|
+
# StyleBuilder cleanroom so barewords define styles; it also receives
|
|
310
|
+
# the builder for the explicit `styles { |css| css.define(...) }` form.
|
|
258
311
|
def self.styles(&block)
|
|
259
312
|
builder = StyleBuilder.new
|
|
260
|
-
|
|
313
|
+
builder.instance_exec(builder, &block) # steep:ignore
|
|
261
314
|
@styles_definitions = builder.to_definitions
|
|
315
|
+
@style_accessor_class = nil
|
|
262
316
|
end
|
|
263
317
|
|
|
264
318
|
def self.styles_definitions
|
|
265
319
|
@styles_definitions ||= {}
|
|
266
320
|
end
|
|
267
321
|
|
|
322
|
+
# Per-class accessor with one real method per declared style name.
|
|
323
|
+
def self.style_accessor_class
|
|
324
|
+
@style_accessor_class ||= StyleAccessor.accessor_for(styles_definitions)
|
|
325
|
+
end
|
|
326
|
+
|
|
268
327
|
# Suspense DSL - register async data loaders
|
|
269
328
|
#
|
|
270
329
|
# @param name [Symbol] Name of the suspense data (becomes accessible as method)
|
|
@@ -447,19 +506,20 @@ module Funicular
|
|
|
447
506
|
end
|
|
448
507
|
|
|
449
508
|
# Override this method in subclasses to define render logic
|
|
450
|
-
def render
|
|
451
|
-
raise "Subclasses must implement render
|
|
509
|
+
def render
|
|
510
|
+
raise "Subclasses must implement render"
|
|
452
511
|
end
|
|
453
512
|
|
|
454
513
|
# Build VDOM tree from render method
|
|
455
514
|
# Called by VDOM::Renderer, Differ, and Patcher
|
|
456
515
|
def build_vdom
|
|
516
|
+
self.class.validate_dsl_conflicts!
|
|
457
517
|
previous_rendering = @rendering
|
|
458
518
|
previous_children = @current_children
|
|
459
519
|
@rendering = true
|
|
460
520
|
@current_children = nil
|
|
461
521
|
begin
|
|
462
|
-
result = render
|
|
522
|
+
result = render
|
|
463
523
|
ensure
|
|
464
524
|
@rendering = previous_rendering
|
|
465
525
|
@current_children = previous_children
|
|
@@ -622,9 +682,45 @@ module Funicular
|
|
|
622
682
|
normalize_vnode(value)
|
|
623
683
|
end
|
|
624
684
|
|
|
685
|
+
# Internal element factory shared by the Tags mixin, FormBuilder, and
|
|
686
|
+
# the framework helpers. One instance per component; ViewContext itself
|
|
687
|
+
# is stateless (the render cursor lives on the component).
|
|
688
|
+
def __view__
|
|
689
|
+
@__view__ ||= ViewContext.new(self)
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def routes
|
|
693
|
+
@runtime.routes
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Bareword DSL helpers available inside render (self is the component).
|
|
697
|
+
def component(component_class, props = {}, &block)
|
|
698
|
+
__view__.component(component_class, props, &block)
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def form_for(model_key, options = {}, &block)
|
|
702
|
+
build_form_for(__view__, model_key, options, &block)
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def link_to(path, **options, &block)
|
|
706
|
+
build_link_to(__view__, path, **options, &block)
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def button_to(path, method: :post, **options, &block)
|
|
710
|
+
build_button_to(__view__, path, method: method, **options, &block)
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def suspense(name, fallback:, error: nil, &block)
|
|
714
|
+
render_suspense(name, fallback: fallback, error: error, &block)
|
|
715
|
+
end
|
|
716
|
+
|
|
625
717
|
def add_child_from_view(child)
|
|
718
|
+
unless @rendering
|
|
719
|
+
raise Funicular::RenderContextError,
|
|
720
|
+
"view DSL called outside render (tags may only be used while the component is rendering)"
|
|
721
|
+
end
|
|
626
722
|
current_children = @current_children
|
|
627
|
-
return unless
|
|
723
|
+
return unless current_children
|
|
628
724
|
|
|
629
725
|
normalized = normalize_vnode(child)
|
|
630
726
|
current_children << normalized if normalized
|
|
@@ -654,8 +750,8 @@ module Funicular
|
|
|
654
750
|
->(event) { event.preventDefault }
|
|
655
751
|
end
|
|
656
752
|
|
|
657
|
-
h.form({ onsubmit: submit_handler, class: form_class }.merge(options)) do
|
|
658
|
-
builder = Funicular::FormBuilder.new(self,
|
|
753
|
+
h.form({ onsubmit: submit_handler, class: form_class }.merge(options)) do
|
|
754
|
+
builder = Funicular::FormBuilder.new(self, h, model_key, options)
|
|
659
755
|
block.call(builder)
|
|
660
756
|
end
|
|
661
757
|
end
|
|
@@ -881,15 +977,9 @@ module Funicular
|
|
|
881
977
|
end
|
|
882
978
|
|
|
883
979
|
def event_target(event)
|
|
884
|
-
target
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
rescue
|
|
888
|
-
target = nil
|
|
889
|
-
end
|
|
890
|
-
return target if target
|
|
891
|
-
|
|
892
|
-
event.target if event.respond_to?(:target)
|
|
980
|
+
event[:target]
|
|
981
|
+
rescue
|
|
982
|
+
nil
|
|
893
983
|
end
|
|
894
984
|
|
|
895
985
|
def add_form_field_value(data, field)
|
data/mrblib/error_boundary.rb
CHANGED
|
@@ -3,17 +3,23 @@ module Funicular
|
|
|
3
3
|
# a fallback UI instead of crashing the entire application.
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# component(ErrorBoundary) do
|
|
7
|
+
# component(RiskyComponent)
|
|
8
8
|
# end
|
|
9
9
|
#
|
|
10
10
|
# With custom fallback:
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# component(ErrorBoundary, fallback: ->(h, error) { h.div { "Error: #{error.message}" } }) do
|
|
12
|
+
# component(RiskyComponent)
|
|
13
13
|
# end
|
|
14
14
|
#
|
|
15
15
|
# Props:
|
|
16
|
-
# - fallback: Proc or Method that receives
|
|
16
|
+
# - fallback: Proc or Method that receives (view_context, error) and
|
|
17
|
+
# returns VDOM. NOTE: unlike everywhere else, the fallback cannot use
|
|
18
|
+
# bareword tags. The proc is created in the parent component's scope
|
|
19
|
+
# but runs during the boundary's render, where only the boundary's
|
|
20
|
+
# cursor is active; a bareword tag would dispatch against the parent
|
|
21
|
+
# and raise RenderContextError. Build elements through the view
|
|
22
|
+
# context argument (`h.div { ... }`) instead.
|
|
17
23
|
# - on_error: Optional callback when error is caught (for logging, reporting)
|
|
18
24
|
#
|
|
19
25
|
class ErrorBoundary < Component
|
|
@@ -60,47 +66,47 @@ module Funicular
|
|
|
60
66
|
end
|
|
61
67
|
end
|
|
62
68
|
|
|
63
|
-
def render
|
|
69
|
+
def render
|
|
64
70
|
if state[:has_error]
|
|
65
|
-
render_fallback
|
|
71
|
+
render_fallback
|
|
66
72
|
else
|
|
67
|
-
render_children
|
|
73
|
+
render_children
|
|
68
74
|
end
|
|
69
75
|
end
|
|
70
76
|
|
|
71
77
|
private
|
|
72
78
|
|
|
73
|
-
def render_fallback
|
|
79
|
+
def render_fallback
|
|
74
80
|
if props[:fallback]
|
|
75
|
-
result = props[:fallback].call(
|
|
81
|
+
result = props[:fallback].call(__view__, state[:error])
|
|
76
82
|
if result.is_a?(VDOM::VNode)
|
|
77
83
|
result
|
|
78
84
|
else
|
|
79
|
-
|
|
85
|
+
div { result.to_s }
|
|
80
86
|
end
|
|
81
87
|
else
|
|
82
|
-
default_fallback
|
|
88
|
+
default_fallback
|
|
83
89
|
end
|
|
84
90
|
end
|
|
85
91
|
|
|
86
|
-
def default_fallback
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
def default_fallback
|
|
93
|
+
div(class: 'error-boundary-fallback', style: 'padding: 20px; background: #fee; border: 1px solid #f00; border-radius: 4px;') do
|
|
94
|
+
h3(style: 'color: #c00; margin: 0 0 10px 0;') { "Something went wrong" }
|
|
89
95
|
if state[:error]
|
|
90
|
-
|
|
96
|
+
div(style: 'font-family: monospace; white-space: pre-wrap; font-size: 12px; color: #600;') do
|
|
91
97
|
"#{state[:error].class}: #{state[:error].message}"
|
|
92
98
|
end
|
|
93
99
|
end
|
|
94
100
|
if Funicular.env.development? && state[:error_info]
|
|
95
|
-
|
|
101
|
+
div(style: 'margin-top: 10px; font-size: 11px; color: #666;') do
|
|
96
102
|
"Component: #{state[:error_info][:component_class]}"
|
|
97
103
|
end
|
|
98
104
|
end
|
|
99
105
|
end
|
|
100
106
|
end
|
|
101
107
|
|
|
102
|
-
def render_children
|
|
103
|
-
|
|
108
|
+
def render_children
|
|
109
|
+
div(class: 'error-boundary-content') do
|
|
104
110
|
children
|
|
105
111
|
end
|
|
106
112
|
end
|
data/mrblib/form_builder.rb
CHANGED
|
@@ -65,10 +65,10 @@ module Funicular
|
|
|
65
65
|
attrs[:class] = css_class unless css_class.empty?
|
|
66
66
|
|
|
67
67
|
# Render field + error message
|
|
68
|
-
@view_context.div do
|
|
69
|
-
|
|
68
|
+
@view_context.div do
|
|
69
|
+
@view_context.input(attrs)
|
|
70
70
|
if has_error
|
|
71
|
-
|
|
71
|
+
@view_context.div(class: @error_class) { error_message }
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
end
|
|
@@ -128,10 +128,10 @@ module Funicular
|
|
|
128
128
|
|
|
129
129
|
attrs[:class] = css_class unless css_class.empty?
|
|
130
130
|
|
|
131
|
-
@view_context.div do
|
|
132
|
-
|
|
131
|
+
@view_context.div do
|
|
132
|
+
@view_context.textarea(attrs)
|
|
133
133
|
if has_error
|
|
134
|
-
|
|
134
|
+
@view_context.div(class: @error_class) { error_message }
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
end
|
|
@@ -194,18 +194,18 @@ module Funicular
|
|
|
194
194
|
|
|
195
195
|
attrs[:class] = css_class unless css_class.empty?
|
|
196
196
|
|
|
197
|
-
@view_context.div do
|
|
198
|
-
|
|
197
|
+
@view_context.div do
|
|
198
|
+
@view_context.select(attrs) do
|
|
199
199
|
choices.each do |choice|
|
|
200
200
|
option_value, option_text = choice.is_a?(Array) ? choice : [choice, choice]
|
|
201
201
|
selected = value.to_s == option_value.to_s
|
|
202
|
-
|
|
202
|
+
@view_context.option(value: option_value, selected: selected) do
|
|
203
203
|
option_text
|
|
204
204
|
end
|
|
205
205
|
end
|
|
206
206
|
end
|
|
207
207
|
if has_error
|
|
208
|
-
|
|
208
|
+
@view_context.div(class: @error_class) { error_message }
|
|
209
209
|
end
|
|
210
210
|
end
|
|
211
211
|
end
|
data/mrblib/funicular.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Funicular
|
|
|
18
18
|
# Guard against redefinition: when the mrblib runtime is loaded into a
|
|
19
19
|
# CRuby/Rails process for SSR, lib/funicular/version.rb has already defined
|
|
20
20
|
# VERSION for the CRuby gem. In the wasm build VERSION is undefined here.
|
|
21
|
-
VERSION = '0.
|
|
21
|
+
VERSION = '0.4.0' unless Funicular.const_defined?(:VERSION)
|
|
22
22
|
|
|
23
23
|
def self.version
|
|
24
24
|
VERSION
|
data/mrblib/styles.rb
CHANGED
|
@@ -25,25 +25,42 @@ module Funicular
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
class
|
|
28
|
+
# Read-side accessor. One subclass is generated per component class with a
|
|
29
|
+
# real method per declared style name (see accessor_for), so lookups never
|
|
30
|
+
# go through method_missing at render time and a typo raises loudly.
|
|
31
|
+
# BasicObject keeps Object/Kernel names (display, hash, ...) usable as
|
|
32
|
+
# style names on both mruby and CRuby.
|
|
33
|
+
class StyleAccessor < BasicObject
|
|
34
|
+
def self.accessor_for(definitions)
|
|
35
|
+
klass = ::Class.new(self) #: singleton(StyleAccessor)
|
|
36
|
+
definitions.each_key do |name|
|
|
37
|
+
klass.__send__(:define_method, name) do |variant = nil|
|
|
38
|
+
self[name, variant] # steep:ignore
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
klass
|
|
42
|
+
end
|
|
43
|
+
|
|
29
44
|
def initialize(definitions)
|
|
30
45
|
@definitions = definitions
|
|
31
46
|
end
|
|
32
47
|
|
|
33
48
|
def [](name, variant = nil)
|
|
34
49
|
style = @definitions[name]
|
|
35
|
-
|
|
50
|
+
unless style
|
|
51
|
+
::Kernel.raise ::ArgumentError,
|
|
52
|
+
"unknown style :#{name} (declared: #{@definitions.keys.map { |k| ":#{k}" }.join(', ')})"
|
|
53
|
+
end
|
|
36
54
|
|
|
37
55
|
if variant.nil?
|
|
38
56
|
# No arguments: return base or value
|
|
39
57
|
StyleValue.new(style[:base] || style[:value] || "")
|
|
40
58
|
elsif variant == true || variant == false
|
|
41
59
|
# Boolean argument: base + active (if true)
|
|
42
|
-
# JS::Object#== now supports direct comparison with Ruby true/false
|
|
43
60
|
base = style[:base] || ""
|
|
44
61
|
active_class = (variant == true) ? (style[:active] || "") : ""
|
|
45
62
|
StyleValue.new("#{base} #{active_class}".strip)
|
|
46
|
-
elsif variant
|
|
63
|
+
elsif ::Symbol === variant
|
|
47
64
|
# Symbol argument: base + variants[symbol]
|
|
48
65
|
base = style[:base] || ""
|
|
49
66
|
variant_class = style[:variants] ? (style[:variants][variant] || "") : ""
|
|
@@ -53,27 +70,100 @@ module Funicular
|
|
|
53
70
|
StyleValue.new(style[:base] || style[:value] || "")
|
|
54
71
|
end
|
|
55
72
|
end
|
|
73
|
+
|
|
74
|
+
def method_missing(name, *_args)
|
|
75
|
+
::Kernel.raise ::NoMethodError,
|
|
76
|
+
"unknown style '#{name}' (declared: #{@definitions.keys.map { |k| ":#{k}" }.join(', ')})"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def respond_to_missing?(name, _include_private = false)
|
|
80
|
+
@definitions.key?(name)
|
|
81
|
+
end
|
|
56
82
|
end
|
|
57
83
|
|
|
58
|
-
class
|
|
84
|
+
# Definition-side builder. The class-level `styles do ... end` block is
|
|
85
|
+
# instance_exec'd on an instance, so barewords define styles:
|
|
86
|
+
#
|
|
87
|
+
# styles do
|
|
88
|
+
# shell "app-shell"
|
|
89
|
+
# button base: "btn", variants: { primary: "btn--primary" }
|
|
90
|
+
# end
|
|
91
|
+
#
|
|
92
|
+
# BasicObject keeps the bareword namespace clean on both VMs; the block
|
|
93
|
+
# also receives the builder as an argument for the explicit form
|
|
94
|
+
# `styles { |css| css.define(:name, ...) }`, needed when a value is
|
|
95
|
+
# computed (a bareword helper call inside the block would be captured as
|
|
96
|
+
# a style definition, and its nil return raises via validation below).
|
|
97
|
+
class StyleBuilder < BasicObject
|
|
98
|
+
# Style names that would clobber the builder/accessor internals.
|
|
99
|
+
RESERVED_NAMES = %i[
|
|
100
|
+
initialize method_missing respond_to_missing? define to_definitions
|
|
101
|
+
validate_options
|
|
102
|
+
== != ! [] equal? __send__ __id__ instance_eval instance_exec
|
|
103
|
+
]
|
|
104
|
+
|
|
59
105
|
def initialize
|
|
60
|
-
@definitions = {}
|
|
106
|
+
@definitions = {} #: Hash[Symbol, untyped]
|
|
61
107
|
end
|
|
62
108
|
|
|
63
109
|
def define(name, value = nil, **options)
|
|
64
|
-
if
|
|
110
|
+
if RESERVED_NAMES.include?(name)
|
|
111
|
+
::Kernel.raise ::ArgumentError, "style name :#{name} is reserved"
|
|
112
|
+
end
|
|
113
|
+
if @definitions.key?(name)
|
|
114
|
+
::Kernel.raise ::ArgumentError, "style :#{name} is already defined"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if ::String === value
|
|
65
118
|
@definitions[name] = { value: value }
|
|
66
|
-
elsif value
|
|
67
|
-
@definitions[name] = value
|
|
68
|
-
elsif !options.empty?
|
|
69
|
-
@definitions[name] = options
|
|
119
|
+
elsif ::Hash === value
|
|
120
|
+
@definitions[name] = validate_options(name, value)
|
|
121
|
+
elsif (::NilClass === value) && !options.empty?
|
|
122
|
+
@definitions[name] = validate_options(name, options)
|
|
70
123
|
else
|
|
71
|
-
raise ArgumentError,
|
|
124
|
+
::Kernel.raise ::ArgumentError,
|
|
125
|
+
"invalid style definition for :#{name}; expected a String, a " \
|
|
126
|
+
"Hash, or keyword options. Note: helper methods cannot be called " \
|
|
127
|
+
"bareword inside a styles block; use " \
|
|
128
|
+
"`styles { |css| css.define(...) }` for computed values."
|
|
129
|
+
end
|
|
130
|
+
self
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def validate_options(name, options)
|
|
134
|
+
options.each do |key, val|
|
|
135
|
+
case key
|
|
136
|
+
when :value, :base, :active
|
|
137
|
+
unless ::String === val
|
|
138
|
+
::Kernel.raise ::ArgumentError,
|
|
139
|
+
"style :#{name} option #{key}: must be a String. Note: helper " \
|
|
140
|
+
"methods cannot be called bareword inside a styles block; use " \
|
|
141
|
+
"`styles { |css| css.define(...) }` for computed values."
|
|
142
|
+
end
|
|
143
|
+
when :variants
|
|
144
|
+
unless (::Hash === val) && val.all? { |_k, v| ::String === v }
|
|
145
|
+
::Kernel.raise ::ArgumentError,
|
|
146
|
+
"style :#{name} option variants: must be a Hash of Symbol => String"
|
|
147
|
+
end
|
|
148
|
+
else
|
|
149
|
+
::Kernel.raise ::ArgumentError,
|
|
150
|
+
"style :#{name} has unknown option #{key.inspect} " \
|
|
151
|
+
"(allowed: value, base, active, variants)"
|
|
152
|
+
end
|
|
72
153
|
end
|
|
154
|
+
options
|
|
73
155
|
end
|
|
74
156
|
|
|
75
157
|
def to_definitions
|
|
76
158
|
@definitions
|
|
77
159
|
end
|
|
160
|
+
|
|
161
|
+
def method_missing(name, value = nil, **options)
|
|
162
|
+
define(name, value, **options)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
166
|
+
true
|
|
167
|
+
end
|
|
78
168
|
end
|
|
79
169
|
end
|
data/mrblib/view_context.rb
CHANGED
|
@@ -1,40 +1,11 @@
|
|
|
1
1
|
module Funicular
|
|
2
2
|
class ViewContext
|
|
3
|
-
HTML_TAGS =
|
|
4
|
-
div span p a data
|
|
5
|
-
h1 h2 h3 h4 h5 h6
|
|
6
|
-
ul ol li
|
|
7
|
-
table thead tbody tr th td
|
|
8
|
-
form input textarea button select option label
|
|
9
|
-
header footer nav section article aside
|
|
10
|
-
img video audio canvas
|
|
11
|
-
br hr
|
|
12
|
-
]
|
|
3
|
+
HTML_TAGS = Funicular::Tags::HTML_TAGS
|
|
13
4
|
|
|
14
5
|
def initialize(component)
|
|
15
6
|
@component = component
|
|
16
7
|
end
|
|
17
8
|
|
|
18
|
-
def state
|
|
19
|
-
@component.state
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def props
|
|
23
|
-
@component.props
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def resources
|
|
27
|
-
@component.resources
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def styles
|
|
31
|
-
@component.styles
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def routes
|
|
35
|
-
@component.runtime.routes
|
|
36
|
-
end
|
|
37
|
-
|
|
38
9
|
def tag(name, props = {}, &block)
|
|
39
10
|
build_element(name, props, &block)
|
|
40
11
|
end
|
|
@@ -66,7 +37,7 @@ module Funicular
|
|
|
66
37
|
end
|
|
67
38
|
|
|
68
39
|
def suspense(name, fallback:, error: nil, &block)
|
|
69
|
-
@component.render_suspense(
|
|
40
|
+
@component.render_suspense(name, fallback: fallback, error: error, &block)
|
|
70
41
|
end
|
|
71
42
|
|
|
72
43
|
def link_to(path, **options, &block)
|
|
@@ -81,7 +52,7 @@ module Funicular
|
|
|
81
52
|
children = [] #: Array[Funicular::VDOM::child_t]
|
|
82
53
|
previous = @component.current_children
|
|
83
54
|
@component.current_children = children
|
|
84
|
-
result = block.call
|
|
55
|
+
result = block.call
|
|
85
56
|
@component.current_children = previous
|
|
86
57
|
|
|
87
58
|
if result && !result.equal?(children) && children.empty?
|
data/sig/component.rbs
CHANGED
|
@@ -17,6 +17,8 @@ module Funicular
|
|
|
17
17
|
def error: (Symbol key) -> untyped
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
include Tags
|
|
21
|
+
|
|
20
22
|
attr_accessor props: Hash[Symbol, untyped]
|
|
21
23
|
attr_accessor vdom: VDOM::VNode | VDOM::Text | nil
|
|
22
24
|
attr_accessor dom_element: JS::Element
|
|
@@ -29,8 +31,13 @@ module Funicular
|
|
|
29
31
|
type suspense_definition = { loader: untyped, on_resolve: untyped, min_delay: Integer? }
|
|
30
32
|
|
|
31
33
|
def initialize: (?Hash[Symbol, untyped] props) -> void
|
|
34
|
+
def self.allow_dsl_override: (*Symbol names) -> void
|
|
35
|
+
def self.dsl_overrides: () -> Array[Symbol]
|
|
36
|
+
def self.method_added: (Symbol name) -> void
|
|
37
|
+
def self.validate_dsl_conflicts!: () -> void
|
|
32
38
|
def self.styles: () { (StyleBuilder) -> void } -> void
|
|
33
39
|
def self.styles_definitions: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
40
|
+
def self.style_accessor_class: () -> singleton(StyleAccessor)
|
|
34
41
|
def self.use_suspense: (Symbol name, untyped loader, ?on_resolve: untyped, ?min_delay: Integer) -> void
|
|
35
42
|
def self.suspense_definitions: () -> Hash[Symbol, suspense_definition]
|
|
36
43
|
|
|
@@ -46,22 +53,29 @@ module Funicular
|
|
|
46
53
|
def suspense_loading?: (*Symbol names) -> bool
|
|
47
54
|
def suspense_error?: (Symbol name) -> bool
|
|
48
55
|
def suspense_error: (Symbol name) -> untyped
|
|
49
|
-
def render_suspense: (
|
|
56
|
+
def render_suspense: (Symbol name, fallback: untyped, ?error: untyped) { (ResourceAccessor) -> untyped } -> untyped
|
|
50
57
|
|
|
51
58
|
def patch: (Hash[Symbol, untyped] new_state) -> void
|
|
52
59
|
def mount: (JS::Element container) -> void
|
|
53
60
|
def hydrate: (JS::Element dom_element) -> void
|
|
54
61
|
def unmount: () -> void
|
|
55
|
-
def render: (
|
|
62
|
+
def render: () -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
|
|
56
63
|
def build_vdom: () -> (VDOM::VNode | VDOM::Text | nil)
|
|
57
64
|
|
|
58
65
|
def bind_events: (JS::Element dom_element, VDOM::VNode | VDOM::Text | nil vnode) -> void
|
|
59
66
|
def collect_refs: (JS::Element dom_element, VDOM::VNode | VDOM::Text | nil vnode, ?Hash[Symbol, JS::Element] refs_map) -> Hash[Symbol, JS::Element]
|
|
60
67
|
def normalize_vnode_for_view: (untyped value) -> (VDOM::Element | VDOM::Text | VDOM::Component | nil)
|
|
68
|
+
def __view__: () -> ViewContext
|
|
69
|
+
def routes: () -> untyped
|
|
70
|
+
def component: (singleton(Component) component_class, ?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Component
|
|
71
|
+
def form_for: (Symbol model_key, ?Hash[Symbol, untyped] options) { (FormBuilder) -> void } -> VDOM::Element
|
|
72
|
+
def link_to: (String path, **untyped options) ?{ -> untyped } -> VDOM::Element
|
|
73
|
+
def button_to: (String path, ?method: Symbol, **untyped options) ?{ -> untyped } -> VDOM::Element
|
|
74
|
+
def suspense: (Symbol name, fallback: untyped, ?error: untyped) { (ResourceAccessor) -> untyped } -> untyped
|
|
61
75
|
def add_child_from_view: (untyped child) -> void
|
|
62
76
|
def build_form_for: (ViewContext h, Symbol model_key, ?Hash[Symbol, untyped] options) { (FormBuilder) -> void } -> VDOM::Element
|
|
63
|
-
def build_link_to: (ViewContext h, String path, **untyped options) { (
|
|
64
|
-
def build_button_to: (ViewContext h, String path, ?method: Symbol, **untyped options) { (
|
|
77
|
+
def build_link_to: (ViewContext h, String path, **untyped options) ?{ () -> untyped } -> VDOM::Element
|
|
78
|
+
def build_button_to: (ViewContext h, String path, ?method: Symbol, **untyped options) ?{ () -> untyped } -> VDOM::Element
|
|
65
79
|
|
|
66
80
|
def component_will_mount: () -> void
|
|
67
81
|
def component_mounted: () -> void
|
data/sig/error_boundary.rbs
CHANGED
|
@@ -5,10 +5,10 @@ module Funicular
|
|
|
5
5
|
def initialize_state: () -> Hash[Symbol, untyped]
|
|
6
6
|
def catch_error: (Exception error, ?Hash[Symbol, untyped]? error_info) -> bool
|
|
7
7
|
def reset: () -> void
|
|
8
|
-
def render: (
|
|
8
|
+
def render: () -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
|
|
9
9
|
|
|
10
|
-
private def render_fallback: (
|
|
11
|
-
private def default_fallback: (
|
|
12
|
-
private def render_children: (
|
|
10
|
+
private def render_fallback: () -> (VDOM::VNode | String)
|
|
11
|
+
private def default_fallback: () -> VDOM::VNode
|
|
12
|
+
private def render_children: () -> VDOM::VNode
|
|
13
13
|
end
|
|
14
14
|
end
|