vident-phlex 0.13.1 → 1.0.0.alpha1

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.
data/README.md CHANGED
@@ -1,851 +1,707 @@
1
1
  # Vident
2
2
 
3
- Vident is a collection of gems that provide a set of tools for building web applications with Ruby on Rails.
3
+ A powerful Ruby gem for building interactive, type-safe components in Rails applications with seamless [Stimulus.js](https://stimulus.hotwired.dev/) integration.
4
4
 
5
- ## Included Gems
5
+ Vident supports both [ViewComponent](https://viewcomponent.org/) and [Phlex](https://www.phlex.fun/) rendering engines while providing a consistent API for creating
6
+ reusable UI components powered by [Stimulus.js](https://stimulus.hotwired.dev/).
6
7
 
7
- The core gems:
8
+ ## Table of Contents
8
9
 
9
- - `vident`: The core Vident library
10
- - `vident-phlex`: Phlex integration for Vident
11
- - `vident-view_component`: ViewComponent integration for Vident
10
+ - [Introduction](#introduction)
11
+ - [Installation](#installation)
12
+ - [Quick Start](#quick-start)
13
+ - [Core Concepts](#core-concepts)
14
+ - [Component DSL](#component-dsl)
15
+ - [Stimulus Integration](#stimulus-integration)
16
+ - [Advanced Features](#advanced-features)
17
+ - [Testing](#testing)
18
+ - [Contributing](#contributing)
12
19
 
13
- Note that you can use both `Phlex` and `ViewComponent` in the same application if desired.
20
+ ## Introduction
14
21
 
15
- And then optional extra features:
22
+ Vident is a collection of gems that enhance Rails view components with:
16
23
 
17
- - `vident-tailwind`: Tailwind CSS integration for Vident
18
- - `vident-typed`: Type system for Vident components
19
- - `vident-typed-minitest`: Minitest integration for typed Vident components
20
- - `vident-typed-phlex`: Phlex integration for typed Vident components
21
- - `vident-typed-view_component`: ViewComponent integration for typed Vident
22
- - `vident-better_html`: Better HTML integration for Vident
24
+ - **Type-safe properties** using the Literal gem
25
+ - **First-class [Stimulus.js](https://stimulus.hotwired.dev/) integration** for interactive behaviors
26
+ - **Support for both [ViewComponent](https://viewcomponent.org/) and [Phlex](https://www.phlex.fun/)** rendering engines
27
+ - **Intelligent CSS class management** with built-in Tailwind CSS merging
28
+ - **Component caching** for improved performance
29
+ - **Declarative DSL** for clean, maintainable component code
23
30
 
24
- ## Directory Structure
31
+ ### Why Vident?
25
32
 
26
- The repository is structured like this:
33
+ Stimulus.js is a powerful framework for adding interactivity to HTML, but managing the data attributes can be cumbersome,
34
+ and refactoring can be error-prone (as say controller names are repeated in many places).
27
35
 
28
- ```
29
- vident/
30
- ├── lib/ # All gem code
31
- │ ├── vident.rb # Core entry point
32
- │ ├── vident-phlex.rb # Gem entry points
33
- │ ├── vident-better_html.rb
34
- │ ├── vident/ # Shared code
35
- │ ├── base.rb
36
- │ ├── phlex/ # Phlex integration
37
- │ ├── better_html/ # Better HTML integration
38
- │ └── ...
39
- ├── test/ # All tests
40
- │ ├── vident/ # Core tests
41
- │ ├── vident-phlex/ # Tests for each gem
42
- │ └── ...
43
- ├── docs/ # Documentation
44
- ├── examples/ # Examples
45
- ├── vident.gemspec # Gemspec for core gem
46
- ├── vident-phlex.gemspec # Gemspecs for each gem
47
- └── ...
48
- ```
49
-
50
- ## Development
51
-
52
- ### Setting Up Development Environment
53
-
54
- ```bash
55
- # Clone the repository
56
- git clone https://github.com/stevegeek/vident.git
57
- cd vident
58
-
59
- # Install dependencies
60
- bundle install
61
- ```
62
-
63
- ### Running Tests
64
-
65
- To run tests for all gems:
66
-
67
- ```bash
68
- rake test
69
- ```
70
-
71
- To run tests for a specific gem:
72
-
73
- ```bash
74
- rake test:vident-phlex
75
- ```
76
-
77
- ### Building and Installing Gems
78
-
79
- To build all gems:
80
-
81
- ```bash
82
- rake build
83
- ```
84
-
85
- To install all gems locally:
86
-
87
- ```bash
88
- rake install
89
- ```
90
-
91
- ## Contributing
92
-
93
- 1. Fork the repository
94
- 2. Create your feature branch (`git checkout -b my-new-feature`)
95
- 3. Commit your changes (`git commit -am 'Add some feature'`)
96
- 4. Push to the branch (`git push origin my-new-feature`)
97
- 5. Create a new Pull Request
98
-
99
- ## License
36
+ Vident simplifies this by providing a declarative DSL for defining Stimulus controllers, actions, targets, and values
37
+ directly within your component classes so you don't need to manually craft data attributes in your templates.
100
38
 
101
- The gems are available as open source under the terms of the [MIT License](LICENSE.txt).
39
+ Vident also ensures that your components are flexible: for example you can easily add to, or override configuration,
40
+ classes etc at the point of rendering.
102
41
 
103
- ---
42
+ Vident's goal is to make building UI components more maintainable, and remove some of the boilerplate code of Stimulus
43
+ without being over-bearing or including too much magic.
104
44
 
105
- # Component Documentation
45
+ ## Installation
106
46
 
107
- ---
108
-
109
- ## gem: vident-typed-view_component
110
-
111
- # Vident::Typed::ViewComponent
112
-
113
- Adds typed attributes to Vident ViewComponent components.
47
+ Add the core gem and your preferred rendering engine integration to your Gemfile:
114
48
 
115
49
  ```ruby
116
- class ApplicationComponent < ::Vident::Typed::ViewComponent::Base
117
- end
118
- ```
50
+ # Core gem (required)
51
+ gem "vident"
119
52
 
120
- For more details see [vident](https://github.com/stevegeek/vident).
121
-
122
- ### Examples
123
-
124
- Before we dive into a specific example note that there are some components implemented in `test/dummy/app/components`.
53
+ # Choose your rendering engine (at least one required)
54
+ gem "vident-view_component" # For ViewComponent support
55
+ gem "vident-phlex" # For Phlex support
56
+ ```
125
57
 
126
- Try them out by starting Rails:
58
+ Then run:
127
59
 
128
60
  ```bash
129
- cd test/dummy
130
61
  bundle install
131
- rails assets:precompile
132
- rails s
133
62
  ```
134
63
 
135
- and visiting http://localhost:3000
64
+ ## Quick Start
136
65
 
137
-
138
- ### A Vident component example (without Stimulus)
139
-
140
- First is an example component that uses `Vident::Typed::ViewComponent::Base` but no Stimulus features.
141
-
142
- It is an avatar component that can either be displayed as an image or as initials.
143
-
144
- It supports numerous sizes and shapes and can optionally have a border. It also generates a cache key for use in fragment caching or etag generation.
66
+ Here's a simple example of a Vident component using ViewComponent:
145
67
 
146
68
  ```ruby
147
- class AvatarComponent < ::Vident::Typed::ViewComponent::Base
148
- include ::Vident::Tailwind
149
- include ::Vident::Caching
150
-
151
- no_stimulus_controller
152
- with_cache_key :attributes
153
-
154
- attribute :url, String, allow_nil: true, allow_blank: false
155
- attribute :initials, String, allow_blank: false
156
-
157
- attribute :shape, Symbol, in: %i[circle square], default: :circle
158
-
159
- attribute :border, :boolean, default: false
160
-
161
- attribute :size, Symbol, in: %i[tiny small normal medium large x_large xx_large], default: :normal
162
-
163
- private
164
-
165
- def default_html_options
166
- if image_avatar?
167
- { class: "inline-block object-contain", src: url, alt: t(".image") }
168
- else
169
- { class: "inline-flex items-center justify-center bg-gray-500" }
170
- end
69
+ # app/components/button_component.rb
70
+ class ButtonComponent < Vident::ViewComponent::Base
71
+ # Define typed properties
72
+ prop :text, String, default: "Click me"
73
+ prop :url, _Nilable(String)
74
+ prop :style, Symbol, in: [:primary, :secondary], default: :primary
75
+ prop :clicked_count, Integer, default: 0
76
+
77
+ # Configure Stimulus integration
78
+ stimulus do
79
+ actions [:click, :handle_click]
80
+ # Static values
81
+ values loading_duration: 1000
82
+ # Map the clicked_count prop as a Stimulus value
83
+ values_from_props :clicked_count
84
+ # Dynamic values using procs (evaluated in component context)
85
+ values item_count: -> { @items.count }
86
+ values api_url: -> { Rails.application.routes.url_helpers.api_items_path }
87
+ # Static and dynamic classes
88
+ classes loading: "opacity-50 cursor-wait"
89
+ classes size: -> { @items.count > 10 ? "large" : "small" }
171
90
  end
172
91
 
173
- def element_classes
174
- [size_classes, shape_class, border? ? "border" : ""]
92
+ def call
93
+ root_element do
94
+ @text
95
+ end
175
96
  end
176
97
 
177
- alias_method :image_avatar?, :url?
178
-
179
- def shape_class
180
- (shape == :circle) ? "rounded-full" : "rounded-md"
181
- end
98
+ private
182
99
 
183
- def size_classes
184
- case size
185
- when :tiny
186
- "w-6 h-6"
187
- when :small
188
- "w-8 h-8"
189
- when :medium
190
- "w-12 h-12"
191
- when :large
192
- "w-14 h-14"
193
- when :x_large
194
- "sm:w-24 sm:h-24 w-16 h-16"
195
- when :xx_large
196
- "sm:w-32 sm:h-32 w-24 h-24"
197
- else
198
- "w-10 h-10"
199
- end
100
+ def root_element_attributes
101
+ {
102
+ element_tag: @url ? :a : :button,
103
+ html_options: { href: @url }.compact
104
+ }
200
105
  end
201
106
 
202
- def text_size_class
203
- case size
204
- when :tiny
205
- "text-xs"
206
- when :small
207
- "text-xs"
208
- when :medium
209
- "text-lg"
210
- when :large
211
- "sm:text-xl text-lg"
212
- when :extra_large
213
- "sm:text-2xl text-xl"
214
- else
215
- "text-medium"
107
+ def element_classes
108
+ base_classes = "btn"
109
+ case @style
110
+ when :primary
111
+ "#{base_classes} btn-primary"
112
+ when :secondary
113
+ "#{base_classes} btn-secondary"
216
114
  end
217
115
  end
218
116
  end
219
117
  ```
220
118
 
221
- ```erb
222
- <%= render root(
223
- element_tag: image_avatar? ? :img : :div,
224
- html_options: default_html_options
225
- ) do %>
226
- <% unless image_avatar? %>
227
- <span class="<%= text_size_class %> font-medium leading-none text-white"><%= initials %></span>
228
- <% end %>
229
- <% end %>
230
119
 
231
- ```
120
+ Add the corresponding Stimulus controller would be:
232
121
 
233
- Example usages:
122
+ ```javascript
123
+ // app/javascript/controllers/button_component_controller.js
124
+ // Can also be "side-car" in the same directory as the component, see the documentation for details
125
+ import { Controller } from "@hotwired/stimulus"
234
126
 
235
- ```erb
236
- <!-- These will render -->
237
- <%= render AvatarComponent.new(url: "https://someurl.com/avatar.jpg", initials: "AB" size: :large) %>
238
- <%= render AvatarComponent.new(url: "https://someurl.com/avatar.jpg", html_options: {alt: "My alt text", class: "object-scale-down"}) %>
239
- <%= render AvatarComponent.new(initials: "SG", size: :small) %>
240
- <%= render AvatarComponent.new(initials: "SG", size: :large, html_options: {class: "border-2 border-red-600"}) %>
241
-
242
- <!-- These will raise an error -->
243
- <!-- missing initals -->
244
- <%= render AvatarComponent.new(url: "https://someurl.com/avatar.jpg", size: :large) %>
245
- <!-- initials blank -->
246
- <%= render AvatarComponent.new(initials: "", size: :large) %>
247
- <!-- invalid size -->
248
- <%= render AvatarComponent.new(initials: "SG", size: :foo_bar) %>
127
+ export default class extends Controller {
128
+ static values = {
129
+ clickedCount: Number,
130
+ loadingDuration: Number
131
+ }
132
+ static classes = ["loading"]
133
+
134
+ handleClick(event) {
135
+ // Increment counter
136
+ this.clickedCountValue++
137
+
138
+ // Add loading state
139
+ this.element.classList.add(this.loadingClass)
140
+ this.element.disabled = true
141
+
142
+ // Use the loading duration from the component
143
+ setTimeout(() => {
144
+ this.element.classList.remove(this.loadingClass)
145
+ this.element.disabled = false
146
+
147
+ // Update text to show count
148
+ this.element.textContent = `${this.element.textContent} (${this.clickedCountValue})`
149
+ }, this.loadingDurationValue)
150
+ }
151
+ }
249
152
  ```
250
153
 
154
+ Use the component in your views:
251
155
 
252
- The following is rendered when used `render AvatarComponent.new(initials: "SG", size: :small, border: true)`:
156
+ ```erb
157
+ <!-- Default clicked count of 0 -->
158
+ <%= render ButtonComponent.new(text: "Save", style: :primary) %>
253
159
 
254
- ```html
255
- <div class="avatar-component w-8 h-8 rounded-full border inline-flex items-center justify-center bg-gray-500" id="avatar-component-9790427-12">
256
- <span class="text-xs font-medium leading-none text-white">SG</span>
257
- </div>
258
- ```
160
+ <!-- Pre-set clicked count -->
161
+ <%= render ButtonComponent.new(text: "Submit", style: :primary, clicked_count: 5) %>
259
162
 
260
- The following is rendered when used `render AvatarComponent.new(url: "https://i.pravatar.cc/300", initials: "AB", html_options: {alt: "My alt text", class: "block"})`:
163
+ <!-- Link variant -->
164
+ <%= render ButtonComponent.new(text: "Cancel", url: "/home", style: :secondary) %>
261
165
 
262
- ```html
263
- <img src="https://i.pravatar.cc/300" alt="My alt text" class="avatar-component w-10 h-10 rounded-full object-contain block" id="avatar-component-7083941-11">
166
+ <!-- Override things -->
167
+ <%= render ButtonComponent.new(text: "Cancel", url: "/home" classes: "bg-red-900", html_options: {role: "button"}) %>
264
168
  ```
265
169
 
266
- ----
170
+ The rendered HTML includes all Stimulus data attributes:
267
171
 
268
- ![Example](examples/avatar.png)
269
-
270
-
271
- ### Another ViewComponent + Vident example with Stimulus
272
-
273
- Consider the following ERB that might be part of an application's views. The app uses `ViewComponent`, `Stimulus` and `Vident`.
274
-
275
- The Greeter is a component that displays a text input and a button. When the button is clicked, the text input's value is
276
- used to greet the user. At the same time the button changes to be a 'reset' button, which resets the greeting when clicked again.
172
+ ```html
173
+ <!-- First button with default count -->
174
+ <button class="bg-blue-500 hover:bg-blue-700 text-white"
175
+ data-controller="button-component"
176
+ data-action="click->button-component#handleClick"
177
+ data-button-component-clicked-count-value="0"
178
+ data-button-component-loading-duration-value="1000"
179
+ data-button-component-loading-class="opacity-50 cursor-wait"
180
+ id="button-component-123">
181
+ Save
182
+ </button>
183
+
184
+ <!-- Second button with pre-set count -->
185
+ <button class="bg-blue-500 hover:bg-blue-700 text-white"
186
+ data-controller="button-component"
187
+ data-action="click->button-component#handleClick"
188
+ data-button-component-clicked-count-value="5"
189
+ data-button-component-loading-duration-value="1000"
190
+ data-button-component-loading-class="opacity-50 cursor-wait"
191
+ id="button-component-456">
192
+ Submit
193
+ </button>
194
+ ```
195
+
196
+ ## Core Concepts
197
+
198
+ ### Component Properties
199
+
200
+ Vident uses the Literal gem to provide type-safe component properties:
277
201
 
278
- ![ex1.gif](examples/ex1.gif)
202
+ ```ruby
203
+ class CardComponent < Vident::ViewComponent::Base
204
+ # Basic property with type
205
+ prop :title, String
206
+
207
+ # Property with default value
208
+ prop :subtitle, String, default: ""
209
+
210
+ # Nullable property
211
+ prop :image_url, _Nilable(String)
212
+
213
+ # Property with validation
214
+ prop :size, _Union(:small, :medium, :large), default: :medium
215
+
216
+ # Boolean property (creates predicate method)
217
+ prop :featured, _Boolean, default: false
218
+ end
219
+ ```
279
220
 
280
- ```erb
281
- <%# app/views/home/index.html.erb %>
282
-
283
- <!-- ... -->
284
-
285
- <!-- render the Greeter ViewComponent (that uses Vident) -->
286
- <%= render ::GreeterComponent.new(cta: "Hey!", html_options: {class: "my-4"}) do |greeter| %>
287
- <%# this component has a slot called `trigger` that renders a `ButtonComponent` (which also uses Vident) %>
288
- <% greeter.with_trigger(
289
-
290
- # The button component has attributes that are typed
291
- before_clicked: "Greet",
292
- after_clicked: "Greeted! Reset?",
293
-
294
- # A stimulus action is added to the button that triggers the `greet` action on the greeter stimulus controller.
295
- # This action will be added to any defined on the button component itself
296
- actions: [
297
- greeter.action(:click, :greet),
298
- ],
299
-
300
- # We can also override the default button classes of our component, or set other HTML attributes
301
- html_options: {
302
- class: "bg-red-500 hover:bg-red-700"
303
- }
304
- ) %>
305
- <% end %>
221
+ ### Built-in Properties
306
222
 
307
- <!-- ... -->
308
- ```
223
+ Every Vident component includes these properties:
309
224
 
310
- The output HTML of the above, using Vident, is:
311
-
312
- ```html
313
- <div class="greeter-component py-2 my-4"
314
- data-controller="greeter-component"
315
- data-greeter-component-pre-click-class="text-md text-gray-500"
316
- data-greeter-component-post-click-class="text-xl text-blue-700"
317
- id="greeter-component-1599855-6">
318
- <input type="text"
319
- data-greeter-component-target="name"
320
- class="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
321
- <button class="button-component ml-4 whitespace-no-wrap bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded bg-red-500 hover:bg-red-700"
322
- data-controller="button-component"
323
- data-action="click->greeter-component#greet button-component#changeMessage"
324
- data-button-component-after-clicked-message="Greeted! Reset?"
325
- data-button-component-before-clicked-message="Greet"
326
- id="button-component-7799479-7">Hey!</button>
327
- <!-- you can also use the `target_tag` helper to render targets -->
328
- <span class="ml-4 text-md text-gray-500"
329
- data-greeter-component-target="output">
330
- ...
331
- </span>
332
- </div>
333
- ```
225
+ - `element_tag` - The HTML tag for the root element (default: `:div`)
226
+ - `id` - The component's DOM ID (auto-generated if not provided)
227
+ - `classes` - Additional CSS classes
228
+ - `html_options` - Hash of HTML attributes
334
229
 
335
- Let's look at the components in more detail.
230
+ ### Root Element Rendering
336
231
 
337
- The main component is the `GreeterComponent`:
232
+ The `root_element` helper method renders your component's root element with all configured attributes:
338
233
 
339
234
  ```ruby
340
- # app/components/greeter_component.rb
235
+ # In your component class
236
+ def element_classes
237
+ ["card", featured? ? "card-featured" : nil]
238
+ end
239
+
240
+ private
341
241
 
342
- class GreeterComponent < ::Vident::ViewComponent::Base
343
- renders_one :trigger, ButtonComponent
242
+ def root_element_attributes
243
+ {
244
+ html_options: { role: "article", "aria-label": title }
245
+ }
344
246
  end
345
247
  ```
346
248
 
347
249
  ```erb
348
- <%# app/components/greeter_component.html.erb %>
349
-
350
- <%# Rendering the `root` element creates a tag which has stimulus `data-*`s, a unique id & other attributes set. %>
351
- <%# The stimulus controller name (identifier) is derived from the component name, and then used to generate the relavent data attribute names. %>
352
-
353
- <%= render root named_classes: {
354
- pre_click: "text-md text-gray-500", # named classes are exposed to Stimulus as `data-<controller>-<n>-class` attributes
355
- post_click: "text-xl text-blue-700",
356
- html_options: {class: "py-2"}
357
- } do |greeter| %>
358
-
359
- <%# `greeter` is the root element and exposes methods to generate stimulus targets and actions %>
360
- <input type="text"
361
- <%= greeter.as_target(:name) %>
362
- class="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
363
-
364
- <%# Render the slot %>
365
- <%= trigger %>
366
-
367
- <%# you can also use the `target_tag` helper to render targets %>
368
- <%= greeter.target_tag(
369
- :span,
370
- :output,
371
- # Stimulus named classes can be referenced to set class attributes at render time
372
- class: "ml-4 #{greeter.named_classes(:pre_click)}"
373
- ) do %>
374
- ...
375
- <% end %>
250
+ <%# In your template %>
251
+ <%= root_element do %>
252
+ <h2><%= title %></h2>
253
+ <p><%= subtitle %></p>
376
254
  <% end %>
377
-
378
255
  ```
379
256
 
380
- ```js
381
- // app/components/greeter_component_controller.js
257
+ ## Component DSL
382
258
 
383
- import { Controller } from "@hotwired/stimulus"
259
+ ### ViewComponent Integration
384
260
 
385
- // This is a Stimulus controller that is automatically registered for the `GreeterComponent`
386
- // and is 'sidecar' to the component. You can see that while in the ERB we use Ruby naming conventions
387
- // with snake_case Symbols, here they are converted to camelCase names. We can also just use camelCase
388
- // in the ERB if we want.
389
- export default class extends Controller {
390
- static targets = [ "name", "output" ]
391
- static classes = [ "preClick", "postClick" ]
392
-
393
- greet() {
394
- this.clicked = !this.clicked;
395
- this.outputTarget.classList.toggle(this.preClickClasses, !this.clicked);
396
- this.outputTarget.classList.toggle(this.postClickClasses, this.clicked);
397
-
398
- if (this.clicked)
399
- this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!`
400
- else
401
- this.clear();
402
- }
261
+ ```ruby
262
+ class MyComponent < Vident::ViewComponent::Base
263
+ # Component code
264
+ end
403
265
 
404
- clear() {
405
- this.outputTarget.textContent = '...';
406
- this.nameTarget.value = '';
407
- }
408
- }
266
+ # Or with an application base class
267
+ class ApplicationComponent < Vident::ViewComponent::Base
268
+ # Shared configuration
269
+ end
270
+
271
+ class MyComponent < ApplicationComponent
272
+ # Component code
273
+ end
409
274
  ```
410
275
 
411
- The slot renders a `ButtonComponent` component:
276
+ ### Phlex Integration
412
277
 
413
278
  ```ruby
414
- # app/components/button_component.rb
415
-
416
- class ButtonComponent < ::Vident::Typed::ViewComponent::Base
417
- # The attributes can specify an expected type, a default value and if nil is allowed.
418
- attribute :after_clicked, String, default: "Greeted!"
419
- attribute :before_clicked, String, allow_nil: false
420
-
421
- # This example is a templateless ViewComponent.
422
- def call
423
- # The button is rendered as a <button> tag with an click action on its own controller.
424
- render root(
425
- element_tag: :button,
426
-
427
- # We can define actions as arrays of Symbols, or pass manually manually crafted strings.
428
- # Here we specify the action name only, implying an action on the current components controller
429
- # and the default event type of `click`.
430
- actions: [:change_message],
431
- # Alternatively: [:click, :change_message] or ["click", "changeMessage"] or even "click->button-component#changeMessage"
432
-
433
- # A couple of data values are also set which will be available to the controller
434
- data_maps: [{after_clicked_message: after_clicked, before_clicked_message: before_clicked}],
435
-
436
- # The <button> tag has a default styling set directly on it. Note that
437
- # if not using utility classes, you can style the component using its
438
- # canonical class name (which is equal to the component's stimulus identifier),
439
- # in this case `button-component`.
440
- html_options: {class: "ml-4 whitespace-no-wrap bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"}
441
- ) do
442
- @before_clicked
279
+ class MyComponent < Vident::Phlex::HTML
280
+ def view_template
281
+ root do
282
+ h1 { "Hello from Phlex!" }
443
283
  end
444
284
  end
445
285
  end
446
286
  ```
447
287
 
448
- ```js
449
- // app/components/button_component_controller.js
450
-
451
- import { Controller } from "@hotwired/stimulus"
452
-
453
- export default class extends Controller {
454
- // The action is in camelCase.
455
- changeMessage() {
456
- this.clicked = !this.clicked;
457
- // The data attributes have their naming convention converted to camelCase.
458
- this.element.textContent = this.clicked ? this.data.get("afterClickedMessage") : this.data.get("beforeClickedMessage");
459
- }
460
- }
288
+ ## Stimulus Integration
461
289
 
462
- ```
290
+ Vident provides comprehensive Stimulus.js integration to add interactivity to your components.
463
291
 
464
- ### Usage
465
- How to use my plugin.
292
+ ### Declarative Stimulus DSL
466
293
 
467
- ### Installation
468
- Add this line to your application's Gemfile:
294
+ Use the `stimulus` block for clean, declarative configuration:
469
295
 
470
296
  ```ruby
471
- gem "vident-typed-view_component"
472
- ```
473
-
474
- And then execute:
475
- ```bash
476
- $ bundle
477
- ```
478
-
479
- Or install it yourself as:
480
- ```bash
481
- $ gem install vident-typed-view_component
297
+ class ToggleComponent < Vident::ViewComponent::Base
298
+ prop :expanded, _Boolean, default: false
299
+
300
+ stimulus do
301
+ # Define actions the controller responds to
302
+ actions :toggle, :expand, :collapse
303
+
304
+ # Define targets for DOM element references
305
+ targets :button, :content
306
+
307
+ # Define static values
308
+ values animation_duration: 300
309
+
310
+ # Define dynamic values using procs (evaluated in component context)
311
+ values item_count: -> { @items.count }
312
+ values current_state: proc { expanded? ? "open" : "closed" }
313
+
314
+ # Map values from component props
315
+ values_from_props :expanded
316
+
317
+ # Define CSS classes for different states
318
+ classes expanded: "block",
319
+ collapsed: "hidden",
320
+ transitioning: "opacity-50"
321
+ end
322
+ end
482
323
  ```
483
324
 
484
- ---
485
-
486
-
487
- ## gem: vident-view_component
488
-
489
- # Vident::ViewComponent
325
+ ### Dynamic Values and Classes with Procs
490
326
 
491
- [ViewComponent](https://viewcomponent.org/) powered [Vident](https://github.com/stevegeek/vident) components.
327
+ The Stimulus DSL supports dynamic values and classes using procs or lambdas that are evaluated in the component instance context:
492
328
 
493
329
  ```ruby
494
- class ApplicationComponent < ::Vident::ViewComponent::Base
330
+ class DynamicComponent < Vident::ViewComponent::Base
331
+ prop :items, _Array(Hash), default: -> { [] }
332
+ prop :loading, _Boolean, default: false
333
+ prop :user, _Nilable(User)
334
+
335
+ stimulus do
336
+ # Mix static and dynamic values in a single call
337
+ values(
338
+ static_config: "always_same",
339
+ item_count: -> { @items.count },
340
+ loading_state: proc { @loading ? "loading" : "idle" },
341
+ user_role: -> { @user&.role || "guest" },
342
+ api_endpoint: -> { Rails.application.routes.url_helpers.api_items_path }
343
+ )
344
+
345
+ # Mix static and dynamic classes
346
+ classes(
347
+ base: "component-container",
348
+ loading: -> { @loading ? "opacity-50 cursor-wait" : "" },
349
+ size: proc { @items.count > 10 ? "large" : "small" },
350
+ theme: -> { current_user&.dark_mode? ? "dark" : "light" }
351
+ )
352
+
353
+ # Dynamic actions and targets
354
+ actions -> { @loading ? [] : [:click, :submit] }
355
+ targets -> { @expanded ? [:content, :toggle] : [:toggle] }
356
+ end
357
+
358
+ private
359
+
360
+ def current_user
361
+ @current_user ||= User.current
362
+ end
495
363
  end
496
364
  ```
497
365
 
498
- For more details see [vident](https://github.com/stevegeek/vident).
499
-
500
- ### Examples
366
+ Procs have access to instance variables, component methods, and Rails helpers.
501
367
 
502
- Before we dive into a specific example note that there are some components implemented in the `test/dummy/app/components`.
368
+ **Important**: Each proc returns a single value for its corresponding stimulus attribute. If a proc returns an array, that entire array is treated as a single value, not multiple separate values. To provide multiple values for an attribute, use multiple procs or mix procs with static values:
503
369
 
504
- Try them out by starting Rails:
505
-
506
- ```bash
507
- cd test/dummy
508
- bundle install
509
- rails assets:precompile
510
- rails s
370
+ ```ruby
371
+ stimulus do
372
+ # Single proc returns a single value (even if it's an array)
373
+ actions -> { @expanded ? [:click, :submit] : :click }
374
+
375
+ # Multiple procs provide multiple values
376
+ actions -> { @can_edit ? :edit : nil },
377
+ -> { @can_delete ? :delete : nil },
378
+ :cancel # static value
379
+
380
+ # This results in: [:edit, :delete, :cancel] (assuming both conditions are true)
381
+ end
511
382
  ```
512
383
 
513
- and visiting http://localhost:3000
514
-
515
-
516
- ### A Vident component example (without Stimulus)
517
-
518
- First is an example component that uses `Vident::ViewComponent::Base` but no Stimulus features.
384
+ ### Scoped Custom Events
519
385
 
520
- It is an avatar component that can either be displayed as an image or as initials. It supports numerous sizes and shapes and can optionally have a border. It also generates a cache key for use in fragment caching or etag generation.
386
+ Vident provides helper methods to generate scoped event names for dispatching custom events that are unique to your component:
521
387
 
522
388
  ```ruby
523
- class AvatarComponent < ::Vident::ViewComponent::Base
524
- include ::Vident::Tailwind
525
- include ::Vident::Caching
526
-
527
- no_stimulus_controller
528
- with_cache_key :attributes
529
-
530
- attribute :url, allow_nil: true
531
- attribute :initials, allow_nil: false
532
-
533
- attribute :shape, default: :circle
534
-
535
- attribute :border, default: false
536
-
537
- attribute :size, default: :normal
538
-
539
- private
540
-
541
- def default_html_options
542
- if image_avatar?
543
- { class: "inline-block object-contain", src: url, alt: t(".image") }
544
- else
545
- { class: "inline-flex items-center justify-center bg-gray-500" }
546
- end
389
+ class MyComponent < Vident::ViewComponent::Base
390
+ stimulus do
391
+ # Define an action that responds to a scoped event
392
+ actions [stimulus_scoped_event_on_window(:data_loaded), :handle_data_loaded]
547
393
  end
548
-
549
- def element_classes
550
- [size_classes, shape_class, border? ? "border" : ""]
394
+
395
+ def handle_click
396
+ # Dispatch a scoped event from JavaScript
397
+ # This would generate: "my-component:dataLoaded"
398
+ puts stimulus_scoped_event(:data_loaded)
399
+
400
+ # For window events, this generates: "my-component:dataLoaded@window"
401
+ puts stimulus_scoped_event_on_window(:data_loaded)
551
402
  end
403
+ end
552
404
 
553
- alias_method :image_avatar?, :url?
405
+ # Available as both class and instance methods:
406
+ MyComponent.stimulus_scoped_event(:data_loaded) # => "my-component:dataLoaded"
407
+ MyComponent.new.stimulus_scoped_event(:data_loaded) # => "my-component:dataLoaded"
408
+ ```
554
409
 
555
- def shape_class
556
- (shape == :circle) ? "rounded-full" : "rounded-md"
557
- end
410
+ This is useful for:
411
+ - Dispatching events from Stimulus controllers to communicate between components
412
+ - Creating unique event names that won't conflict with other components
413
+ - Setting up window-level event listeners with scoped names
558
414
 
559
- def size_classes
560
- case size
561
- when :tiny
562
- "w-6 h-6"
563
- when :small
564
- "w-8 h-8"
565
- when :medium
566
- "w-12 h-12"
567
- when :large
568
- "w-14 h-14"
569
- when :x_large
570
- "sm:w-24 sm:h-24 w-16 h-16"
571
- when :xx_large
572
- "sm:w-32 sm:h-32 w-24 h-24"
573
- else
574
- "w-10 h-10"
575
- end
576
- end
415
+ ### Manual Stimulus Configuration
577
416
 
578
- def text_size_class
579
- case size
580
- when :tiny
581
- "text-xs"
582
- when :small
583
- "text-xs"
584
- when :medium
585
- "text-lg"
586
- when :large
587
- "sm:text-xl text-lg"
588
- when :extra_large
589
- "sm:text-2xl text-xl"
590
- else
591
- "text-medium"
592
- end
417
+ For more control, configure Stimulus attributes manually:
418
+
419
+ ```ruby
420
+ class CustomComponent < Vident::ViewComponent::Base
421
+ private
422
+
423
+ def root_element_attributes
424
+ {
425
+ element_tag: :article,
426
+ stimulus_controllers: ["custom", "analytics"],
427
+ stimulus_actions: [
428
+ [:click, :handleClick],
429
+ [:custom_event, :handleCustom]
430
+ ],
431
+ stimulus_values: {
432
+ endpoint: "/api/data",
433
+ refresh_interval: 5000
434
+ },
435
+ stimulus_targets: {
436
+ container: true
437
+ }
438
+ }
593
439
  end
594
440
  end
595
441
  ```
596
442
 
443
+ or you can use tag helpers to generate HTML with Stimulus attributes:
444
+
597
445
  ```erb
598
- <%= render root(
599
- element_tag: image_avatar? ? :img : :div,
600
- html_options: default_html_options
601
- ) do %>
602
- <% unless image_avatar? %>
603
- <span class="<%= text_size_class %> font-medium leading-none text-white"><%= initials %></span>
446
+ <%= content_tag(:input, type: "text", class: "...", data: {**greeter.stimulus_target(:name)}) %>
447
+ <%= content_tag(:button, @cta, class: "...", data: {**greeter.stimulus_action([:click, :greet])}) do %>
448
+ <%= @cta %>
604
449
  <% end %>
605
- <% end %>
606
- ```
450
+ <%= content_tag(:span, class: "...", data: {**greeter.stimulus_target(:output)}) %>
607
451
 
608
- Example usages:
452
+ <%# OR use the vident tag helper %>
609
453
 
610
- ```erb
611
- <%= render AvatarComponent.new(url: "https://someurl.com/avatar.jpg", initials: "AB" size: :large) %>
612
- <%= render AvatarComponent.new(url: "https://someurl.com/avatar.jpg", html_options: {alt: "My alt text", class: "object-scale-down"}) %>
613
- <%= render AvatarComponent.new(initials: "SG", size: :small) %>
614
- <%= render AvatarComponent.new(initials: "SG", size: :large, html_options: {class: "border-2 border-red-600"}) %>
454
+ <%= greeter.tag(:input, stimulus_target: :name, type: "text", class: "...") %>
455
+ <%= greeter.tag(:button, stimulus_action: [:click, :greet], class: "...") do %>
456
+ <%= @cta %>
457
+ <% end %>
458
+ <%= greeter.tag(:span, stimulus_target: :output, class: "...") %>
615
459
  ```
616
460
 
617
- The following is rendered when used `render AvatarComponent.new(initials: "SG", size: :small, border: true)`:
461
+ or in your Phlex templates:
618
462
 
619
- ```html
620
- <div class="avatar-component w-8 h-8 rounded-full border inline-flex items-center justify-center bg-gray-500" id="avatar-component-9790427-12">
621
- <span class="text-xs font-medium leading-none text-white">SG</span>
622
- </div>
463
+ ```ruby
464
+ root_element do |greeter|
465
+ input(type: "text", data: {**greeter.stimulus_target(:name)}, class: %(...))
466
+ trigger_or_default(greeter)
467
+ greeter.tag(:span, stimulus_target: :output, class: "ml-4 #{greeter.class_list_for_stimulus_classes(:pre_click)}") do
468
+ plain %( ... )
469
+ end
470
+ end
623
471
  ```
624
472
 
625
- The following is rendered when used `render AvatarComponent.new(url: "https://i.pravatar.cc/300", initials: "AB", html_options: {alt: "My alt text", class: "block"})`:
473
+ or directly in the ViewComponent template (eg with ERB) using the `as_stimulus_*` helpers
626
474
 
627
- ```html
628
- <img src="https://i.pravatar.cc/300" alt="My alt text" class="avatar-component w-10 h-10 rounded-full object-contain block" id="avatar-component-7083941-11">
475
+ ```erb
476
+ <%# HTML embellishment approach, most familiar to working with HTML in ERB, but is injecting directly into open HTML tags... %>
477
+ <input type="text"
478
+ <%= greeter.as_stimulus_targets(:name) %>
479
+ class="...">
480
+ <button <%= greeter.as_stimulus_actions([:click, :greet]) %>
481
+ class="...">
482
+ <%= @cta %>
483
+ </button>
484
+ <span <%= greeter.as_stimulus_targets(:output) %> class="..."></span>
629
485
  ```
630
486
 
631
- ----
632
487
 
633
- ![Example](examples/avatar.png)
488
+ ### Stimulus Helpers in Templates
634
489
 
635
- ### Usage
636
- How to use my plugin.
490
+ Vident provides helper methods for generating Stimulus attributes:
637
491
 
638
- ### Installation
639
- Add this line to your application's Gemfile:
640
-
641
- ```ruby
642
- gem "vident-view_component"
492
+ ```erb
493
+ <%= render root do |component| %>
494
+ <!-- Create a target -->
495
+ <div <%= component.as_target(:content) %>>
496
+ Content here
497
+ </div>
498
+
499
+ <!-- Create an action -->
500
+ <button <%= component.as_action(:click, :toggle) %>>
501
+ Toggle
502
+ </button>
503
+
504
+ <!-- Use the tag helper -->
505
+ <%= component.tag :div, stimulus_target: :output, class: "mt-4" do %>
506
+ Output here
507
+ <% end %>
508
+
509
+ <!-- Multiple targets/actions -->
510
+ <input <%= component.as_targets(:input, :field) %>
511
+ <%= component.as_actions([:input, :validate], [:change, :save]) %>>
512
+ <% end %>
643
513
  ```
644
514
 
645
- And then execute:
646
- ```bash
647
- $ bundle
648
- ```
515
+ ### Stimulus Outlets
649
516
 
650
- Or install it yourself as:
651
- ```bash
652
- $ gem install vident-view_component
653
- ```
517
+ Connect components via Stimulus outlets:
654
518
 
655
- ---
656
519
 
657
- ## gem: vident-better_html
658
520
 
659
- # Vident::BetterHtml
660
- Short description and motivation.
661
521
 
662
- ### Usage
663
- How to use my plugin.
522
+ ### Stimulus Controller Naming
664
523
 
665
- ```ruby
666
- BetterHtml.config = BetterHtml::Config.new(YAML.load(File.read(".better-html.yml")))
524
+ Vident automatically generates Stimulus controller names based on your component class:
667
525
 
668
- BetterHtml.configure do |config|
669
- config.template_exclusion_filter = proc { |filename| !filename.start_with?(Rails.root.to_s) }
670
- end
671
- # ViewComponent needs to do this hack to work in certain cases
672
- # see https://github.com/Shopify/better-html/pull/98
673
- class BetterHtml::HtmlAttributes
674
- alias_method :to_s_without_html_safe, :to_s
526
+ - `ButtonComponent` → `button-component`
527
+ - `Admin::UserCardComponent` `admin--user-card-component`
528
+ - `MyApp::WidgetComponent` → `my-app--widget-component`
675
529
 
676
- def to_s
677
- to_s_without_html_safe.html_safe
678
- end
679
- end
680
- ```
530
+ ### Working with Child Components
681
531
 
682
- ### Installation
683
- Add this line to your application's Gemfile:
532
+ Setting Stimulus configuration between parent and child components:
684
533
 
685
534
  ```ruby
686
- gem "vident-better_html"
687
- ```
688
-
689
- And then execute:
690
- ```bash
691
- $ bundle
535
+ class ParentComponent < Vident::ViewComponent::Base
536
+ renders_one :a_nested_component, ButtonComponent
537
+
538
+ stimulus do
539
+ actions :handleTrigger
540
+ end
541
+ end
692
542
  ```
693
543
 
694
- Or install it yourself as:
695
- ```bash
696
- $ gem install vident-better_html
544
+ ```erb
545
+ <%= root_element do |parent| %>
546
+ <% parent.with_a_nested_component(
547
+ text: "Click me",
548
+ stimulus_actions: [
549
+ parent.stimulus_action(:click, :handleTrigger)
550
+ ]
551
+ ) %>
552
+ <% end %>
697
553
  ```
698
554
 
699
- ---
555
+ This creates a nested component that once clicked triggers the parent components `handleTrigger` action.
700
556
 
701
- ## gem: vident-phlex
557
+ ## Other Features
702
558
 
703
- # Vident::Phlex
559
+ ### Custom Element Tags
704
560
 
705
- [Phlex](https://phlex.fun/) powered [Vident](https://github.com/stevegeek/vident) components.
561
+ Change the root element tag dynamically:
706
562
 
707
563
  ```ruby
708
- class ApplicationComponent < ::Vident::Phlex::HTML
564
+ class LinkOrButtonComponent < Vident::ViewComponent::Base
565
+ prop :url, _Nilable(String)
566
+
567
+ private
568
+
569
+ def root_element_attributes
570
+ {
571
+ element_tag: url? ? :a : :button,
572
+ html_options: {
573
+ href: url,
574
+ type: url? ? nil : "button"
575
+ }.compact
576
+ }
577
+ end
709
578
  end
710
579
  ```
711
580
 
712
- For more details see [vident](https://github.com/stevegeek/vident).
713
-
714
- ### Usage
715
- How to use my plugin.
581
+ ### Intelligent Class Management
716
582
 
717
- ### Installation
718
- Add this line to your application's Gemfile:
583
+ Vident intelligently merges CSS classes from multiple sources:
719
584
 
720
585
  ```ruby
721
- gem "vident-phlex"
722
- ```
723
-
724
- And then execute:
725
- ```bash
726
- $ bundle
586
+ class StyledComponent < Vident::ViewComponent::Base
587
+ prop :variant, Symbol, default: :default
588
+
589
+ private
590
+
591
+ # Classes on the root element
592
+ def element_classes
593
+ ["base-class", variant_class]
594
+ end
595
+
596
+ def variant_class
597
+ case @variant
598
+ when :primary then "text-blue-600 bg-blue-100"
599
+ when :danger then "text-red-600 bg-red-100"
600
+ else "text-gray-600 bg-gray-100"
601
+ end
602
+ end
603
+ end
727
604
  ```
728
605
 
729
- Or install it yourself as:
730
- ```bash
731
- $ gem install vident-phlex
606
+ Usage:
607
+ ```erb
608
+ <!-- All classes are intelligently merged -->
609
+ <%= render StyledComponent.new(
610
+ variant: :primary,
611
+ classes: "rounded-lg shadow"
612
+ ) %>
613
+ <!-- Result: class="base-class text-blue-600 bg-blue-100 rounded-lg shadow" -->
732
614
  ```
733
615
 
734
- ---
735
-
736
- ## gem: vident-tailwind
737
-
738
- # Vident::Tailwind
739
- Short description and motivation.
740
-
741
- ### Usage
742
- How to use my plugin.
616
+ ### Tailwind CSS Integration
743
617
 
744
- ### Installation
745
- Add this line to your application's Gemfile:
618
+ Vident includes built-in support for Tailwind CSS class merging when the `tailwind_merge` gem is available:
746
619
 
747
620
  ```ruby
748
- gem "vident-tailwind"
749
- ```
750
-
751
- And then execute:
752
- ```bash
753
- $ bundle
754
- ```
755
-
756
- Or install it yourself as:
757
- ```bash
758
- $ gem install vident-tailwind
621
+ class TailwindComponent < Vident::ViewComponent::Base
622
+ prop :size, Symbol, default: :medium
623
+
624
+ private
625
+
626
+ def element_classes
627
+ # Conflicts with size_class will be resolved automatically
628
+ "p-2 text-sm #{size_class}"
629
+ end
630
+
631
+ def size_class
632
+ case @size
633
+ when :small then "p-1 text-xs"
634
+ when :large then "p-4 text-lg"
635
+ else "p-2 text-base"
636
+ end
637
+ end
638
+ end
759
639
  ```
760
640
 
761
- ---
641
+ ### Component Caching
762
642
 
763
- ## gem: vident-typed-minitest
764
-
765
- # Vident::Typed::Minitest
766
- Short description and motivation.
767
-
768
- ### Usage
769
- How to use my plugin.
770
-
771
- ### Installation
772
- Add this line to your application's Gemfile:
643
+ Enable fragment caching for expensive components:
773
644
 
774
645
  ```ruby
775
- gem "vident-typed-minitest"
776
- ```
777
-
778
- And then execute:
779
- ```bash
780
- $ bundle
646
+ class ExpensiveComponent < Vident::ViewComponent::Base
647
+ include Vident::Caching
648
+
649
+ with_cache_key :to_h # Cache based on all attributes
650
+ # or
651
+ with_cache_key :id, :updated_at # Cache based on specific attributes
652
+ end
781
653
  ```
782
654
 
783
- Or install it yourself as:
784
- ```bash
785
- $ gem install vident-typed-minitest
655
+ ```erb
656
+ <% cache component.cache_key do %>
657
+ <%= render component %>
658
+ <% end %>
786
659
  ```
787
660
 
788
- ---
789
-
790
- ## gem: vident-typed-phlex
791
661
 
792
- # Vident::Typed::Phlex
662
+ ## Testing
793
663
 
794
- Adds typed attributes to Vident Phlex based components.
664
+ Vident components work seamlessly with testing frameworks that support ViewComponent or Phlex.
795
665
 
796
- ```ruby
797
- class ApplicationComponent < ::Vident::Typed::Phlex::HTML
798
- end
799
- ```
800
-
801
- For more details see [vident](https://github.com/stevegeek/vident).
802
-
803
- ### Usage
804
- How to use my plugin.
805
-
806
- ### Installation
807
- Add this line to your application's Gemfile:
666
+ ## Development
808
667
 
809
- ```ruby
810
- gem "vident-typed-phlex"
811
- ```
668
+ ### Running Tests
812
669
 
813
- And then execute:
814
670
  ```bash
815
- $ bundle
671
+ # Run all tests
672
+ bin/rails test
816
673
  ```
817
674
 
818
- Or install it yourself as:
819
- ```bash
820
- $ gem install vident-typed-phlex
821
- ```
675
+ ### Local Development
822
676
 
823
- ---
677
+ ```bash
678
+ # Clone the repository
679
+ git clone https://github.com/stevegeek/vident.git
680
+ cd vident
824
681
 
682
+ # Install dependencies
683
+ bundle install
825
684
 
826
- ## gem: vident-typed
685
+ # Run the dummy app
686
+ cd test/dummy
687
+ rails s
688
+ ```
827
689
 
828
- # Vident::Typed
829
- Short description and motivation.
690
+ ## Contributing
830
691
 
831
- ### Usage
832
- How to use my plugin.
692
+ 1. Fork the repository
693
+ 2. Create your feature branch (`git checkout -b feature/my-new-feature`)
694
+ 3. Write tests for your changes
695
+ 4. Commit your changes (`git commit -am 'Add new feature'`)
696
+ 5. Push to the branch (`git push origin feature/my-new-feature`)
697
+ 6. Create a Pull Request
833
698
 
834
- ### Installation
835
- Add this line to your application's Gemfile:
699
+ ## License
836
700
 
837
- ```ruby
838
- gem "vident-typed"
839
- ```
701
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
840
702
 
841
- And then execute:
842
- ```bash
843
- $ bundle
844
- ```
703
+ ## Credits
845
704
 
846
- Or install it yourself as:
847
- ```bash
848
- $ gem install vident-typed
849
- ```
705
+ Vident is maintained by [Stephen Ierodiaconou](https://github.com/stevegeek).
850
706
 
851
- ---
707
+ Special thanks to the ViewComponent and Phlex communities for their excellent component frameworks that Vident builds upon.