funicular 0.2.1 → 0.3.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 +54 -0
- data/README.md +2 -2
- data/Rakefile +23 -15
- data/demo/test_chartjs.html +17 -17
- data/demo/test_component.html +15 -15
- data/demo/test_error_boundary.html +41 -41
- data/demo/test_router.html +57 -57
- data/demo/tic-tac-toe.html +29 -29
- data/docs/architecture.md +43 -30
- data/lib/funicular/compiler.rb +13 -13
- data/lib/funicular/helpers/picoruby_helper.rb +24 -1
- data/lib/funicular/ssr/runtime.rb +2 -0
- data/lib/funicular/ssr.rb +2 -1
- data/lib/funicular/testing/node_runner.rb +1 -1
- data/lib/funicular/vendor/mrbc/VERSION +1 -0
- data/lib/funicular/vendor/{picorbc/picorbc.js → mrbc/mrbc.js} +14 -1
- data/lib/funicular/vendor/mrbc/mrbc.wasm +0 -0
- data/lib/funicular/vendor/picoruby/VERSION +1 -1
- data/lib/funicular/vendor/picoruby/debug/init.iife.js +19 -4
- data/lib/funicular/vendor/picoruby/debug/picoruby.js +16 -10
- data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby/dist/init.iife.js +19 -4
- data/lib/funicular/vendor/picoruby/dist/picoruby.js +1 -1
- 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 +85 -84
- 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 +47 -46
- data/lib/tasks/funicular.rake +1 -1
- data/minitest/commands_routes_test.rb +97 -0
- data/minitest/compiler_test.rb +195 -0
- data/minitest/configuration_test.rb +64 -0
- data/minitest/fixtures/funicular_app/components/greeting_component.rb +6 -6
- data/minitest/form_for_test.rb +2 -2
- data/minitest/funicular_test.rb +28 -2
- data/minitest/hydration_test.rb +2 -2
- data/minitest/middleware_test.rb +154 -0
- data/minitest/picoruby_helper_test.rb +139 -0
- data/minitest/route_parser_test.rb +139 -0
- data/minitest/schema_test.rb +23 -0
- data/minitest/ssr_test.rb +57 -0
- data/minitest/support/rails_stub.rb +59 -0
- data/minitest/test_helper.rb +20 -0
- data/minitest/testing_test.rb +267 -0
- data/minitest/view_context_test.rb +101 -0
- data/mrblib/component.rb +158 -237
- data/mrblib/debug.rb +7 -6
- data/mrblib/differ.rb +3 -1
- data/mrblib/error_boundary.rb +19 -27
- data/mrblib/form_builder.rb +28 -24
- data/mrblib/funicular.rb +2 -1
- data/mrblib/html_serializer.rb +14 -13
- data/mrblib/http.rb +12 -1
- data/mrblib/patcher.rb +12 -9
- data/mrblib/router.rb +9 -5
- data/mrblib/runtime.rb +28 -0
- data/mrblib/styles.rb +13 -17
- data/mrblib/vdom.rb +90 -9
- data/mrblib/view_context.rb +135 -0
- data/sig/component.rbs +34 -85
- data/sig/error_boundary.rbs +4 -4
- data/sig/form_builder.rbs +2 -1
- data/sig/html_serializer.rbs +2 -1
- data/sig/http.rbs +1 -0
- data/sig/patcher.rbs +1 -1
- data/sig/router.rbs +2 -13
- data/sig/runtime.rbs +13 -0
- data/sig/styles.rbs +3 -4
- data/sig/vdom.rbs +11 -2
- data/sig/view_context.rbs +47 -0
- metadata +18 -5
- data/lib/funicular/vendor/picorbc/VERSION +0 -1
- data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
data/mrblib/vdom.rb
CHANGED
|
@@ -6,6 +6,69 @@ module Funicular
|
|
|
6
6
|
|
|
7
7
|
URL_ATTRIBUTES = %w[href src action formaction data poster xlink:href]
|
|
8
8
|
|
|
9
|
+
# VDOM input is normally authored by application code, but props and tags
|
|
10
|
+
# can also be assembled from hashes. Validate names before either the DOM
|
|
11
|
+
# renderer or the SSR serializer sees them so an attacker-controlled key
|
|
12
|
+
# cannot become HTML syntax.
|
|
13
|
+
SCRIPTING_ELEMENTS = %w[script]
|
|
14
|
+
|
|
15
|
+
def self.valid_tag_name?(name)
|
|
16
|
+
str = name.to_s
|
|
17
|
+
return false if str.empty?
|
|
18
|
+
|
|
19
|
+
index = 0
|
|
20
|
+
str.each_byte do |byte|
|
|
21
|
+
alpha = (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122)
|
|
22
|
+
digit = byte >= 48 && byte <= 57
|
|
23
|
+
return false unless alpha || (index > 0 && (digit || byte == 45))
|
|
24
|
+
index += 1
|
|
25
|
+
end
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.valid_attribute_name?(name)
|
|
30
|
+
str = name.to_s
|
|
31
|
+
return false if str.empty?
|
|
32
|
+
|
|
33
|
+
str.each_byte do |byte|
|
|
34
|
+
alpha = (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122)
|
|
35
|
+
digit = byte >= 48 && byte <= 57
|
|
36
|
+
punctuation = byte == 45 || byte == 46 || byte == 58 || byte == 95
|
|
37
|
+
return false unless alpha || digit || punctuation
|
|
38
|
+
end
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.event_attribute?(name)
|
|
43
|
+
name.to_s.downcase.start_with?('on')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.unsafe_url?(name, value)
|
|
47
|
+
return false unless URL_ATTRIBUTES.include?(name.to_s.downcase)
|
|
48
|
+
|
|
49
|
+
# Browsers discard ASCII tabs and newlines while parsing URLs, so test
|
|
50
|
+
# the same canonical form rather than only a literal "javascript:".
|
|
51
|
+
raw = value.to_s
|
|
52
|
+
start = 0
|
|
53
|
+
start += 1 while (byte = raw.getbyte(start)) && byte <= 32
|
|
54
|
+
|
|
55
|
+
normalized = (raw[start..-1] || "")
|
|
56
|
+
.gsub("\t", "")
|
|
57
|
+
.gsub("\n", "")
|
|
58
|
+
.gsub("\r", "")
|
|
59
|
+
.gsub("\f", "")
|
|
60
|
+
.strip
|
|
61
|
+
.downcase
|
|
62
|
+
normalized.start_with?('javascript:') || normalized.start_with?('vbscript:')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.blocked_attribute?(name, value)
|
|
66
|
+
normalized_name = name.to_s.downcase
|
|
67
|
+
event_attribute?(normalized_name) ||
|
|
68
|
+
normalized_name == 'srcdoc' ||
|
|
69
|
+
unsafe_url?(normalized_name, value)
|
|
70
|
+
end
|
|
71
|
+
|
|
9
72
|
class VNode
|
|
10
73
|
attr_reader :type, :key
|
|
11
74
|
|
|
@@ -20,8 +83,20 @@ module Funicular
|
|
|
20
83
|
def initialize(tag, props = {}, children = [])
|
|
21
84
|
super(:element)
|
|
22
85
|
@tag = tag.to_s
|
|
86
|
+
unless VDOM.valid_tag_name?(@tag)
|
|
87
|
+
raise ArgumentError, "Invalid VDOM tag name: #{@tag.inspect}"
|
|
88
|
+
end
|
|
89
|
+
if SCRIPTING_ELEMENTS.include?(@tag.downcase)
|
|
90
|
+
raise ArgumentError, "Unsafe VDOM tag: #{@tag.inspect}"
|
|
91
|
+
end
|
|
92
|
+
|
|
23
93
|
@key = props.delete(:key)
|
|
24
94
|
@props = props || {}
|
|
95
|
+
@props.each_key do |name|
|
|
96
|
+
unless VDOM.valid_attribute_name?(name)
|
|
97
|
+
raise ArgumentError, "Invalid VDOM attribute name: #{name.inspect}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
25
100
|
@children = normalize_children(children || [])
|
|
26
101
|
end
|
|
27
102
|
|
|
@@ -71,26 +146,29 @@ module Funicular
|
|
|
71
146
|
end
|
|
72
147
|
|
|
73
148
|
class Component < VNode
|
|
74
|
-
attr_reader :component_class, :props
|
|
75
|
-
attr_accessor :instance
|
|
149
|
+
attr_reader :component_class, :props, :children
|
|
150
|
+
attr_accessor :instance, :runtime
|
|
76
151
|
|
|
77
|
-
def initialize(component_class, props = {})
|
|
152
|
+
def initialize(component_class, props = {}, children = [])
|
|
78
153
|
super(:component)
|
|
79
154
|
@component_class = component_class
|
|
80
155
|
@key = props.delete(:key)
|
|
81
156
|
@props = props
|
|
157
|
+
@children = children || []
|
|
82
158
|
@instance = nil
|
|
159
|
+
@runtime = nil
|
|
83
160
|
end
|
|
84
161
|
|
|
85
162
|
def ==(other)
|
|
86
163
|
return false unless other.is_a?(Component)
|
|
87
|
-
@component_class == other.component_class && @props == other.props
|
|
164
|
+
@component_class == other.component_class && @props == other.props && @children == other.children
|
|
88
165
|
end
|
|
89
166
|
end
|
|
90
167
|
|
|
91
168
|
class Renderer
|
|
92
|
-
def initialize(doc = nil)
|
|
169
|
+
def initialize(doc = nil, runtime = nil)
|
|
93
170
|
@doc = doc || JS.document
|
|
171
|
+
@runtime = runtime
|
|
94
172
|
@error_boundary_stack = []
|
|
95
173
|
end
|
|
96
174
|
|
|
@@ -122,13 +200,14 @@ module Funicular
|
|
|
122
200
|
|
|
123
201
|
element.props.each do |key, value|
|
|
124
202
|
key_str = key.to_s
|
|
125
|
-
|
|
203
|
+
normalized_key = key_str.downcase
|
|
204
|
+
if VDOM.event_attribute?(normalized_key)
|
|
126
205
|
# Event handlers are handled by Funicular::Component and should not be set as attributes.
|
|
127
206
|
# warn "Funicular: Attempted to set event handler '#{key_str}' as an attribute. This will be ignored."
|
|
128
|
-
elsif
|
|
129
|
-
# Prevent
|
|
207
|
+
elsif VDOM.blocked_attribute?(normalized_key, value)
|
|
208
|
+
# Prevent active HTML content and unsafe URL schemes.
|
|
130
209
|
puts "[WARN] Funicular: Blocked potentially malicious value for attribute '#{key_str}'."
|
|
131
|
-
elsif BOOLEAN_ATTRIBUTES.include?(
|
|
210
|
+
elsif BOOLEAN_ATTRIBUTES.include?(normalized_key)
|
|
132
211
|
# Handle boolean attributes
|
|
133
212
|
if value.nil? || value.to_s == "false"
|
|
134
213
|
# Do not set attribute (leave it absent)
|
|
@@ -176,6 +255,8 @@ module Funicular
|
|
|
176
255
|
|
|
177
256
|
def render_component(component_vnode, parent)
|
|
178
257
|
instance = component_vnode.component_class.new(component_vnode.props)
|
|
258
|
+
instance.runtime = component_vnode.runtime || @runtime || Funicular::Runtime.new
|
|
259
|
+
instance.children = component_vnode.children
|
|
179
260
|
component_vnode.instance = instance
|
|
180
261
|
|
|
181
262
|
is_error_boundary = instance.is_a?(Funicular::ErrorBoundary)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
module Funicular
|
|
2
|
+
class ViewContext
|
|
3
|
+
HTML_TAGS = %w[
|
|
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
|
+
]
|
|
13
|
+
|
|
14
|
+
def initialize(component)
|
|
15
|
+
@component = component
|
|
16
|
+
end
|
|
17
|
+
|
|
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
|
+
def tag(name, props = {}, &block)
|
|
39
|
+
build_element(name, props, &block)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
HTML_TAGS.each do |tag_name|
|
|
43
|
+
define_method(tag_name) do |props = {}, &block|
|
|
44
|
+
tag(tag_name, props, &block) # steep:ignore
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def component(component_class, props = {}, &block)
|
|
49
|
+
unless component_class.is_a?(Class) && component_class.ancestors.include?(Funicular::Component)
|
|
50
|
+
raise "h.component expects a Funicular::Component class"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
children = if block
|
|
54
|
+
capture(&block)
|
|
55
|
+
else
|
|
56
|
+
[] #: Array[Funicular::VDOM::child_t]
|
|
57
|
+
end
|
|
58
|
+
vnode = VDOM::Component.new(component_class, props, children)
|
|
59
|
+
vnode.runtime = @component.runtime
|
|
60
|
+
add_child(vnode)
|
|
61
|
+
vnode
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def form_for(model_key, options = {}, &block)
|
|
65
|
+
@component.build_form_for(self, model_key, options, &block)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def suspense(name, fallback:, error: nil, &block)
|
|
69
|
+
@component.render_suspense(self, name, fallback: fallback, error: error, &block)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def link_to(path, **options, &block)
|
|
73
|
+
@component.build_link_to(self, path, **options, &block)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def button_to(path, method: :post, **options, &block)
|
|
77
|
+
@component.build_button_to(self, path, method: method, **options, &block)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def capture(&block)
|
|
81
|
+
children = [] #: Array[Funicular::VDOM::child_t]
|
|
82
|
+
previous = @component.current_children
|
|
83
|
+
@component.current_children = children
|
|
84
|
+
result = block.call(self)
|
|
85
|
+
@component.current_children = previous
|
|
86
|
+
|
|
87
|
+
if result && !result.equal?(children) && children.empty?
|
|
88
|
+
if result.is_a?(Array)
|
|
89
|
+
result.each do |item|
|
|
90
|
+
if item.is_a?(String)
|
|
91
|
+
children << item
|
|
92
|
+
else
|
|
93
|
+
normalized = @component.normalize_vnode_for_view(item)
|
|
94
|
+
children << normalized if normalized
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
elsif result.is_a?(String)
|
|
98
|
+
children << result
|
|
99
|
+
else
|
|
100
|
+
normalized = @component.normalize_vnode_for_view(result)
|
|
101
|
+
children << normalized if normalized
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
children
|
|
105
|
+
ensure
|
|
106
|
+
@component.current_children = previous
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def add_child(child)
|
|
110
|
+
@component.add_child_from_view(child)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def build_element(tag_name, props = {}, &block)
|
|
116
|
+
children = if block
|
|
117
|
+
capture(&block)
|
|
118
|
+
else
|
|
119
|
+
[] #: Array[Funicular::VDOM::child_t]
|
|
120
|
+
end
|
|
121
|
+
normalized_props = normalize_props(props || {})
|
|
122
|
+
element = VDOM::Element.new(tag_name.to_s, normalized_props, children)
|
|
123
|
+
add_child(element)
|
|
124
|
+
element
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def normalize_props(props)
|
|
128
|
+
normalized = {} #: Hash[Symbol, untyped]
|
|
129
|
+
props.each do |key, value|
|
|
130
|
+
normalized[key] = key == :class && value.is_a?(StyleValue) ? value.to_s : value
|
|
131
|
+
end
|
|
132
|
+
normalized
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
data/sig/component.rbs
CHANGED
|
@@ -3,65 +3,66 @@ module Funicular
|
|
|
3
3
|
class StateAccessor
|
|
4
4
|
def initialize: (Hash[Symbol, untyped] state_hash) -> void
|
|
5
5
|
def []: (Symbol key) -> untyped
|
|
6
|
-
def
|
|
7
|
-
def
|
|
8
|
-
def
|
|
6
|
+
def fetch: (Symbol key, ?untyped default) -> untyped
|
|
7
|
+
def key?: (Symbol key) -> bool
|
|
8
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
class ResourceAccessor
|
|
12
|
+
def initialize: (Hash[Symbol, untyped] data, Hash[Symbol, Symbol] states, Hash[Symbol, untyped] errors) -> void
|
|
13
|
+
def []: (Symbol key) -> untyped
|
|
14
|
+
def fetch: (Symbol key, ?untyped default) -> untyped
|
|
15
|
+
def loading?: (Symbol key) -> bool
|
|
16
|
+
def error?: (Symbol key) -> bool
|
|
17
|
+
def error: (Symbol key) -> untyped
|
|
18
|
+
end
|
|
12
19
|
|
|
13
20
|
attr_accessor props: Hash[Symbol, untyped]
|
|
14
21
|
attr_accessor vdom: VDOM::VNode | VDOM::Text | nil
|
|
15
22
|
attr_accessor dom_element: JS::Element
|
|
16
23
|
attr_accessor mounted: bool
|
|
24
|
+
attr_accessor runtime: Runtime
|
|
25
|
+
attr_accessor children: Array[VDOM::child_t]
|
|
26
|
+
attr_accessor current_children: Array[VDOM::child_t]?
|
|
17
27
|
attr_reader refs: Hash[Symbol, JS::Element]
|
|
18
|
-
@event_listeners: Array[Integer]
|
|
19
|
-
@child_components: Array[Component]
|
|
20
|
-
@suspense_data: Hash[Symbol, untyped]
|
|
21
|
-
@suspense_states: Hash[Symbol, Symbol]
|
|
22
|
-
@suspense_errors: Hash[Symbol, untyped]
|
|
23
|
-
@suspense_pending_timers: Array[Integer]
|
|
24
|
-
self.@suspense_definitions: Hash[Symbol, suspense_definition]
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
type suspense_definition = { loader: untyped, on_resolve: untyped, min_delay: Integer? }
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
def initialize: (?Hash[Symbol, untyped] props) -> void
|
|
29
32
|
def self.styles: () { (StyleBuilder) -> void } -> void
|
|
30
33
|
def self.styles_definitions: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
31
|
-
|
|
32
|
-
# Suspense DSL class methods
|
|
33
|
-
# Note: Using untyped for Proc types to avoid instance_exec block type mismatch warnings
|
|
34
|
-
type suspense_definition = { loader: untyped, on_resolve: untyped, min_delay: Integer? }
|
|
35
34
|
def self.use_suspense: (Symbol name, untyped loader, ?on_resolve: untyped, ?min_delay: Integer) -> void
|
|
36
35
|
def self.suspense_definitions: () -> Hash[Symbol, suspense_definition]
|
|
37
36
|
|
|
38
37
|
def state: () -> StateAccessor
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def s: () -> StyleAccessor
|
|
42
|
-
|
|
43
|
-
# Public API
|
|
38
|
+
def resources: () -> ResourceAccessor
|
|
39
|
+
def styles: () -> StyleAccessor
|
|
44
40
|
def initialize_state: () -> Hash[Symbol, untyped]
|
|
41
|
+
def seed_state: (Hash[untyped, untyped]? state_hash) -> self
|
|
45
42
|
|
|
46
|
-
# Suspense instance methods
|
|
47
43
|
def load_suspense_data: () -> void
|
|
48
44
|
def load_single_suspense: (Symbol name, ?suspense_definition? definition) -> void
|
|
49
45
|
def reload_suspense: (Symbol name) -> void
|
|
50
46
|
def suspense_loading?: (*Symbol names) -> bool
|
|
51
47
|
def suspense_error?: (Symbol name) -> bool
|
|
52
48
|
def suspense_error: (Symbol name) -> untyped
|
|
53
|
-
def
|
|
49
|
+
def render_suspense: (ViewContext h, Symbol name, fallback: untyped, ?error: untyped) { (ViewContext, ResourceAccessor) -> untyped } -> untyped
|
|
54
50
|
|
|
55
51
|
def patch: (Hash[Symbol, untyped] new_state) -> void
|
|
56
52
|
def mount: (JS::Element container) -> void
|
|
57
53
|
def hydrate: (JS::Element dom_element) -> void
|
|
58
|
-
def seed_state: (Hash[untyped, untyped]? state_hash) -> self
|
|
59
54
|
def unmount: () -> void
|
|
60
|
-
def render: () -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
|
|
61
|
-
def bind_events: (JS::Element dom_element, VDOM::VNode | VDOM::Text | nil vnode) -> void
|
|
55
|
+
def render: (ViewContext h) -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
|
|
62
56
|
def build_vdom: () -> (VDOM::VNode | VDOM::Text | nil)
|
|
63
57
|
|
|
64
|
-
|
|
58
|
+
def bind_events: (JS::Element dom_element, VDOM::VNode | VDOM::Text | nil vnode) -> void
|
|
59
|
+
def collect_refs: (JS::Element dom_element, VDOM::VNode | VDOM::Text | nil vnode, ?Hash[Symbol, JS::Element] refs_map) -> Hash[Symbol, JS::Element]
|
|
60
|
+
def normalize_vnode_for_view: (untyped value) -> (VDOM::Element | VDOM::Text | VDOM::Component | nil)
|
|
61
|
+
def add_child_from_view: (untyped child) -> void
|
|
62
|
+
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) { (ViewContext) -> untyped } -> VDOM::Element
|
|
64
|
+
def build_button_to: (ViewContext h, String path, ?method: Symbol, **untyped options) { (ViewContext) -> untyped } -> VDOM::Element
|
|
65
|
+
|
|
65
66
|
def component_will_mount: () -> void
|
|
66
67
|
def component_mounted: () -> void
|
|
67
68
|
def component_will_update: () -> void
|
|
@@ -70,22 +71,9 @@ module Funicular
|
|
|
70
71
|
def component_unmounted: () -> void
|
|
71
72
|
def component_raised: (Exception e) -> void
|
|
72
73
|
|
|
73
|
-
# Child component helper
|
|
74
|
-
def component: (Class component_class, ?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Component
|
|
75
|
-
|
|
76
|
-
# Rails-style form helper
|
|
77
|
-
def form_for: (Symbol model_key, ?Hash[Symbol, untyped] options) { (FormBuilder) -> void } -> VDOM::Element
|
|
78
|
-
|
|
79
|
-
# Rails-style routing helpers
|
|
80
|
-
def link_to: (String path, ?method: Symbol, ?navigate: bool, **untyped options) { -> untyped } -> VDOM::Element
|
|
81
|
-
def method_missing: (Symbol method, *untyped args) -> untyped
|
|
82
|
-
def respond_to_missing?: (Symbol method, ?bool include_private) -> bool
|
|
83
|
-
|
|
84
|
-
# Transition helpers
|
|
85
74
|
def remove_via: (String element_id, String from, String to, ?duration: Integer) ?{ () -> void } -> void
|
|
86
75
|
def add_via: (String element_id, String from, String to, ?duration: Integer) ?{ () -> void } -> void
|
|
87
76
|
|
|
88
|
-
# Private methods
|
|
89
77
|
private def handle_link_click: (String path) -> void
|
|
90
78
|
private def handle_link_with_method: (String path, Symbol method) -> void
|
|
91
79
|
private def handle_link_response: (HTTP::Response response, String path, Symbol method) -> void
|
|
@@ -93,57 +81,18 @@ module Funicular
|
|
|
93
81
|
private def re_render: () -> void
|
|
94
82
|
private def normalize_vnode: (untyped value) -> (VDOM::Element | VDOM::Text | VDOM::Component | nil)
|
|
95
83
|
private def add_data_component_attribute: (VDOM::VNode vnode) -> void
|
|
96
|
-
private def collect_refs: (JS::Element dom_element, VDOM::VNode | VDOM::Text | nil vnode, ?Hash[Symbol, JS::Element] refs_map) -> Hash[Symbol, JS::Element]
|
|
97
84
|
private def cleanup_events: () -> void
|
|
98
85
|
private def cleanup_suspense_timers: () -> void
|
|
99
|
-
private def add_child: (untyped child) -> void
|
|
100
86
|
private def collect_child_components: (VDOM::VNode | VDOM::Text | nil vnode) -> void
|
|
101
87
|
private def collect_child_components_recursive: (VDOM::VNode | VDOM::Text | nil vnode, Array[Component] components) -> void
|
|
102
|
-
|
|
103
|
-
|
|
88
|
+
private def collect_form_data: (untyped event, Symbol model_key) -> Hash[Symbol, untyped]
|
|
89
|
+
private def collect_dom_form_data: (untyped event) -> Hash[Symbol, untyped]
|
|
90
|
+
private def collect_state_form_data: (Symbol model_key) -> Hash[Symbol, untyped]
|
|
91
|
+
private def event_target: (untyped event) -> untyped
|
|
92
|
+
private def add_form_field_value: (Hash[Symbol, untyped] data, untyped field) -> void
|
|
104
93
|
private def hydration_match?: (untyped vnode, untyped dom_element) -> bool
|
|
105
94
|
private def warn_hydration_mismatch: (untyped vnode, untyped dom_element) -> void
|
|
106
95
|
private def full_render_fallback: (VDOM::VNode | VDOM::Text | nil new_vdom, untyped server_dom) -> JS::Element
|
|
107
96
|
private def hydrate_child_components: (untyped vnode, untyped dom_element) -> void
|
|
108
|
-
|
|
109
|
-
# HTML element methods (DSL)
|
|
110
|
-
def div: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
111
|
-
def span: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
112
|
-
def p: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
113
|
-
def a: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
114
|
-
def h1: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
115
|
-
def h2: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
116
|
-
def h3: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
117
|
-
def h4: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
118
|
-
def h5: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
119
|
-
def h6: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
120
|
-
def ul: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
121
|
-
def ol: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
122
|
-
def li: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
123
|
-
def table: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
124
|
-
def thead: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
125
|
-
def tbody: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
126
|
-
def tr: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
127
|
-
def th: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
128
|
-
def td: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
129
|
-
def form: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
130
|
-
def input: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
131
|
-
def textarea: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
132
|
-
def button: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
133
|
-
def select: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
134
|
-
def option: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
135
|
-
def label: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
136
|
-
def header: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
137
|
-
def footer: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
138
|
-
def nav: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
139
|
-
def section: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
140
|
-
def article: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
141
|
-
def aside: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
142
|
-
def img: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
143
|
-
def video: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
144
|
-
def audio: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
145
|
-
def canvas: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
146
|
-
def br: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
147
|
-
def hr: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
148
97
|
end
|
|
149
98
|
end
|
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: () -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
|
|
8
|
+
def render: (ViewContext h) -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
|
|
9
9
|
|
|
10
|
-
private def render_fallback: () -> (VDOM::VNode | String)
|
|
11
|
-
private def default_fallback: () -> VDOM::VNode
|
|
12
|
-
private def render_children: () -> VDOM::VNode
|
|
10
|
+
private def render_fallback: (ViewContext h) -> (VDOM::VNode | String)
|
|
11
|
+
private def default_fallback: (ViewContext h) -> VDOM::VNode
|
|
12
|
+
private def render_children: (ViewContext h) -> VDOM::VNode
|
|
13
13
|
end
|
|
14
14
|
end
|
data/sig/form_builder.rbs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
module Funicular
|
|
2
2
|
class FormBuilder
|
|
3
3
|
attr_reader component: Component
|
|
4
|
+
attr_reader view_context: ViewContext
|
|
4
5
|
attr_reader model_key: Symbol
|
|
5
6
|
attr_reader options: Hash[Symbol, untyped]
|
|
6
7
|
|
|
7
|
-
def initialize: (Component component, Symbol model_key, ?Hash[Symbol, untyped] options) -> void
|
|
8
|
+
def initialize: (Component component, ViewContext view_context, Symbol model_key, ?Hash[Symbol, untyped] options) -> void
|
|
8
9
|
|
|
9
10
|
# Field builder methods
|
|
10
11
|
def text_field: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
|
data/sig/html_serializer.rbs
CHANGED
|
@@ -4,7 +4,8 @@ module Funicular
|
|
|
4
4
|
VOID_ELEMENTS: Array[String]
|
|
5
5
|
SKIP_PROPS: Array[Symbol]
|
|
6
6
|
|
|
7
|
-
def self.serialize: (VNode? vnode) -> String
|
|
7
|
+
def self.serialize: (VNode? vnode, ?Runtime? runtime) -> String
|
|
8
|
+
def initialize: (?Runtime? runtime) -> void
|
|
8
9
|
def render: (VNode? vnode) -> String
|
|
9
10
|
|
|
10
11
|
private
|
data/sig/http.rbs
CHANGED
|
@@ -32,6 +32,7 @@ module Funicular
|
|
|
32
32
|
private def self.now_seconds: () -> Integer
|
|
33
33
|
private def self.cache_hit?: (untyped entry, Integer ttl) -> bool
|
|
34
34
|
private def self.serve_from_cache: (Hash[String, untyped] entry) { (Response) -> void } -> void
|
|
35
|
+
private def self.parse_response_body: (untyped text) -> untyped
|
|
35
36
|
private def self.request: (String method, String url, Hash[untyped, untyped]? body, ?cache: Integer?) { (Response) -> void } -> void
|
|
36
37
|
end
|
|
37
38
|
end
|
data/sig/patcher.rbs
CHANGED
|
@@ -3,7 +3,7 @@ module Funicular
|
|
|
3
3
|
BOOLEAN_ATTRIBUTES: Array[String]
|
|
4
4
|
|
|
5
5
|
class Patcher
|
|
6
|
-
def initialize: (?JS::Element? doc) -> void
|
|
6
|
+
def initialize: (?JS::Element? doc, ?Runtime? runtime) -> void
|
|
7
7
|
# `apply` accepts any DOM node so text-node patches (e.g. :replace)
|
|
8
8
|
# can recurse into it; Element-only operations are narrowed inside.
|
|
9
9
|
def apply: (JS::Object element, Array[patch_t] patches) -> JS::Object
|
data/sig/router.rbs
CHANGED
|
@@ -2,22 +2,12 @@ module Funicular
|
|
|
2
2
|
type route_constraints_t = Hash[Symbol, Regexp]
|
|
3
3
|
type route_definition_t = { method: Symbol, path: String, component: singleton(Component), name: String?, pattern_segments: Array[String], constraints: route_constraints_t }
|
|
4
4
|
|
|
5
|
-
# URL helper methods are dynamically generated based on route definitions
|
|
6
|
-
# Example: router.get('/users/:id', to: UserComponent, as: 'user')
|
|
7
|
-
# generates: user_path(id) -> String
|
|
8
|
-
module RouteHelpers
|
|
9
|
-
# Dynamic methods are generated at runtime by Router#generate_url_helper
|
|
10
|
-
# Method signatures depend on route parameters:
|
|
11
|
-
# - No parameters: def helper_name_path: () -> String
|
|
12
|
-
# - With parameters: def helper_name_path: (Integer | String | untyped) -> String
|
|
13
|
-
def method_missing: (Symbol method, *untyped args) -> String
|
|
14
|
-
def respond_to_missing?: (Symbol method, ?bool include_private) -> bool
|
|
15
|
-
end
|
|
16
|
-
|
|
17
5
|
class Router
|
|
18
6
|
attr_reader routes: Array[route_definition_t]
|
|
19
7
|
attr_reader current_component: Component?
|
|
20
8
|
attr_reader current_path: String?
|
|
9
|
+
attr_reader url_helpers: Module
|
|
10
|
+
attr_reader route_helpers: untyped
|
|
21
11
|
|
|
22
12
|
def initialize: (JS::Element? container) -> void
|
|
23
13
|
def get: (String path, to: singleton(Component), ?as: String?, ?constraints: route_constraints_t?) -> void
|
|
@@ -31,7 +21,6 @@ module Funicular
|
|
|
31
21
|
def start: (?hydrate: bool) -> void
|
|
32
22
|
def stop: () -> void
|
|
33
23
|
def navigate: (String path) -> void
|
|
34
|
-
def current_hash_path: () -> String
|
|
35
24
|
def current_location_path: () -> String
|
|
36
25
|
|
|
37
26
|
private def add_route_with_method: (Symbol method, String path, singleton(Component) component_class, String? name, ?route_constraints_t? constraints) -> void
|
data/sig/runtime.rbs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Funicular
|
|
2
|
+
class Runtime
|
|
3
|
+
attr_accessor router: Router?
|
|
4
|
+
|
|
5
|
+
def initialize: (?Router? router) -> void
|
|
6
|
+
def routes: () -> untyped
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module EmptyRoutes
|
|
10
|
+
def self.method_missing: (Symbol name, *untyped args) -> untyped
|
|
11
|
+
def self.respond_to_missing?: (Symbol name, ?bool include_private) -> bool
|
|
12
|
+
end
|
|
13
|
+
end
|
data/sig/styles.rbs
CHANGED
|
@@ -7,11 +7,10 @@ module Funicular
|
|
|
7
7
|
def to_s: () -> String
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
# StyleAccessor provides access to defined styles
|
|
10
|
+
# StyleAccessor provides explicit access to defined styles.
|
|
11
11
|
class StyleAccessor
|
|
12
12
|
def initialize: (Hash[Symbol, Hash[Symbol, untyped]] definitions) -> void
|
|
13
|
-
|
|
14
|
-
private def respond_to_missing?: (Symbol name, ?bool include_private) -> bool
|
|
13
|
+
def []: (Symbol name, ?(Symbol | bool) variant) -> StyleValue
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
# StyleBuilder is used to define styles within the styles {} block
|
|
@@ -19,7 +18,7 @@ module Funicular
|
|
|
19
18
|
@definitions: Hash[Symbol, Hash[Symbol, untyped]]
|
|
20
19
|
|
|
21
20
|
def initialize: () -> void
|
|
21
|
+
def define: (Symbol name, ?(String | Hash[Symbol, untyped]) value, **untyped options) -> void
|
|
22
22
|
def to_definitions: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
23
|
-
private def method_missing: (Symbol name, *untyped args) -> void
|
|
24
23
|
end
|
|
25
24
|
end
|
data/sig/vdom.rbs
CHANGED
|
@@ -4,6 +4,13 @@ module Funicular
|
|
|
4
4
|
type child_t = Element | Text | Component | String | Array[Element | Text | Component | String]
|
|
5
5
|
|
|
6
6
|
URL_ATTRIBUTES: Array[String]
|
|
7
|
+
SCRIPTING_ELEMENTS: Array[String]
|
|
8
|
+
|
|
9
|
+
def self.valid_tag_name?: (untyped name) -> bool
|
|
10
|
+
def self.valid_attribute_name?: (untyped name) -> bool
|
|
11
|
+
def self.event_attribute?: (untyped name) -> bool
|
|
12
|
+
def self.unsafe_url?: (untyped name, untyped value) -> bool
|
|
13
|
+
def self.blocked_attribute?: (untyped name, untyped value) -> bool
|
|
7
14
|
|
|
8
15
|
class VNode
|
|
9
16
|
attr_reader type: Symbol
|
|
@@ -32,7 +39,7 @@ module Funicular
|
|
|
32
39
|
class Renderer
|
|
33
40
|
@error_boundary_stack: Array[Funicular::ErrorBoundary]
|
|
34
41
|
|
|
35
|
-
def initialize: (?JS::Element? doc) -> void
|
|
42
|
+
def initialize: (?JS::Element? doc, ?Runtime? runtime) -> void
|
|
36
43
|
def render: (VDOM::VNode | VDOM::Text | nil vnode, ?JS::Element? parent) -> JS::Element
|
|
37
44
|
|
|
38
45
|
private def current_error_boundary: () -> ErrorBoundary?
|
|
@@ -50,9 +57,11 @@ module Funicular
|
|
|
50
57
|
class Component < VNode
|
|
51
58
|
attr_reader component_class: untyped
|
|
52
59
|
attr_reader props: Hash[Symbol, untyped]
|
|
60
|
+
attr_reader children: Array[child_t]
|
|
53
61
|
attr_accessor instance: untyped
|
|
62
|
+
attr_accessor runtime: Runtime?
|
|
54
63
|
|
|
55
|
-
def initialize: (untyped component_class, ?Hash[Symbol, untyped] props) -> void
|
|
64
|
+
def initialize: (untyped component_class, ?Hash[Symbol, untyped] props, ?Array[child_t] children) -> void
|
|
56
65
|
def ==: (untyped other) -> bool
|
|
57
66
|
end
|
|
58
67
|
end
|