funicular 0.3.0 → 0.5.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 +486 -1
- data/demo/local_notes.html +207 -0
- data/demo/test_chartjs.html +9 -9
- data/demo/test_component.html +8 -8
- data/demo/test_error_boundary.html +44 -41
- data/demo/test_router.html +48 -48
- data/demo/tic-tac-toe.html +25 -25
- data/docs/architecture.md +227 -12
- data/docs/local_database.md +1035 -0
- data/lib/funicular/assets/funicular.rb +14 -0
- data/lib/funicular/configuration.rb +65 -0
- data/lib/funicular/epoch_header.rb +69 -0
- data/lib/funicular/epoch_stamping.rb +66 -0
- data/lib/funicular/helpers/picoruby_helper.rb +96 -1
- data/lib/funicular/railtie.rb +30 -0
- data/lib/funicular/schema.rb +45 -12
- data/lib/funicular/session_epoch.rb +110 -0
- data/lib/funicular/ssr/runtime.rb +58 -12
- data/lib/funicular/ssr.rb +25 -0
- data/lib/funicular/testing/node_runner.mjs +19 -0
- data/lib/funicular/testing.rb +47 -0
- data/lib/funicular/vendor/mrbc/VERSION +1 -1
- data/lib/funicular/vendor/mrbc/mrbc.js +655 -574
- data/lib/funicular/vendor/mrbc/mrbc.wasm +0 -0
- data/lib/funicular/vendor/picoruby/VERSION +1 -1
- data/lib/funicular/vendor/picoruby/debug/picoruby.js +800 -530
- data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby/dist/picoruby.js +2 -2
- 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 +2 -6909
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
- data/lib/funicular/version.rb +1 -1
- data/lib/funicular.rb +1 -0
- data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +37 -38
- data/lib/tasks/funicular.rake +10 -2
- data/minitest/callback_error_visibility_test.rb +48 -0
- data/minitest/configuration_test.rb +78 -0
- data/minitest/dsl_test.rb +264 -0
- data/minitest/epoch_header_test.rb +149 -0
- data/minitest/epoch_stamping_test.rb +225 -0
- data/minitest/fixtures/funicular_app/components/greeting_component.rb +5 -5
- data/minitest/fixtures/funicular_app/components/probe_component.rb +15 -0
- data/minitest/form_for_test.rb +2 -2
- data/minitest/hydration_test.rb +2 -2
- data/minitest/navigation_guard_test.rb +65 -0
- data/minitest/picoruby_helper_test.rb +236 -0
- data/minitest/schema_test.rb +47 -0
- data/minitest/session_epoch_test.rb +122 -0
- data/minitest/sig_tags_test.rb +30 -0
- data/minitest/ssr_database_test.rb +78 -0
- data/minitest/ssr_reload_test.rb +106 -0
- data/minitest/ssr_test.rb +41 -0
- data/minitest/testing_ensure_compiled_test.rb +52 -0
- data/minitest/validations_test.rb +35 -5
- data/minitest/view_context_test.rb +15 -15
- data/mrbgem.rake +2 -0
- data/mrblib/0_tags.rb +62 -0
- data/mrblib/cable.rb +1 -1
- data/mrblib/component.rb +226 -24
- data/mrblib/db.rb +3116 -0
- data/mrblib/error_boundary.rb +25 -19
- data/mrblib/file_upload.rb +17 -7
- data/mrblib/form_builder.rb +10 -10
- data/mrblib/funicular.rb +136 -17
- data/mrblib/http.rb +84 -107
- data/mrblib/model.rb +1178 -23
- data/mrblib/relation.rb +342 -0
- data/mrblib/router.rb +45 -4
- data/mrblib/styles.rb +122 -12
- data/mrblib/view_context.rb +3 -32
- data/sig/component.rbs +25 -4
- data/sig/db.rbs +328 -0
- data/sig/error_boundary.rbs +4 -4
- data/sig/funicular.rbs +5 -0
- data/sig/http.rbs +8 -21
- data/sig/model.rbs +101 -7
- data/sig/relation.rbs +44 -0
- data/sig/router.rbs +1 -0
- data/sig/styles.rbs +19 -5
- data/sig/tags.rbs +54 -0
- data/sig/view_context.rbs +47 -34
- metadata +23 -2
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +0 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Local Notes - Funicular</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<div><a href="../index.html">Back</a></div>
|
|
9
|
+
<h1>Local Notes (Funicular local database)</h1>
|
|
10
|
+
<!-- A page declaring storage :local models must choose its
|
|
11
|
+
namespace explicitly (docs decision 12) or the boot fails
|
|
12
|
+
loud. A Rails app gets these attributes from
|
|
13
|
+
picoruby_include_tag; this static page carries them by hand. -->
|
|
14
|
+
<div id="app"
|
|
15
|
+
data-funicular-local-database="true"
|
|
16
|
+
data-funicular-application-id="local_notes_demo"
|
|
17
|
+
data-funicular-anonymous-only="true"></div>
|
|
18
|
+
|
|
19
|
+
<div class="explain">
|
|
20
|
+
<p>A server-less demo of the local database layer:
|
|
21
|
+
<code>storage :local</code>, <code>.local</code> queries,
|
|
22
|
+
<code>watch</code>, snapshot persistence, the single-writer
|
|
23
|
+
election, and <code>Funicular::DB.wipe</code>.</p>
|
|
24
|
+
<ul>
|
|
25
|
+
<li><b>Add some notes, then reload the page.</b> The writer tab
|
|
26
|
+
snapshots the database into IndexedDB about half a second
|
|
27
|
+
after the last write, and the next visit restores it.</li>
|
|
28
|
+
<li><b>Open this page in a second tab.</b> Only one tab per
|
|
29
|
+
browser profile wins the writer election; the other becomes a
|
|
30
|
+
persistent reader whose writes raise. Close the writer tab
|
|
31
|
+
and reload the reader to take over.</li>
|
|
32
|
+
<li><b>Press "Wipe everything".</b> Both databases of this
|
|
33
|
+
namespace are dropped and rebuilt empty, the stored snapshots
|
|
34
|
+
are deleted, and the watcher re-renders onto empty tables.</li>
|
|
35
|
+
<li>The list re-renders through
|
|
36
|
+
<code>watch(:notes) { Note.local.order(created_at: :desc) }</code>
|
|
37
|
+
after every change event; no manual state bookkeeping.</li>
|
|
38
|
+
</ul>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<script type="text/ruby">
|
|
42
|
+
class Note < Funicular::Model
|
|
43
|
+
table_name "local_notes"
|
|
44
|
+
storage :local do
|
|
45
|
+
migrate 1 do |t|
|
|
46
|
+
t.string :title
|
|
47
|
+
t.timestamps
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class NotesApp < Funicular::Component
|
|
53
|
+
READER_MESSAGE =
|
|
54
|
+
"This tab is a persistent reader: another tab holds the " \
|
|
55
|
+
"writer lock. Close the writer tab and reload to write here."
|
|
56
|
+
|
|
57
|
+
def initialize_state
|
|
58
|
+
# start boots the database before anything mounts, so the
|
|
59
|
+
# durability verdict is already final here.
|
|
60
|
+
{
|
|
61
|
+
notes: [],
|
|
62
|
+
message: "",
|
|
63
|
+
durability: Funicular::DB.durability,
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def component_mounted
|
|
68
|
+
watch(:notes) { Note.local.order(created_at: :desc) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def handle_add(event)
|
|
72
|
+
event.preventDefault
|
|
73
|
+
field = JS.document.getElementById("note-title")
|
|
74
|
+
title = field[:value].to_s
|
|
75
|
+
return if title.empty?
|
|
76
|
+
begin
|
|
77
|
+
Note.create(title: title)
|
|
78
|
+
field[:value] = ""
|
|
79
|
+
patch(message: "")
|
|
80
|
+
rescue Funicular::DB::ReadOnlyTabError
|
|
81
|
+
patch(message: READER_MESSAGE)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def handle_delete(note)
|
|
86
|
+
-> {
|
|
87
|
+
begin
|
|
88
|
+
note.destroy
|
|
89
|
+
patch(message: "")
|
|
90
|
+
rescue Funicular::DB::ReadOnlyTabError
|
|
91
|
+
patch(message: READER_MESSAGE)
|
|
92
|
+
end
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def handle_wipe(event)
|
|
97
|
+
event.preventDefault
|
|
98
|
+
begin
|
|
99
|
+
Funicular::DB.wipe
|
|
100
|
+
patch(message: "Wiped: tables rebuilt empty, snapshots deleted.")
|
|
101
|
+
rescue Funicular::DB::ReadOnlyTabError
|
|
102
|
+
patch(message: READER_MESSAGE)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render
|
|
107
|
+
notes = state[:notes]
|
|
108
|
+
div(class: "notes-app") do
|
|
109
|
+
p(class: "status") do
|
|
110
|
+
"durability: #{state[:durability]} | notes: #{notes.size}"
|
|
111
|
+
end
|
|
112
|
+
div(class: "composer") do
|
|
113
|
+
# A placeholder disappears as soon as text is typed, so it
|
|
114
|
+
# cannot be the field's accessible name.
|
|
115
|
+
label(for: "note-title") { "Note title" }
|
|
116
|
+
input(type: "text", id: "note-title",
|
|
117
|
+
placeholder: "What to remember?")
|
|
118
|
+
button(onclick: :handle_add) { "Add" }
|
|
119
|
+
button(onclick: :handle_wipe, class: "danger") do
|
|
120
|
+
"Wipe everything"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
p(class: "message") { state[:message] }
|
|
124
|
+
ul(class: "notes") do
|
|
125
|
+
notes.each do |note|
|
|
126
|
+
li(key: note.id) do
|
|
127
|
+
span(class: "title") { note.title }
|
|
128
|
+
span(class: "stamp") { note.created_at.to_s }
|
|
129
|
+
button(onclick: handle_delete(note), class: "delete") do
|
|
130
|
+
"delete"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# No load_schemas round: a local-only app boots right inside
|
|
140
|
+
# start's gate. The namespace comes from the hand-written
|
|
141
|
+
# data-funicular-* attributes on the #app container above: pages
|
|
142
|
+
# with storage :local models must declare anonymous_only (or a
|
|
143
|
+
# user key) explicitly -- a silent shared default would merge
|
|
144
|
+
# every user's data, so the boot fails loud without it.
|
|
145
|
+
Funicular.start(NotesApp, container: 'app')
|
|
146
|
+
</script>
|
|
147
|
+
<script src="../init.iife.js"></script>
|
|
148
|
+
</body>
|
|
149
|
+
<style>
|
|
150
|
+
.notes-app {
|
|
151
|
+
max-width: 560px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.status {
|
|
155
|
+
color: #555;
|
|
156
|
+
font-family: monospace;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.composer input {
|
|
160
|
+
margin-right: 6px;
|
|
161
|
+
padding: 4px 6px;
|
|
162
|
+
width: 260px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.composer button {
|
|
166
|
+
margin-right: 6px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.composer .danger {
|
|
170
|
+
color: #a00;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.message {
|
|
174
|
+
color: #a00;
|
|
175
|
+
min-height: 1.2em;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
ul.notes {
|
|
179
|
+
list-style: none;
|
|
180
|
+
padding: 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
ul.notes li {
|
|
184
|
+
border-bottom: 1px solid #ddd;
|
|
185
|
+
display: flex;
|
|
186
|
+
gap: 10px;
|
|
187
|
+
padding: 6px 2px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
ul.notes .title {
|
|
191
|
+
flex: 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
ul.notes .stamp {
|
|
195
|
+
color: #888;
|
|
196
|
+
font-size: 0.85em;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.explain {
|
|
200
|
+
border-top: 1px solid #ccc;
|
|
201
|
+
color: #333;
|
|
202
|
+
margin-top: 24px;
|
|
203
|
+
max-width: 640px;
|
|
204
|
+
padding-top: 12px;
|
|
205
|
+
}
|
|
206
|
+
</style>
|
|
207
|
+
</html>
|
data/demo/test_chartjs.html
CHANGED
|
@@ -118,17 +118,17 @@
|
|
|
118
118
|
puts "update_chart: Chart.js update('active') called"
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
-
def render
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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")
|
|
126
126
|
end
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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" }
|
|
130
130
|
end
|
|
131
|
-
|
|
131
|
+
div(id: "info") do
|
|
132
132
|
current = state[:data_sets][state[:month_index]]
|
|
133
133
|
"Current data: #{current[:labels].join(', ')}"
|
|
134
134
|
end
|
data/demo/test_component.html
CHANGED
|
@@ -48,17 +48,17 @@
|
|
|
48
48
|
patch(count: state[:count] - 1)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
def render
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
def render
|
|
52
|
+
div(id: "counter-app") do
|
|
53
|
+
h2 { "Counter: #{state[:count]}" }
|
|
54
|
+
div(id: "status") do
|
|
55
55
|
"Mounted: #{state[:mounted]}"
|
|
56
56
|
end
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
div do
|
|
58
|
+
button(id: "increment-btn", onclick: :increment) { "+1" }
|
|
59
|
+
button(id: "decrement-btn", onclick: :decrement) { "-1" }
|
|
60
60
|
end
|
|
61
|
-
|
|
61
|
+
div(ref: :display, id: "display") do
|
|
62
62
|
"Current value: #{state[:count]}"
|
|
63
63
|
end
|
|
64
64
|
end
|
|
@@ -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
|
|
95
95
|
raise "BrokenComponent intentionally threw this error!"
|
|
96
96
|
end
|
|
97
97
|
end
|
|
@@ -107,80 +107,83 @@
|
|
|
107
107
|
patch(count: state[:count] + 1)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
def render
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
def render
|
|
111
|
+
div(class: 'demo-card') do
|
|
112
|
+
h3 { "Working Component" }
|
|
113
|
+
p { "Count: #{state[:count]}" }
|
|
114
|
+
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
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
# NOTE: an ErrorBoundary fallback runs during the BOUNDARY's render,
|
|
129
|
+
# not this component's, so it cannot use bareword tags. It receives
|
|
130
|
+
# the boundary's view context and builds elements through it.
|
|
131
|
+
custom_fallback = ->(h, error) {
|
|
132
|
+
h.div(style: 'background: linear-gradient(135deg, #ff6b6b, #ee5a5a); color: white; padding: 20px; border-radius: 8px;') do
|
|
133
|
+
h.h3(style: 'margin-top: 0;') { "Custom Error Handler" }
|
|
134
|
+
h.p { "Caught: #{error.message}" }
|
|
135
|
+
h.p(style: 'font-size: 12px; opacity: 0.8;') { "This is a custom fallback provided via props." }
|
|
133
136
|
end
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
div do
|
|
140
|
+
h2 { "Error Boundary Demonstrations" }
|
|
138
141
|
|
|
139
142
|
# Demo 1: Default fallback
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
div(class: 'demo-card') do
|
|
144
|
+
h3 { "Demo 1: Default Fallback UI" }
|
|
145
|
+
p { "ErrorBoundary wraps a component that always throws. Default fallback is shown." }
|
|
146
|
+
component(Funicular::ErrorBoundary, on_error: error_handler) do
|
|
147
|
+
component(BrokenComponent)
|
|
145
148
|
end
|
|
146
149
|
end
|
|
147
150
|
|
|
148
151
|
# Demo 2: Custom fallback
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
div(class: 'demo-card') do
|
|
153
|
+
h3 { "Demo 2: Custom Fallback UI" }
|
|
154
|
+
p { "ErrorBoundary with a custom fallback prop." }
|
|
155
|
+
component(Funicular::ErrorBoundary,
|
|
153
156
|
fallback: custom_fallback,
|
|
154
157
|
on_error: error_handler
|
|
155
|
-
) do
|
|
156
|
-
|
|
158
|
+
) do
|
|
159
|
+
component(BrokenComponent)
|
|
157
160
|
end
|
|
158
161
|
end
|
|
159
162
|
|
|
160
163
|
# Demo 3: Mixed content - some work, some fail
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
div(class: 'demo-card') do
|
|
165
|
+
h3 { "Demo 3: Isolated Error Boundaries" }
|
|
166
|
+
p { "Each component has its own ErrorBoundary. One failure doesn't affect others." }
|
|
164
167
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
div(style: 'display: flex; gap: 15px; flex-wrap: wrap;') do
|
|
169
|
+
div(style: 'flex: 1; min-width: 200px;') do
|
|
170
|
+
h4 { "Working:" }
|
|
171
|
+
component(Funicular::ErrorBoundary) do
|
|
172
|
+
component(WorkingComponent)
|
|
170
173
|
end
|
|
171
174
|
end
|
|
172
175
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
div(style: 'flex: 1; min-width: 200px;') do
|
|
177
|
+
h4 { "Broken:" }
|
|
178
|
+
component(Funicular::ErrorBoundary) do
|
|
179
|
+
component(BrokenComponent)
|
|
177
180
|
end
|
|
178
181
|
end
|
|
179
182
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
div(style: 'flex: 1; min-width: 200px;') do
|
|
184
|
+
h4 { "Also Working:" }
|
|
185
|
+
component(Funicular::ErrorBoundary) do
|
|
186
|
+
component(WorkingComponent)
|
|
184
187
|
end
|
|
185
188
|
end
|
|
186
189
|
end
|
data/demo/test_router.html
CHANGED
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
}
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def render
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
48
|
end
|
|
49
49
|
end
|
|
50
50
|
end
|
|
@@ -63,12 +63,12 @@
|
|
|
63
63
|
Funicular.router.navigate('/home')
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
def render
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
def render
|
|
67
|
+
div(class: 'login-page') do
|
|
68
|
+
h2(class: 'page-title') { 'Login Page' }
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
div do
|
|
71
|
+
input(
|
|
72
72
|
type: 'text',
|
|
73
73
|
placeholder: 'Username',
|
|
74
74
|
value: state[:username],
|
|
@@ -76,11 +76,11 @@
|
|
|
76
76
|
)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
div do
|
|
80
|
+
button(onclick: :on_login_click) { 'Login' }
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
div(class: 'status') do
|
|
84
84
|
"Username: #{state[:username]}"
|
|
85
85
|
end
|
|
86
86
|
end
|
|
@@ -102,20 +102,20 @@
|
|
|
102
102
|
Funicular.router.navigate('/settings')
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
-
def render
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
def render
|
|
106
|
+
div(class: 'home-page') do
|
|
107
|
+
h2(class: 'page-title') { 'Home Page' }
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
div do
|
|
110
110
|
"Counter: #{state[:count]}"
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
div do
|
|
114
|
+
button(onclick: :increment) { 'Increment' }
|
|
115
|
+
button(onclick: :go_to_settings) { 'Go to Settings' }
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
div(class: 'status') do
|
|
119
119
|
"You are on the home page. Click around to test routing!"
|
|
120
120
|
end
|
|
121
121
|
end
|
|
@@ -143,25 +143,25 @@
|
|
|
143
143
|
Funicular.router.navigate('/home')
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
-
def render
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
def render
|
|
147
|
+
div(class: 'settings-page') do
|
|
148
|
+
h2(class: 'page-title') { 'Settings Page' }
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
div do
|
|
151
|
+
span { "Theme: #{state[:theme]}" }
|
|
152
|
+
button(onclick: :toggle_theme) { 'Toggle Theme' }
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
div do
|
|
156
|
+
span { "Notifications: #{state[:notifications] ? 'ON' : 'OFF'}" }
|
|
157
|
+
button(onclick: :toggle_notifications) { 'Toggle Notifications' }
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
div do
|
|
161
|
+
button(onclick: :go_to_home) { 'Back to Home' }
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
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
|
-
|
|
190
|
-
|
|
188
|
+
def render
|
|
189
|
+
div(class: 'users-page') do
|
|
190
|
+
h2(class: 'page-title') { 'Users List' }
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
ul do
|
|
193
193
|
state[:users].each do |user|
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
li do
|
|
195
|
+
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
|
-
|
|
202
|
+
div(class: 'status') do
|
|
203
203
|
"Click a user to see dynamic route with :id parameter"
|
|
204
204
|
end
|
|
205
205
|
end
|
|
@@ -216,19 +216,19 @@
|
|
|
216
216
|
Funicular.router.navigate('/users')
|
|
217
217
|
end
|
|
218
218
|
|
|
219
|
-
def render
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
def render
|
|
220
|
+
div(class: 'user-detail-page') do
|
|
221
|
+
h2(class: 'page-title') { "User Detail" }
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
div do
|
|
224
224
|
"Viewing user ID: #{state[:user_id]}"
|
|
225
225
|
end
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
div do
|
|
228
|
+
button(onclick: :go_back) { 'Back to Users' }
|
|
229
229
|
end
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
div(class: 'status') do
|
|
232
232
|
"This page demonstrates dynamic routing with params[:id] = #{state[:user_id]}"
|
|
233
233
|
end
|
|
234
234
|
end
|
data/demo/tic-tac-toe.html
CHANGED
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
on_click.call if on_click
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def render
|
|
42
|
-
|
|
41
|
+
def render
|
|
42
|
+
button(class: 'square', onclick: :handle_click) do
|
|
43
43
|
props[:value] || ''
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
}
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
def render
|
|
58
|
+
def render
|
|
59
59
|
squares = props[:squares]
|
|
60
60
|
x_is_next = props[:x_is_next]
|
|
61
61
|
|
|
@@ -66,22 +66,22 @@
|
|
|
66
66
|
"Next player: #{x_is_next ? 'X' : 'O'}"
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
div do
|
|
70
|
+
div(class: 'status') { status }
|
|
71
|
+
div(class: 'board-row') do
|
|
72
|
+
component(Square, value: squares[0], on_click: handle_click(0))
|
|
73
|
+
component(Square, value: squares[1], on_click: handle_click(1))
|
|
74
|
+
component(Square, value: squares[2], on_click: handle_click(2))
|
|
75
75
|
end
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
div(class: 'board-row') do
|
|
77
|
+
component(Square, value: squares[3], on_click: handle_click(3))
|
|
78
|
+
component(Square, value: squares[4], on_click: handle_click(4))
|
|
79
|
+
component(Square, value: squares[5], on_click: handle_click(5))
|
|
80
80
|
end
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
div(class: 'board-row') do
|
|
82
|
+
component(Square, value: squares[6], on_click: handle_click(6))
|
|
83
|
+
component(Square, value: squares[7], on_click: handle_click(7))
|
|
84
|
+
component(Square, value: squares[8], on_click: handle_click(8))
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
end
|
|
@@ -115,30 +115,30 @@
|
|
|
115
115
|
}
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
def render
|
|
118
|
+
def render
|
|
119
119
|
history = state[:history]
|
|
120
120
|
current_move = state[:current_move]
|
|
121
121
|
x_is_next = (current_move % 2 == 0)
|
|
122
122
|
current_squares = history[current_move]
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
div(class: 'game') do
|
|
125
|
+
div(class: 'game-board') do
|
|
126
|
+
component(Board,
|
|
127
127
|
x_is_next: x_is_next,
|
|
128
128
|
squares: current_squares,
|
|
129
129
|
on_play: ->(i) { handle_play(i) }
|
|
130
130
|
)
|
|
131
131
|
end
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
div(class: 'game-info') do
|
|
133
|
+
ol do
|
|
134
134
|
history.each_with_index do |_squares, move|
|
|
135
135
|
description = if move > 0
|
|
136
136
|
"Go to move ##{move}"
|
|
137
137
|
else
|
|
138
138
|
'Go to game start'
|
|
139
139
|
end
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
li(key: move) do
|
|
141
|
+
button(onclick: jump_to(move)) { description }
|
|
142
142
|
end
|
|
143
143
|
end
|
|
144
144
|
end
|