funicular 0.5.0 → 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 +77 -0
- data/lib/funicular/testing/node_runner.mjs +5 -1
- data/lib/funicular/vendor/mrbc/VERSION +1 -1
- data/lib/funicular/vendor/mrbc/mrbc.js +13 -9
- 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 +80 -15
- 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/lib/funicular/version.rb +4 -3
- data/lib/tasks/funicular.rake +23 -15
- data/minitest/ssr_test.rb +16 -0
- data/mrblib/component.rb +41 -6
- data/mrblib/debug.rb +54 -10
- data/mrblib/differ.rb +10 -10
- data/mrblib/funicular.rb +2 -5
- data/mrblib/html_serializer.rb +28 -15
- data/mrblib/patcher.rb +52 -26
- data/mrblib/router.rb +87 -7
- data/mrblib/vdom.rb +17 -7
- data/mrblib/version.rb +11 -0
- data/sig/component.rbs +2 -0
- data/sig/debug.rbs +5 -0
- data/sig/html_serializer.rbs +3 -1
- data/sig/router.rbs +8 -2
- data/sig/vdom.rbs +2 -0
- metadata +5 -4
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
|
|
@@ -100,6 +110,11 @@ module Funicular
|
|
|
100
110
|
@children = normalize_children(children || [])
|
|
101
111
|
end
|
|
102
112
|
|
|
113
|
+
def ==(other)
|
|
114
|
+
return false unless other.is_a?(Element)
|
|
115
|
+
@tag == other.tag && @props == other.props && @children == other.children
|
|
116
|
+
end
|
|
117
|
+
|
|
103
118
|
private
|
|
104
119
|
|
|
105
120
|
def normalize_children(children)
|
|
@@ -115,8 +130,8 @@ module Funicular
|
|
|
115
130
|
# Recursively normalize nested arrays
|
|
116
131
|
# @type var child: Array[Funicular::VDOM::child_t]
|
|
117
132
|
result.concat(normalize_children(child))
|
|
118
|
-
when nil
|
|
119
|
-
|
|
133
|
+
when nil, false
|
|
134
|
+
result << ''
|
|
120
135
|
else
|
|
121
136
|
# Convert other types to strings
|
|
122
137
|
result << child.to_s
|
|
@@ -124,11 +139,6 @@ module Funicular
|
|
|
124
139
|
end
|
|
125
140
|
result
|
|
126
141
|
end
|
|
127
|
-
|
|
128
|
-
def ==(other)
|
|
129
|
-
return false unless other.is_a?(Element)
|
|
130
|
-
@tag == other.tag && @props == other.props && @children == other.children
|
|
131
|
-
end
|
|
132
142
|
end
|
|
133
143
|
|
|
134
144
|
class Text < VNode
|
data/mrblib/version.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Single source of truth for the Funicular version.
|
|
2
|
+
#
|
|
3
|
+
# This file lives in mrblib so that it is compiled into PicoRuby.wasm along
|
|
4
|
+
# with the rest of the runtime. The CRuby gem reuses it via
|
|
5
|
+
# lib/funicular/version.rb, so the version is defined in exactly one place.
|
|
6
|
+
#
|
|
7
|
+
# Keep this file free of any dependency: it must evaluate standalone in
|
|
8
|
+
# both PicoRuby and CRuby, regardless of load order.
|
|
9
|
+
module Funicular
|
|
10
|
+
VERSION = '0.5.2'
|
|
11
|
+
end
|
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/debug.rbs
CHANGED
|
@@ -20,7 +20,12 @@ module Funicular
|
|
|
20
20
|
def self.last_error: () -> Hash[Symbol, untyped]?
|
|
21
21
|
def self.error_count: () -> Integer
|
|
22
22
|
|
|
23
|
+
INSPECT_MAX_DEPTH: Integer
|
|
24
|
+
INSPECT_MAX_ITEMS: Integer
|
|
25
|
+
|
|
23
26
|
private def self.error_registry: () -> Array[Hash[Symbol, untyped]]
|
|
27
|
+
private def self.safe_inspect: (untyped value, ?Integer depth) -> String
|
|
28
|
+
private def self.safe_inspect_object: (untyped value, Integer depth) -> String
|
|
24
29
|
private def self.get_state_keys: (Funicular::Component) -> Array[String]
|
|
25
30
|
private def self.get_child_ids: (Funicular::Component) -> Array[Integer]
|
|
26
31
|
private def self.collect_direct_children: (Funicular::VDOM::VNode, Array[Funicular::Component]) -> void
|
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/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
|
|
@@ -156,6 +156,7 @@ files:
|
|
|
156
156
|
- mrblib/store_singleton.rb
|
|
157
157
|
- mrblib/styles.rb
|
|
158
158
|
- mrblib/vdom.rb
|
|
159
|
+
- mrblib/version.rb
|
|
159
160
|
- mrblib/view_context.rb
|
|
160
161
|
- sig/cable.rbs
|
|
161
162
|
- sig/component.rbs
|
|
@@ -186,7 +187,7 @@ homepage: https://github.com/picoruby/funicular
|
|
|
186
187
|
licenses:
|
|
187
188
|
- MIT
|
|
188
189
|
metadata:
|
|
189
|
-
homepage_uri: https://picoruby.org/funicular
|
|
190
|
+
homepage_uri: https://picoruby.org/funicular
|
|
190
191
|
source_code_uri: https://github.com/picoruby/funicular
|
|
191
192
|
changelog_uri: https://github.com/picoruby/funicular/blob/master/CHANGELOG.md
|
|
192
193
|
post_install_message: |2+
|
|
@@ -223,8 +224,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
223
224
|
- !ruby/object:Gem::Version
|
|
224
225
|
version: '0'
|
|
225
226
|
requirements: []
|
|
226
|
-
rubygems_version: 4.0.
|
|
227
|
+
rubygems_version: 4.0.20
|
|
227
228
|
specification_version: 4
|
|
228
|
-
summary: Rails plugin for client-side Ruby development with
|
|
229
|
+
summary: Rails plugin for client-side Ruby development with PicoRuby
|
|
229
230
|
test_files: []
|
|
230
231
|
...
|