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.
data/mrblib/router.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Funicular
2
2
  class Router
3
- attr_reader :routes, :current_component, :current_path, :url_helpers, :route_helpers
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 @current_component&.navigation_guard
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 = @current_component&.navigation_guard
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
- component_class, params = find_route(path)
188
+ route, params = match_route(path)
164
189
 
165
- unless component_class
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[:component], params] if match
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
- # Skip nil values
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
@@ -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 current_component: Component?
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.0
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-getting-started
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.10
227
+ rubygems_version: 4.0.20
227
228
  specification_version: 4
228
- summary: Rails plugin for client-side Ruby development with mruby
229
+ summary: Rails plugin for client-side Ruby development with PicoRuby
229
230
  test_files: []
230
231
  ...