SkillAgentSearch skills...

Vident

Create flexible & maintainable Stimulus powered view component libraries

Install / Use

/learn @stevegeek/Vident
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Vident

A powerful Ruby gem for building interactive, type-safe components in Rails applications with seamless Stimulus.js integration.

Vident supports both ViewComponent and Phlex rendering engines while providing a consistent API for creating reusable UI components powered by Stimulus.js.

Table of Contents

Introduction

Vident is a collection of gems that enhance Rails view components with:

  • Type-safe properties using the Literal gem
  • First-class Stimulus.js integration for interactive behaviors
  • Support for both ViewComponent and Phlex rendering engines
  • Intelligent CSS class management with built-in Tailwind CSS merging
  • Component caching for improved performance
  • Declarative DSL for clean, maintainable component code

Why Vident?

Stimulus.js is a powerful framework for adding interactivity to HTML, but managing the data attributes can be cumbersome, and refactoring can be error-prone (as say controller names are repeated in many places).

Vident simplifies this by providing a declarative DSL for defining Stimulus controllers, actions, targets, and values directly within your component classes so you don't need to manually craft data attributes in your templates.

Vident also ensures that your components are flexible: for example you can easily add to, or override configuration, classes etc at the point of rendering.

Vident's goal is to make building UI components more maintainable, and remove some of the boilerplate code of Stimulus without being over-bearing or including too much magic.

Installation

Add the core gem and your preferred rendering engine integration to your Gemfile:

# Core gem (required)
gem "vident"

# Choose your rendering engine (at least one required)
gem "vident-view_component"  # For ViewComponent support
gem "vident-phlex"           # For Phlex support

Then run:

bundle install

Quick Start

Here's a simple example of a Vident component using ViewComponent:

# app/components/button_component.rb
class ButtonComponent < Vident::ViewComponent::Base
  # Define typed properties
  prop :text, String, default: "Click me"
  prop :url, _Nilable(String)
  prop :style, _Union(:primary, :secondary), default: :primary
  prop :clicked_count, Integer, default: 0
  
  # Configure Stimulus integration
  stimulus do
    # Setup actions, including with proc to evaluate on instance 
    actions [:click, :handle_click], 
            -> { [stimulus_scoped_event(:my_custom_event), :handle_this] if should_handle_this? }
    # Map the clicked_count prop as a Stimulus value
    values_from_props :clicked_count
    # Dynamic values using procs (evaluated in component context)
    values item_count: -> { @items.count },
           api_url: -> { Rails.application.routes.url_helpers.api_items_path },
           loading_duration: 1000 # or set static values
    # Static and dynamic classes
    classes loading: "opacity-50 cursor-wait",
            size: -> { @items.count > 10 ? "large" : "small" }
  end

  def call
    root_element do |component|
      # Wire up targets etc
      component.tag(:span, stimulus_target: :status) do
        @text
      end
    end
  end

  private

  # Configure your components root HTML element
  def root_element_attributes
    {
      element_tag: @url ? :a : :button,
      html_options: { href: @url }.compact
    }
  end

  # optionally add logic to determine initial classes
  def root_element_classes
    base_classes = "btn"
    case @style
    when :primary
      "#{base_classes} btn-primary"
    when :secondary
      "#{base_classes} btn-secondary"
    end
  end
end

Add the corresponding Stimulus controller would be:

// app/javascript/controllers/button_component_controller.js
// Can also be "side-car" in the same directory as the component, see the documentation for details
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = { 
    clickedCount: Number, 
    loadingDuration: Number 
  }
  static classes = ["loading"]
  static targets = ["status"]
  
  handleClick(event) {
    // Increment counter
    this.clickedCountValue++
    
    // Store original text
    const originalText = this.statusTarget.textContent
    
    // Add loading state
    this.element.classList.add(this.loadingClass)
    this.element.disabled = true
    this.statusTarget.textContent = "Loading..."
    
    // Use the loading duration from the component
    setTimeout(() => {
      this.element.classList.remove(this.loadingClass)
      this.element.disabled = false
      
      // Update text to show count
      this.statusTarget.textContent = `${originalText} (${this.clickedCountValue})`
    }, this.loadingDurationValue)
  }
}

Use the component in your views:

<!-- Default clicked count of 0 -->
<%= render ButtonComponent.new(text: "Save", style: :primary) %>

<!-- Pre-set clicked count -->
<%= render ButtonComponent.new(text: "Submit", style: :primary, clicked_count: 5) %>

<!-- Link variant -->
<%= render ButtonComponent.new(text: "Cancel", url: "/home", style: :secondary) %>

<!-- Override things -->
<%= render ButtonComponent.new(text: "Cancel", url: "/home" classes: "bg-red-900", html_options: {role: "button"}) %>

The rendered HTML includes all Stimulus data attributes:

<!-- First button with default count -->
<button class="bg-blue-500 hover:bg-blue-700 text-white" 
        data-controller="button-component" 
        data-action="click->button-component#handleClick"
        data-button-component-clicked-count-value="0"
        data-button-component-loading-duration-value="1000"
        data-button-component-loading-class="opacity-50 cursor-wait"
        id="button-component-123">
  <span data-button-component-target="status">Save</span>
</button>

<!-- Second button with pre-set count -->
<button class="bg-blue-500 hover:bg-blue-700 text-white" 
        data-controller="button-component" 
        data-action="click->button-component#handleClick"
        data-button-component-clicked-count-value="5"
        data-button-component-loading-duration-value="1000"
        data-button-component-loading-class="opacity-50 cursor-wait"
        id="button-component-456">
  <span data-button-component-target="status">Submit</span>
</button>

Core Concepts

Component Properties

Vident uses the Literal gem to provide type-safe component properties:

class CardComponent < Vident::ViewComponent::Base
  # Basic property with type
  prop :title, String
  
  # Property with default value
  prop :subtitle, String, default: ""
  
  # Nullable property
  prop :image_url, _Nilable(String)
  
  # Property with validation
  prop :size, _Union(:small, :medium, :large), default: :medium
  
  # Boolean property (creates predicate method)
  prop :featured, _Boolean, default: false
end

Post-Initialization Hooks

Vident provides a hook for performing actions after component initialization:

class MyComponent < Vident::ViewComponent::Base
  prop :data, Hash, default: -> { {} }
  
  def after_component_initialize
    @processed_data = process_data(@data)
  end
  
  private
  
  def process_data(data)
    # Your initialization logic here
    data.transform_values(&:upcase)
  end
end

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.

Built-in Properties

Every Vident component includes these properties:

  • element_tag - The HTML tag for the root element (default: :div)
  • id - The component's DOM ID (auto-generated if not provided)
  • classes - Additional CSS classes
  • html_options - Hash of HTML attributes

Root Element Rendering

The root_element helper method renders your component's root element with all configured attributes:

# In your component class
def root_element_classes
  ["card", featured? ? "card-featured" : nil]
end

private

def root_element_attributes
  {
    html_options: { role: "article", "aria-label": title }
  }
end
<%# In your template %>
<%= root_element do %>
  <h2><%= title %></h2>
  <p><%= subtitle %></p>
<% end %>

Component DSL

ViewComponent Integration

class MyComponent < Vident::ViewComponent::Base
  # Component code
end

# Or with an application base class
class ApplicationComponent < Vident::ViewComponent::Base
  # Shared configuration
end

class MyComponent < ApplicationComponent
  # Component code
end

Phlex Integration

class MyComponent < Vident::Phlex::HTML
  def view_template
    root do
      h1 { "Hello from Phlex!" }
    end
  end
end

Stimulus Integration

Vident provides comprehensive Stimulus.js integration to add interactivity to your components.

Declarative Stimulus DSL

Use the stimulus block for clean, declarative configuration:

class ToggleComponent < Vident::ViewComponent::Base
  prop :expanded, _Boolean, default: false
  
  stimulus do
    # Define actions the controller responds to
    actions :toggle, :expand, :collapse
    
    # Define targets for DOM element references
    targets :button, :content
    
    # Define static values
    values animation_duration: 300
    
    # Define dynamic values using procs (evaluated in component context)
    va

Related Skills

View on GitHub
GitHub Stars39
CategoryDevelopment
Updated3mo ago
Forks1

Languages

Ruby

Security Score

92/100

Audited on Dec 12, 2025

No findings