funicular 0.1.0 → 0.2.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 +24 -0
- data/README.md +10 -2
- data/Rakefile +29 -0
- data/docs/architecture.md +113 -404
- data/lib/funicular/assets/funicular.css +23 -0
- data/lib/funicular/compiler.rb +23 -15
- data/lib/funicular/helpers/picoruby_helper.rb +65 -3
- data/lib/funicular/middleware.rb +34 -9
- data/lib/funicular/plugin.rb +147 -0
- data/lib/funicular/schema.rb +167 -0
- data/lib/funicular/ssr/runtime.rb +101 -0
- data/lib/funicular/ssr.rb +51 -0
- data/lib/funicular/testing/node_runner.mjs +293 -0
- data/lib/funicular/testing/node_runner.rb +190 -0
- data/lib/funicular/testing.rb +22 -0
- data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
- data/lib/funicular/vendor/picoruby/debug/picoruby.js +94 -75
- data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
- 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 -0
- data/lib/funicular/vendor/picoruby-test-node/picoruby.js +6885 -0
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +1 -0
- data/lib/funicular/version.rb +1 -1
- data/lib/funicular.rb +3 -0
- data/lib/generators/funicular/chat/chat_generator.rb +104 -0
- data/lib/generators/funicular/chat/templates/application_cable_channel.rb.tt +4 -0
- data/lib/generators/funicular/chat/templates/application_cable_connection.rb.tt +4 -0
- data/lib/generators/funicular/chat/templates/create_funicular_chat_messages.rb.tt +10 -0
- data/lib/generators/funicular/chat/templates/funicular_chat.css.tt +141 -0
- data/lib/generators/funicular/chat/templates/funicular_chat_channel.rb.tt +5 -0
- data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +135 -0
- data/lib/generators/funicular/chat/templates/funicular_chat_component_picotest.rb.tt +64 -0
- data/lib/generators/funicular/chat/templates/funicular_chat_controller.rb.tt +4 -0
- data/lib/generators/funicular/chat/templates/funicular_chat_message.rb.tt +13 -0
- data/lib/generators/funicular/chat/templates/funicular_chat_messages_controller.rb.tt +23 -0
- data/lib/generators/funicular/chat/templates/initializer.rb.tt +4 -0
- data/lib/generators/funicular/chat/templates/show.html.erb.tt +6 -0
- data/lib/tasks/funicular.rake +87 -4
- data/minitest/fixtures/funicular_app/components/greeting_component.rb +16 -0
- data/minitest/fixtures/funicular_app/initializer.rb +5 -0
- data/minitest/hydration_test.rb +87 -0
- data/minitest/plugin_test.rb +51 -0
- data/minitest/schema_test.rb +106 -0
- data/minitest/ssr_test.rb +94 -0
- data/minitest/validations_test.rb +183 -0
- data/mrbgem.rake +1 -0
- data/mrblib/0_validations.rb +206 -0
- data/mrblib/1_validators.rb +180 -0
- data/mrblib/cable.rb +24 -9
- data/mrblib/component.rb +172 -33
- data/mrblib/debug.rb +3 -0
- data/mrblib/differ.rb +47 -37
- data/mrblib/file_upload.rb +9 -1
- data/mrblib/form_builder.rb +21 -5
- data/mrblib/funicular.rb +97 -8
- data/mrblib/html_serializer.rb +121 -0
- data/mrblib/http.rb +123 -29
- data/mrblib/model.rb +50 -0
- data/mrblib/patcher.rb +74 -8
- data/mrblib/router.rb +40 -3
- data/mrblib/store.rb +304 -0
- data/mrblib/store_collection.rb +171 -0
- data/mrblib/store_singleton.rb +79 -0
- data/sig/cable.rbs +1 -0
- data/sig/component.rbs +13 -5
- data/sig/funicular.rbs +14 -1
- data/sig/html_serializer.rbs +20 -0
- data/sig/http.rbs +21 -6
- data/sig/model.rbs +6 -1
- data/sig/patcher.rbs +4 -1
- data/sig/router.rbs +3 -2
- data/sig/store.rbs +89 -0
- data/sig/store_collection.rbs +43 -0
- data/sig/store_singleton.rbs +19 -0
- data/sig/validations.rbs +103 -0
- data/sig/vdom.rbs +6 -6
- metadata +47 -12
- data/docs/README.md +0 -419
- data/docs/advanced-features.md +0 -632
- data/docs/components-and-state.md +0 -539
- data/docs/data-fetching.md +0 -528
- data/docs/forms.md +0 -446
- data/docs/rails-integration.md +0 -426
- data/docs/realtime.md +0 -543
- data/docs/routing-and-navigation.md +0 -427
- data/docs/styling.md +0 -285
|
@@ -1,539 +0,0 @@
|
|
|
1
|
-
# Components and State Management
|
|
2
|
-
|
|
3
|
-
This guide covers how to create components, manage state, and handle component lifecycle in Funicular.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
- [Creating Components](#creating-components)
|
|
8
|
-
- [Event Handlers](#event-handlers)
|
|
9
|
-
- [State Management](#state-management)
|
|
10
|
-
- [Props](#props)
|
|
11
|
-
- [Component Lifecycle](#component-lifecycle)
|
|
12
|
-
- [Component Communication](#component-communication)
|
|
13
|
-
- [Refs](#refs)
|
|
14
|
-
|
|
15
|
-
## Creating Components
|
|
16
|
-
|
|
17
|
-
Components are Ruby classes that inherit from `Funicular::Component`. Each component must implement a `render` method that returns a VDOM representation of the UI.
|
|
18
|
-
|
|
19
|
-
### Basic Component
|
|
20
|
-
|
|
21
|
-
```ruby
|
|
22
|
-
class Counter < Funicular::Component
|
|
23
|
-
def initialize_state
|
|
24
|
-
{ count: 0 }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def increment
|
|
28
|
-
patch(count: state.count + 1)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def decrement
|
|
32
|
-
patch(count: state.count - 1)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def render
|
|
36
|
-
div do
|
|
37
|
-
h1 { "Counter" }
|
|
38
|
-
p { "Current count: #{state.count}" }
|
|
39
|
-
button(onclick: :increment) { "Increment" }
|
|
40
|
-
button(onclick: :decrement) { "Decrement" }
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### Mounting a Component
|
|
47
|
-
|
|
48
|
-
```ruby
|
|
49
|
-
# Mount to a container element
|
|
50
|
-
Funicular.start(Counter, container: "app")
|
|
51
|
-
|
|
52
|
-
# Or use the router (recommended for SPAs)
|
|
53
|
-
Funicular.start(container: 'app') do |router|
|
|
54
|
-
router.get('/counter', to: Counter, as: 'counter')
|
|
55
|
-
end
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Event Handlers
|
|
59
|
-
|
|
60
|
-
Funicular provides a unified callback system for handling events. All event handlers accept **Symbol**, **Method**, or **Proc**.
|
|
61
|
-
|
|
62
|
-
### Three Ways to Handle Events
|
|
63
|
-
|
|
64
|
-
```ruby
|
|
65
|
-
class ExampleComponent < Funicular::Component
|
|
66
|
-
def handle_click(event)
|
|
67
|
-
puts "Button clicked!"
|
|
68
|
-
patch(clicked: true)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def render
|
|
72
|
-
div do
|
|
73
|
-
# 1. Symbol (recommended for method references)
|
|
74
|
-
button(onclick: :handle_click) { "Click Me" }
|
|
75
|
-
|
|
76
|
-
# 2. Method object (useful for passing to child components)
|
|
77
|
-
button(onclick: method(:handle_click)) { "Click Me" }
|
|
78
|
-
|
|
79
|
-
# 3. Proc/Lambda (for inline logic)
|
|
80
|
-
button(onclick: -> { patch(count: state.count + 1) }) { "Increment" }
|
|
81
|
-
button(onclick: ->(e) { puts e.target[:value] }) { "With Event" }
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### When to Use Each
|
|
88
|
-
|
|
89
|
-
**Use Symbol (`:method_name`)** - Most common:
|
|
90
|
-
- Clean and concise
|
|
91
|
-
- Best for method references
|
|
92
|
-
- Works for all event types
|
|
93
|
-
|
|
94
|
-
```ruby
|
|
95
|
-
button(onclick: :handle_submit) { "Submit" }
|
|
96
|
-
input(oninput: :handle_input, onchange: :handle_change)
|
|
97
|
-
form_for(:user, on_submit: :handle_submit)
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
**Use Method (`method(:name)`)** - For passing to child components:
|
|
101
|
-
- Explicitly creates a Method object
|
|
102
|
-
- Preserves `self` binding
|
|
103
|
-
- Useful when passing callbacks as props
|
|
104
|
-
|
|
105
|
-
```ruby
|
|
106
|
-
component(ChildComponent, {
|
|
107
|
-
on_delete: method(:handle_delete),
|
|
108
|
-
on_update: method(:handle_update)
|
|
109
|
-
})
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
**Use Proc/Lambda** - For inline logic:
|
|
113
|
-
- Short, one-off handlers
|
|
114
|
-
- Direct state updates
|
|
115
|
-
- Complex logic that doesn't warrant a separate method
|
|
116
|
-
|
|
117
|
-
```ruby
|
|
118
|
-
button(onclick: -> { patch(count: state.count + 1) }) { "+" }
|
|
119
|
-
button(onclick: -> { Funicular.router.navigate('/home') }) { "Home" }
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### Event Object
|
|
123
|
-
|
|
124
|
-
When using Symbol or Method, the event object is automatically passed:
|
|
125
|
-
|
|
126
|
-
```ruby
|
|
127
|
-
def handle_input(event)
|
|
128
|
-
value = event.target[:value]
|
|
129
|
-
patch(search_query: value)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Event passed automatically
|
|
133
|
-
input(oninput: :handle_input)
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
With Proc/Lambda, you can optionally receive the event:
|
|
137
|
-
|
|
138
|
-
```ruby
|
|
139
|
-
# With event
|
|
140
|
-
input(oninput: ->(e) { patch(value: e.target[:value]) })
|
|
141
|
-
|
|
142
|
-
# Without event (arity = 0)
|
|
143
|
-
button(onclick: -> { patch(clicked: true) })
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
### Form Submissions
|
|
147
|
-
|
|
148
|
-
The `form_for` helper also accepts all three types:
|
|
149
|
-
|
|
150
|
-
```ruby
|
|
151
|
-
# Symbol (recommended)
|
|
152
|
-
form_for(:user, on_submit: :handle_submit) do |f|
|
|
153
|
-
# ...
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Method object
|
|
157
|
-
form_for(:user, on_submit: method(:handle_submit)) do |f|
|
|
158
|
-
# ...
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Proc
|
|
162
|
-
form_for(:user, on_submit: ->(data) { User.create(data) }) do |f|
|
|
163
|
-
# ...
|
|
164
|
-
end
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## State Management
|
|
168
|
-
|
|
169
|
-
State is component-local data that determines what gets rendered. When state changes, the component re-renders.
|
|
170
|
-
|
|
171
|
-
### Initializing State
|
|
172
|
-
|
|
173
|
-
Override `initialize_state` to set initial state:
|
|
174
|
-
|
|
175
|
-
```ruby
|
|
176
|
-
class TodoList < Funicular::Component
|
|
177
|
-
def initialize_state
|
|
178
|
-
{
|
|
179
|
-
todos: [],
|
|
180
|
-
input: "",
|
|
181
|
-
filter: :all
|
|
182
|
-
}
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### Reading State
|
|
188
|
-
|
|
189
|
-
Access state via the `state` accessor (read-only):
|
|
190
|
-
|
|
191
|
-
```ruby
|
|
192
|
-
def render
|
|
193
|
-
div do
|
|
194
|
-
p { "Total: #{state.todos.length}" }
|
|
195
|
-
p { "Input: #{state.input}" }
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Updating State
|
|
201
|
-
|
|
202
|
-
Use `patch()` to update state. The component will automatically re-render:
|
|
203
|
-
|
|
204
|
-
```ruby
|
|
205
|
-
def handle_input(event)
|
|
206
|
-
patch(input: event.target[:value])
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def add_todo
|
|
210
|
-
new_todo = { id: Time.now.to_i, text: state.input, done: false }
|
|
211
|
-
patch(
|
|
212
|
-
todos: state.todos + [new_todo],
|
|
213
|
-
input: ""
|
|
214
|
-
)
|
|
215
|
-
end
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
**Important**: `patch()` merges the new state with existing state (shallow merge). Direct state mutation is blocked:
|
|
219
|
-
|
|
220
|
-
```ruby
|
|
221
|
-
# ❌ This will raise an error
|
|
222
|
-
state.count = 5
|
|
223
|
-
|
|
224
|
-
# ✅ Use patch() instead
|
|
225
|
-
patch(count: 5)
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### Updating Nested State
|
|
229
|
-
|
|
230
|
-
For deeply nested state, manually construct the new structure:
|
|
231
|
-
|
|
232
|
-
```ruby
|
|
233
|
-
def initialize_state
|
|
234
|
-
{
|
|
235
|
-
user: {
|
|
236
|
-
profile: { name: "Alice", age: 30 },
|
|
237
|
-
settings: { theme: "dark" }
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def update_name(new_name)
|
|
243
|
-
patch(
|
|
244
|
-
user: state.user.merge(
|
|
245
|
-
profile: state.user[:profile].merge(name: new_name)
|
|
246
|
-
)
|
|
247
|
-
)
|
|
248
|
-
end
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
## Props
|
|
252
|
-
|
|
253
|
-
Props are data passed from parent to child components. They are immutable within the child component.
|
|
254
|
-
|
|
255
|
-
### Passing Props
|
|
256
|
-
|
|
257
|
-
```ruby
|
|
258
|
-
# Parent component
|
|
259
|
-
class App < Funicular::Component
|
|
260
|
-
def render
|
|
261
|
-
div do
|
|
262
|
-
component(UserCard, {
|
|
263
|
-
name: "Alice",
|
|
264
|
-
email: "alice@example.com",
|
|
265
|
-
avatar_url: "/images/alice.png"
|
|
266
|
-
})
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
end
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### Receiving Props
|
|
273
|
-
|
|
274
|
-
Access props via the `props` accessor:
|
|
275
|
-
|
|
276
|
-
```ruby
|
|
277
|
-
class UserCard < Funicular::Component
|
|
278
|
-
def render
|
|
279
|
-
div(class: "card") do
|
|
280
|
-
img(src: props[:avatar_url])
|
|
281
|
-
h3 { props[:name] }
|
|
282
|
-
p { props[:email] }
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### Passing Callbacks
|
|
289
|
-
|
|
290
|
-
Props can include Proc/Lambda callbacks for child-to-parent communication:
|
|
291
|
-
|
|
292
|
-
```ruby
|
|
293
|
-
# Parent
|
|
294
|
-
class TodoApp < Funicular::Component
|
|
295
|
-
def handle_delete(id)
|
|
296
|
-
patch(todos: state.todos.reject { |t| t[:id] == id })
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
def render
|
|
300
|
-
state.todos.map do |todo|
|
|
301
|
-
component(TodoItem, {
|
|
302
|
-
todo: todo,
|
|
303
|
-
on_delete: method(:handle_delete)
|
|
304
|
-
})
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
# Child
|
|
310
|
-
class TodoItem < Funicular::Component
|
|
311
|
-
def handle_delete_click
|
|
312
|
-
props[:on_delete].call(props[:todo][:id])
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def render
|
|
316
|
-
div do
|
|
317
|
-
span { props[:todo][:text] }
|
|
318
|
-
button(onclick: :handle_delete_click) { "Delete" }
|
|
319
|
-
end
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
## Component Lifecycle
|
|
325
|
-
|
|
326
|
-
Funicular provides lifecycle hooks for component initialization and cleanup.
|
|
327
|
-
|
|
328
|
-
### component_mounted
|
|
329
|
-
|
|
330
|
-
Called after the component is mounted to the DOM:
|
|
331
|
-
|
|
332
|
-
```ruby
|
|
333
|
-
class ChatComponent < Funicular::Component
|
|
334
|
-
def component_mounted
|
|
335
|
-
# Subscribe to WebSocket
|
|
336
|
-
@subscription = Cable.subscribe("ChatChannel") do |message|
|
|
337
|
-
patch(messages: state.messages + [message])
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
# Load initial data
|
|
341
|
-
load_suspense_data if self.class.suspense_definitions.any?
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
### component_unmounted
|
|
347
|
-
|
|
348
|
-
Called before the component is removed from the DOM:
|
|
349
|
-
|
|
350
|
-
```ruby
|
|
351
|
-
class ChatComponent < Funicular::Component
|
|
352
|
-
def component_unmounted
|
|
353
|
-
# Cleanup: unsubscribe from WebSocket
|
|
354
|
-
@subscription&.unsubscribe
|
|
355
|
-
|
|
356
|
-
# Clear timers
|
|
357
|
-
@timer_ids.each { |id| JS.global.clearTimeout(id) }
|
|
358
|
-
end
|
|
359
|
-
end
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
### Lifecycle Order
|
|
363
|
-
|
|
364
|
-
```
|
|
365
|
-
1. new(props) # Constructor
|
|
366
|
-
2. initialize_state # Define initial state
|
|
367
|
-
3. render # First render
|
|
368
|
-
4. component_mounted # After DOM insertion
|
|
369
|
-
5. [state changes...] # User interactions
|
|
370
|
-
6. render (re-renders) # On each state update
|
|
371
|
-
7. component_unmounted # Before removal
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
## Component Communication
|
|
375
|
-
|
|
376
|
-
### Parent to Child: Props
|
|
377
|
-
|
|
378
|
-
Pass data down via props (see [Props](#props) section above).
|
|
379
|
-
|
|
380
|
-
### Child to Parent: Callbacks
|
|
381
|
-
|
|
382
|
-
Pass callback Procs via props:
|
|
383
|
-
|
|
384
|
-
```ruby
|
|
385
|
-
# Parent defines handler
|
|
386
|
-
def handle_change(new_value)
|
|
387
|
-
patch(value: new_value)
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
# Parent passes callback
|
|
391
|
-
component(InputField, {
|
|
392
|
-
value: state.value,
|
|
393
|
-
on_change: ->(new_value) { handle_change(new_value) }
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
# Child calls callback
|
|
397
|
-
props[:on_change].call(new_value)
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
### Sibling Communication
|
|
401
|
-
|
|
402
|
-
Lift state to common parent:
|
|
403
|
-
|
|
404
|
-
```ruby
|
|
405
|
-
class Dashboard < Funicular::Component
|
|
406
|
-
def initialize_state
|
|
407
|
-
{ selected_item: nil }
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
def render
|
|
411
|
-
div do
|
|
412
|
-
component(ItemList, {
|
|
413
|
-
items: state.items,
|
|
414
|
-
on_select: ->(item) { patch(selected_item: item) }
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
component(ItemDetails, {
|
|
418
|
-
item: state.selected_item
|
|
419
|
-
})
|
|
420
|
-
end
|
|
421
|
-
end
|
|
422
|
-
end
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
## Refs
|
|
426
|
-
|
|
427
|
-
Refs provide direct access to DOM elements, useful for:
|
|
428
|
-
- Focus management
|
|
429
|
-
- Text selection
|
|
430
|
-
- Integrating with JavaScript libraries
|
|
431
|
-
|
|
432
|
-
### Creating Refs
|
|
433
|
-
|
|
434
|
-
Use the `ref` prop and access via `refs` accessor:
|
|
435
|
-
|
|
436
|
-
```ruby
|
|
437
|
-
class SearchBox < Funicular::Component
|
|
438
|
-
def component_mounted
|
|
439
|
-
# Focus input on mount
|
|
440
|
-
refs[:search_input]&.focus()
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
def render
|
|
444
|
-
div do
|
|
445
|
-
input(ref: :search_input, type: "text", placeholder: "Search...")
|
|
446
|
-
button(onclick: -> { refs[:search_input].focus() }) { "Focus" }
|
|
447
|
-
end
|
|
448
|
-
end
|
|
449
|
-
end
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
### Refs with JavaScript Libraries
|
|
453
|
-
|
|
454
|
-
Refs are essential for integrating JS libraries:
|
|
455
|
-
|
|
456
|
-
```ruby
|
|
457
|
-
class ChartComponent < Funicular::Component
|
|
458
|
-
def component_mounted
|
|
459
|
-
# Delegate to Chart.js
|
|
460
|
-
canvas = refs[:chart_canvas]
|
|
461
|
-
@chart = JS.global.Chart.new(canvas, chart_config)
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
def component_unmounted
|
|
465
|
-
@chart&.destroy()
|
|
466
|
-
end
|
|
467
|
-
|
|
468
|
-
def render
|
|
469
|
-
div do
|
|
470
|
-
canvas(ref: :chart_canvas, width: 400, height: 300)
|
|
471
|
-
end
|
|
472
|
-
end
|
|
473
|
-
end
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
## Best Practices
|
|
477
|
-
|
|
478
|
-
### Keep Components Small
|
|
479
|
-
|
|
480
|
-
Break large components into smaller, focused ones:
|
|
481
|
-
|
|
482
|
-
```ruby
|
|
483
|
-
# ❌ Too large
|
|
484
|
-
class UserDashboard < Funicular::Component
|
|
485
|
-
def render
|
|
486
|
-
div do
|
|
487
|
-
# 500 lines of mixed UI code
|
|
488
|
-
end
|
|
489
|
-
end
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# ✅ Decomposed
|
|
493
|
-
class UserDashboard < Funicular::Component
|
|
494
|
-
def render
|
|
495
|
-
div do
|
|
496
|
-
component(UserProfile)
|
|
497
|
-
component(ActivityFeed)
|
|
498
|
-
component(SettingsPanel)
|
|
499
|
-
end
|
|
500
|
-
end
|
|
501
|
-
end
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
### Avoid Deep Props Drilling
|
|
505
|
-
|
|
506
|
-
If passing props through many levels, consider:
|
|
507
|
-
- Component composition
|
|
508
|
-
- Lifting state higher
|
|
509
|
-
- Model layer for shared server data
|
|
510
|
-
|
|
511
|
-
### Use Descriptive State Keys
|
|
512
|
-
|
|
513
|
-
```ruby
|
|
514
|
-
# ❌ Unclear
|
|
515
|
-
{ x: true, y: 5, z: [] }
|
|
516
|
-
|
|
517
|
-
# ✅ Descriptive
|
|
518
|
-
{ is_loading: true, page_number: 5, search_results: [] }
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
### Normalize State Shape
|
|
522
|
-
|
|
523
|
-
Prefer flat structures over deep nesting:
|
|
524
|
-
|
|
525
|
-
```ruby
|
|
526
|
-
# ❌ Deeply nested
|
|
527
|
-
{
|
|
528
|
-
users: [
|
|
529
|
-
{ id: 1, posts: [{ id: 10, comments: [...] }] }
|
|
530
|
-
]
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
# ✅ Normalized
|
|
534
|
-
{
|
|
535
|
-
users: { 1 => { id: 1, name: "Alice" } },
|
|
536
|
-
posts: { 10 => { id: 10, user_id: 1 } },
|
|
537
|
-
comments: { 100 => { id: 100, post_id: 10 } }
|
|
538
|
-
}
|
|
539
|
-
```
|