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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +2 -2
  4. data/Rakefile +23 -15
  5. data/demo/test_chartjs.html +17 -17
  6. data/demo/test_component.html +15 -15
  7. data/demo/test_error_boundary.html +41 -41
  8. data/demo/test_router.html +57 -57
  9. data/demo/tic-tac-toe.html +29 -29
  10. data/docs/architecture.md +43 -30
  11. data/lib/funicular/compiler.rb +13 -13
  12. data/lib/funicular/helpers/picoruby_helper.rb +24 -1
  13. data/lib/funicular/ssr/runtime.rb +2 -0
  14. data/lib/funicular/ssr.rb +2 -1
  15. data/lib/funicular/testing/node_runner.rb +1 -1
  16. data/lib/funicular/vendor/mrbc/VERSION +1 -0
  17. data/lib/funicular/vendor/{picorbc/picorbc.js → mrbc/mrbc.js} +14 -1
  18. data/lib/funicular/vendor/mrbc/mrbc.wasm +0 -0
  19. data/lib/funicular/vendor/picoruby/VERSION +1 -1
  20. data/lib/funicular/vendor/picoruby/debug/init.iife.js +19 -4
  21. data/lib/funicular/vendor/picoruby/debug/picoruby.js +16 -10
  22. data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
  23. data/lib/funicular/vendor/picoruby/dist/init.iife.js +19 -4
  24. data/lib/funicular/vendor/picoruby/dist/picoruby.js +1 -1
  25. data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
  26. data/lib/funicular/vendor/picoruby-test-node/VERSION +1 -1
  27. data/lib/funicular/vendor/picoruby-test-node/picoruby.js +85 -84
  28. data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
  29. data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +1 -1
  30. data/lib/funicular/version.rb +1 -1
  31. data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +47 -46
  32. data/lib/tasks/funicular.rake +1 -1
  33. data/minitest/commands_routes_test.rb +97 -0
  34. data/minitest/compiler_test.rb +195 -0
  35. data/minitest/configuration_test.rb +64 -0
  36. data/minitest/fixtures/funicular_app/components/greeting_component.rb +6 -6
  37. data/minitest/form_for_test.rb +2 -2
  38. data/minitest/funicular_test.rb +28 -2
  39. data/minitest/hydration_test.rb +2 -2
  40. data/minitest/middleware_test.rb +154 -0
  41. data/minitest/picoruby_helper_test.rb +139 -0
  42. data/minitest/route_parser_test.rb +139 -0
  43. data/minitest/schema_test.rb +23 -0
  44. data/minitest/ssr_test.rb +57 -0
  45. data/minitest/support/rails_stub.rb +59 -0
  46. data/minitest/test_helper.rb +20 -0
  47. data/minitest/testing_test.rb +267 -0
  48. data/minitest/view_context_test.rb +101 -0
  49. data/mrblib/component.rb +158 -237
  50. data/mrblib/debug.rb +7 -6
  51. data/mrblib/differ.rb +3 -1
  52. data/mrblib/error_boundary.rb +19 -27
  53. data/mrblib/form_builder.rb +28 -24
  54. data/mrblib/funicular.rb +2 -1
  55. data/mrblib/html_serializer.rb +14 -13
  56. data/mrblib/http.rb +12 -1
  57. data/mrblib/patcher.rb +12 -9
  58. data/mrblib/router.rb +9 -5
  59. data/mrblib/runtime.rb +28 -0
  60. data/mrblib/styles.rb +13 -17
  61. data/mrblib/vdom.rb +90 -9
  62. data/mrblib/view_context.rb +135 -0
  63. data/sig/component.rbs +34 -85
  64. data/sig/error_boundary.rbs +4 -4
  65. data/sig/form_builder.rbs +2 -1
  66. data/sig/html_serializer.rbs +2 -1
  67. data/sig/http.rbs +1 -0
  68. data/sig/patcher.rbs +1 -1
  69. data/sig/router.rbs +2 -13
  70. data/sig/runtime.rbs +13 -0
  71. data/sig/styles.rbs +3 -4
  72. data/sig/vdom.rbs +11 -2
  73. data/sig/view_context.rbs +47 -0
  74. metadata +18 -5
  75. data/lib/funicular/vendor/picorbc/VERSION +0 -1
  76. data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
data/mrblib/component.rb CHANGED
@@ -5,46 +5,77 @@ module Funicular
5
5
  @state = state_hash
6
6
  end
7
7
 
8
- # Support dynamic key access: state[:key] or state[variable_key]
9
8
  def [](key)
10
9
  @state[key]
11
10
  end
12
- def method_missing(method, *args)
13
- if method.to_s.end_with?('=')
14
- key = method.to_s[0..-2] || method
15
- raise "Use patch(#{key}: value) to update state"
16
- else
17
- @state[method]
18
- end
11
+
12
+ def fetch(key, default = nil)
13
+ return @state.fetch(key) if default.nil? && @state.key?(key)
14
+ @state.fetch(key, default)
15
+ end
16
+
17
+ def key?(key)
18
+ @state.key?(key)
19
+ end
20
+
21
+ def to_h
22
+ @state
23
+ end
24
+ end
25
+
26
+ class ResourceAccessor
27
+ def initialize(data, states, errors)
28
+ @data = data
29
+ @states = states
30
+ @errors = errors
31
+ end
32
+
33
+ def [](key)
34
+ @data[key]
35
+ end
36
+
37
+ def fetch(key, default = nil)
38
+ return @data.fetch(key) if default.nil? && @data.key?(key)
39
+ @data.fetch(key, default)
40
+ end
41
+
42
+ def loading?(key)
43
+ @states[key] == :pending || @states[key] == :loading
44
+ end
45
+
46
+ def error?(key)
47
+ @states[key] == :rejected
19
48
  end
20
49
 
21
- def respond_to_missing?(method, include_private = false)
22
- return false if method.to_s.end_with?('=')
23
- @state.key?(method) || super
50
+ def error(key)
51
+ @errors[key]
24
52
  end
25
53
  end
26
54
 
27
- attr_accessor :props, :vdom, :dom_element, :mounted
55
+ attr_accessor :props, :vdom, :dom_element, :mounted, :runtime, :children, :current_children
28
56
  attr_reader :refs
29
57
 
30
58
  def initialize(props = {})
31
59
  @props = props
32
60
  @state = initialize_state || {}
33
61
  @state_accessor = nil
62
+ @resource_accessor = nil
34
63
  @style_accessor = nil
64
+ @runtime = Funicular::Runtime.new
65
+ @children = [] #: Array[Funicular::VDOM::child_t]
35
66
  @vdom = nil
36
67
  @dom_element = nil
37
- @refs = {}
38
- @event_listeners = []
68
+ @refs = {} #: Hash[Symbol, JS::Element]
69
+ @event_listeners = [] #: Array[untyped]
39
70
  @mounted = false
40
71
  @updating = false
41
- @child_components = []
72
+ @child_components = [] #: Array[Funicular::Component]
42
73
 
43
74
  # Initialize suspense state
44
- @suspense_data = {}
45
- @suspense_states = {} # :pending, :loading, :resolved, :rejected
46
- @suspense_errors = {}
47
- @suspense_pending_timers = [] # Track pending setTimeout IDs for cleanup
75
+ @suspense_data = {} #: Hash[Symbol, untyped]
76
+ @suspense_states = {} #: Hash[Symbol, Symbol]
77
+ @suspense_errors = {} #: Hash[Symbol, untyped]
78
+ @suspense_pending_timers = [] #: Array[untyped]
48
79
  self.class.suspense_definitions.each_key do |name|
49
80
  @suspense_states[name] = :pending
50
81
  end
@@ -57,6 +88,14 @@ module Funicular
57
88
  @state_accessor ||= StateAccessor.new(@state)
58
89
  end
59
90
 
91
+ def resources
92
+ @resource_accessor ||= ResourceAccessor.new(@suspense_data, @suspense_states, @suspense_errors)
93
+ end
94
+
95
+ def styles
96
+ @style_accessor ||= StyleAccessor.new(self.class.styles_definitions)
97
+ end
98
+
60
99
  # Override this method in subclasses to define initial state
61
100
  def initialize_state
62
101
  {}
@@ -66,7 +105,7 @@ module Funicular
66
105
  # Used by SSR to inject server data, and by client hydration to restore
67
106
  # the same state from window.__FUNICULAR_STATE__ so the first client
68
107
  # render matches the server HTML. Top-level keys are symbolized so they
69
- # are reachable via state.foo (StateAccessor uses symbol keys); nested
108
+ # are reachable via state[:foo] (StateAccessor uses symbol keys); nested
70
109
  # values are left untouched (components read them with string keys).
71
110
  def seed_state(state_hash)
72
111
  return self if state_hash.nil?
@@ -191,30 +230,25 @@ module Funicular
191
230
  # ) do
192
231
  # div { user.name }
193
232
  # end
194
- def suspense(fallback:, error: nil, &block)
233
+ def render_suspense(h, name, fallback:, error: nil, &block)
195
234
  current_children = @current_children
196
235
  child_count_before = current_children&.size
197
236
  result = nil
198
237
 
199
- # Check for any rejected suspense
200
- rejected_name = self.class.suspense_definitions.keys.find { |name| @suspense_states[name] == :rejected }
201
- if rejected_name
238
+ if @suspense_states[name] == :rejected
202
239
  result = if error
203
- error.call(@suspense_errors[rejected_name])
240
+ error.call(h, @suspense_errors[name])
204
241
  else
205
- fallback.call
242
+ fallback.call(h)
206
243
  end
207
- elsif suspense_loading?
208
- # Check if any suspense is still loading
209
- # Note: Loading is started in mount(), not here
210
- result = fallback.call
244
+ elsif suspense_loading?(name)
245
+ result = fallback.call(h)
211
246
  else
212
- # All suspense data loaded, render content
213
- result = block.call
247
+ result = block.call(h, resources)
214
248
  end
215
249
 
216
250
  if current_children && current_children.size == child_count_before
217
- add_child(result)
251
+ add_child_from_view(result)
218
252
  end
219
253
 
220
254
  result
@@ -223,7 +257,7 @@ module Funicular
223
257
  # Class methods for styles DSL
224
258
  def self.styles(&block)
225
259
  builder = StyleBuilder.new
226
- builder.instance_eval(&block)
260
+ block.call(builder)
227
261
  @styles_definitions = builder.to_definitions
228
262
  end
229
263
 
@@ -256,12 +290,7 @@ module Funicular
256
290
  end
257
291
 
258
292
  def self.suspense_definitions
259
- @suspense_definitions ||= {} # steep:ignore UnannotatedEmptyCollection
260
- end
261
-
262
- # Instance method to access styles
263
- def s
264
- @style_accessor ||= StyleAccessor.new(self.class.styles_definitions)
293
+ @suspense_definitions ||= {}
265
294
  end
266
295
 
267
296
  # Update state and trigger re-render
@@ -301,7 +330,7 @@ module Funicular
301
330
 
302
331
  @container = container
303
332
  new_vdom = build_vdom
304
- @dom_element = VDOM::Renderer.new.render(new_vdom)
333
+ @dom_element = VDOM::Renderer.new(nil, @runtime).render(new_vdom)
305
334
  bind_events(@dom_element, new_vdom)
306
335
  collect_refs(@dom_element, new_vdom)
307
336
  collect_child_components(new_vdom)
@@ -397,7 +426,7 @@ module Funicular
397
426
  @child_components.each do |child|
398
427
  child.unmount if child.respond_to?(:unmount)
399
428
  end
400
- @child_components = []
429
+ @child_components = [] #: Array[Funicular::Component]
401
430
 
402
431
  cleanup_events
403
432
  cleanup_suspense_timers
@@ -418,17 +447,23 @@ module Funicular
418
447
  end
419
448
 
420
449
  # Override this method in subclasses to define render logic
421
- def render
422
- raise "Subclasses must implement the render method"
450
+ def render(h)
451
+ raise "Subclasses must implement render(h)"
423
452
  end
424
453
 
425
454
  # Build VDOM tree from render method
426
455
  # Called by VDOM::Renderer, Differ, and Patcher
427
456
  def build_vdom
457
+ previous_rendering = @rendering
458
+ previous_children = @current_children
428
459
  @rendering = true
429
460
  @current_children = nil
430
- result = render
431
- @rendering = false
461
+ begin
462
+ result = render(ViewContext.new(self))
463
+ ensure
464
+ @rendering = previous_rendering
465
+ @current_children = previous_children
466
+ end
432
467
 
433
468
  # Convert render result to VNode
434
469
  vnode = normalize_vnode(result)
@@ -452,7 +487,7 @@ module Funicular
452
487
 
453
488
  vnode.props.each do |key, value|
454
489
  key_str = key.to_s
455
- next unless key_str.start_with?('on')
490
+ next unless VDOM.event_attribute?(key_str)
456
491
 
457
492
  event_name = key_str[2..-1]&.downcase || ""
458
493
  event_types << event_name
@@ -563,7 +598,7 @@ module Funicular
563
598
  @event_listeners.each do |callback_id|
564
599
  JS::Object.removeEventListener(callback_id)
565
600
  end
566
- @event_listeners = []
601
+ @event_listeners = [] #: Array[untyped]
567
602
 
568
603
  # NOTE: Do NOT cleanup child component events here!
569
604
  # Child components manage their own events and will cleanup
@@ -576,7 +611,74 @@ module Funicular
576
611
  @suspense_pending_timers.each do |timer_id|
577
612
  JS.global.clearTimeout(timer_id)
578
613
  end
579
- @suspense_pending_timers = []
614
+ @suspense_pending_timers = [] #: Array[untyped]
615
+ end
616
+
617
+ private
618
+
619
+ public
620
+
621
+ def normalize_vnode_for_view(value)
622
+ normalize_vnode(value)
623
+ end
624
+
625
+ def add_child_from_view(child)
626
+ current_children = @current_children
627
+ return unless @rendering && current_children
628
+
629
+ normalized = normalize_vnode(child)
630
+ current_children << normalized if normalized
631
+ end
632
+
633
+ def build_form_for(h, model_key, options = {}, &block)
634
+ on_submit = options.delete(:on_submit)
635
+ form_class = options.delete(:class)
636
+
637
+ submit_handler = if on_submit
638
+ ->(event) do
639
+ event.preventDefault
640
+ form_data = collect_form_data(event, model_key)
641
+
642
+ case on_submit
643
+ when Symbol
644
+ send(on_submit, form_data)
645
+ when Method
646
+ on_submit.call(form_data)
647
+ when Proc
648
+ on_submit.call(form_data)
649
+ else
650
+ raise "on_submit must be Symbol, Method, or Proc, got #{on_submit.class}"
651
+ end
652
+ end
653
+ else
654
+ ->(event) { event.preventDefault }
655
+ end
656
+
657
+ h.form({ onsubmit: submit_handler, class: form_class }.merge(options)) do |hh|
658
+ builder = Funicular::FormBuilder.new(self, hh, model_key, options)
659
+ block.call(builder)
660
+ end
661
+ end
662
+
663
+ def build_link_to(h, path, method: :get, **options, &block)
664
+ merged_options = options.merge(href: path)
665
+ merged_options[:onclick] = ->(event) {
666
+ event.preventDefault
667
+ if method.to_s.downcase.to_sym == :get
668
+ handle_link_click(path)
669
+ else
670
+ handle_link_with_method(path, method)
671
+ end
672
+ }
673
+ h.a(merged_options, &block)
674
+ end
675
+
676
+ def build_button_to(h, path, method: :post, **options, &block)
677
+ merged_options = options.merge(
678
+ type: options[:type] || "button",
679
+ onclick: -> { handle_link_with_method(path, method) }
680
+ )
681
+ h.button(merged_options, &block)
580
682
  end
581
683
 
582
684
  private
@@ -615,7 +717,7 @@ module Funicular
615
717
  cleanup_events
616
718
 
617
719
  unless patches.empty?
618
- new_dom_element = VDOM::Patcher.new.apply(@dom_element, patches)
720
+ new_dom_element = VDOM::Patcher.new(nil, @runtime).apply(@dom_element, patches)
619
721
  # apply returns JS::Object (it must accept text-node patches), but
620
722
  # the component's root is always an Element. Narrow to JS::Element.
621
723
  @dom_element = new_dom_element if new_dom_element.is_a?(JS::Element)
@@ -698,7 +800,7 @@ module Funicular
698
800
  # appendChild: the stale node already has a place in the document, so we
699
801
  # replaceChild instead.
700
802
  def full_render_fallback(new_vdom, server_dom)
701
- fresh = VDOM::Renderer.new.render(new_vdom)
803
+ fresh = VDOM::Renderer.new(nil, @runtime).render(new_vdom)
702
804
  parent = server_dom.parentNode
703
805
  parent.replaceChild(fresh, server_dom) if parent
704
806
  bind_events(fresh, new_vdom)
@@ -721,6 +823,8 @@ module Funicular
721
823
 
722
824
  if child.is_a?(VDOM::Component)
723
825
  instance = child.component_class.new(child.props)
826
+ instance.runtime = child.runtime || @runtime
827
+ instance.children = child.children
724
828
  child.instance = instance
725
829
  instance.hydrate(child_dom)
726
830
  elsif child.is_a?(VDOM::Element)
@@ -731,7 +835,7 @@ module Funicular
731
835
 
732
836
  # Collect child component instances from VDOM tree
733
837
  def collect_child_components(vnode)
734
- @child_components = []
838
+ @child_components = [] #: Array[Funicular::Component]
735
839
  collect_child_components_recursive(vnode, @child_components)
736
840
  end
737
841
 
@@ -750,135 +854,6 @@ module Funicular
750
854
  end
751
855
  end
752
856
 
753
- # DSL methods for HTML elements
754
- HTML_TAGS = %w[
755
- div span p a
756
- h1 h2 h3 h4 h5 h6
757
- ul ol li
758
- table thead tbody tr th td
759
- form input textarea button select option label
760
- header footer nav section article aside
761
- img video audio canvas
762
- br hr
763
- ]
764
-
765
- # The HTML tag DSL methods are public: besides being called inside render
766
- # (implicit self), they are invoked by collaborators such as FormBuilder
767
- # with an explicit receiver (@component.div). A private method would forbid
768
- # that under CRuby (it is tolerated under mruby), which would break SSR of
769
- # any component using form_for. Keep them public on both VMs.
770
- public
771
-
772
- HTML_TAGS.each do |tag|
773
- define_method(tag) do |props = {}, &block|
774
- # @type self: Component
775
- children = [] #: Array[Funicular::VDOM::child_t]
776
-
777
- if block
778
- prev_children = @current_children
779
- @current_children = children
780
- # @type var block: Proc
781
- result = block.call
782
- @current_children = prev_children
783
-
784
- # Add block result to children if not already added via add_child
785
- if result && !result.equal?(children) && children.empty?
786
- normalized = normalize_vnode(result)
787
- children << normalized if normalized
788
- end
789
- end
790
-
791
- # Normalize props (convert StyleValue to String for :class)
792
- normalized_props = {}
793
- # @type var props: Hash[Symbol, String]
794
- props.each do |key, value|
795
- if key == :class && value.is_a?(StyleValue)
796
- normalized_props[key] = value.to_s
797
- else
798
- normalized_props[key] = value
799
- end
800
- end
801
-
802
- # @type var normalized_props: Hash[Symbol, String]
803
- element = VDOM::Element.new(tag, normalized_props, children)
804
-
805
- # If we're inside another element's block, add this element to parent's children
806
- if @rendering && @current_children
807
- @current_children << element
808
- end
809
-
810
- element
811
- end
812
- end
813
-
814
- private
815
-
816
- # Helper to add children in DSL blocks
817
- def add_child(child)
818
- return unless @rendering && @current_children
819
-
820
- normalized = normalize_vnode(child)
821
- @current_children << normalized if normalized
822
- end
823
-
824
- # Helper method to render child components in DSL
825
- # Accepts optional block for passing children to the component
826
- def component(component_class, props = {}, &block)
827
- unless component_class.is_a?(Class) && component_class.ancestors.include?(Funicular::Component)
828
- raise "component() expects a Funicular::Component class"
829
- end
830
-
831
- # If a block is provided, store the children builder in props
832
- # This allows components like ErrorBoundary to control child rendering
833
- if block
834
- props = props.merge(children_block: block)
835
- end
836
-
837
- vnode = VDOM::Component.new(component_class, props)
838
-
839
- # If we're inside another element's block, add this component to parent's children
840
- if @rendering && @current_children
841
- @current_children << vnode
842
- end
843
-
844
- vnode
845
- end
846
-
847
- # Rails-style form_for helper
848
- def form_for(model_key, options = {}, &block)
849
- on_submit = options.delete(:on_submit)
850
- form_class = options.delete(:class)
851
-
852
- # Build submit handler
853
- submit_handler = if on_submit
854
- ->(event) do
855
- event.preventDefault
856
-
857
- form_data = collect_form_data(event, model_key)
858
-
859
- # Call the submit handler (Symbol, Method, or Proc)
860
- case on_submit
861
- when Symbol
862
- send(on_submit, form_data)
863
- when Method
864
- on_submit.call(form_data)
865
- when Proc
866
- on_submit.call(form_data)
867
- else
868
- raise "on_submit must be Symbol, Method, or Proc, got #{on_submit.class}"
869
- end
870
- end
871
- else
872
- ->(event) { event.preventDefault }
873
- end
874
-
875
- # Render form
876
- form({ onsubmit: submit_handler, class: form_class }.merge(options)) do
877
- builder = Funicular::FormBuilder.new(self, model_key, options)
878
- block.call(builder)
879
- end
880
- end
881
-
882
857
  def collect_form_data(event, model_key)
883
858
  form_data = collect_dom_form_data(event)
884
859
  return form_data unless form_data.empty?
@@ -937,7 +912,7 @@ module Funicular
937
912
  end
938
913
 
939
914
  def collect_state_form_data(model_key)
940
- model_data = state.send(model_key)
915
+ model_data = state[model_key]
941
916
  if model_data.is_a?(Hash)
942
917
  model_data
943
918
  elsif model_data.respond_to?(:instance_variables)
@@ -952,34 +927,9 @@ module Funicular
952
927
  end
953
928
  end
954
929
 
955
- # Rails-style link_to helper
956
- # Default behavior: Fetch API for same-page actions (SPA-friendly)
957
- # Use navigate: true for router navigation (different component tree)
958
- def link_to(path, method: :get, navigate: false, **options, &block)
959
- if navigate
960
- # Navigation: Use <a> tag with real href for browser features
961
- # (right-click -> open in new tab, link preview, etc.)
962
- # Note: preventDefault is automatically called by js_add_event_listener
963
- # for <a> tags to enable SPA navigation
964
- merged_options = options.merge(href: path)
965
- merged_options[:onclick] = ->(event) {
966
- event.preventDefault # Called for clarity, but already handled by JS layer
967
- handle_link_click(path)
968
- }
969
- a(merged_options, &block)
970
- else
971
- # Action: Use <div> tag (semantically more appropriate for actions)
972
- # No href needed, purely onclick-driven
973
- merged_options = options.merge(
974
- onclick: -> { handle_link_with_method(path, method) }
975
- )
976
- div(merged_options, &block)
977
- end
978
- end
979
-
980
930
  # Handle router navigation (navigate using History API)
981
931
  def handle_link_click(path)
982
- Funicular.router&.navigate(path)
932
+ @runtime.router&.navigate(path)
983
933
  end
984
934
 
985
935
  # Handle link action via Fetch API
@@ -1008,35 +958,6 @@ module Funicular
1008
958
  end
1009
959
  end
1010
960
 
1011
- # Enable URL helpers from RouteHelpers module and suspense data accessors
1012
- def method_missing(method, *args)
1013
- # Check for suspense data accessor
1014
- if self.class.suspense_definitions.key?(method)
1015
- return @suspense_data[method]
1016
- end
1017
-
1018
- if Funicular.const_defined?(:RouteHelpers)
1019
- helpers = Funicular::RouteHelpers
1020
- if helpers.instance_methods.include?(method)
1021
- # Include helpers module and retry
1022
- self.class.include(helpers) unless self.class.include?(helpers)
1023
- return send(method, *args)
1024
- end
1025
- end
1026
- super
1027
- end
1028
-
1029
- def respond_to_missing?(method, include_private = false)
1030
- # Check for suspense data accessor
1031
- return true if self.class.suspense_definitions.key?(method)
1032
-
1033
- if Funicular.const_defined?(:RouteHelpers)
1034
- Funicular::RouteHelpers.instance_methods.include?(method) || super
1035
- else
1036
- super
1037
- end
1038
- end
1039
-
1040
961
  # Transition Helpers
1041
962
  # These methods provide a declarative way to animate element removal/addition
1042
963
  # using CSS transitions, inspired by Vue.js and Alpine.js transition systems
data/mrblib/debug.rb CHANGED
@@ -152,16 +152,17 @@ module Funicular
152
152
 
153
153
  result = {} #: Hash[String, String]
154
154
  component.instance_variables.each do |var|
155
- next if var == :@state
156
- next if var.to_s.start_with?('@__debug')
157
- if var == :@vdom || var == :@child_components
158
- result[var.to_s] = "<omitted>"
155
+ name = var.to_s
156
+ next if name == '@state'
157
+ next if name.start_with?('@__debug')
158
+ if name == '@vdom' || name == '@child_components'
159
+ result[name] = "<omitted>"
159
160
  next
160
161
  end
161
162
  begin
162
- result[var.to_s] = component.instance_variable_get(var).inspect
163
+ result[name] = component.instance_variable_get(var).inspect
163
164
  rescue
164
- result[var.to_s] = "<error inspecting value>"
165
+ result[name] = "<error inspecting value>"
165
166
  end
166
167
  end
167
168
  JSON.generate(result)
data/mrblib/differ.rb CHANGED
@@ -213,11 +213,13 @@ module Funicular
213
213
  # If only Proc props changed, update props but skip VDOM rebuild
214
214
  unless data_props_changed
215
215
  instance.props = new_node.props
216
+ instance.children = new_node.children
216
217
  return []
217
218
  end
218
219
 
219
220
  old_internal_vdom = instance.vdom
220
221
  instance.props = new_node.props
222
+ instance.children = new_node.children
221
223
  new_internal_vdom = instance.build_vdom
222
224
  internal_patches = diff(old_internal_vdom, new_internal_vdom)
223
225
 
@@ -227,7 +229,7 @@ module Funicular
227
229
  end
228
230
 
229
231
  # Original logic: If props changed, replace the entire component
230
- if old_node.props != new_node.props
232
+ if old_node.props != new_node.props || old_node.children != new_node.children
231
233
  return [[:replace, new_node, old_node]]
232
234
  end
233
235