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