vident 1.0.0 → 1.0.2
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 +12 -0
- data/README.md +4 -1
- data/lib/vident/component_attribute_resolver.rb +27 -8
- data/lib/vident/component_class_lists.rb +7 -1
- data/lib/vident/stimulus_builder.rb +28 -11
- data/lib/vident/stimulus_helper.rb +4 -4
- data/lib/vident/version.rb +1 -1
- data/lib/vident2/caching.rb +93 -0
- data/lib/vident2/component.rb +538 -0
- data/lib/vident2/engine.rb +18 -0
- data/lib/vident2/error.rb +30 -0
- data/lib/vident2/internals/action_builder.rb +101 -0
- data/lib/vident2/internals/attribute_writer.rb +22 -0
- data/lib/vident2/internals/class_list_builder.rb +79 -0
- data/lib/vident2/internals/declaration.rb +17 -0
- data/lib/vident2/internals/declarations.rb +76 -0
- data/lib/vident2/internals/draft.rb +60 -0
- data/lib/vident2/internals/dsl.rb +198 -0
- data/lib/vident2/internals/plan.rb +12 -0
- data/lib/vident2/internals/registry.rb +41 -0
- data/lib/vident2/internals/resolver.rb +306 -0
- data/lib/vident2/internals/target_builder.rb +29 -0
- data/lib/vident2/phlex/html.rb +84 -0
- data/lib/vident2/phlex.rb +9 -0
- data/lib/vident2/stimulus/action.rb +140 -0
- data/lib/vident2/stimulus/class_map.rb +69 -0
- data/lib/vident2/stimulus/collection.rb +42 -0
- data/lib/vident2/stimulus/controller.rb +59 -0
- data/lib/vident2/stimulus/naming.rb +26 -0
- data/lib/vident2/stimulus/null.rb +16 -0
- data/lib/vident2/stimulus/outlet.rb +113 -0
- data/lib/vident2/stimulus/param.rb +62 -0
- data/lib/vident2/stimulus/target.rb +57 -0
- data/lib/vident2/stimulus/value.rb +77 -0
- data/lib/vident2/tailwind.rb +19 -0
- data/lib/vident2/version.rb +5 -0
- data/lib/vident2/view_component/base.rb +124 -0
- data/lib/vident2/view_component.rb +9 -0
- data/lib/vident2.rb +50 -0
- data/skills/vident/SKILL.md +11 -2
- data/skills/vident/api-reference.md +518 -0
- data/skills/vident/examples.md +492 -0
- metadata +35 -1
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# Vident worked examples
|
|
2
|
+
|
|
3
|
+
End-to-end walkthroughs. Every example here runs against the public API verified in
|
|
4
|
+
`test/public_api_spec/` and the dummy app under `test/dummy/app/components/`. If a pattern
|
|
5
|
+
isn't shown below but is referenced in SKILL.md, it almost certainly shows up in
|
|
6
|
+
`test/dummy/app/components/dashboard/`.
|
|
7
|
+
|
|
8
|
+
The examples are grouped by shape:
|
|
9
|
+
|
|
10
|
+
1. [Dashboard: outlets + scoped events + StimulusNull](#1-dashboard-outlets--scoped-events--stimulusnull) (Phlex)
|
|
11
|
+
2. [Greeter with slot trigger: parent-child Stimulus wiring](#2-greeter-with-slot-trigger) (ViewComponent + Phlex)
|
|
12
|
+
3. [ERB syntax: three ways to emit data attributes in a template](#3-erb-three-ways-to-emit-data-attributes)
|
|
13
|
+
4. [Avatar: conditional root tag + class-list precedence](#4-avatar-conditional-root-tag--class-list-precedence) (Phlex)
|
|
14
|
+
5. [Stimulus params on sibling buttons sharing one handler](#5-stimulus-params-on-sibling-buttons)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Dashboard: outlets + scoped events + StimulusNull
|
|
19
|
+
|
|
20
|
+
A page hosts many release cards; cards are filterable via a filter bar; selecting a card
|
|
21
|
+
opens a detail panel; promoting/cancelling a card fires a toast. Full source in
|
|
22
|
+
`test/dummy/app/components/dashboard/`.
|
|
23
|
+
|
|
24
|
+
### Page (host of card outlets)
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
module Dashboard
|
|
28
|
+
class PageComponent < ApplicationComponent
|
|
29
|
+
prop :releases, _Array(Hash), default: -> { [] }
|
|
30
|
+
prop :active_filter, _Union(:all, :pending, :deployed, :failed), default: :all
|
|
31
|
+
|
|
32
|
+
stimulus do
|
|
33
|
+
values active_filter: -> { @active_filter.to_s },
|
|
34
|
+
count: -> { @releases.size }
|
|
35
|
+
|
|
36
|
+
# Listen to a scoped `filterChanged` event dispatched on window by FilterBar.
|
|
37
|
+
# Ruby side: reference the DISPATCHER's class.
|
|
38
|
+
actions -> { [FilterBarComponent.stimulus_scoped_event_on_window(:filter_changed), :handle_filter_changed] }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def view_template
|
|
42
|
+
root_element(class: "space-y-6") do |page|
|
|
43
|
+
render FilterBarComponent.new(active_filter: @active_filter, total: @releases.size)
|
|
44
|
+
|
|
45
|
+
div(class: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4") do
|
|
46
|
+
@releases.each do |release|
|
|
47
|
+
# `stimulus_outlet_host: page` is the child-registers-with-host hook:
|
|
48
|
+
# each card's initialize calls `page.add_stimulus_outlets(self)`, which
|
|
49
|
+
# writes a `data-dashboard--page-component-dashboard--release-card-component-outlet`
|
|
50
|
+
# onto the page root. No need to list cards in the page's `stimulus do`.
|
|
51
|
+
render ReleaseCardComponent.new(**release, stimulus_outlet_host: page)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
render DetailPanelComponent.new
|
|
56
|
+
render ToastComponent.new
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
// dashboard/page_component_controller.js
|
|
65
|
+
export default class extends Controller {
|
|
66
|
+
static values = { activeFilter: String, count: Number }
|
|
67
|
+
static outlets = ["dashboard--release-card-component"]
|
|
68
|
+
|
|
69
|
+
handleFilterChanged(event) {
|
|
70
|
+
const { filter, query } = event.detail ?? {}
|
|
71
|
+
if (filter !== undefined) this.activeFilterValue = filter
|
|
72
|
+
this.#applyFilter(query ?? "")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// GOTCHA: do not iterate `this.dashboardReleaseCardComponentOutlets` inside
|
|
76
|
+
// dashboardReleaseCardComponentOutletConnected — Stimulus warns for each
|
|
77
|
+
// selector match whose controller hasn't attached yet. Iterate on real events.
|
|
78
|
+
|
|
79
|
+
#applyFilter(query) {
|
|
80
|
+
const q = query.trim().toLowerCase()
|
|
81
|
+
let visible = 0
|
|
82
|
+
for (const card of this.dashboardReleaseCardComponentOutlets) {
|
|
83
|
+
const show = (this.activeFilterValue === "all" || card.statusValue === this.activeFilterValue)
|
|
84
|
+
&& (q === "" || card.nameValue.toLowerCase().includes(q))
|
|
85
|
+
card.setVisible(show)
|
|
86
|
+
if (show) visible += 1
|
|
87
|
+
}
|
|
88
|
+
this.countValue = visible
|
|
89
|
+
this.dispatch("filterApplied", { detail: { count: visible }, target: window })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Release card (self-registers with host, uses `classes` DSL + SSR)
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
module Dashboard
|
|
98
|
+
class ReleaseCardComponent < ApplicationComponent
|
|
99
|
+
prop :release_id, Integer
|
|
100
|
+
prop :name, String
|
|
101
|
+
prop :version, String
|
|
102
|
+
prop :environment, _Union(:production, :staging, :preview), default: :staging
|
|
103
|
+
prop :status, _Union(:pending, :deployed, :failed), default: :pending
|
|
104
|
+
|
|
105
|
+
# `stimulus_outlet_host:` is inherited from Vident::Component — no prop
|
|
106
|
+
# declaration needed in this class.
|
|
107
|
+
|
|
108
|
+
stimulus do
|
|
109
|
+
values_from_props :release_id, :name, :status
|
|
110
|
+
|
|
111
|
+
# Proc sees @status at render time; emits
|
|
112
|
+
# `data-<this-controller>-status-class="..."` for the JS side, AND the same
|
|
113
|
+
# value is inlined via `class_list_for_stimulus_classes(:status)` below
|
|
114
|
+
# for SSR first paint.
|
|
115
|
+
classes status: -> {
|
|
116
|
+
case @status
|
|
117
|
+
when :deployed then "border-green-500 bg-green-50"
|
|
118
|
+
when :failed then "border-red-500 bg-red-50"
|
|
119
|
+
else "border-yellow-400 bg-yellow-50"
|
|
120
|
+
end
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
actions [:click, :select]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def view_template
|
|
127
|
+
root_element(
|
|
128
|
+
class: "block cursor-pointer rounded-lg border-2 p-4 shadow-sm #{class_list_for_stimulus_classes(:status)}",
|
|
129
|
+
role: "button",
|
|
130
|
+
tabindex: 0
|
|
131
|
+
) do |card|
|
|
132
|
+
# Two buttons share one `apply` handler. `event.params.kind` on the JS side
|
|
133
|
+
# tells them apart — see example 5 for the params idiom.
|
|
134
|
+
card.child_element(
|
|
135
|
+
:button,
|
|
136
|
+
stimulus_action: [:click, :apply],
|
|
137
|
+
stimulus_target: :promote_button,
|
|
138
|
+
stimulus_params: { kind: "promote" },
|
|
139
|
+
type: "button", class: "..."
|
|
140
|
+
) { "Promote" }
|
|
141
|
+
|
|
142
|
+
card.child_element(
|
|
143
|
+
:button,
|
|
144
|
+
stimulus_action: [:click, :apply],
|
|
145
|
+
stimulus_target: :cancel_button,
|
|
146
|
+
stimulus_params: { kind: "cancel" },
|
|
147
|
+
type: "button", class: "..."
|
|
148
|
+
) { "Cancel" }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
// dashboard/release_card_component_controller.js
|
|
157
|
+
export default class extends Controller {
|
|
158
|
+
static targets = ["promoteButton", "cancelButton"]
|
|
159
|
+
static values = { releaseId: Number, name: String, status: String }
|
|
160
|
+
|
|
161
|
+
select(event) {
|
|
162
|
+
if (event.target.closest("button")) return // let buttons handle themselves
|
|
163
|
+
this.dispatch("selected", { detail: this.#payload(), target: window })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
apply(event) {
|
|
167
|
+
const kind = event.params.kind // "promote" | "cancel"
|
|
168
|
+
this.#disable()
|
|
169
|
+
this.dispatch(`${kind}d`, { detail: this.#payload(), target: window })
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
setVisible(show) { this.element.classList.toggle("hidden", !show) }
|
|
173
|
+
|
|
174
|
+
#payload() { return { releaseId: this.releaseIdValue, name: this.nameValue, status: this.statusValue } }
|
|
175
|
+
#disable() { this.promoteButtonTarget.disabled = true; this.cancelButtonTarget.disabled = true }
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Detail panel (StimulusNull + keyboard modifier action)
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
module Dashboard
|
|
183
|
+
class DetailPanelComponent < ApplicationComponent
|
|
184
|
+
stimulus do
|
|
185
|
+
# Vident::StimulusNull emits the literal string "null" as the data attribute
|
|
186
|
+
# value. Stimulus's Object parser runs it through JSON.parse, so `releaseValue`
|
|
187
|
+
# starts as JS `null` instead of the default `{}`. Use ONLY with Object/Array
|
|
188
|
+
# typed Stimulus values — for String/Number/Boolean the "null" string reads
|
|
189
|
+
# as garbage. A bare `nil` would omit the attribute entirely (Stimulus uses
|
|
190
|
+
# its per-type default); StimulusNull is an explicit "emit null" opt-in.
|
|
191
|
+
values release: -> { Vident::StimulusNull }
|
|
192
|
+
|
|
193
|
+
classes state: "fixed right-0 top-0 h-full w-80 border-l bg-white p-6 shadow-xl transition-transform duration-200 translate-x-full"
|
|
194
|
+
|
|
195
|
+
# Three action entries in one `actions` call:
|
|
196
|
+
# 1. scoped window event from ReleaseCard → opens the panel
|
|
197
|
+
# 2. Hash form with keyboard filter + @window → Escape closes it.
|
|
198
|
+
# Expands to `keydown.esc@window->dashboard--detail-panel-component#close`.
|
|
199
|
+
# 3. plain `:close` — the close button's local click target
|
|
200
|
+
actions -> { [ReleaseCardComponent.stimulus_scoped_event_on_window(:selected), :handle_selected] },
|
|
201
|
+
{ event: :keydown, method: :close, keyboard: "esc", window: true },
|
|
202
|
+
:close
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def view_template
|
|
206
|
+
root_element(class: class_list_for_stimulus_classes(:state)) do |panel|
|
|
207
|
+
panel.child_element(:button, stimulus_action: :close, type: "button") { "X" }
|
|
208
|
+
panel.child_element(:div, stimulus_target: :body, class: "mt-4 space-y-2") do
|
|
209
|
+
p(class: "italic text-gray-400") { "Click a release to see details." }
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
// dashboard/detail_panel_component_controller.js
|
|
219
|
+
export default class extends Controller {
|
|
220
|
+
static targets = ["body"]
|
|
221
|
+
static values = { release: Object }
|
|
222
|
+
|
|
223
|
+
handleSelected(event) {
|
|
224
|
+
this.releaseValue = event.detail
|
|
225
|
+
this.#render()
|
|
226
|
+
this.element.classList.remove("translate-x-full")
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
close() { this.element.classList.add("translate-x-full") }
|
|
230
|
+
|
|
231
|
+
#render() {
|
|
232
|
+
const r = this.releaseValue
|
|
233
|
+
if (!r || !r.releaseId) return
|
|
234
|
+
this.bodyTarget.innerHTML = `<p>${r.name} — ${r.status}</p>`
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Identifier walk:
|
|
240
|
+
`Dashboard::ReleaseCardComponent` → class method `stimulus_identifier` returns
|
|
241
|
+
`"dashboard--release-card-component"`. `stimulus_scoped_event_on_window(:selected)`
|
|
242
|
+
returns the Symbol `:"dashboard--release-card-component:selected@window"`. On the JS
|
|
243
|
+
side, the card's `this.dispatch("selected", { target: window })` fires an event of type
|
|
244
|
+
`dashboard--release-card-component:selected` on window, matching exactly.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 2. Greeter with slot trigger
|
|
249
|
+
|
|
250
|
+
Parent exposes a named slot; parent passes its own action descriptor into the slot at
|
|
251
|
+
render time so the slot triggers a method on the parent.
|
|
252
|
+
|
|
253
|
+
### ViewComponent + ERB
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
# app/components/greeters/greeter_with_trigger_component.rb
|
|
257
|
+
module Greeters
|
|
258
|
+
class GreeterWithTriggerComponent < Vident::ViewComponent::Base
|
|
259
|
+
renders_one :trigger, GreeterButtonComponent
|
|
260
|
+
|
|
261
|
+
def root_element_attributes
|
|
262
|
+
{
|
|
263
|
+
stimulus_classes: {
|
|
264
|
+
pre_click: "text-md text-gray-500",
|
|
265
|
+
post_click: "text-xl text-blue-700"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Default fallback — used when the consumer doesn't pass a custom trigger.
|
|
271
|
+
def default_trigger
|
|
272
|
+
GreeterButtonComponent.new(
|
|
273
|
+
before_clicked_message: "Click me to greet.",
|
|
274
|
+
after_clicked_message: "Greeted! Click to reset.",
|
|
275
|
+
stimulus_actions: [stimulus_action(:click, :greet)]
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```erb
|
|
283
|
+
<%= root_element do |greeter| %>
|
|
284
|
+
<input type="text"
|
|
285
|
+
<%= greeter.as_stimulus_target(:name) %>
|
|
286
|
+
class="shadow appearance-none border rounded py-2 px-3">
|
|
287
|
+
|
|
288
|
+
<% if trigger? %>
|
|
289
|
+
<%= trigger %>
|
|
290
|
+
<% end %>
|
|
291
|
+
|
|
292
|
+
<%= greeter.child_element(:span, stimulus_target: :output,
|
|
293
|
+
class: "ml-4 #{greeter.class_list_for_stimulus_classes(:pre_click)}") do %>
|
|
294
|
+
...
|
|
295
|
+
<% end %>
|
|
296
|
+
<% end %>
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
At the render site, the consumer can override the trigger while still wiring it to the
|
|
300
|
+
parent's action:
|
|
301
|
+
|
|
302
|
+
```erb
|
|
303
|
+
<%= render GreeterWithTriggerComponent.new do |greeter| %>
|
|
304
|
+
<% greeter.with_trigger(
|
|
305
|
+
before_clicked_message: "Custom label",
|
|
306
|
+
stimulus_actions: [greeter.stimulus_action(:click, :greet)]
|
|
307
|
+
) %>
|
|
308
|
+
<% end %>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`greeter.stimulus_action(:click, :greet)` returns a `Vident::StimulusAction` whose
|
|
312
|
+
`controller` is the parent's (greeter's) identifier, so the click handler on the child's
|
|
313
|
+
button routes to `greeter-with-trigger-component#greet`, not to the child.
|
|
314
|
+
|
|
315
|
+
### Phlex version
|
|
316
|
+
|
|
317
|
+
Same component, Phlex syntax:
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
module PhlexGreeters
|
|
321
|
+
class GreeterWithTriggerComponent < ApplicationComponent
|
|
322
|
+
def trigger(**args)
|
|
323
|
+
@trigger ||= GreeterButtonComponent.new(**args)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
private
|
|
327
|
+
|
|
328
|
+
def trigger_or_default(greeter)
|
|
329
|
+
return render(@trigger) if @trigger
|
|
330
|
+
|
|
331
|
+
render(trigger(
|
|
332
|
+
before_clicked_message: "Greet",
|
|
333
|
+
stimulus_actions: [greeter.stimulus_action(:click, :greet)]
|
|
334
|
+
))
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def root_element_attributes
|
|
338
|
+
{ stimulus_classes: { pre_click: "text-md text-gray-500", post_click: "text-xl text-blue-700" } }
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def view_template(&)
|
|
342
|
+
vanish(&) # capture & discard the block content so consumers can call `#trigger` inside it
|
|
343
|
+
root_element do |greeter|
|
|
344
|
+
input(type: "text", data: { **greeter.stimulus_target(:name) })
|
|
345
|
+
trigger_or_default(greeter)
|
|
346
|
+
greeter.child_element(:span, stimulus_target: :output,
|
|
347
|
+
class: "ml-4 #{greeter.class_list_for_stimulus_classes(:pre_click)}")
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## 3. ERB: three ways to emit data attributes
|
|
357
|
+
|
|
358
|
+
ViewComponent/ERB users have three stylistic choices for attaching Stimulus wiring to
|
|
359
|
+
a hand-authored HTML tag. All three are equivalent; pick one per file for consistency.
|
|
360
|
+
|
|
361
|
+
```erb
|
|
362
|
+
<%= root_element do |greeter| %>
|
|
363
|
+
<%# (a) Inline `as_stimulus_*` helpers — embed the raw data-* attributes directly in the HTML tag. %>
|
|
364
|
+
<%# Most compatible with better_html only if you allow embedded expressions inside tag bodies. %>
|
|
365
|
+
<input type="text"
|
|
366
|
+
<%= greeter.as_stimulus_target(:name) %>
|
|
367
|
+
class="...">
|
|
368
|
+
<button <%= greeter.as_stimulus_action([:click, :greet]) %>
|
|
369
|
+
class="...">
|
|
370
|
+
<%= @cta %>
|
|
371
|
+
</button>
|
|
372
|
+
|
|
373
|
+
<%# (b) Rails `content_tag` with `data:` spread — works anywhere `content_tag` does, %>
|
|
374
|
+
<%# plays nicely with strict HTML linters. Singular helpers return a Hash shape %>
|
|
375
|
+
<%# like { "data-greeter-target" => "name" }, spread with `**`. %>
|
|
376
|
+
<%= content_tag(:input, nil, type: "text", data: { **greeter.stimulus_target(:name) }) %>
|
|
377
|
+
<%= content_tag(:button, @cta, data: { **greeter.stimulus_action([:click, :greet]) }) %>
|
|
378
|
+
|
|
379
|
+
<%# (c) Vident's `child_element` helper — one call, tag + stimulus_* kwargs + block. %>
|
|
380
|
+
<%# Plural kwargs (`stimulus_actions:`) take an Enumerable; singular take one entry. %>
|
|
381
|
+
<%= greeter.child_element(:input, stimulus_target: :name, type: "text", class: "...") %>
|
|
382
|
+
<%= greeter.child_element(:button, stimulus_action: [:click, :greet], class: "...") do %>
|
|
383
|
+
<%= @cta %>
|
|
384
|
+
<% end %>
|
|
385
|
+
<% end %>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Phlex users have two choices — `child_element` (identical) and the native Phlex tag
|
|
389
|
+
methods with `data: { **component.stimulus_target(:name) }`.
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## 4. Avatar: conditional root tag + class-list precedence
|
|
394
|
+
|
|
395
|
+
Shows `element_tag:` varying by prop, `no_stimulus_controller`, `with_cache_key`, and
|
|
396
|
+
the full class-list precedence via `root_element_classes`.
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
module Phlex
|
|
400
|
+
class AvatarComponent < ApplicationComponent
|
|
401
|
+
no_stimulus_controller # don't emit the implied `data-controller`
|
|
402
|
+
with_cache_key # relies on ApplicationComponent `include Vident::Caching` — see api-reference.md
|
|
403
|
+
|
|
404
|
+
prop :url, _Nilable(String), predicate: :private, reader: :public
|
|
405
|
+
prop :initials, String, reader: :public
|
|
406
|
+
prop :shape, Symbol, default: :circle, reader: :public
|
|
407
|
+
prop :border, _Boolean, default: false, predicate: :private, reader: :public
|
|
408
|
+
prop :size, Symbol, default: :normal, reader: :public
|
|
409
|
+
|
|
410
|
+
private
|
|
411
|
+
|
|
412
|
+
def view_template
|
|
413
|
+
root_element do
|
|
414
|
+
span(class: "#{text_size_class} font-medium leading-none text-white") { @initials } unless image_avatar?
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Flip the root to <img> when a URL is given.
|
|
419
|
+
def root_element_attributes
|
|
420
|
+
{
|
|
421
|
+
element_tag: image_avatar? ? :img : :div,
|
|
422
|
+
html_options: default_html_options
|
|
423
|
+
}
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def default_html_options
|
|
427
|
+
if image_avatar?
|
|
428
|
+
{ class: "inline-block object-contain", src: @url, alt: "Profile image" }
|
|
429
|
+
else
|
|
430
|
+
{ class: "inline-flex items-center justify-center bg-gray-500" }
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Lower precedence than `html_options[:class]` — wins only when `html_options` has no `:class`.
|
|
435
|
+
def root_element_classes
|
|
436
|
+
[size_classes, shape_class, (@border ? "border" : "")]
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def image_avatar? = @url.present?
|
|
440
|
+
def shape_class = (@shape == :circle) ? "rounded-full" : "rounded-md"
|
|
441
|
+
def size_classes = { tiny: "w-6 h-6", small: "w-8 h-8", medium: "w-12 h-12" }[@size] || "w-10 h-10"
|
|
442
|
+
def text_size_class = (@size == :tiny || @size == :small) ? "text-xs" : "text-medium"
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Because this AvatarComponent sets `html_options[:class]` in `root_element_attributes`,
|
|
448
|
+
`root_element_classes` is NOT applied — `html_options[:class]` wins per the precedence
|
|
449
|
+
rules in SKILL.md §4. If you want the `root_element_classes` values kept AND extra
|
|
450
|
+
overrides, merge them yourself (see `PhlexGreeters::InheritedGreeterComponent` in the
|
|
451
|
+
dummy app for a `tailwind_merge`-aware merge).
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## 5. Stimulus params on sibling buttons
|
|
456
|
+
|
|
457
|
+
Both buttons fire the same `apply` action on the parent card controller; the
|
|
458
|
+
per-button `stimulus_params:` tells the handler which one fired via `event.params.kind`:
|
|
459
|
+
|
|
460
|
+
```ruby
|
|
461
|
+
card.child_element(:button,
|
|
462
|
+
stimulus_action: [:click, :apply],
|
|
463
|
+
stimulus_params: { kind: "promote" }) { "Promote" }
|
|
464
|
+
|
|
465
|
+
card.child_element(:button,
|
|
466
|
+
stimulus_action: [:click, :apply],
|
|
467
|
+
stimulus_params: { kind: "cancel" }) { "Cancel" }
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
```js
|
|
471
|
+
apply(event) {
|
|
472
|
+
const kind = event.params.kind // "promote" | "cancel"
|
|
473
|
+
this.dispatch(`${kind}d`, { detail: this.#payload(), target: window })
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Element-scoped, not action-scoped.** In Stimulus, params live on the element, so every
|
|
478
|
+
action on the same element sees the same `event.params`. Vident mirrors this: `params`
|
|
479
|
+
is a sibling of `actions` in the DSL, not nested inside it. If you need per-action
|
|
480
|
+
params, split the buttons. This is usually preferable anyway — the shared-handler
|
|
481
|
+
pattern above is a tiny bit RPC-ish and is shown because params are useful to know
|
|
482
|
+
about, not because one action/two params is the recommended shape.
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Where to read more
|
|
487
|
+
|
|
488
|
+
- `test/dummy/app/components/dashboard/` — the full dashboard (5 components + JS) is
|
|
489
|
+
Vident's reference example. Every feature in SKILL.md is exercised there.
|
|
490
|
+
- `test/public_api_spec/specs/core_dsl.rb` — one locked-behaviour test per input shape
|
|
491
|
+
of every DSL primitive. Useful when unsure about an edge case.
|
|
492
|
+
- `test/dummy/app/components/greeters/` (ERB) and `test/dummy/app/components/phlex_greeters/` (Phlex) — side-by-side renditions of the same component in both engines.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vident
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephen Ierodiaconou
|
|
@@ -120,7 +120,41 @@ files:
|
|
|
120
120
|
- lib/vident/stimulus_value_collection.rb
|
|
121
121
|
- lib/vident/tailwind.rb
|
|
122
122
|
- lib/vident/version.rb
|
|
123
|
+
- lib/vident2.rb
|
|
124
|
+
- lib/vident2/caching.rb
|
|
125
|
+
- lib/vident2/component.rb
|
|
126
|
+
- lib/vident2/engine.rb
|
|
127
|
+
- lib/vident2/error.rb
|
|
128
|
+
- lib/vident2/internals/action_builder.rb
|
|
129
|
+
- lib/vident2/internals/attribute_writer.rb
|
|
130
|
+
- lib/vident2/internals/class_list_builder.rb
|
|
131
|
+
- lib/vident2/internals/declaration.rb
|
|
132
|
+
- lib/vident2/internals/declarations.rb
|
|
133
|
+
- lib/vident2/internals/draft.rb
|
|
134
|
+
- lib/vident2/internals/dsl.rb
|
|
135
|
+
- lib/vident2/internals/plan.rb
|
|
136
|
+
- lib/vident2/internals/registry.rb
|
|
137
|
+
- lib/vident2/internals/resolver.rb
|
|
138
|
+
- lib/vident2/internals/target_builder.rb
|
|
139
|
+
- lib/vident2/phlex.rb
|
|
140
|
+
- lib/vident2/phlex/html.rb
|
|
141
|
+
- lib/vident2/stimulus/action.rb
|
|
142
|
+
- lib/vident2/stimulus/class_map.rb
|
|
143
|
+
- lib/vident2/stimulus/collection.rb
|
|
144
|
+
- lib/vident2/stimulus/controller.rb
|
|
145
|
+
- lib/vident2/stimulus/naming.rb
|
|
146
|
+
- lib/vident2/stimulus/null.rb
|
|
147
|
+
- lib/vident2/stimulus/outlet.rb
|
|
148
|
+
- lib/vident2/stimulus/param.rb
|
|
149
|
+
- lib/vident2/stimulus/target.rb
|
|
150
|
+
- lib/vident2/stimulus/value.rb
|
|
151
|
+
- lib/vident2/tailwind.rb
|
|
152
|
+
- lib/vident2/version.rb
|
|
153
|
+
- lib/vident2/view_component.rb
|
|
154
|
+
- lib/vident2/view_component/base.rb
|
|
123
155
|
- skills/vident/SKILL.md
|
|
156
|
+
- skills/vident/api-reference.md
|
|
157
|
+
- skills/vident/examples.md
|
|
124
158
|
homepage: https://github.com/stevegeek/vident
|
|
125
159
|
licenses:
|
|
126
160
|
- MIT
|