funicular 0.0.1 → 0.1.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 +56 -1
- data/README.md +58 -20
- data/Rakefile +74 -2
- data/demo/keymap_editor.html +582 -0
- data/demo/test_cable.html +179 -0
- data/demo/test_chartjs.html +235 -0
- data/demo/test_component.html +201 -0
- data/demo/test_diff_patch.html +146 -0
- data/demo/test_error_boundary.html +284 -0
- data/demo/test_router.html +257 -0
- data/demo/test_vdom.html +100 -0
- data/demo/tic-tac-toe.html +201 -0
- data/docs/README.md +419 -0
- data/docs/advanced-features.md +632 -0
- data/docs/architecture.md +409 -0
- data/docs/components-and-state.md +539 -0
- data/docs/data-fetching.md +528 -0
- data/docs/forms.md +446 -0
- data/docs/rails-integration.md +426 -0
- data/docs/realtime.md +543 -0
- data/docs/routing-and-navigation.md +427 -0
- data/docs/styling.md +285 -0
- data/exe/funicular +32 -0
- data/lib/funicular/assets/funicular.rb +21 -0
- data/lib/funicular/assets/funicular_debug.css +73 -0
- data/lib/funicular/assets/funicular_debug.js +183 -0
- data/lib/funicular/commands/routes.rb +69 -0
- data/lib/funicular/compiler.rb +135 -0
- data/lib/funicular/configuration.rb +76 -0
- data/lib/funicular/helpers/picoruby_helper.rb +50 -0
- data/lib/funicular/middleware.rb +98 -0
- data/lib/funicular/railtie.rb +26 -0
- data/lib/funicular/route_parser.rb +137 -0
- data/lib/funicular/vendor/picorbc/VERSION +1 -0
- data/lib/funicular/vendor/picorbc/picorbc.js +5283 -0
- data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
- data/lib/funicular/vendor/picoruby/VERSION +1 -0
- data/lib/funicular/vendor/picoruby/debug/init.iife.js +130 -0
- data/lib/funicular/vendor/picoruby/debug/picoruby.js +6404 -0
- data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby/dist/init.iife.js +130 -0
- data/lib/funicular/vendor/picoruby/dist/picoruby.js +2 -0
- data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
- data/lib/funicular/version.rb +1 -1
- data/lib/funicular.rb +29 -1
- data/lib/tasks/funicular.rake +135 -0
- data/minitest/funicular_test.rb +13 -0
- data/minitest/test_helper.rb +7 -0
- data/mrbgem.rake +15 -0
- data/mrblib/cable.rb +417 -0
- data/mrblib/component.rb +911 -0
- data/mrblib/debug.rb +205 -0
- data/mrblib/differ.rb +244 -0
- data/mrblib/environment_inquirer.rb +34 -0
- data/mrblib/error_boundary.rb +125 -0
- data/mrblib/file_upload.rb +184 -0
- data/mrblib/form_builder.rb +284 -0
- data/mrblib/funicular.rb +156 -0
- data/mrblib/http.rb +89 -0
- data/mrblib/model.rb +146 -0
- data/mrblib/patcher.rb +203 -0
- data/mrblib/router.rb +229 -0
- data/mrblib/styles.rb +83 -0
- data/mrblib/vdom.rb +273 -0
- data/sig/cable.rbs +65 -0
- data/sig/component.rbs +141 -0
- data/sig/debug.rbs +28 -0
- data/sig/differ.rbs +18 -0
- data/sig/environment_iquirer.rbs +10 -0
- data/sig/error_boundary.rbs +14 -0
- data/sig/file_upload.rbs +18 -0
- data/sig/form_builder.rbs +29 -0
- data/sig/funicular.rbs +11 -1
- data/sig/http.rbs +22 -0
- data/sig/model.rbs +23 -0
- data/sig/patcher.rbs +15 -0
- data/sig/router.rbs +43 -0
- data/sig/styles.rbs +25 -0
- data/sig/vdom.rbs +59 -0
- metadata +119 -8
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Test Diff/Patch - PicoRuby Funicular</title>
|
|
6
|
+
<link rel="stylesheet" href="../test.css">
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div><a href="../index.html">Back</a></div>
|
|
10
|
+
<h1>Diff/Patch Test Suite</h1>
|
|
11
|
+
<div id="test-results"></div>
|
|
12
|
+
<div id="container"></div>
|
|
13
|
+
|
|
14
|
+
<script type="text/ruby">
|
|
15
|
+
require 'js'
|
|
16
|
+
require 'funicular'
|
|
17
|
+
|
|
18
|
+
doc = JS.document
|
|
19
|
+
results = doc.getElementById('test-results')
|
|
20
|
+
container = doc.getElementById('container')
|
|
21
|
+
|
|
22
|
+
def test_case(name, &block)
|
|
23
|
+
doc = JS.document
|
|
24
|
+
results = doc.getElementById('test-results')
|
|
25
|
+
div = doc.createElement('div')
|
|
26
|
+
div.className = 'test-case'
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
block.call
|
|
30
|
+
div.className = 'test-case pass'
|
|
31
|
+
div.textContent = "PASS: #{name}"
|
|
32
|
+
rescue => e
|
|
33
|
+
div.className = 'test-case fail'
|
|
34
|
+
div.textContent = "FAIL: #{name} - #{e.message}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
results.appendChild(div)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
test_case("Diff text change") do
|
|
41
|
+
old = Funicular::VDOM.create_text('Hello')
|
|
42
|
+
new = Funicular::VDOM.create_text('World')
|
|
43
|
+
patches = Funicular::VDOM.diff(old, new)
|
|
44
|
+
raise "Diff failed" if patches.empty?
|
|
45
|
+
raise "Wrong patch type" unless patches[0][0] == :replace
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
test_case("Diff props change") do
|
|
49
|
+
old = Funicular::VDOM.create_element('button', { class: 'red' })
|
|
50
|
+
new = Funicular::VDOM.create_element('button', { class: 'blue' })
|
|
51
|
+
patches = Funicular::VDOM.diff(old, new)
|
|
52
|
+
raise "Diff failed" if patches.empty?
|
|
53
|
+
raise "Wrong patch type" unless patches[0][0] == :props
|
|
54
|
+
raise "Wrong prop value" unless patches[0][1][:class] == 'blue'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
test_case("Diff no change") do
|
|
58
|
+
old = Funicular::VDOM.create_element('div', { id: 'test' }, 'Same')
|
|
59
|
+
new = Funicular::VDOM.create_element('div', { id: 'test' }, 'Same')
|
|
60
|
+
patches = Funicular::VDOM.diff(old, new)
|
|
61
|
+
raise "Should have no patches" unless patches.empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
test_case("Diff tag change") do
|
|
65
|
+
old = Funicular::VDOM.create_element('div')
|
|
66
|
+
new = Funicular::VDOM.create_element('span')
|
|
67
|
+
patches = Funicular::VDOM.diff(old, new)
|
|
68
|
+
raise "Diff failed" if patches.empty?
|
|
69
|
+
raise "Wrong patch type" unless patches[0][0] == :replace
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
test_case("Diff child addition") do
|
|
73
|
+
old = Funicular::VDOM.create_element('ul')
|
|
74
|
+
new = Funicular::VDOM.create_element('ul', {},
|
|
75
|
+
Funicular::VDOM.create_element('li', {}, 'Item 1')
|
|
76
|
+
)
|
|
77
|
+
patches = Funicular::VDOM.diff(old, new)
|
|
78
|
+
raise "Diff failed" if patches.empty?
|
|
79
|
+
raise "Wrong patch structure" unless patches[0].is_a?(Array)
|
|
80
|
+
raise "Wrong child index" unless patches[0][0] == 0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
test_case("Patch text content") do
|
|
84
|
+
container = doc.getElementById('container')
|
|
85
|
+
container.innerHTML = ''
|
|
86
|
+
|
|
87
|
+
old_vnode = Funicular::VDOM.create_element('p', {}, 'Old text')
|
|
88
|
+
dom_node = Funicular::VDOM.render(old_vnode, container)
|
|
89
|
+
|
|
90
|
+
new_vnode = Funicular::VDOM.create_element('p', {}, 'New text')
|
|
91
|
+
patches = Funicular::VDOM.diff(old_vnode, new_vnode)
|
|
92
|
+
|
|
93
|
+
child = container.children[0]
|
|
94
|
+
Funicular::VDOM.patch(child, patches)
|
|
95
|
+
|
|
96
|
+
updated_text = child[:textContent].to_s
|
|
97
|
+
raise "Patch failed: got '#{updated_text}'" unless updated_text == 'New text'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
test_case("Patch attributes") do
|
|
101
|
+
container = doc.getElementById('container')
|
|
102
|
+
container.innerHTML = ''
|
|
103
|
+
|
|
104
|
+
old_vnode = Funicular::VDOM.create_element('div', { class: 'old', id: 'test' })
|
|
105
|
+
dom_node = Funicular::VDOM.render(old_vnode, container)
|
|
106
|
+
|
|
107
|
+
new_vnode = Funicular::VDOM.create_element('div', { class: 'new', id: 'test' })
|
|
108
|
+
patches = Funicular::VDOM.diff(old_vnode, new_vnode)
|
|
109
|
+
|
|
110
|
+
child = container.children[0]
|
|
111
|
+
Funicular::VDOM.patch(child, patches)
|
|
112
|
+
|
|
113
|
+
updated_class = child[:className].to_s
|
|
114
|
+
raise "Patch failed: got '#{updated_class}'" unless updated_class == 'new'
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
test_case("Patch nested elements") do
|
|
118
|
+
container = doc.getElementById('container')
|
|
119
|
+
container.innerHTML = ''
|
|
120
|
+
|
|
121
|
+
old_vnode = Funicular::VDOM.create_element('div', {},
|
|
122
|
+
Funicular::VDOM.create_element('p', {}, 'Old')
|
|
123
|
+
)
|
|
124
|
+
dom_node = Funicular::VDOM.render(old_vnode, container)
|
|
125
|
+
|
|
126
|
+
new_vnode = Funicular::VDOM.create_element('div', {},
|
|
127
|
+
Funicular::VDOM.create_element('p', {}, 'New')
|
|
128
|
+
)
|
|
129
|
+
patches = Funicular::VDOM.diff(old_vnode, new_vnode)
|
|
130
|
+
|
|
131
|
+
child = container.children[0]
|
|
132
|
+
Funicular::VDOM.patch(child, patches)
|
|
133
|
+
|
|
134
|
+
p_element = child[:children][0]
|
|
135
|
+
updated_text = p_element[:textContent].to_s
|
|
136
|
+
raise "Patch failed: got '#{updated_text}'" unless updated_text == 'New'
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
summary = doc.createElement('h2')
|
|
140
|
+
summary.textContent = 'All tests completed!'
|
|
141
|
+
summary.style = 'color: green; margin-top: 20px;'
|
|
142
|
+
results.appendChild(summary)
|
|
143
|
+
</script>
|
|
144
|
+
<script src="../init.iife.js"></script>
|
|
145
|
+
</body>
|
|
146
|
+
</html>
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Funicular Error Boundary Test</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
font-family: Arial, sans-serif;
|
|
9
|
+
padding: 20px;
|
|
10
|
+
max-width: 900px;
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
}
|
|
13
|
+
.test-section {
|
|
14
|
+
margin: 20px 0;
|
|
15
|
+
padding: 15px;
|
|
16
|
+
border: 1px solid #ccc;
|
|
17
|
+
border-radius: 5px;
|
|
18
|
+
}
|
|
19
|
+
.test-result {
|
|
20
|
+
margin: 10px 0;
|
|
21
|
+
padding: 10px;
|
|
22
|
+
border-radius: 4px;
|
|
23
|
+
}
|
|
24
|
+
.test-result.pass {
|
|
25
|
+
background-color: #d4edda;
|
|
26
|
+
color: #155724;
|
|
27
|
+
}
|
|
28
|
+
.test-result.fail {
|
|
29
|
+
background-color: #f8d7da;
|
|
30
|
+
color: #721c24;
|
|
31
|
+
}
|
|
32
|
+
#app {
|
|
33
|
+
margin-top: 20px;
|
|
34
|
+
padding: 20px;
|
|
35
|
+
border: 2px solid #007bff;
|
|
36
|
+
border-radius: 5px;
|
|
37
|
+
}
|
|
38
|
+
button {
|
|
39
|
+
margin: 5px;
|
|
40
|
+
padding: 8px 16px;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
}
|
|
43
|
+
.demo-card {
|
|
44
|
+
border: 1px solid #ddd;
|
|
45
|
+
border-radius: 8px;
|
|
46
|
+
padding: 15px;
|
|
47
|
+
margin: 10px 0;
|
|
48
|
+
background: #f9f9f9;
|
|
49
|
+
}
|
|
50
|
+
.demo-card h3 {
|
|
51
|
+
margin-top: 0;
|
|
52
|
+
color: #333;
|
|
53
|
+
}
|
|
54
|
+
.debug-panel {
|
|
55
|
+
background: #e9ecef;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
padding: 15px;
|
|
58
|
+
margin-top: 20px;
|
|
59
|
+
}
|
|
60
|
+
.debug-panel h4 {
|
|
61
|
+
margin-top: 0;
|
|
62
|
+
color: #495057;
|
|
63
|
+
}
|
|
64
|
+
pre {
|
|
65
|
+
background: #1e1e1e;
|
|
66
|
+
color: #d4d4d4;
|
|
67
|
+
padding: 10px;
|
|
68
|
+
border-radius: 4px;
|
|
69
|
+
overflow-x: auto;
|
|
70
|
+
font-size: 12px;
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<div><a href="../index.html">Back</a></div>
|
|
76
|
+
<h1>Funicular Error Boundary Demo</h1>
|
|
77
|
+
|
|
78
|
+
<p>This demo shows how ErrorBoundary catches errors from child components and displays fallback UI.</p>
|
|
79
|
+
|
|
80
|
+
<div id="test-results"></div>
|
|
81
|
+
<div id="app"></div>
|
|
82
|
+
|
|
83
|
+
<div class="debug-panel">
|
|
84
|
+
<h4>Debug Information</h4>
|
|
85
|
+
<p>Open browser console to see error logs.</p>
|
|
86
|
+
<button id="show-errors">Show Caught Errors</button>
|
|
87
|
+
<button id="show-tree">Show Component Tree</button>
|
|
88
|
+
<pre id="debug-output"></pre>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<script type="text/ruby">
|
|
92
|
+
# A component that always throws an error during render
|
|
93
|
+
class BrokenComponent < Funicular::Component
|
|
94
|
+
def render
|
|
95
|
+
raise "BrokenComponent intentionally threw this error!"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A working component to show mixed content
|
|
100
|
+
class WorkingComponent < Funicular::Component
|
|
101
|
+
def initialize_state
|
|
102
|
+
{ count: 0 }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def increment(event)
|
|
106
|
+
event.preventDefault
|
|
107
|
+
patch(count: state[:count] + 1)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def render
|
|
111
|
+
div(class: 'demo-card') do
|
|
112
|
+
h3 { "Working Component" }
|
|
113
|
+
p { "Count: #{state[:count]}" }
|
|
114
|
+
button(onclick: :increment) { "Increment" }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Main demo application
|
|
120
|
+
class ErrorBoundaryDemo < Funicular::Component
|
|
121
|
+
def render
|
|
122
|
+
error_handler = ->(error, info) {
|
|
123
|
+
puts "[Demo] Error caught by ErrorBoundary:"
|
|
124
|
+
puts " Error: #{error.message}"
|
|
125
|
+
puts " Component: #{info&.dig(:component_class)}"
|
|
126
|
+
}
|
|
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." }
|
|
133
|
+
end
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
div do
|
|
137
|
+
h2 { "Error Boundary Demonstrations" }
|
|
138
|
+
|
|
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)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
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,
|
|
153
|
+
fallback: custom_fallback,
|
|
154
|
+
on_error: error_handler
|
|
155
|
+
) do
|
|
156
|
+
component(BrokenComponent)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
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." }
|
|
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)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
div(style: 'flex: 1; min-width: 200px;') do
|
|
174
|
+
h4 { "Broken:" }
|
|
175
|
+
component(Funicular::ErrorBoundary) do
|
|
176
|
+
component(BrokenComponent)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
div(style: 'flex: 1; min-width: 200px;') do
|
|
181
|
+
h4 { "Also Working:" }
|
|
182
|
+
component(Funicular::ErrorBoundary) do
|
|
183
|
+
component(WorkingComponent)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def log_test(name, passed)
|
|
193
|
+
results = JS.document.getElementById('test-results')
|
|
194
|
+
div = JS.document.createElement('div')
|
|
195
|
+
div.className = passed ? 'test-result pass' : 'test-result fail'
|
|
196
|
+
div.textContent = "#{passed ? 'PASS' : 'FAIL'}: #{name}"
|
|
197
|
+
results.appendChild(div)
|
|
198
|
+
puts "#{passed ? 'PASS' : 'FAIL'}: #{name}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def run_tests
|
|
202
|
+
puts "Starting Error Boundary tests..."
|
|
203
|
+
|
|
204
|
+
# Test 1: ErrorBoundary class exists
|
|
205
|
+
begin
|
|
206
|
+
exists = Funicular.const_defined?(:ErrorBoundary) && Funicular::ErrorBoundary.is_a?(Class)
|
|
207
|
+
log_test("ErrorBoundary class exists", exists)
|
|
208
|
+
rescue => e
|
|
209
|
+
log_test("ErrorBoundary class exists", false)
|
|
210
|
+
puts "Error: #{e.message}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Test 2: ErrorBoundary inherits from Component
|
|
214
|
+
begin
|
|
215
|
+
inherits = Funicular::ErrorBoundary.ancestors.include?(Funicular::Component)
|
|
216
|
+
log_test("ErrorBoundary inherits from Component", inherits)
|
|
217
|
+
rescue => e
|
|
218
|
+
log_test("ErrorBoundary inherits from Component", false)
|
|
219
|
+
puts "Error: #{e.message}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Test 3: ErrorBoundary has initial state
|
|
223
|
+
begin
|
|
224
|
+
boundary = Funicular::ErrorBoundary.new
|
|
225
|
+
has_state = boundary.state[:has_error] == false && boundary.state[:error].nil?
|
|
226
|
+
log_test("ErrorBoundary has correct initial state", has_state)
|
|
227
|
+
rescue => e
|
|
228
|
+
log_test("ErrorBoundary has correct initial state", false)
|
|
229
|
+
puts "Error: #{e.message}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Test 4: catch_error method works
|
|
233
|
+
begin
|
|
234
|
+
boundary = Funicular::ErrorBoundary.new
|
|
235
|
+
test_error = RuntimeError.new("Test error")
|
|
236
|
+
boundary.catch_error(test_error, { component_class: "TestComponent" })
|
|
237
|
+
caught = boundary.state[:has_error] == true && boundary.state[:error] == test_error
|
|
238
|
+
log_test("catch_error updates state correctly", caught)
|
|
239
|
+
rescue => e
|
|
240
|
+
log_test("catch_error updates state correctly", false)
|
|
241
|
+
puts "Error: #{e.message}"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Test 5: Debug module tracks errors
|
|
245
|
+
begin
|
|
246
|
+
Funicular::Debug.clear_errors
|
|
247
|
+
boundary = Funicular::ErrorBoundary.new
|
|
248
|
+
test_error = RuntimeError.new("Debug test error")
|
|
249
|
+
boundary.catch_error(test_error, { component_class: "DebugTestComponent" })
|
|
250
|
+
|
|
251
|
+
last_err = Funicular::Debug.last_error
|
|
252
|
+
tracked = last_err && last_err[:error_message] == "Debug test error"
|
|
253
|
+
log_test("Debug module tracks errors", tracked)
|
|
254
|
+
rescue => e
|
|
255
|
+
log_test("Debug module tracks errors", false)
|
|
256
|
+
puts "Error: #{e.message}"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
puts "Unit tests completed! Starting demo..."
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Run tests first
|
|
263
|
+
run_tests
|
|
264
|
+
|
|
265
|
+
# Setup debug panel buttons
|
|
266
|
+
JS.document.getElementById('show-errors').addEventListener('click') do
|
|
267
|
+
output = JS.document.getElementById('debug-output')
|
|
268
|
+
errors = Funicular::Debug.error_list
|
|
269
|
+
output.textContent = "Caught Errors:\n#{errors}"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
JS.document.getElementById('show-tree').addEventListener('click') do
|
|
273
|
+
output = JS.document.getElementById('debug-output')
|
|
274
|
+
tree = Funicular::Debug.component_tree
|
|
275
|
+
output.textContent = "Component Tree:\n#{tree}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Start the demo application
|
|
279
|
+
container = JS.document.getElementById('app')
|
|
280
|
+
Funicular.start(ErrorBoundaryDemo, container: container)
|
|
281
|
+
</script>
|
|
282
|
+
<script src="../init.iife.js"></script>
|
|
283
|
+
</body>
|
|
284
|
+
</html>
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Funicular Router Test</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: Arial, sans-serif; padding: 20px; }
|
|
8
|
+
nav { margin-bottom: 20px; padding: 10px; background: #f0f0f0; border-radius: 5px; }
|
|
9
|
+
nav a { margin-right: 15px; text-decoration: none; color: #007bff; cursor: pointer; }
|
|
10
|
+
nav a:hover { text-decoration: underline; }
|
|
11
|
+
#app { padding: 20px; border: 2px solid #007bff; border-radius: 5px; min-height: 200px; }
|
|
12
|
+
.page-title { color: #333; margin-bottom: 15px; }
|
|
13
|
+
button { padding: 8px 16px; margin: 5px; cursor: pointer; background: #007bff; color: white; border: none; border-radius: 4px; }
|
|
14
|
+
button:hover { background: #0056b3; }
|
|
15
|
+
.status { margin-top: 10px; padding: 10px; background: #e7f3ff; border-radius: 4px; }
|
|
16
|
+
.info { margin-bottom: 10px; padding: 10px; background: #fff3cd; border-radius: 4px; font-size: 14px; }
|
|
17
|
+
</style>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div><a href="../index.html">Back</a></div>
|
|
21
|
+
<h1>Funicular Router Test</h1>
|
|
22
|
+
|
|
23
|
+
<div class="info">
|
|
24
|
+
This demo uses pathname-based routing (History API).
|
|
25
|
+
Navigation links use <code>Funicular.router.navigate()</code>.
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<nav id="nav"></nav>
|
|
29
|
+
|
|
30
|
+
<div id="app"></div>
|
|
31
|
+
|
|
32
|
+
<script type="text/ruby">
|
|
33
|
+
# Navigation component (shared across all pages)
|
|
34
|
+
class NavComponent < Funicular::Component
|
|
35
|
+
def go_to(path)
|
|
36
|
+
->(event) {
|
|
37
|
+
event.preventDefault
|
|
38
|
+
Funicular.router.navigate(path)
|
|
39
|
+
}
|
|
40
|
+
end
|
|
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' }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class LoginComponent < Funicular::Component
|
|
53
|
+
def initialize_state
|
|
54
|
+
{ username: '' }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def on_username_input(event)
|
|
58
|
+
patch(username: event.target[:value].to_s)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def on_login_click(event)
|
|
62
|
+
event.preventDefault
|
|
63
|
+
Funicular.router.navigate('/home')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def render
|
|
67
|
+
div(class: 'login-page') do
|
|
68
|
+
h2(class: 'page-title') { 'Login Page' }
|
|
69
|
+
|
|
70
|
+
div do
|
|
71
|
+
input(
|
|
72
|
+
type: 'text',
|
|
73
|
+
placeholder: 'Username',
|
|
74
|
+
value: state.username,
|
|
75
|
+
oninput: :on_username_input
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
div do
|
|
80
|
+
button(onclick: :on_login_click) { 'Login' }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
div(class: 'status') do
|
|
84
|
+
"Username: #{state.username}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class HomeComponent < Funicular::Component
|
|
91
|
+
def initialize_state
|
|
92
|
+
{ count: 0 }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def increment(event)
|
|
96
|
+
event.preventDefault
|
|
97
|
+
patch(count: state.count + 1)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def go_to_settings(event)
|
|
101
|
+
event.preventDefault
|
|
102
|
+
Funicular.router.navigate('/settings')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def render
|
|
106
|
+
div(class: 'home-page') do
|
|
107
|
+
h2(class: 'page-title') { 'Home Page' }
|
|
108
|
+
|
|
109
|
+
div do
|
|
110
|
+
"Counter: #{state.count}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
div do
|
|
114
|
+
button(onclick: :increment) { 'Increment' }
|
|
115
|
+
button(onclick: :go_to_settings) { 'Go to Settings' }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
div(class: 'status') do
|
|
119
|
+
"You are on the home page. Click around to test routing!"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class SettingsComponent < Funicular::Component
|
|
126
|
+
def initialize_state
|
|
127
|
+
{ theme: 'light', notifications: true }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def toggle_theme(event)
|
|
131
|
+
event.preventDefault
|
|
132
|
+
new_theme = state.theme == 'light' ? 'dark' : 'light'
|
|
133
|
+
patch(theme: new_theme)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def toggle_notifications(event)
|
|
137
|
+
event.preventDefault
|
|
138
|
+
patch(notifications: !state.notifications)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def go_to_home(event)
|
|
142
|
+
event.preventDefault
|
|
143
|
+
Funicular.router.navigate('/home')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def render
|
|
147
|
+
div(class: 'settings-page') do
|
|
148
|
+
h2(class: 'page-title') { 'Settings Page' }
|
|
149
|
+
|
|
150
|
+
div do
|
|
151
|
+
span { "Theme: #{state.theme}" }
|
|
152
|
+
button(onclick: :toggle_theme) { 'Toggle Theme' }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
div do
|
|
156
|
+
span { "Notifications: #{state.notifications ? 'ON' : 'OFF'}" }
|
|
157
|
+
button(onclick: :toggle_notifications) { 'Toggle Notifications' }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
div do
|
|
161
|
+
button(onclick: :go_to_home) { 'Back to Home' }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
div(class: 'status') do
|
|
165
|
+
"Configure your settings here"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Demonstrates dynamic route with :id parameter
|
|
172
|
+
class UsersComponent < Funicular::Component
|
|
173
|
+
def initialize_state
|
|
174
|
+
{ users: [
|
|
175
|
+
{ id: 1, name: 'Alice' },
|
|
176
|
+
{ id: 2, name: 'Bob' },
|
|
177
|
+
{ id: 3, name: 'Charlie' }
|
|
178
|
+
]}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def go_to_user(id)
|
|
182
|
+
->(event) {
|
|
183
|
+
event.preventDefault
|
|
184
|
+
Funicular.router.navigate("/users/#{id}")
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def render
|
|
189
|
+
div(class: 'users-page') do
|
|
190
|
+
h2(class: 'page-title') { 'Users List' }
|
|
191
|
+
|
|
192
|
+
ul do
|
|
193
|
+
state.users.each do |user|
|
|
194
|
+
li do
|
|
195
|
+
a(href: "/users/#{user[:id]}", onclick: go_to_user(user[:id])) do
|
|
196
|
+
user[:name]
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
div(class: 'status') do
|
|
203
|
+
"Click a user to see dynamic route with :id parameter"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
class UserDetailComponent < Funicular::Component
|
|
210
|
+
def initialize_state
|
|
211
|
+
{ user_id: @props[:id] }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def go_back(event)
|
|
215
|
+
event.preventDefault
|
|
216
|
+
Funicular.router.navigate('/users')
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def render
|
|
220
|
+
div(class: 'user-detail-page') do
|
|
221
|
+
h2(class: 'page-title') { "User Detail" }
|
|
222
|
+
|
|
223
|
+
div do
|
|
224
|
+
"Viewing user ID: #{state.user_id}"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
div do
|
|
228
|
+
button(onclick: :go_back) { 'Back to Users' }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
div(class: 'status') do
|
|
232
|
+
"This page demonstrates dynamic routing with params[:id] = #{state.user_id}"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Start router with Rails-style DSL
|
|
239
|
+
puts "Starting Funicular Router..."
|
|
240
|
+
Funicular.start(container: 'app') do |router|
|
|
241
|
+
# Rails-style route definitions with named routes
|
|
242
|
+
router.get '/login', to: LoginComponent, as: :login
|
|
243
|
+
router.get '/home', to: HomeComponent, as: :home
|
|
244
|
+
router.get '/settings', to: SettingsComponent, as: :settings
|
|
245
|
+
router.get '/users', to: UsersComponent, as: :users
|
|
246
|
+
router.get '/users/:id', to: UserDetailComponent, as: :user
|
|
247
|
+
|
|
248
|
+
router.set_default('/login')
|
|
249
|
+
end
|
|
250
|
+
puts "Router started!"
|
|
251
|
+
|
|
252
|
+
# Mount navigation component separately
|
|
253
|
+
NavComponent.new.mount(JS.global.document.getElementById('nav'))
|
|
254
|
+
</script>
|
|
255
|
+
<script src="../init.iife.js"></script>
|
|
256
|
+
</body>
|
|
257
|
+
</html>
|