funicular 0.5.1 → 0.5.2
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 +33 -0
- data/lib/funicular/testing/node_runner.mjs +5 -1
- data/lib/funicular/vendor/mrbc/VERSION +1 -1
- 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 +8 -8
- data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
- 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 +1 -1
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
- data/minitest/ssr_test.rb +16 -0
- data/mrblib/component.rb +41 -6
- data/mrblib/differ.rb +5 -7
- data/mrblib/html_serializer.rb +28 -15
- data/mrblib/patcher.rb +5 -11
- data/mrblib/router.rb +87 -7
- data/mrblib/vdom.rb +12 -2
- data/mrblib/version.rb +1 -1
- data/sig/component.rbs +2 -0
- data/sig/html_serializer.rbs +3 -1
- data/sig/patcher.rbs +0 -1
- data/sig/router.rbs +8 -2
- data/sig/vdom.rbs +2 -0
- metadata +3 -3
|
Binary file
|
data/minitest/ssr_test.rb
CHANGED
|
@@ -32,6 +32,22 @@ class SSRTest < Minitest::Test
|
|
|
32
32
|
serialize(el("p", {}, ["a & b <c>"]))
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def test_marks_empty_slots_and_seams_between_texts
|
|
36
|
+
assert_equal "<div><!----><b></b>a<!---->b<i></i>c</div>",
|
|
37
|
+
serialize(el("div", {}, [nil, el("b"), "a", "b", el("i"), "c"]))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_raw_text_elements_are_not_marked
|
|
41
|
+
assert_equal "<textarea>ab</textarea>", serialize(el("textarea", {}, [nil, "a", "b"]))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_doubles_the_leading_newline_the_parser_strips
|
|
45
|
+
assert_equal "<pre>\n\nfoo</pre>", serialize(el("pre", {}, ["\nfoo"]))
|
|
46
|
+
assert_equal "<textarea>\n\nfoo</textarea>", serialize(el("textarea", {}, ["\nfoo"]))
|
|
47
|
+
assert_equal "<pre>foo\n</pre>", serialize(el("pre", {}, ["foo\n"]))
|
|
48
|
+
assert_equal "<div>\nfoo</div>", serialize(el("div", {}, ["\nfoo"]))
|
|
49
|
+
end
|
|
50
|
+
|
|
35
51
|
def test_escapes_attribute_values
|
|
36
52
|
assert_equal '<div title=""hi""></div>',
|
|
37
53
|
serialize(el("div", { title: '"hi"' }))
|
data/mrblib/component.rb
CHANGED
|
@@ -418,6 +418,14 @@ module Funicular
|
|
|
418
418
|
@suspense_definitions ||= {}
|
|
419
419
|
end
|
|
420
420
|
|
|
421
|
+
# True while patch is re-rendering this component. Callers that would
|
|
422
|
+
# re-enter patch from inside that render (for example a child page that
|
|
423
|
+
# navigates in component_mounted) must take another path, because patch
|
|
424
|
+
# ignores nested calls.
|
|
425
|
+
def updating?
|
|
426
|
+
@updating
|
|
427
|
+
end
|
|
428
|
+
|
|
421
429
|
# Update state and trigger re-render
|
|
422
430
|
def patch(new_state)
|
|
423
431
|
return unless @mounted
|
|
@@ -709,8 +717,8 @@ module Funicular
|
|
|
709
717
|
end
|
|
710
718
|
|
|
711
719
|
# Recursively bind events for children
|
|
712
|
-
if vnode.children
|
|
713
|
-
children = dom_element
|
|
720
|
+
if vnode.children
|
|
721
|
+
children = VDOM.child_nodes(dom_element)
|
|
714
722
|
vnode.children.each_with_index do |child_vnode, index|
|
|
715
723
|
if child_vnode.is_a?(VDOM::Element)
|
|
716
724
|
child_element = children[index]
|
|
@@ -748,8 +756,8 @@ module Funicular
|
|
|
748
756
|
refs_map[ref_name] = dom_element
|
|
749
757
|
end
|
|
750
758
|
|
|
751
|
-
if vnode.children
|
|
752
|
-
children = dom_element
|
|
759
|
+
if vnode.children
|
|
760
|
+
children = VDOM.child_nodes(dom_element)
|
|
753
761
|
vnode.children.each_with_index do |child_vnode, index|
|
|
754
762
|
if child_vnode.is_a?(VDOM::Element)
|
|
755
763
|
child_element = children[index]
|
|
@@ -810,6 +818,20 @@ module Funicular
|
|
|
810
818
|
__view__.component(component_class, props, &block)
|
|
811
819
|
end
|
|
812
820
|
|
|
821
|
+
def outlet
|
|
822
|
+
router = @runtime&.router
|
|
823
|
+
depth = router&.current_layouts&.index(self.class)
|
|
824
|
+
return unless router && depth
|
|
825
|
+
|
|
826
|
+
nested = router.current_layouts[depth + 1]
|
|
827
|
+
return component(nested, preserve: true, __route__: router.current_path) if nested
|
|
828
|
+
|
|
829
|
+
route = router.current_route
|
|
830
|
+
return unless route
|
|
831
|
+
|
|
832
|
+
component(route[0], route[1])
|
|
833
|
+
end
|
|
834
|
+
|
|
813
835
|
def form_for(model_key, options = {}, &block)
|
|
814
836
|
build_form_for(__view__, model_key, options, &block)
|
|
815
837
|
end
|
|
@@ -1024,9 +1046,22 @@ module Funicular
|
|
|
1024
1046
|
return unless vnode.is_a?(VDOM::Element)
|
|
1025
1047
|
return unless dom_element
|
|
1026
1048
|
|
|
1027
|
-
dom_children = dom_element
|
|
1049
|
+
dom_children = VDOM.child_nodes(dom_element)
|
|
1050
|
+
skipped = 0
|
|
1051
|
+
previous_text = false
|
|
1028
1052
|
vnode.children.each_with_index do |child, index|
|
|
1029
|
-
|
|
1053
|
+
text = VDOM.text(child)
|
|
1054
|
+
child_dom = dom_children[index + skipped]
|
|
1055
|
+
if text && child_dom && child_dom[:nodeType] == 8
|
|
1056
|
+
if text.empty?
|
|
1057
|
+
dom_element.replaceChild(dom_element[:ownerDocument].createTextNode(''), child_dom)
|
|
1058
|
+
elsif previous_text
|
|
1059
|
+
dom_element.removeChild(child_dom)
|
|
1060
|
+
skipped += 1
|
|
1061
|
+
child_dom = dom_children[index + skipped]
|
|
1062
|
+
end
|
|
1063
|
+
end
|
|
1064
|
+
previous_text = !text.to_s.empty?
|
|
1030
1065
|
next unless child_dom
|
|
1031
1066
|
|
|
1032
1067
|
if child.is_a?(VDOM::Component)
|
data/mrblib/differ.rb
CHANGED
|
@@ -29,6 +29,8 @@ module Funicular
|
|
|
29
29
|
else
|
|
30
30
|
if old_node == new_node
|
|
31
31
|
[]
|
|
32
|
+
elsif old_node.is_a?(String) && new_node.is_a?(String)
|
|
33
|
+
[[:text, new_node]]
|
|
32
34
|
else
|
|
33
35
|
[[:replace, new_node, old_node]]
|
|
34
36
|
end
|
|
@@ -38,11 +40,7 @@ module Funicular
|
|
|
38
40
|
private
|
|
39
41
|
|
|
40
42
|
def self.diff_text(old_node, new_node)
|
|
41
|
-
|
|
42
|
-
[[:replace, new_node, old_node]]
|
|
43
|
-
else
|
|
44
|
-
[]
|
|
45
|
-
end
|
|
43
|
+
old_node.content == new_node.content ? [] : [[:text, new_node.content]]
|
|
46
44
|
end
|
|
47
45
|
|
|
48
46
|
def self.diff_element(old_node, new_node)
|
|
@@ -158,10 +156,10 @@ module Funicular
|
|
|
158
156
|
# "Loading..." placeholder that never disappeared once the keyed
|
|
159
157
|
# list it was replaced by arrived. Raw String children are text
|
|
160
158
|
# nodes in the DOM (normalize_children keeps them as Strings), so
|
|
161
|
-
# they must be collected here as well
|
|
159
|
+
# they must be collected here as well. normalize_children never
|
|
160
|
+
# leaves nil in the list, so every entry is a real DOM node.
|
|
162
161
|
removes = [] #: Array[[Integer, child_t]]
|
|
163
162
|
old_children.each_with_index do |old_child, old_index|
|
|
164
|
-
next if old_child.nil?
|
|
165
163
|
next if matched_old_indices[old_index]
|
|
166
164
|
removes << [old_index, old_child]
|
|
167
165
|
has_change = true
|
data/mrblib/html_serializer.rb
CHANGED
|
@@ -18,6 +18,13 @@ module Funicular
|
|
|
18
18
|
# Props that must never be emitted as HTML attributes.
|
|
19
19
|
SKIP_PROPS = %i[ref key]
|
|
20
20
|
|
|
21
|
+
RAW_TEXT_ELEMENTS = %w[textarea title style]
|
|
22
|
+
|
|
23
|
+
# The HTML parser drops one newline right after the start tag of these
|
|
24
|
+
# elements. Emit an extra newline so the text node the client VDOM
|
|
25
|
+
# expects survives parsing with its content intact.
|
|
26
|
+
NEWLINE_STRIPPING_ELEMENTS = %w[pre textarea listing]
|
|
27
|
+
|
|
21
28
|
def self.serialize(vnode, runtime = nil)
|
|
22
29
|
new(runtime).render(vnode)
|
|
23
30
|
end
|
|
@@ -50,27 +57,33 @@ module Funicular
|
|
|
50
57
|
tag = element.tag
|
|
51
58
|
attrs = serialize_props(element.props)
|
|
52
59
|
|
|
53
|
-
|
|
60
|
+
lower = tag.downcase
|
|
61
|
+
if VOID_ELEMENTS.include?(lower)
|
|
54
62
|
"<#{tag}#{attrs}>"
|
|
55
63
|
else
|
|
56
|
-
|
|
64
|
+
inner = render_children(element.children, !RAW_TEXT_ELEMENTS.include?(lower))
|
|
65
|
+
inner = "\n#{inner}" if NEWLINE_STRIPPING_ELEMENTS.include?(lower) && inner.start_with?("\n")
|
|
66
|
+
"<#{tag}#{attrs}>#{inner}</#{tag}>"
|
|
57
67
|
end
|
|
58
68
|
end
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
# Element#children is flattened by normalize_children, so nested arrays
|
|
71
|
+
# need no handling here.
|
|
72
|
+
def render_children(children, marks)
|
|
73
|
+
previous_text = false
|
|
74
|
+
children.map do |child|
|
|
75
|
+
text = VDOM.text(child)
|
|
76
|
+
html = if text
|
|
77
|
+
escape_html(text)
|
|
78
|
+
elsif child.is_a?(VNode)
|
|
79
|
+
render(child)
|
|
80
|
+
else
|
|
81
|
+
''
|
|
71
82
|
end
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
html = "<!---->#{html}" if marks && text && (text.empty? || previous_text)
|
|
84
|
+
previous_text = !text.to_s.empty?
|
|
85
|
+
html
|
|
86
|
+
end.join
|
|
74
87
|
end
|
|
75
88
|
|
|
76
89
|
def render_text(text)
|
data/mrblib/patcher.rb
CHANGED
|
@@ -24,6 +24,8 @@ module Funicular
|
|
|
24
24
|
# Props patches only make sense for Element nodes (text nodes have
|
|
25
25
|
# no attributes); narrow before delegating.
|
|
26
26
|
update_props(element, patch[1]) if element.is_a?(JS::Element)
|
|
27
|
+
when :text
|
|
28
|
+
element[:nodeValue] = patch[1].to_s
|
|
27
29
|
when :update_and_rebind
|
|
28
30
|
instance, internal_patches, new_vdom = patch[1], patch[2], patch[3]
|
|
29
31
|
old_dom_element = instance.dom_element
|
|
@@ -85,7 +87,7 @@ module Funicular
|
|
|
85
87
|
ops = patch[1]
|
|
86
88
|
removes = patch[2]
|
|
87
89
|
|
|
88
|
-
snapshot =
|
|
90
|
+
snapshot = VDOM.child_nodes(element)
|
|
89
91
|
|
|
90
92
|
# Phase 1: removes (descending old_index)
|
|
91
93
|
sorted_removes = removes.sort { |a, b| b[0] <=> a[0] }
|
|
@@ -146,7 +148,7 @@ module Funicular
|
|
|
146
148
|
when Integer
|
|
147
149
|
child_index = patch[0]
|
|
148
150
|
child_patches = patch[1]
|
|
149
|
-
children =
|
|
151
|
+
children = VDOM.child_nodes(element)
|
|
150
152
|
child_element = children[child_index]
|
|
151
153
|
if child_element.nil?
|
|
152
154
|
# No existing child at this index - we need to create new elements
|
|
@@ -164,7 +166,7 @@ module Funicular
|
|
|
164
166
|
end
|
|
165
167
|
elsif child_element.is_a?(JS::Object)
|
|
166
168
|
# Recurse into the child node. apply handles both Element and
|
|
167
|
-
# text Node cases (the latter
|
|
169
|
+
# text Node cases (the latter via :text or :replace).
|
|
168
170
|
apply(child_element, child_patches)
|
|
169
171
|
end
|
|
170
172
|
end
|
|
@@ -174,14 +176,6 @@ module Funicular
|
|
|
174
176
|
|
|
175
177
|
private
|
|
176
178
|
|
|
177
|
-
# childNodes (not children) so that text nodes are included. Each call
|
|
178
|
-
# crosses the wasm boundary once per child, so callers should read it
|
|
179
|
-
# once and work on the returned array.
|
|
180
|
-
def child_nodes_array(element)
|
|
181
|
-
child_nodes = element[:childNodes]
|
|
182
|
-
child_nodes.is_a?(JS::Object) ? child_nodes.to_a : [] #: Array[untyped]
|
|
183
|
-
end
|
|
184
|
-
|
|
185
179
|
def unmount_component(vnode)
|
|
186
180
|
return unless vnode.is_a?(VDOM::Component) && vnode.instance
|
|
187
181
|
vnode.instance.unmount
|
data/mrblib/router.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Funicular
|
|
2
2
|
class Router
|
|
3
|
-
attr_reader :routes, :
|
|
3
|
+
attr_reader :routes, :current_path, :url_helpers, :route_helpers, :current_route, :current_layouts
|
|
4
4
|
|
|
5
5
|
def initialize(container)
|
|
6
6
|
@container = container
|
|
@@ -8,6 +8,10 @@ module Funicular
|
|
|
8
8
|
@default_route = nil
|
|
9
9
|
@current_component = nil
|
|
10
10
|
@current_path = nil
|
|
11
|
+
@current_route = nil
|
|
12
|
+
@current_layouts = []
|
|
13
|
+
@layout_stack = [] #: Array[singleton(Component)]
|
|
14
|
+
@layout_root = nil
|
|
11
15
|
@popstate_callback_id = nil
|
|
12
16
|
@beforeunload_callback_id = nil
|
|
13
17
|
@url_helpers = Module.new
|
|
@@ -47,6 +51,27 @@ module Funicular
|
|
|
47
51
|
@default_route = path
|
|
48
52
|
end
|
|
49
53
|
|
|
54
|
+
def layout(component_class)
|
|
55
|
+
@layout_stack.push(component_class)
|
|
56
|
+
yield
|
|
57
|
+
ensure
|
|
58
|
+
@layout_stack.pop
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def current_component
|
|
62
|
+
layout_root = @layout_root
|
|
63
|
+
route = @current_route
|
|
64
|
+
return @current_component unless layout_root && route
|
|
65
|
+
|
|
66
|
+
instance = layout_root #: Component?
|
|
67
|
+
chain = (@current_layouts[1..-1] || []) + [route[0]]
|
|
68
|
+
chain.each do |klass|
|
|
69
|
+
return nil unless instance
|
|
70
|
+
instance = find_component_instance(instance.vdom, klass)
|
|
71
|
+
end
|
|
72
|
+
instance
|
|
73
|
+
end
|
|
74
|
+
|
|
50
75
|
# Resolve a path to [component_class, params] without any DOM/JS work.
|
|
51
76
|
# Public entry point used by server-side rendering.
|
|
52
77
|
def match(path)
|
|
@@ -91,7 +116,7 @@ module Funicular
|
|
|
91
116
|
# a guard is active. sync: the decision must be made on the JS
|
|
92
117
|
# event dispatch stack, so the guard must not suspend.
|
|
93
118
|
@beforeunload_callback_id = JS.global.addEventListener('beforeunload', sync: true) do |event|
|
|
94
|
-
if
|
|
119
|
+
if current_component&.navigation_guard
|
|
95
120
|
event.preventDefault
|
|
96
121
|
event[:returnValue] = ''
|
|
97
122
|
end
|
|
@@ -136,7 +161,7 @@ module Funicular
|
|
|
136
161
|
# allowed; a String from the guard prompts the user via
|
|
137
162
|
# Funicular.confirm. True when no component or no guard.
|
|
138
163
|
def leave_allowed?
|
|
139
|
-
message =
|
|
164
|
+
message = current_component&.navigation_guard
|
|
140
165
|
return true unless message
|
|
141
166
|
Funicular.confirm(message)
|
|
142
167
|
end
|
|
@@ -160,16 +185,19 @@ module Funicular
|
|
|
160
185
|
@hydrate_initial = false
|
|
161
186
|
|
|
162
187
|
# Find matching route
|
|
163
|
-
|
|
188
|
+
route, params = match_route(path)
|
|
164
189
|
|
|
165
|
-
unless
|
|
190
|
+
unless route
|
|
166
191
|
# Maybe render a 404 component?
|
|
167
192
|
return
|
|
168
193
|
end
|
|
194
|
+
component_class = route[:component]
|
|
169
195
|
|
|
170
196
|
# Don't remount if already on this path
|
|
171
197
|
return if @current_path == path
|
|
172
198
|
|
|
199
|
+
return render_with_layouts(route, params, path, hydrate_now) unless route[:layouts].empty?
|
|
200
|
+
|
|
173
201
|
# Unmount current component
|
|
174
202
|
unmount_current_component
|
|
175
203
|
|
|
@@ -198,10 +226,56 @@ module Funicular
|
|
|
198
226
|
@current_component.mount(@container)
|
|
199
227
|
end
|
|
200
228
|
|
|
229
|
+
def render_with_layouts(route, params, path, hydrate_now)
|
|
230
|
+
if hydrate_now && Funicular.first_element_child(@container)
|
|
231
|
+
puts '[Funicular] Layouts do not support hydration; rendering fresh.'
|
|
232
|
+
@container[:innerHTML] = ''
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
layouts = route[:layouts]
|
|
236
|
+
layout_root = @layout_root
|
|
237
|
+
# Reuse the mounted chain only under the same outermost layout, and
|
|
238
|
+
# only when that root is not mid-render: Component#patch drops nested
|
|
239
|
+
# calls, so a page that navigates from component_mounted during a
|
|
240
|
+
# re-render would otherwise change the URL and leave the old page in
|
|
241
|
+
# the DOM. Remounting the chain matches what bare routes do.
|
|
242
|
+
reusable = layout_root && layout_root.mounted && !layout_root.updating? &&
|
|
243
|
+
@current_layouts.first == layouts.first
|
|
244
|
+
unmount_current_component unless reusable
|
|
245
|
+
|
|
246
|
+
@current_path = path
|
|
247
|
+
@current_route = [route[:component], params]
|
|
248
|
+
@current_layouts = layouts
|
|
249
|
+
|
|
250
|
+
if layout_root && reusable
|
|
251
|
+
layout_root.patch(__route__: path)
|
|
252
|
+
else
|
|
253
|
+
layout_root = layouts.first.new
|
|
254
|
+
layout_root.runtime = @runtime
|
|
255
|
+
@layout_root = layout_root
|
|
256
|
+
layout_root.mount(@container)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def find_component_instance(vnode, component_class)
|
|
261
|
+
return unless vnode.is_a?(VDOM::Element) || vnode.is_a?(VDOM::Component)
|
|
262
|
+
return vnode.instance if vnode.is_a?(VDOM::Component) && vnode.component_class == component_class
|
|
263
|
+
|
|
264
|
+
vnode.children.each do |child|
|
|
265
|
+
found = find_component_instance(child, component_class)
|
|
266
|
+
return found if found
|
|
267
|
+
end
|
|
268
|
+
nil
|
|
269
|
+
end
|
|
270
|
+
|
|
201
271
|
# Unmount current component
|
|
202
272
|
def unmount_current_component
|
|
203
273
|
@current_component&.unmount
|
|
204
274
|
@current_component = nil
|
|
275
|
+
@layout_root&.unmount
|
|
276
|
+
@layout_root = nil
|
|
277
|
+
@current_layouts = []
|
|
278
|
+
@current_route = nil
|
|
205
279
|
@current_path = nil
|
|
206
280
|
end
|
|
207
281
|
|
|
@@ -215,7 +289,8 @@ module Funicular
|
|
|
215
289
|
component: component_class,
|
|
216
290
|
name: name,
|
|
217
291
|
pattern_segments: pattern_segments,
|
|
218
|
-
constraints: constraints || {}
|
|
292
|
+
constraints: constraints || {},
|
|
293
|
+
layouts: @layout_stack.dup
|
|
219
294
|
}
|
|
220
295
|
# @type var route: Funicular::route_definition_t
|
|
221
296
|
@routes << route
|
|
@@ -273,6 +348,11 @@ module Funicular
|
|
|
273
348
|
end
|
|
274
349
|
|
|
275
350
|
def find_route(path)
|
|
351
|
+
route, params = match_route(path)
|
|
352
|
+
[route && route[:component], params]
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def match_route(path)
|
|
276
356
|
path_segments = path.split('/').reject { |s| s.empty? }
|
|
277
357
|
params = {} #: Hash[Symbol, untyped]
|
|
278
358
|
|
|
@@ -302,7 +382,7 @@ module Funicular
|
|
|
302
382
|
end
|
|
303
383
|
end
|
|
304
384
|
|
|
305
|
-
return [route
|
|
385
|
+
return [route, params] if match
|
|
306
386
|
end
|
|
307
387
|
|
|
308
388
|
[nil, params] # No route found
|
data/mrblib/vdom.rb
CHANGED
|
@@ -39,6 +39,16 @@ module Funicular
|
|
|
39
39
|
true
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
def self.text(child)
|
|
43
|
+
return child if child.is_a?(String)
|
|
44
|
+
child.content.to_s if child.is_a?(Text)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.child_nodes(element)
|
|
48
|
+
nodes = element[:childNodes]
|
|
49
|
+
nodes.is_a?(JS::Object) ? nodes.to_a : [] #: Array[untyped]
|
|
50
|
+
end
|
|
51
|
+
|
|
42
52
|
def self.event_attribute?(name)
|
|
43
53
|
name.to_s.downcase.start_with?('on')
|
|
44
54
|
end
|
|
@@ -120,8 +130,8 @@ module Funicular
|
|
|
120
130
|
# Recursively normalize nested arrays
|
|
121
131
|
# @type var child: Array[Funicular::VDOM::child_t]
|
|
122
132
|
result.concat(normalize_children(child))
|
|
123
|
-
when nil
|
|
124
|
-
|
|
133
|
+
when nil, false
|
|
134
|
+
result << ''
|
|
125
135
|
else
|
|
126
136
|
# Convert other types to strings
|
|
127
137
|
result << child.to_s
|
data/mrblib/version.rb
CHANGED
data/sig/component.rbs
CHANGED
|
@@ -61,6 +61,7 @@ module Funicular
|
|
|
61
61
|
def render_suspense: (Symbol name, fallback: untyped, ?error: untyped) { (ResourceAccessor) -> untyped } -> untyped
|
|
62
62
|
|
|
63
63
|
def navigation_guard: () -> String?
|
|
64
|
+
def updating?: () -> bool
|
|
64
65
|
def patch: (Hash[Symbol, untyped] new_state) -> void
|
|
65
66
|
def mount: (JS::Element container) -> void
|
|
66
67
|
def hydrate: (JS::Element dom_element) -> void
|
|
@@ -75,6 +76,7 @@ module Funicular
|
|
|
75
76
|
def __view__: () -> ViewContext
|
|
76
77
|
def routes: () -> untyped
|
|
77
78
|
def component: (singleton(Component) component_class, ?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Component
|
|
79
|
+
def outlet: () -> VDOM::Component?
|
|
78
80
|
def form_for: (Symbol model_key, ?Hash[Symbol, untyped] options) { (FormBuilder) -> void } -> VDOM::Element
|
|
79
81
|
def link_to: (String path, **untyped options) ?{ -> untyped } -> VDOM::Element
|
|
80
82
|
def button_to: (String path, ?method: Symbol, **untyped options) ?{ -> untyped } -> VDOM::Element
|
data/sig/html_serializer.rbs
CHANGED
|
@@ -3,6 +3,8 @@ module Funicular
|
|
|
3
3
|
class HTMLSerializer
|
|
4
4
|
VOID_ELEMENTS: Array[String]
|
|
5
5
|
SKIP_PROPS: Array[Symbol]
|
|
6
|
+
RAW_TEXT_ELEMENTS: Array[String]
|
|
7
|
+
NEWLINE_STRIPPING_ELEMENTS: Array[String]
|
|
6
8
|
|
|
7
9
|
def self.serialize: (VNode? vnode, ?Runtime? runtime) -> String
|
|
8
10
|
def initialize: (?Runtime? runtime) -> void
|
|
@@ -10,7 +12,7 @@ module Funicular
|
|
|
10
12
|
|
|
11
13
|
private
|
|
12
14
|
def render_element: (Element element) -> String
|
|
13
|
-
def render_children: (Array[child_t] children) -> String
|
|
15
|
+
def render_children: (Array[child_t] children, bool marks) -> String
|
|
14
16
|
def render_text: (Text text) -> String
|
|
15
17
|
def render_component: (Component component_vnode) -> String
|
|
16
18
|
def serialize_props: (Hash[Symbol, untyped] props) -> String
|
data/sig/patcher.rbs
CHANGED
|
@@ -9,7 +9,6 @@ module Funicular
|
|
|
9
9
|
def apply: (JS::Object element, Array[patch_t] patches) -> JS::Object
|
|
10
10
|
|
|
11
11
|
# Accepts JS::Object since callers narrow via is_a?(JS::Element) at runtime
|
|
12
|
-
private def child_nodes_array: (JS::Object element) -> Array[untyped]
|
|
13
12
|
private def update_props: (JS::Object element, Hash[Symbol, String?] props_patch) -> void
|
|
14
13
|
private def create_element: (untyped vnode) -> JS::Object
|
|
15
14
|
private def unmount_component: (VDOM::VNode vnode) -> void
|
data/sig/router.rbs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
module Funicular
|
|
2
2
|
type route_constraints_t = Hash[Symbol, Regexp]
|
|
3
|
-
type route_definition_t = { method: Symbol, path: String, component: singleton(Component), name: String?, pattern_segments: Array[String], constraints: route_constraints_t }
|
|
3
|
+
type route_definition_t = { method: Symbol, path: String, component: singleton(Component), name: String?, pattern_segments: Array[String], constraints: route_constraints_t, layouts: Array[singleton(Component)] }
|
|
4
4
|
|
|
5
5
|
class Router
|
|
6
6
|
attr_reader routes: Array[route_definition_t]
|
|
7
|
-
attr_reader
|
|
7
|
+
attr_reader current_route: [singleton(Component), Hash[Symbol, untyped]]?
|
|
8
|
+
attr_reader current_layouts: Array[singleton(Component)]
|
|
8
9
|
attr_reader current_path: String?
|
|
9
10
|
attr_reader url_helpers: Module
|
|
10
11
|
attr_reader route_helpers: untyped
|
|
@@ -17,6 +18,8 @@ module Funicular
|
|
|
17
18
|
def delete: (String path, to: singleton(Component), ?as: String?, ?constraints: route_constraints_t?) -> void
|
|
18
19
|
def add_route: (String path, singleton(Component) component_class, ?as: String?, ?constraints: route_constraints_t?) -> void
|
|
19
20
|
def set_default: (String path) -> void
|
|
21
|
+
def layout: (singleton(Component) component_class) { () -> void } -> void
|
|
22
|
+
def current_component: () -> Component?
|
|
20
23
|
def match: (String path) -> [singleton(Component)?, Hash[Symbol, untyped]]
|
|
21
24
|
def start: (?hydrate: bool) -> void
|
|
22
25
|
def stop: () -> void
|
|
@@ -28,7 +31,10 @@ module Funicular
|
|
|
28
31
|
private def generate_url_helper: (String name, String path_pattern) -> void
|
|
29
32
|
private def extract_param_names: (String path_pattern) -> Array[Symbol]
|
|
30
33
|
private def handle_route_change: () -> void
|
|
34
|
+
private def render_with_layouts: (route_definition_t route, Hash[Symbol, untyped] params, String path, bool? hydrate_now) -> void
|
|
35
|
+
private def find_component_instance: (untyped vnode, singleton(Component) component_class) -> Component?
|
|
31
36
|
private def unmount_current_component: () -> void
|
|
32
37
|
private def find_route: (String path) -> [singleton(Component) | nil, Hash[Symbol, untyped]]
|
|
38
|
+
private def match_route: (String path) -> [route_definition_t?, Hash[Symbol, untyped]]
|
|
33
39
|
end
|
|
34
40
|
end
|
data/sig/vdom.rbs
CHANGED
|
@@ -7,6 +7,8 @@ module Funicular
|
|
|
7
7
|
SCRIPTING_ELEMENTS: Array[String]
|
|
8
8
|
|
|
9
9
|
def self.valid_tag_name?: (untyped name) -> bool
|
|
10
|
+
def self.text: (untyped child) -> String?
|
|
11
|
+
def self.child_nodes: (untyped element) -> Array[untyped]
|
|
10
12
|
def self.valid_attribute_name?: (untyped name) -> bool
|
|
11
13
|
def self.event_attribute?: (untyped name) -> bool
|
|
12
14
|
def self.unsafe_url?: (untyped name, untyped value) -> bool
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: funicular
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- HASUMI Hitoshi
|
|
@@ -187,7 +187,7 @@ homepage: https://github.com/picoruby/funicular
|
|
|
187
187
|
licenses:
|
|
188
188
|
- MIT
|
|
189
189
|
metadata:
|
|
190
|
-
homepage_uri: https://picoruby.org/funicular
|
|
190
|
+
homepage_uri: https://picoruby.org/funicular
|
|
191
191
|
source_code_uri: https://github.com/picoruby/funicular
|
|
192
192
|
changelog_uri: https://github.com/picoruby/funicular/blob/master/CHANGELOG.md
|
|
193
193
|
post_install_message: |2+
|
|
@@ -226,6 +226,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
226
226
|
requirements: []
|
|
227
227
|
rubygems_version: 4.0.20
|
|
228
228
|
specification_version: 4
|
|
229
|
-
summary: Rails plugin for client-side Ruby development with
|
|
229
|
+
summary: Rails plugin for client-side Ruby development with PicoRuby
|
|
230
230
|
test_files: []
|
|
231
231
|
...
|