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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56720ac2a4a7635871515e66f55bf5ed272bb49cb7cc3a403671494720ef16d7
|
|
4
|
+
data.tar.gz: 7bf29c93a9480501c53e9b10cb33d29eb73ec22964300aed1c3e124206a3ab40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 297df4162ffbe5259e2fe5c7738a310714a42ff3e1704386560c65d27e9a6d5aa19f5374e700915b388e37f9879926e2454296e9a39bfee4080d04310e2df295
|
|
7
|
+
data.tar.gz: c56295e03efd38afc789b876717f3ce05a0631f02197843362ff627084db12fc3ecdd925b9c8ce437dbc8cff1b6fca9ff9b2b9396a5c22e24fd203fbcf50740d
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
### Added
|
|
4
4
|
|
|
5
|
+
- 0.3.0 rendering architecture: `render(h)` now receives a `ViewContext`
|
|
6
|
+
facade for elements, components, forms, styles, resources, and routes.
|
|
7
|
+
- Per-app `Runtime` context for route helpers and renderer/serializer
|
|
8
|
+
propagation, enabling isolated route helper sets across multiple apps.
|
|
9
|
+
|
|
10
|
+
### Breaking Changes
|
|
11
|
+
|
|
12
|
+
- **0.3.0 is a deliberate breaking DSL redesign. Existing Funicular
|
|
13
|
+
components written for 0.2.x require source changes.**
|
|
14
|
+
- Component render methods must now accept a view context:
|
|
15
|
+
`def render(h)`. The former implicit component-level DSL methods for HTML
|
|
16
|
+
tags, `component`, `form_for`, `link_to`, `button_to`, `suspense`, styles,
|
|
17
|
+
resources, and route helpers have been removed.
|
|
18
|
+
- HTML and framework helpers are now called through `h`, for example
|
|
19
|
+
`h.div`, `h.component(...)`, `h.form_for(...)`, `h.link_to(...)`,
|
|
20
|
+
`h.suspense(...)`, `h.styles[...]`, `h.resources[...]`, and `h.routes`.
|
|
21
|
+
- Component state reads are explicit: use `state[:key]`, `state.fetch(:key)`,
|
|
22
|
+
or `h.state[:key]`. The old `state.key_name` method-style access has been
|
|
23
|
+
removed.
|
|
24
|
+
- Style definitions are explicit: use `styles { |css| css.define(...) }`.
|
|
25
|
+
The old dynamic style definition DSL has been removed.
|
|
26
|
+
- Component children are stored as `VDOM::Component#children`. The old
|
|
27
|
+
`children_block` prop path has been removed and no compatibility shim is
|
|
28
|
+
provided.
|
|
29
|
+
- Route helpers are scoped by `Funicular::Runtime`; global
|
|
30
|
+
`Funicular::RouteHelpers` injection has been removed. Code that depends on
|
|
31
|
+
route helpers should use `h.routes`.
|
|
32
|
+
- `FormBuilder`, `ErrorBoundary`, SSR, hydration, renderer, patcher, and HTML
|
|
33
|
+
serialization now operate through the same `ViewContext` / `Runtime`
|
|
34
|
+
architecture.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Since mruby-compiler-prism, which used to be mruby-compiler2 producing
|
|
39
|
+
picorbc, has become the default compiler for mruby, we changed the name
|
|
40
|
+
from picorbc to mrbc.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- Harden VDOM rendering against HTML and script injection in both SSR and
|
|
45
|
+
browser rendering: validate tag and attribute names, reject `script`
|
|
46
|
+
elements, and consistently block case-obfuscated event handlers, `srcdoc`,
|
|
47
|
+
and unsafe URL schemes including control-character variants.
|
|
48
|
+
|
|
49
|
+
## [0.2.1] - 2026-06-15
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **Funicular::Component**: Add `name` field to form to find state changed.
|
|
54
|
+
|
|
55
|
+
## [0.2.0] - 2026-06-11
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
5
59
|
- **Funicular::Store DSL**: Declarative client-side stores backed by
|
|
6
60
|
IndexedDB. Subclass `Funicular::Store::Singleton` (one value per scope)
|
|
7
61
|
or `Funicular::Store::Collection` (ordered list per scope) and use
|
data/README.md
CHANGED
|
@@ -72,13 +72,13 @@ git clone --recurse-submodules https://github.com/picoruby/picoruby.git
|
|
|
72
72
|
cd picoruby/mrbgems/picoruby-funicular
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
The CRubyGem side (`lib/`, `funicular.gemspec`, etc.) can be developed and tested independently inside that directory, but `rake copy_wasm` — which vendors the PicoRuby.wasm and
|
|
75
|
+
The CRubyGem side (`lib/`, `funicular.gemspec`, etc.) can be developed and tested independently inside that directory, but `rake copy_wasm` — which vendors the PicoRuby.wasm and mrbc wasm artifacts into the gem — relies on sibling directories within the picoruby repository (`mrbgems/picoruby-wasm/npm/`).
|
|
76
76
|
Running it from a standalone checkout will fail.
|
|
77
77
|
|
|
78
78
|
## Testing
|
|
79
79
|
|
|
80
80
|
- CRubygem (Rails integration) test: `rake test` in this repository
|
|
81
|
-
- PicoGem Funicular test: `rake test:gems:
|
|
81
|
+
- PicoGem Funicular test: `rake test:gems:wasm[picoruby-funicular]` in picoruby where mrbgems/picoruby-funicular exists as a submodule
|
|
82
82
|
|
|
83
83
|
## Contributing
|
|
84
84
|
|
data/Rakefile
CHANGED
|
@@ -9,9 +9,17 @@ Rake::TestTask.new(:test) do |t|
|
|
|
9
9
|
t.test_files = FileList["minitest/**/*_test.rb"]
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
namespace :test do
|
|
13
|
+
desc "Run the test suite with SimpleCov coverage measurement (report in coverage/)"
|
|
14
|
+
task :coverage do
|
|
15
|
+
ENV["COVERAGE"] = "1"
|
|
16
|
+
Rake::Task["test"].invoke
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
12
20
|
task default: :test
|
|
13
21
|
|
|
14
|
-
desc "Copy picoruby and
|
|
22
|
+
desc "Copy picoruby and mrbc wasm artifacts from picoruby-wasm into the gem"
|
|
15
23
|
task :copy_wasm do
|
|
16
24
|
require "fileutils"
|
|
17
25
|
require "json"
|
|
@@ -59,25 +67,25 @@ task :copy_wasm do
|
|
|
59
67
|
puts " wrote picoruby/VERSION (#{picoruby_version})"
|
|
60
68
|
|
|
61
69
|
# ------------------------------------------------------------------
|
|
62
|
-
# 2)
|
|
70
|
+
# 2) mrbc compiler (node CLI, run by Funicular::Compiler)
|
|
63
71
|
# ------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
abort "Missing #{
|
|
72
|
+
mrbc_src = File.join(npm_root, "mrbc", "debug")
|
|
73
|
+
mrbc_dest = File.join(vendor_root, "mrbc")
|
|
74
|
+
abort "Missing #{mrbc_src}" unless Dir.exist?(mrbc_src)
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
mrbc_version = JSON.parse(File.read(File.join(npm_root, "mrbc", "package.json"))).fetch("version")
|
|
77
|
+
mrbc_files = %w[mrbc.js mrbc.wasm]
|
|
70
78
|
|
|
71
|
-
FileUtils.rm_rf(
|
|
72
|
-
FileUtils.mkdir_p(
|
|
73
|
-
|
|
74
|
-
src_file = File.join(
|
|
79
|
+
FileUtils.rm_rf(mrbc_dest)
|
|
80
|
+
FileUtils.mkdir_p(mrbc_dest)
|
|
81
|
+
mrbc_files.each do |fname|
|
|
82
|
+
src_file = File.join(mrbc_src, fname)
|
|
75
83
|
abort "Missing file: #{src_file}" unless File.exist?(src_file)
|
|
76
|
-
FileUtils.copy_file(src_file, File.join(
|
|
84
|
+
FileUtils.copy_file(src_file, File.join(mrbc_dest, fname))
|
|
77
85
|
end
|
|
78
|
-
File.chmod(0755, File.join(
|
|
79
|
-
File.write(File.join(
|
|
80
|
-
puts " copied
|
|
86
|
+
File.chmod(0755, File.join(mrbc_dest, "mrbc.js"))
|
|
87
|
+
File.write(File.join(mrbc_dest, "VERSION"), "#{mrbc_version}\n")
|
|
88
|
+
puts " copied mrbc (#{mrbc_version})"
|
|
81
89
|
|
|
82
90
|
# ------------------------------------------------------------------
|
|
83
91
|
# 3) PicoRuby runtime for DOM-backed Node.js tests
|
data/demo/test_chartjs.html
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
return
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
current_data = state
|
|
47
|
+
current_data = state[:data_sets][state[:month_index]]
|
|
48
48
|
|
|
49
49
|
config = {
|
|
50
50
|
type: 'line',
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def component_updated
|
|
72
|
-
puts "component_updated: Updating Chart.js with data index #{state
|
|
72
|
+
puts "component_updated: Updating Chart.js with data index #{state[:month_index]}"
|
|
73
73
|
update_chart
|
|
74
74
|
end
|
|
75
75
|
|
|
@@ -80,22 +80,22 @@
|
|
|
80
80
|
|
|
81
81
|
def next_data_set(event)
|
|
82
82
|
event.preventDefault if event
|
|
83
|
-
new_index = (state
|
|
84
|
-
puts "next_data_set: Changing index from #{state
|
|
83
|
+
new_index = (state[:month_index] + 1) % state[:data_sets].size
|
|
84
|
+
puts "next_data_set: Changing index from #{state[:month_index]} to #{new_index}"
|
|
85
85
|
patch(month_index: new_index)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def prev_data_set(event)
|
|
89
89
|
event.preventDefault if event
|
|
90
|
-
new_index = (state
|
|
91
|
-
puts "prev_data_set: Changing index from #{state
|
|
90
|
+
new_index = (state[:month_index] - 1) % state[:data_sets].size
|
|
91
|
+
puts "prev_data_set: Changing index from #{state[:month_index]} to #{new_index}"
|
|
92
92
|
patch(month_index: new_index)
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
def update_chart
|
|
96
96
|
return unless @chart
|
|
97
97
|
|
|
98
|
-
current_data = state
|
|
98
|
+
current_data = state[:data_sets][state[:month_index]]
|
|
99
99
|
puts "update_chart: Setting data to #{current_data[:labels].join(', ')}"
|
|
100
100
|
|
|
101
101
|
# Convert to JS objects
|
|
@@ -118,18 +118,18 @@
|
|
|
118
118
|
puts "update_chart: Chart.js update('active') called"
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
-
def render
|
|
122
|
-
div(id: "dashboard") do
|
|
123
|
-
h2 { "Sales Dashboard" }
|
|
124
|
-
div(id: "chart-container") do
|
|
125
|
-
canvas(ref: :chart_canvas, id: "chart")
|
|
121
|
+
def render(h)
|
|
122
|
+
h.div(id: "dashboard") do |hh|
|
|
123
|
+
hh.h2 { "Sales Dashboard" }
|
|
124
|
+
hh.div(id: "chart-container") do |hhh|
|
|
125
|
+
hhh.canvas(ref: :chart_canvas, id: "chart")
|
|
126
126
|
end
|
|
127
|
-
div do
|
|
128
|
-
button(id: "prev-btn", onclick: :prev_data_set) { "Previous Quarter" }
|
|
129
|
-
button(id: "next-btn", onclick: :next_data_set) { "Next Quarter" }
|
|
127
|
+
hh.div do |hhh|
|
|
128
|
+
hhh.button(id: "prev-btn", onclick: :prev_data_set) { "Previous Quarter" }
|
|
129
|
+
hhh.button(id: "next-btn", onclick: :next_data_set) { "Next Quarter" }
|
|
130
130
|
end
|
|
131
|
-
div(id: "info") do
|
|
132
|
-
current = state
|
|
131
|
+
hh.div(id: "info") do
|
|
132
|
+
current = state[:data_sets][state[:month_index]]
|
|
133
133
|
"Current data: #{current[:labels].join(', ')}"
|
|
134
134
|
end
|
|
135
135
|
end
|
data/demo/test_component.html
CHANGED
|
@@ -40,26 +40,26 @@
|
|
|
40
40
|
|
|
41
41
|
def increment(event)
|
|
42
42
|
event.preventDefault if event
|
|
43
|
-
patch(count: state
|
|
43
|
+
patch(count: state[:count] + 1)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def decrement(event)
|
|
47
47
|
event.preventDefault if event
|
|
48
|
-
patch(count: state
|
|
48
|
+
patch(count: state[:count] - 1)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
def render
|
|
52
|
-
div(id: "counter-app") do
|
|
53
|
-
h2 { "Counter: #{state
|
|
54
|
-
div(id: "status") do
|
|
55
|
-
"Mounted: #{state
|
|
51
|
+
def render(h)
|
|
52
|
+
h.div(id: "counter-app") do |hh|
|
|
53
|
+
hh.h2 { "Counter: #{state[:count]}" }
|
|
54
|
+
hh.div(id: "status") do
|
|
55
|
+
"Mounted: #{state[:mounted]}"
|
|
56
56
|
end
|
|
57
|
-
div do
|
|
58
|
-
button(id: "increment-btn", onclick: :increment) { "+1" }
|
|
59
|
-
button(id: "decrement-btn", onclick: :decrement) { "-1" }
|
|
57
|
+
hh.div do |hhh|
|
|
58
|
+
hhh.button(id: "increment-btn", onclick: :increment) { "+1" }
|
|
59
|
+
hhh.button(id: "decrement-btn", onclick: :decrement) { "-1" }
|
|
60
60
|
end
|
|
61
|
-
div(ref: :display, id: "display") do
|
|
62
|
-
"Current value: #{state
|
|
61
|
+
hh.div(ref: :display, id: "display") do
|
|
62
|
+
"Current value: #{state[:count]}"
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
end
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
# Test 2: Initial state
|
|
90
90
|
begin
|
|
91
91
|
counter = TestCounter.new
|
|
92
|
-
initial_state = counter.state
|
|
92
|
+
initial_state = counter.state[:count] == 0
|
|
93
93
|
log_test("Initial state", initial_state)
|
|
94
94
|
rescue => e
|
|
95
95
|
log_test("Initial state", false)
|
|
@@ -139,13 +139,13 @@
|
|
|
139
139
|
has_button = btn != nil
|
|
140
140
|
|
|
141
141
|
# Test handler logic by calling it directly (standard testing approach)
|
|
142
|
-
initial_count = counter.state
|
|
142
|
+
initial_count = counter.state[:count]
|
|
143
143
|
counter.increment(nil)
|
|
144
144
|
sleep 0.05
|
|
145
145
|
|
|
146
146
|
h2_element = JS.document.querySelector('#counter-app h2')
|
|
147
147
|
text = h2_element.textContent.to_s
|
|
148
|
-
handler_works = counter.state
|
|
148
|
+
handler_works = counter.state[:count] == initial_count + 1 && text.include?("#{initial_count + 1}")
|
|
149
149
|
|
|
150
150
|
log_test("Event handler (click)", has_button && handler_works)
|
|
151
151
|
rescue => e
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
<script type="text/ruby">
|
|
92
92
|
# A component that always throws an error during render
|
|
93
93
|
class BrokenComponent < Funicular::Component
|
|
94
|
-
def render
|
|
94
|
+
def render(h)
|
|
95
95
|
raise "BrokenComponent intentionally threw this error!"
|
|
96
96
|
end
|
|
97
97
|
end
|
|
@@ -107,80 +107,80 @@
|
|
|
107
107
|
patch(count: state[:count] + 1)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
def render
|
|
111
|
-
div(class: 'demo-card') do
|
|
112
|
-
h3 { "Working Component" }
|
|
113
|
-
p { "Count: #{state[:count]}" }
|
|
114
|
-
button(onclick: :increment) { "Increment" }
|
|
110
|
+
def render(h)
|
|
111
|
+
h.div(class: 'demo-card') do |hh|
|
|
112
|
+
hh.h3 { "Working Component" }
|
|
113
|
+
hh.p { "Count: #{state[:count]}" }
|
|
114
|
+
hh.button(onclick: :increment) { "Increment" }
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
# Main demo application
|
|
120
120
|
class ErrorBoundaryDemo < Funicular::Component
|
|
121
|
-
def render
|
|
121
|
+
def render(h)
|
|
122
122
|
error_handler = ->(error, info) {
|
|
123
123
|
puts "[Demo] Error caught by ErrorBoundary:"
|
|
124
124
|
puts " Error: #{error.message}"
|
|
125
125
|
puts " Component: #{info&.dig(:component_class)}"
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
custom_fallback = ->(error) {
|
|
129
|
-
div(style: 'background: linear-gradient(135deg, #ff6b6b, #ee5a5a); color: white; padding: 20px; border-radius: 8px;') do
|
|
130
|
-
h3(style: 'margin-top: 0;') { "Custom Error Handler" }
|
|
131
|
-
p { "Caught: #{error.message}" }
|
|
132
|
-
p(style: 'font-size: 12px; opacity: 0.8;') { "This is a custom fallback provided via props." }
|
|
128
|
+
custom_fallback = ->(hh, error) {
|
|
129
|
+
hh.div(style: 'background: linear-gradient(135deg, #ff6b6b, #ee5a5a); color: white; padding: 20px; border-radius: 8px;') do |hhh|
|
|
130
|
+
hhh.h3(style: 'margin-top: 0;') { "Custom Error Handler" }
|
|
131
|
+
hhh.p { "Caught: #{error.message}" }
|
|
132
|
+
hhh.p(style: 'font-size: 12px; opacity: 0.8;') { "This is a custom fallback provided via props." }
|
|
133
133
|
end
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
div do
|
|
137
|
-
h2 { "Error Boundary Demonstrations" }
|
|
136
|
+
h.div do |hh|
|
|
137
|
+
hh.h2 { "Error Boundary Demonstrations" }
|
|
138
138
|
|
|
139
139
|
# Demo 1: Default fallback
|
|
140
|
-
div(class: 'demo-card') do
|
|
141
|
-
h3 { "Demo 1: Default Fallback UI" }
|
|
142
|
-
p { "ErrorBoundary wraps a component that always throws. Default fallback is shown." }
|
|
143
|
-
component(Funicular::ErrorBoundary, on_error: error_handler) do
|
|
144
|
-
component(BrokenComponent)
|
|
140
|
+
hh.div(class: 'demo-card') do |card|
|
|
141
|
+
card.h3 { "Demo 1: Default Fallback UI" }
|
|
142
|
+
card.p { "ErrorBoundary wraps a component that always throws. Default fallback is shown." }
|
|
143
|
+
card.component(Funicular::ErrorBoundary, on_error: error_handler) do |boundary|
|
|
144
|
+
boundary.component(BrokenComponent)
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
# Demo 2: Custom fallback
|
|
149
|
-
div(class: 'demo-card') do
|
|
150
|
-
h3 { "Demo 2: Custom Fallback UI" }
|
|
151
|
-
p { "ErrorBoundary with a custom fallback prop." }
|
|
152
|
-
component(Funicular::ErrorBoundary,
|
|
149
|
+
hh.div(class: 'demo-card') do |card|
|
|
150
|
+
card.h3 { "Demo 2: Custom Fallback UI" }
|
|
151
|
+
card.p { "ErrorBoundary with a custom fallback prop." }
|
|
152
|
+
card.component(Funicular::ErrorBoundary,
|
|
153
153
|
fallback: custom_fallback,
|
|
154
154
|
on_error: error_handler
|
|
155
|
-
) do
|
|
156
|
-
component(BrokenComponent)
|
|
155
|
+
) do |boundary|
|
|
156
|
+
boundary.component(BrokenComponent)
|
|
157
157
|
end
|
|
158
158
|
end
|
|
159
159
|
|
|
160
160
|
# Demo 3: Mixed content - some work, some fail
|
|
161
|
-
div(class: 'demo-card') do
|
|
162
|
-
h3 { "Demo 3: Isolated Error Boundaries" }
|
|
163
|
-
p { "Each component has its own ErrorBoundary. One failure doesn't affect others." }
|
|
161
|
+
hh.div(class: 'demo-card') do |card|
|
|
162
|
+
card.h3 { "Demo 3: Isolated Error Boundaries" }
|
|
163
|
+
card.p { "Each component has its own ErrorBoundary. One failure doesn't affect others." }
|
|
164
164
|
|
|
165
|
-
div(style: 'display: flex; gap: 15px; flex-wrap: wrap;') do
|
|
166
|
-
div(style: 'flex: 1; min-width: 200px;') do
|
|
167
|
-
h4 { "Working:" }
|
|
168
|
-
component(Funicular::ErrorBoundary) do
|
|
169
|
-
component(WorkingComponent)
|
|
165
|
+
card.div(style: 'display: flex; gap: 15px; flex-wrap: wrap;') do |row|
|
|
166
|
+
row.div(style: 'flex: 1; min-width: 200px;') do |col|
|
|
167
|
+
col.h4 { "Working:" }
|
|
168
|
+
col.component(Funicular::ErrorBoundary) do |boundary|
|
|
169
|
+
boundary.component(WorkingComponent)
|
|
170
170
|
end
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
-
div(style: 'flex: 1; min-width: 200px;') do
|
|
174
|
-
h4 { "Broken:" }
|
|
175
|
-
component(Funicular::ErrorBoundary) do
|
|
176
|
-
component(BrokenComponent)
|
|
173
|
+
row.div(style: 'flex: 1; min-width: 200px;') do |col|
|
|
174
|
+
col.h4 { "Broken:" }
|
|
175
|
+
col.component(Funicular::ErrorBoundary) do |boundary|
|
|
176
|
+
boundary.component(BrokenComponent)
|
|
177
177
|
end
|
|
178
178
|
end
|
|
179
179
|
|
|
180
|
-
div(style: 'flex: 1; min-width: 200px;') do
|
|
181
|
-
h4 { "Also Working:" }
|
|
182
|
-
component(Funicular::ErrorBoundary) do
|
|
183
|
-
component(WorkingComponent)
|
|
180
|
+
row.div(style: 'flex: 1; min-width: 200px;') do |col|
|
|
181
|
+
col.h4 { "Also Working:" }
|
|
182
|
+
col.component(Funicular::ErrorBoundary) do |boundary|
|
|
183
|
+
boundary.component(WorkingComponent)
|
|
184
184
|
end
|
|
185
185
|
end
|
|
186
186
|
end
|
data/demo/test_router.html
CHANGED
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
}
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def render
|
|
43
|
-
nav do
|
|
44
|
-
a(href: '/login', onclick: go_to('/login')) { 'Login' }
|
|
45
|
-
a(href: '/home', onclick: go_to('/home')) { 'Home' }
|
|
46
|
-
a(href: '/settings', onclick: go_to('/settings')) { 'Settings' }
|
|
47
|
-
a(href: '/users', onclick: go_to('/users')) { 'Users' }
|
|
42
|
+
def render(h)
|
|
43
|
+
h.nav do |hh|
|
|
44
|
+
hh.a(href: '/login', onclick: go_to('/login')) { 'Login' }
|
|
45
|
+
hh.a(href: '/home', onclick: go_to('/home')) { 'Home' }
|
|
46
|
+
hh.a(href: '/settings', onclick: go_to('/settings')) { 'Settings' }
|
|
47
|
+
hh.a(href: '/users', onclick: go_to('/users')) { 'Users' }
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
end
|
|
@@ -63,25 +63,25 @@
|
|
|
63
63
|
Funicular.router.navigate('/home')
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
def render
|
|
67
|
-
div(class: 'login-page') do
|
|
68
|
-
h2(class: 'page-title') { 'Login Page' }
|
|
66
|
+
def render(h)
|
|
67
|
+
h.div(class: 'login-page') do |hh|
|
|
68
|
+
hh.h2(class: 'page-title') { 'Login Page' }
|
|
69
69
|
|
|
70
|
-
div do
|
|
71
|
-
input(
|
|
70
|
+
hh.div do |hhh|
|
|
71
|
+
hhh.input(
|
|
72
72
|
type: 'text',
|
|
73
73
|
placeholder: 'Username',
|
|
74
|
-
value: state
|
|
74
|
+
value: state[:username],
|
|
75
75
|
oninput: :on_username_input
|
|
76
76
|
)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
div do
|
|
80
|
-
button(onclick: :on_login_click) { 'Login' }
|
|
79
|
+
hh.div do |hhh|
|
|
80
|
+
hhh.button(onclick: :on_login_click) { 'Login' }
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
div(class: 'status') do
|
|
84
|
-
"Username: #{state
|
|
83
|
+
hh.div(class: 'status') do
|
|
84
|
+
"Username: #{state[:username]}"
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
end
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
|
|
95
95
|
def increment(event)
|
|
96
96
|
event.preventDefault
|
|
97
|
-
patch(count: state
|
|
97
|
+
patch(count: state[:count] + 1)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def go_to_settings(event)
|
|
@@ -102,20 +102,20 @@
|
|
|
102
102
|
Funicular.router.navigate('/settings')
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
-
def render
|
|
106
|
-
div(class: 'home-page') do
|
|
107
|
-
h2(class: 'page-title') { 'Home Page' }
|
|
105
|
+
def render(h)
|
|
106
|
+
h.div(class: 'home-page') do |hh|
|
|
107
|
+
hh.h2(class: 'page-title') { 'Home Page' }
|
|
108
108
|
|
|
109
|
-
div do
|
|
110
|
-
"Counter: #{state
|
|
109
|
+
hh.div do
|
|
110
|
+
"Counter: #{state[:count]}"
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
-
div do
|
|
114
|
-
button(onclick: :increment) { 'Increment' }
|
|
115
|
-
button(onclick: :go_to_settings) { 'Go to Settings' }
|
|
113
|
+
hh.div do |hhh|
|
|
114
|
+
hhh.button(onclick: :increment) { 'Increment' }
|
|
115
|
+
hhh.button(onclick: :go_to_settings) { 'Go to Settings' }
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
div(class: 'status') do
|
|
118
|
+
hh.div(class: 'status') do
|
|
119
119
|
"You are on the home page. Click around to test routing!"
|
|
120
120
|
end
|
|
121
121
|
end
|
|
@@ -129,13 +129,13 @@
|
|
|
129
129
|
|
|
130
130
|
def toggle_theme(event)
|
|
131
131
|
event.preventDefault
|
|
132
|
-
new_theme = state
|
|
132
|
+
new_theme = state[:theme] == 'light' ? 'dark' : 'light'
|
|
133
133
|
patch(theme: new_theme)
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
def toggle_notifications(event)
|
|
137
137
|
event.preventDefault
|
|
138
|
-
patch(notifications: !state
|
|
138
|
+
patch(notifications: !state[:notifications])
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
def go_to_home(event)
|
|
@@ -143,25 +143,25 @@
|
|
|
143
143
|
Funicular.router.navigate('/home')
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
-
def render
|
|
147
|
-
div(class: 'settings-page') do
|
|
148
|
-
h2(class: 'page-title') { 'Settings Page' }
|
|
146
|
+
def render(h)
|
|
147
|
+
h.div(class: 'settings-page') do |hh|
|
|
148
|
+
hh.h2(class: 'page-title') { 'Settings Page' }
|
|
149
149
|
|
|
150
|
-
div do
|
|
151
|
-
span { "Theme: #{state
|
|
152
|
-
button(onclick: :toggle_theme) { 'Toggle Theme' }
|
|
150
|
+
hh.div do |hhh|
|
|
151
|
+
hhh.span { "Theme: #{state[:theme]}" }
|
|
152
|
+
hhh.button(onclick: :toggle_theme) { 'Toggle Theme' }
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
-
div do
|
|
156
|
-
span { "Notifications: #{state
|
|
157
|
-
button(onclick: :toggle_notifications) { 'Toggle Notifications' }
|
|
155
|
+
hh.div do |hhh|
|
|
156
|
+
hhh.span { "Notifications: #{state[:notifications] ? 'ON' : 'OFF'}" }
|
|
157
|
+
hhh.button(onclick: :toggle_notifications) { 'Toggle Notifications' }
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
-
div do
|
|
161
|
-
button(onclick: :go_to_home) { 'Back to Home' }
|
|
160
|
+
hh.div do |hhh|
|
|
161
|
+
hhh.button(onclick: :go_to_home) { 'Back to Home' }
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
div(class: 'status') do
|
|
164
|
+
hh.div(class: 'status') do
|
|
165
165
|
"Configure your settings here"
|
|
166
166
|
end
|
|
167
167
|
end
|
|
@@ -185,21 +185,21 @@
|
|
|
185
185
|
}
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
-
def render
|
|
189
|
-
div(class: 'users-page') do
|
|
190
|
-
h2(class: 'page-title') { 'Users List' }
|
|
188
|
+
def render(h)
|
|
189
|
+
h.div(class: 'users-page') do |hh|
|
|
190
|
+
hh.h2(class: 'page-title') { 'Users List' }
|
|
191
191
|
|
|
192
|
-
ul do
|
|
193
|
-
state
|
|
194
|
-
li do
|
|
195
|
-
a(href: "/users/#{user[:id]}", onclick: go_to_user(user[:id])) do
|
|
192
|
+
hh.ul do |hhh|
|
|
193
|
+
state[:users].each do |user|
|
|
194
|
+
hhh.li do |item|
|
|
195
|
+
item.a(href: "/users/#{user[:id]}", onclick: go_to_user(user[:id])) do
|
|
196
196
|
user[:name]
|
|
197
197
|
end
|
|
198
198
|
end
|
|
199
199
|
end
|
|
200
200
|
end
|
|
201
201
|
|
|
202
|
-
div(class: 'status') do
|
|
202
|
+
hh.div(class: 'status') do
|
|
203
203
|
"Click a user to see dynamic route with :id parameter"
|
|
204
204
|
end
|
|
205
205
|
end
|
|
@@ -216,20 +216,20 @@
|
|
|
216
216
|
Funicular.router.navigate('/users')
|
|
217
217
|
end
|
|
218
218
|
|
|
219
|
-
def render
|
|
220
|
-
div(class: 'user-detail-page') do
|
|
221
|
-
h2(class: 'page-title') { "User Detail" }
|
|
219
|
+
def render(h)
|
|
220
|
+
h.div(class: 'user-detail-page') do |hh|
|
|
221
|
+
hh.h2(class: 'page-title') { "User Detail" }
|
|
222
222
|
|
|
223
|
-
div do
|
|
224
|
-
"Viewing user ID: #{state
|
|
223
|
+
hh.div do
|
|
224
|
+
"Viewing user ID: #{state[:user_id]}"
|
|
225
225
|
end
|
|
226
226
|
|
|
227
|
-
div do
|
|
228
|
-
button(onclick: :go_back) { 'Back to Users' }
|
|
227
|
+
hh.div do |hhh|
|
|
228
|
+
hhh.button(onclick: :go_back) { 'Back to Users' }
|
|
229
229
|
end
|
|
230
230
|
|
|
231
|
-
div(class: 'status') do
|
|
232
|
-
"This page demonstrates dynamic routing with params[:id] = #{state
|
|
231
|
+
hh.div(class: 'status') do
|
|
232
|
+
"This page demonstrates dynamic routing with params[:id] = #{state[:user_id]}"
|
|
233
233
|
end
|
|
234
234
|
end
|
|
235
235
|
end
|