vident-phlex 0.13.1 → 1.0.0.alpha2

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,737 @@
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
100
-
101
- The gems are available as open source under the terms of the [MIT License](LICENSE.txt).
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.
102
38
 
103
- ---
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.
104
41
 
105
- # Component Documentation
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.
106
44
 
107
- ---
45
+ ## Installation
108
46
 
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
- ```
119
-
120
- For more details see [vident](https://github.com/stevegeek/vident).
50
+ # Core gem (required)
51
+ gem "vident"
121
52
 
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
136
-
64
+ ## Quick Start
137
65
 
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 |component|
94
+ component.tag(:span, stimulus_target: :status) do
95
+ @text
96
+ end
97
+ end
175
98
  end
176
99
 
177
- alias_method :image_avatar?, :url?
100
+ private
178
101
 
179
- def shape_class
180
- (shape == :circle) ? "rounded-full" : "rounded-md"
102
+ def root_element_attributes
103
+ {
104
+ element_tag: @url ? :a : :button,
105
+ html_options: { href: @url }.compact
106
+ }
181
107
  end
182
108
 
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
200
- end
201
-
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"
109
+ def element_classes
110
+ base_classes = "btn"
111
+ case @style
112
+ when :primary
113
+ "#{base_classes} btn-primary"
114
+ when :secondary
115
+ "#{base_classes} btn-secondary"
216
116
  end
217
117
  end
218
118
  end
219
119
  ```
220
120
 
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
121
 
231
- ```
122
+ Add the corresponding Stimulus controller would be:
232
123
 
233
- Example usages:
124
+ ```javascript
125
+ // app/javascript/controllers/button_component_controller.js
126
+ // Can also be "side-car" in the same directory as the component, see the documentation for details
127
+ import { Controller } from "@hotwired/stimulus"
234
128
 
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) %>
129
+ export default class extends Controller {
130
+ static values = {
131
+ clickedCount: Number,
132
+ loadingDuration: Number
133
+ }
134
+ static classes = ["loading"]
135
+ static targets = ["status"]
136
+
137
+ handleClick(event) {
138
+ // Increment counter
139
+ this.clickedCountValue++
140
+
141
+ // Store original text
142
+ const originalText = this.statusTarget.textContent
143
+
144
+ // Add loading state
145
+ this.element.classList.add(this.loadingClass)
146
+ this.element.disabled = true
147
+ this.statusTarget.textContent = "Loading..."
148
+
149
+ // Use the loading duration from the component
150
+ setTimeout(() => {
151
+ this.element.classList.remove(this.loadingClass)
152
+ this.element.disabled = false
153
+
154
+ // Update text to show count
155
+ this.statusTarget.textContent = `${originalText} (${this.clickedCountValue})`
156
+ }, this.loadingDurationValue)
157
+ }
158
+ }
249
159
  ```
250
160
 
161
+ Use the component in your views:
251
162
 
252
- The following is rendered when used `render AvatarComponent.new(initials: "SG", size: :small, border: true)`:
163
+ ```erb
164
+ <!-- Default clicked count of 0 -->
165
+ <%= render ButtonComponent.new(text: "Save", style: :primary) %>
253
166
 
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
- ```
167
+ <!-- Pre-set clicked count -->
168
+ <%= render ButtonComponent.new(text: "Submit", style: :primary, clicked_count: 5) %>
259
169
 
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"})`:
170
+ <!-- Link variant -->
171
+ <%= render ButtonComponent.new(text: "Cancel", url: "/home", style: :secondary) %>
261
172
 
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">
173
+ <!-- Override things -->
174
+ <%= render ButtonComponent.new(text: "Cancel", url: "/home" classes: "bg-red-900", html_options: {role: "button"}) %>
264
175
  ```
265
176
 
266
- ----
177
+ The rendered HTML includes all Stimulus data attributes:
267
178
 
268
- ![Example](examples/avatar.png)
179
+ ```html
180
+ <!-- First button with default count -->
181
+ <button class="bg-blue-500 hover:bg-blue-700 text-white"
182
+ data-controller="button-component"
183
+ data-action="click->button-component#handleClick"
184
+ data-button-component-clicked-count-value="0"
185
+ data-button-component-loading-duration-value="1000"
186
+ data-button-component-loading-class="opacity-50 cursor-wait"
187
+ id="button-component-123">
188
+ <span data-button-component-target="status">Save</span>
189
+ </button>
190
+
191
+ <!-- Second button with pre-set count -->
192
+ <button class="bg-blue-500 hover:bg-blue-700 text-white"
193
+ data-controller="button-component"
194
+ data-action="click->button-component#handleClick"
195
+ data-button-component-clicked-count-value="5"
196
+ data-button-component-loading-duration-value="1000"
197
+ data-button-component-loading-class="opacity-50 cursor-wait"
198
+ id="button-component-456">
199
+ <span data-button-component-target="status">Submit</span>
200
+ </button>
201
+ ```
202
+
203
+ ## Core Concepts
204
+
205
+ ### Component Properties
206
+
207
+ Vident uses the Literal gem to provide type-safe component properties:
269
208
 
209
+ ```ruby
210
+ class CardComponent < Vident::ViewComponent::Base
211
+ # Basic property with type
212
+ prop :title, String
213
+
214
+ # Property with default value
215
+ prop :subtitle, String, default: ""
216
+
217
+ # Nullable property
218
+ prop :image_url, _Nilable(String)
219
+
220
+ # Property with validation
221
+ prop :size, _Union(:small, :medium, :large), default: :medium
222
+
223
+ # Boolean property (creates predicate method)
224
+ prop :featured, _Boolean, default: false
225
+ end
226
+ ```
270
227
 
271
- ### Another ViewComponent + Vident example with Stimulus
228
+ ### Post-Initialization Hooks
272
229
 
273
- Consider the following ERB that might be part of an application's views. The app uses `ViewComponent`, `Stimulus` and `Vident`.
230
+ Vident provides a hook for performing actions after component initialization:
274
231
 
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.
232
+ ```ruby
233
+ class MyComponent < Vident::ViewComponent::Base
234
+ prop :data, Hash, default: -> { {} }
235
+
236
+ def after_component_initialize
237
+ @processed_data = process_data(@data)
238
+ end
239
+
240
+ private
241
+
242
+ def process_data(data)
243
+ # Your initialization logic here
244
+ data.transform_values(&:upcase)
245
+ end
246
+ end
247
+ ```
277
248
 
278
- ![ex1.gif](examples/ex1.gif)
249
+ **Important**: If you decide to override Literal's `after_initialize`, you **must** call `super` first to ensure Vident's initialization completes properly. Alternatively, use `after_component_initialize` which doesn't require calling `super`.
279
250
 
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 %>
251
+ ### Built-in Properties
306
252
 
307
- <!-- ... -->
308
- ```
253
+ Every Vident component includes these properties:
309
254
 
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
- ```
255
+ - `element_tag` - The HTML tag for the root element (default: `:div`)
256
+ - `id` - The component's DOM ID (auto-generated if not provided)
257
+ - `classes` - Additional CSS classes
258
+ - `html_options` - Hash of HTML attributes
334
259
 
335
- Let's look at the components in more detail.
260
+ ### Root Element Rendering
336
261
 
337
- The main component is the `GreeterComponent`:
262
+ The `root_element` helper method renders your component's root element with all configured attributes:
338
263
 
339
264
  ```ruby
340
- # app/components/greeter_component.rb
265
+ # In your component class
266
+ def element_classes
267
+ ["card", featured? ? "card-featured" : nil]
268
+ end
269
+
270
+ private
341
271
 
342
- class GreeterComponent < ::Vident::ViewComponent::Base
343
- renders_one :trigger, ButtonComponent
272
+ def root_element_attributes
273
+ {
274
+ html_options: { role: "article", "aria-label": title }
275
+ }
344
276
  end
345
277
  ```
346
278
 
347
279
  ```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 %>
280
+ <%# In your template %>
281
+ <%= root_element do %>
282
+ <h2><%= title %></h2>
283
+ <p><%= subtitle %></p>
376
284
  <% end %>
377
-
378
285
  ```
379
286
 
380
- ```js
381
- // app/components/greeter_component_controller.js
287
+ ## Component DSL
382
288
 
383
- import { Controller } from "@hotwired/stimulus"
289
+ ### ViewComponent Integration
384
290
 
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
- }
291
+ ```ruby
292
+ class MyComponent < Vident::ViewComponent::Base
293
+ # Component code
294
+ end
403
295
 
404
- clear() {
405
- this.outputTarget.textContent = '...';
406
- this.nameTarget.value = '';
407
- }
408
- }
296
+ # Or with an application base class
297
+ class ApplicationComponent < Vident::ViewComponent::Base
298
+ # Shared configuration
299
+ end
300
+
301
+ class MyComponent < ApplicationComponent
302
+ # Component code
303
+ end
409
304
  ```
410
305
 
411
- The slot renders a `ButtonComponent` component:
306
+ ### Phlex Integration
412
307
 
413
308
  ```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
309
+ class MyComponent < Vident::Phlex::HTML
310
+ def view_template
311
+ root do
312
+ h1 { "Hello from Phlex!" }
443
313
  end
444
314
  end
445
315
  end
446
316
  ```
447
317
 
448
- ```js
449
- // app/components/button_component_controller.js
318
+ ## Stimulus Integration
450
319
 
451
- import { Controller } from "@hotwired/stimulus"
320
+ Vident provides comprehensive Stimulus.js integration to add interactivity to your components.
452
321
 
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
- }
461
-
462
- ```
322
+ ### Declarative Stimulus DSL
463
323
 
464
- ### Usage
465
- How to use my plugin.
466
-
467
- ### Installation
468
- Add this line to your application's Gemfile:
324
+ Use the `stimulus` block for clean, declarative configuration:
469
325
 
470
326
  ```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
327
+ class ToggleComponent < Vident::ViewComponent::Base
328
+ prop :expanded, _Boolean, default: false
329
+
330
+ stimulus do
331
+ # Define actions the controller responds to
332
+ actions :toggle, :expand, :collapse
333
+
334
+ # Define targets for DOM element references
335
+ targets :button, :content
336
+
337
+ # Define static values
338
+ values animation_duration: 300
339
+
340
+ # Define dynamic values using procs (evaluated in component context)
341
+ values item_count: -> { @items.count }
342
+ values current_state: proc { expanded? ? "open" : "closed" }
343
+
344
+ # Map values from component props
345
+ values_from_props :expanded
346
+
347
+ # Define CSS classes for different states
348
+ classes expanded: "block",
349
+ collapsed: "hidden",
350
+ transitioning: "opacity-50"
351
+ end
352
+ end
482
353
  ```
483
354
 
484
- ---
355
+ ### Dynamic Values and Classes with Procs
485
356
 
486
-
487
- ## gem: vident-view_component
488
-
489
- # Vident::ViewComponent
490
-
491
- [ViewComponent](https://viewcomponent.org/) powered [Vident](https://github.com/stevegeek/vident) components.
357
+ The Stimulus DSL supports dynamic values and classes using procs or lambdas that are evaluated in the component instance context:
492
358
 
493
359
  ```ruby
494
- class ApplicationComponent < ::Vident::ViewComponent::Base
360
+ class DynamicComponent < Vident::ViewComponent::Base
361
+ prop :items, _Array(Hash), default: -> { [] }
362
+ prop :loading, _Boolean, default: false
363
+ prop :user, _Nilable(User)
364
+
365
+ stimulus do
366
+ # Mix static and dynamic values in a single call
367
+ values(
368
+ static_config: "always_same",
369
+ item_count: -> { @items.count },
370
+ loading_state: proc { @loading ? "loading" : "idle" },
371
+ user_role: -> { @user&.role || "guest" },
372
+ api_endpoint: -> { Rails.application.routes.url_helpers.api_items_path }
373
+ )
374
+
375
+ # Mix static and dynamic classes
376
+ classes(
377
+ base: "component-container",
378
+ loading: -> { @loading ? "opacity-50 cursor-wait" : "" },
379
+ size: proc { @items.count > 10 ? "large" : "small" },
380
+ theme: -> { current_user&.dark_mode? ? "dark" : "light" }
381
+ )
382
+
383
+ # Dynamic actions and targets
384
+ actions -> { @loading ? [] : [:click, :submit] }
385
+ targets -> { @expanded ? [:content, :toggle] : [:toggle] }
386
+ end
387
+
388
+ private
389
+
390
+ def current_user
391
+ @current_user ||= User.current
392
+ end
495
393
  end
496
394
  ```
497
395
 
498
- For more details see [vident](https://github.com/stevegeek/vident).
396
+ Procs have access to instance variables, component methods, and Rails helpers.
499
397
 
500
- ### Examples
398
+ **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:
501
399
 
502
- Before we dive into a specific example note that there are some components implemented in the `test/dummy/app/components`.
503
-
504
- Try them out by starting Rails:
505
-
506
- ```bash
507
- cd test/dummy
508
- bundle install
509
- rails assets:precompile
510
- rails s
400
+ ```ruby
401
+ stimulus do
402
+ # Single proc returns a single value (even if it's an array)
403
+ actions -> { @expanded ? [:click, :submit] : :click }
404
+
405
+ # Multiple procs provide multiple values
406
+ actions -> { @can_edit ? :edit : nil },
407
+ -> { @can_delete ? :delete : nil },
408
+ :cancel # static value
409
+
410
+ # This results in: [:edit, :delete, :cancel] (assuming both conditions are true)
411
+ end
511
412
  ```
512
413
 
513
- and visiting http://localhost:3000
414
+ ### Scoped Custom Events
514
415
 
515
-
516
- ### A Vident component example (without Stimulus)
517
-
518
- First is an example component that uses `Vident::ViewComponent::Base` but no Stimulus features.
519
-
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.
416
+ Vident provides helper methods to generate scoped event names for dispatching custom events that are unique to your component:
521
417
 
522
418
  ```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
419
+ class MyComponent < Vident::ViewComponent::Base
420
+ stimulus do
421
+ # Define an action that responds to a scoped event
422
+ actions -> { [stimulus_scoped_event_on_window(:data_loaded), :handle_data_loaded] }
547
423
  end
548
-
549
- def element_classes
550
- [size_classes, shape_class, border? ? "border" : ""]
424
+
425
+ def handle_click
426
+ # Dispatch a scoped event from JavaScript
427
+ # This would generate: "my-component:dataLoaded"
428
+ puts stimulus_scoped_event(:data_loaded)
429
+
430
+ # For window events, this generates: "my-component:dataLoaded@window"
431
+ puts stimulus_scoped_event_on_window(:data_loaded)
551
432
  end
433
+ end
552
434
 
553
- alias_method :image_avatar?, :url?
435
+ # Available as both class and instance methods:
436
+ MyComponent.stimulus_scoped_event(:data_loaded) # => "my-component:dataLoaded"
437
+ MyComponent.new.stimulus_scoped_event(:data_loaded) # => "my-component:dataLoaded"
438
+ ```
554
439
 
555
- def shape_class
556
- (shape == :circle) ? "rounded-full" : "rounded-md"
557
- end
440
+ This is useful for:
441
+ - Dispatching events from Stimulus controllers to communicate between components
442
+ - Creating unique event names that won't conflict with other components
443
+ - Setting up window-level event listeners with scoped names
558
444
 
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
445
+ ### Manual Stimulus Configuration
577
446
 
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
447
+ For more control, configure Stimulus attributes manually:
448
+
449
+ ```ruby
450
+ class CustomComponent < Vident::ViewComponent::Base
451
+ private
452
+
453
+ def root_element_attributes
454
+ {
455
+ element_tag: :article,
456
+ stimulus_controllers: ["custom", "analytics"],
457
+ stimulus_actions: [
458
+ [:click, :handleClick],
459
+ [:custom_event, :handleCustom]
460
+ ],
461
+ stimulus_values: {
462
+ endpoint: "/api/data",
463
+ refresh_interval: 5000
464
+ },
465
+ stimulus_targets: {
466
+ container: true
467
+ }
468
+ }
593
469
  end
594
470
  end
595
471
  ```
596
472
 
473
+ or you can use tag helpers to generate HTML with Stimulus attributes:
474
+
597
475
  ```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>
476
+ <%= content_tag(:input, type: "text", class: "...", data: {**greeter.stimulus_target(:name)}) %>
477
+ <%= content_tag(:button, @cta, class: "...", data: {**greeter.stimulus_action([:click, :greet])}) do %>
478
+ <%= @cta %>
604
479
  <% end %>
605
- <% end %>
606
- ```
480
+ <%= content_tag(:span, class: "...", data: {**greeter.stimulus_target(:output)}) %>
607
481
 
608
- Example usages:
482
+ <%# OR use the vident tag helper %>
609
483
 
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"}) %>
484
+ <%= greeter.tag(:input, stimulus_target: :name, type: "text", class: "...") %>
485
+ <%= greeter.tag(:button, stimulus_action: [:click, :greet], class: "...") do %>
486
+ <%= @cta %>
487
+ <% end %>
488
+ <%= greeter.tag(:span, stimulus_target: :output, class: "...") %>
615
489
  ```
616
490
 
617
- The following is rendered when used `render AvatarComponent.new(initials: "SG", size: :small, border: true)`:
491
+ or in your Phlex templates:
618
492
 
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>
493
+ ```ruby
494
+ root_element do |greeter|
495
+ input(type: "text", data: {**greeter.stimulus_target(:name)}, class: %(...))
496
+ trigger_or_default(greeter)
497
+ greeter.tag(:span, stimulus_target: :output, class: "ml-4 #{greeter.class_list_for_stimulus_classes(:pre_click)}") do
498
+ plain %( ... )
499
+ end
500
+ end
623
501
  ```
624
502
 
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"})`:
503
+ or directly in the ViewComponent template (eg with ERB) using the `as_stimulus_*` helpers
626
504
 
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">
505
+ ```erb
506
+ <%# HTML embellishment approach, most familiar to working with HTML in ERB, but is injecting directly into open HTML tags... %>
507
+ <input type="text"
508
+ <%= greeter.as_stimulus_targets(:name) %>
509
+ class="...">
510
+ <button <%= greeter.as_stimulus_actions([:click, :greet]) %>
511
+ class="...">
512
+ <%= @cta %>
513
+ </button>
514
+ <span <%= greeter.as_stimulus_targets(:output) %> class="..."></span>
629
515
  ```
630
516
 
631
- ----
632
-
633
- ![Example](examples/avatar.png)
634
517
 
635
- ### Usage
636
- How to use my plugin.
518
+ ### Stimulus Helpers in Templates
637
519
 
638
- ### Installation
639
- Add this line to your application's Gemfile:
520
+ Vident provides helper methods for generating Stimulus attributes:
640
521
 
641
- ```ruby
642
- gem "vident-view_component"
522
+ ```erb
523
+ <%= render root do |component| %>
524
+ <!-- Create a target -->
525
+ <div <%= component.as_target(:content) %>>
526
+ Content here
527
+ </div>
528
+
529
+ <!-- Create an action -->
530
+ <button <%= component.as_action(:click, :toggle) %>>
531
+ Toggle
532
+ </button>
533
+
534
+ <!-- Use the tag helper -->
535
+ <%= component.tag :div, stimulus_target: :output, class: "mt-4" do %>
536
+ Output here
537
+ <% end %>
538
+
539
+ <!-- Multiple targets/actions -->
540
+ <input <%= component.as_targets(:input, :field) %>
541
+ <%= component.as_actions([:input, :validate], [:change, :save]) %>>
542
+ <% end %>
643
543
  ```
644
544
 
645
- And then execute:
646
- ```bash
647
- $ bundle
648
- ```
545
+ ### Stimulus Outlets
649
546
 
650
- Or install it yourself as:
651
- ```bash
652
- $ gem install vident-view_component
653
- ```
547
+ Connect components via Stimulus outlets:
654
548
 
655
- ---
656
549
 
657
- ## gem: vident-better_html
658
550
 
659
- # Vident::BetterHtml
660
- Short description and motivation.
661
551
 
662
- ### Usage
663
- How to use my plugin.
552
+ ### Stimulus Controller Naming
664
553
 
665
- ```ruby
666
- BetterHtml.config = BetterHtml::Config.new(YAML.load(File.read(".better-html.yml")))
554
+ Vident automatically generates Stimulus controller names based on your component class:
667
555
 
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
556
+ - `ButtonComponent` → `button-component`
557
+ - `Admin::UserCardComponent` `admin--user-card-component`
558
+ - `MyApp::WidgetComponent` → `my-app--widget-component`
675
559
 
676
- def to_s
677
- to_s_without_html_safe.html_safe
678
- end
679
- end
680
- ```
560
+ ### Working with Child Components
681
561
 
682
- ### Installation
683
- Add this line to your application's Gemfile:
562
+ Setting Stimulus configuration between parent and child components:
684
563
 
685
564
  ```ruby
686
- gem "vident-better_html"
687
- ```
688
-
689
- And then execute:
690
- ```bash
691
- $ bundle
565
+ class ParentComponent < Vident::ViewComponent::Base
566
+ renders_one :a_nested_component, ButtonComponent
567
+
568
+ stimulus do
569
+ actions :handleTrigger
570
+ end
571
+ end
692
572
  ```
693
573
 
694
- Or install it yourself as:
695
- ```bash
696
- $ gem install vident-better_html
574
+ ```erb
575
+ <%= root_element do |parent| %>
576
+ <% parent.with_a_nested_component(
577
+ text: "Click me",
578
+ stimulus_actions: [
579
+ parent.stimulus_action(:click, :handleTrigger)
580
+ ]
581
+ ) %>
582
+ <% end %>
697
583
  ```
698
584
 
699
- ---
585
+ This creates a nested component that once clicked triggers the parent components `handleTrigger` action.
700
586
 
701
- ## gem: vident-phlex
587
+ ## Other Features
702
588
 
703
- # Vident::Phlex
589
+ ### Custom Element Tags
704
590
 
705
- [Phlex](https://phlex.fun/) powered [Vident](https://github.com/stevegeek/vident) components.
591
+ Change the root element tag dynamically:
706
592
 
707
593
  ```ruby
708
- class ApplicationComponent < ::Vident::Phlex::HTML
594
+ class LinkOrButtonComponent < Vident::ViewComponent::Base
595
+ prop :url, _Nilable(String)
596
+
597
+ private
598
+
599
+ def root_element_attributes
600
+ {
601
+ element_tag: url? ? :a : :button,
602
+ html_options: {
603
+ href: url,
604
+ type: url? ? nil : "button"
605
+ }.compact
606
+ }
607
+ end
709
608
  end
710
609
  ```
711
610
 
712
- For more details see [vident](https://github.com/stevegeek/vident).
611
+ ### Intelligent Class Management
713
612
 
714
- ### Usage
715
- How to use my plugin.
716
-
717
- ### Installation
718
- Add this line to your application's Gemfile:
613
+ Vident intelligently merges CSS classes from multiple sources:
719
614
 
720
615
  ```ruby
721
- gem "vident-phlex"
722
- ```
723
-
724
- And then execute:
725
- ```bash
726
- $ bundle
616
+ class StyledComponent < Vident::ViewComponent::Base
617
+ prop :variant, Symbol, default: :default
618
+
619
+ private
620
+
621
+ # Classes on the root element
622
+ def element_classes
623
+ ["base-class", variant_class]
624
+ end
625
+
626
+ def variant_class
627
+ case @variant
628
+ when :primary then "text-blue-600 bg-blue-100"
629
+ when :danger then "text-red-600 bg-red-100"
630
+ else "text-gray-600 bg-gray-100"
631
+ end
632
+ end
633
+ end
727
634
  ```
728
635
 
729
- Or install it yourself as:
730
- ```bash
731
- $ gem install vident-phlex
636
+ Usage:
637
+ ```erb
638
+ <!-- All classes are intelligently merged -->
639
+ <%= render StyledComponent.new(
640
+ variant: :primary,
641
+ classes: "rounded-lg shadow"
642
+ ) %>
643
+ <!-- Result: class="base-class text-blue-600 bg-blue-100 rounded-lg shadow" -->
732
644
  ```
733
645
 
734
- ---
735
-
736
- ## gem: vident-tailwind
737
-
738
- # Vident::Tailwind
739
- Short description and motivation.
646
+ ### Tailwind CSS Integration
740
647
 
741
- ### Usage
742
- How to use my plugin.
743
-
744
- ### Installation
745
- Add this line to your application's Gemfile:
648
+ Vident includes built-in support for Tailwind CSS class merging when the `tailwind_merge` gem is available:
746
649
 
747
650
  ```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
651
+ class TailwindComponent < Vident::ViewComponent::Base
652
+ prop :size, Symbol, default: :medium
653
+
654
+ private
655
+
656
+ def element_classes
657
+ # Conflicts with size_class will be resolved automatically
658
+ "p-2 text-sm #{size_class}"
659
+ end
660
+
661
+ def size_class
662
+ case @size
663
+ when :small then "p-1 text-xs"
664
+ when :large then "p-4 text-lg"
665
+ else "p-2 text-base"
666
+ end
667
+ end
668
+ end
759
669
  ```
760
670
 
761
- ---
762
-
763
- ## gem: vident-typed-minitest
764
-
765
- # Vident::Typed::Minitest
766
- Short description and motivation.
767
-
768
- ### Usage
769
- How to use my plugin.
671
+ ### Component Caching
770
672
 
771
- ### Installation
772
- Add this line to your application's Gemfile:
673
+ Enable fragment caching for expensive components:
773
674
 
774
675
  ```ruby
775
- gem "vident-typed-minitest"
776
- ```
777
-
778
- And then execute:
779
- ```bash
780
- $ bundle
676
+ class ExpensiveComponent < Vident::ViewComponent::Base
677
+ include Vident::Caching
678
+
679
+ with_cache_key :to_h # Cache based on all attributes
680
+ # or
681
+ with_cache_key :id, :updated_at # Cache based on specific attributes
682
+ end
781
683
  ```
782
684
 
783
- Or install it yourself as:
784
- ```bash
785
- $ gem install vident-typed-minitest
685
+ ```erb
686
+ <% cache component.cache_key do %>
687
+ <%= render component %>
688
+ <% end %>
786
689
  ```
787
690
 
788
- ---
789
-
790
- ## gem: vident-typed-phlex
791
-
792
- # Vident::Typed::Phlex
793
691
 
794
- Adds typed attributes to Vident Phlex based components.
692
+ ## Testing
795
693
 
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.
694
+ Vident components work seamlessly with testing frameworks that support ViewComponent or Phlex.
805
695
 
806
- ### Installation
807
- Add this line to your application's Gemfile:
696
+ ## Development
808
697
 
809
- ```ruby
810
- gem "vident-typed-phlex"
811
- ```
698
+ ### Running Tests
812
699
 
813
- And then execute:
814
700
  ```bash
815
- $ bundle
701
+ # Run all tests
702
+ bin/rails test
816
703
  ```
817
704
 
818
- Or install it yourself as:
819
- ```bash
820
- $ gem install vident-typed-phlex
821
- ```
705
+ ### Local Development
822
706
 
823
- ---
707
+ ```bash
708
+ # Clone the repository
709
+ git clone https://github.com/stevegeek/vident.git
710
+ cd vident
824
711
 
712
+ # Install dependencies
713
+ bundle install
825
714
 
826
- ## gem: vident-typed
715
+ # Run the dummy app
716
+ cd test/dummy
717
+ rails s
718
+ ```
827
719
 
828
- # Vident::Typed
829
- Short description and motivation.
720
+ ## Contributing
830
721
 
831
- ### Usage
832
- How to use my plugin.
722
+ 1. Fork the repository
723
+ 2. Create your feature branch (`git checkout -b feature/my-new-feature`)
724
+ 3. Write tests for your changes
725
+ 4. Commit your changes (`git commit -am 'Add new feature'`)
726
+ 5. Push to the branch (`git push origin feature/my-new-feature`)
727
+ 6. Create a Pull Request
833
728
 
834
- ### Installation
835
- Add this line to your application's Gemfile:
729
+ ## License
836
730
 
837
- ```ruby
838
- gem "vident-typed"
839
- ```
731
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
840
732
 
841
- And then execute:
842
- ```bash
843
- $ bundle
844
- ```
733
+ ## Credits
845
734
 
846
- Or install it yourself as:
847
- ```bash
848
- $ gem install vident-typed
849
- ```
735
+ Vident is maintained by [Stephen Ierodiaconou](https://github.com/stevegeek).
850
736
 
851
- ---
737
+ Special thanks to the ViewComponent and Phlex communities for their excellent component frameworks that Vident builds upon.