{"id":13878287,"url":"https://github.com/stevegeek/vident","last_synced_at":"2025-09-08T19:31:24.959Z","repository":{"id":63722123,"uuid":"569330843","full_name":"stevegeek/vident","owner":"stevegeek","description":"Create flexible \u0026 maintainable Stimulus powered view component libraries","archived":false,"fork":false,"pushed_at":"2025-07-25T13:04:13.000Z","size":886,"stargazers_count":38,"open_issues_count":3,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-01T19:42:17.352Z","etag":null,"topics":["phlex","rails","ruby","stimulus","stimulusjs","viewcomponent"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stevegeek.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-11-22T15:28:49.000Z","updated_at":"2025-08-20T08:05:57.000Z","dependencies_parsed_at":"2024-02-20T23:28:17.836Z","dependency_job_id":"ab336462-d4ba-446a-91a2-ffb5228ffa85","html_url":"https://github.com/stevegeek/vident","commit_stats":{"total_commits":125,"total_committers":2,"mean_commits":62.5,"dds":0.008000000000000007,"last_synced_commit":"422974fa794059de7209a3be1681da817a875d38"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/stevegeek/vident","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevegeek%2Fvident","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevegeek%2Fvident/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevegeek%2Fvident/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevegeek%2Fvident/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stevegeek","download_url":"https://codeload.github.com/stevegeek/vident/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevegeek%2Fvident/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274231438,"owners_count":25245585,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-08T02:00:09.813Z","response_time":121,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["phlex","rails","ruby","stimulus","stimulusjs","viewcomponent"],"created_at":"2024-08-06T08:01:45.244Z","updated_at":"2025-09-08T19:31:24.947Z","avatar_url":"https://github.com/stevegeek.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Vident\n\nA powerful Ruby gem for building interactive, type-safe components in Rails applications with seamless [Stimulus.js](https://stimulus.hotwired.dev/) integration. \n\nVident supports both [ViewComponent](https://viewcomponent.org/) and [Phlex](https://www.phlex.fun/) rendering engines while providing a consistent API for creating \nreusable UI components powered by [Stimulus.js](https://stimulus.hotwired.dev/).\n\n## Table of Contents\n\n- [Introduction](#introduction)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Core Concepts](#core-concepts)\n- [Component DSL](#component-dsl)\n- [Stimulus Integration](#stimulus-integration)\n- [Advanced Features](#advanced-features)\n- [Testing](#testing)\n- [Contributing](#contributing)\n\n## Introduction\n\nVident is a collection of gems that enhance Rails view components with:\n\n- **Type-safe properties** using the Literal gem\n- **First-class [Stimulus.js](https://stimulus.hotwired.dev/) integration** for interactive behaviors\n- **Support for both [ViewComponent](https://viewcomponent.org/) and [Phlex](https://www.phlex.fun/)** rendering engines\n- **Intelligent CSS class management** with built-in Tailwind CSS merging\n- **Component caching** for improved performance\n- **Declarative DSL** for clean, maintainable component code\n\n### Why Vident?\n\nStimulus.js is a powerful framework for adding interactivity to HTML, but managing the data attributes can be cumbersome,\nand refactoring can be error-prone (as say controller names are repeated in many places).\n\nVident simplifies this by providing a declarative DSL for defining Stimulus controllers, actions, targets, and values\ndirectly within your component classes so you don't need to manually craft data attributes in your templates.\n\nVident also ensures that your components are flexible: for example you can easily add to, or override configuration,\nclasses etc at the point of rendering.\n\nVident's goal is to make building UI components more maintainable, and remove some of the boilerplate code of Stimulus \nwithout being over-bearing or including too much magic.\n\n## Installation\n\nAdd the core gem and your preferred rendering engine integration to your Gemfile:\n\n```ruby\n# Core gem (required)\ngem \"vident\"\n\n# Choose your rendering engine (at least one required)\ngem \"vident-view_component\"  # For ViewComponent support\ngem \"vident-phlex\"           # For Phlex support\n```\n\nThen run:\n\n```bash\nbundle install\n```\n\n## Quick Start\n\nHere's a simple example of a Vident component using ViewComponent:\n\n```ruby\n# app/components/button_component.rb\nclass ButtonComponent \u003c Vident::ViewComponent::Base\n  # Define typed properties\n  prop :text, String, default: \"Click me\"\n  prop :url, _Nilable(String)\n  prop :style, _Union(:primary, :secondary), default: :primary\n  prop :clicked_count, Integer, default: 0\n  \n  # Configure Stimulus integration\n  stimulus do\n    # Setup actions, including with proc to evaluate on instance \n    actions [:click, :handle_click], \n            -\u003e { [stimulus_scoped_event(:my_custom_event), :handle_this] if should_handle_this? }\n    # Map the clicked_count prop as a Stimulus value\n    values_from_props :clicked_count\n    # Dynamic values using procs (evaluated in component context)\n    values item_count: -\u003e { @items.count },\n           api_url: -\u003e { Rails.application.routes.url_helpers.api_items_path },\n           loading_duration: 1000 # or set static values\n    # Static and dynamic classes\n    classes loading: \"opacity-50 cursor-wait\",\n            size: -\u003e { @items.count \u003e 10 ? \"large\" : \"small\" }\n  end\n\n  def call\n    root_element do |component|\n      # Wire up targets etc\n      component.tag(:span, stimulus_target: :status) do\n        @text\n      end\n    end\n  end\n\n  private\n\n  # Configure your components root HTML element\n  def root_element_attributes\n    {\n      element_tag: @url ? :a : :button,\n      html_options: { href: @url }.compact\n    }\n  end\n\n  # optionally add logic to determine initial classes\n  def root_element_classes\n    base_classes = \"btn\"\n    case @style\n    when :primary\n      \"#{base_classes} btn-primary\"\n    when :secondary\n      \"#{base_classes} btn-secondary\"\n    end\n  end\nend\n```\n\n\nAdd the corresponding Stimulus controller would be:\n\n```javascript\n// app/javascript/controllers/button_component_controller.js\n// Can also be \"side-car\" in the same directory as the component, see the documentation for details\nimport { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  static values = { \n    clickedCount: Number, \n    loadingDuration: Number \n  }\n  static classes = [\"loading\"]\n  static targets = [\"status\"]\n  \n  handleClick(event) {\n    // Increment counter\n    this.clickedCountValue++\n    \n    // Store original text\n    const originalText = this.statusTarget.textContent\n    \n    // Add loading state\n    this.element.classList.add(this.loadingClass)\n    this.element.disabled = true\n    this.statusTarget.textContent = \"Loading...\"\n    \n    // Use the loading duration from the component\n    setTimeout(() =\u003e {\n      this.element.classList.remove(this.loadingClass)\n      this.element.disabled = false\n      \n      // Update text to show count\n      this.statusTarget.textContent = `${originalText} (${this.clickedCountValue})`\n    }, this.loadingDurationValue)\n  }\n}\n```\n\nUse the component in your views:\n\n```erb\n\u003c!-- Default clicked count of 0 --\u003e\n\u003c%= render ButtonComponent.new(text: \"Save\", style: :primary) %\u003e\n\n\u003c!-- Pre-set clicked count --\u003e\n\u003c%= render ButtonComponent.new(text: \"Submit\", style: :primary, clicked_count: 5) %\u003e\n\n\u003c!-- Link variant --\u003e\n\u003c%= render ButtonComponent.new(text: \"Cancel\", url: \"/home\", style: :secondary) %\u003e\n\n\u003c!-- Override things --\u003e\n\u003c%= render ButtonComponent.new(text: \"Cancel\", url: \"/home\" classes: \"bg-red-900\", html_options: {role: \"button\"}) %\u003e\n```\n\nThe rendered HTML includes all Stimulus data attributes:\n\n```html\n\u003c!-- First button with default count --\u003e\n\u003cbutton class=\"bg-blue-500 hover:bg-blue-700 text-white\" \n        data-controller=\"button-component\" \n        data-action=\"click-\u003ebutton-component#handleClick\"\n        data-button-component-clicked-count-value=\"0\"\n        data-button-component-loading-duration-value=\"1000\"\n        data-button-component-loading-class=\"opacity-50 cursor-wait\"\n        id=\"button-component-123\"\u003e\n  \u003cspan data-button-component-target=\"status\"\u003eSave\u003c/span\u003e\n\u003c/button\u003e\n\n\u003c!-- Second button with pre-set count --\u003e\n\u003cbutton class=\"bg-blue-500 hover:bg-blue-700 text-white\" \n        data-controller=\"button-component\" \n        data-action=\"click-\u003ebutton-component#handleClick\"\n        data-button-component-clicked-count-value=\"5\"\n        data-button-component-loading-duration-value=\"1000\"\n        data-button-component-loading-class=\"opacity-50 cursor-wait\"\n        id=\"button-component-456\"\u003e\n  \u003cspan data-button-component-target=\"status\"\u003eSubmit\u003c/span\u003e\n\u003c/button\u003e\n```\n\n## Core Concepts\n\n### Component Properties\n\nVident uses the Literal gem to provide type-safe component properties:\n\n```ruby\nclass CardComponent \u003c Vident::ViewComponent::Base\n  # Basic property with type\n  prop :title, String\n  \n  # Property with default value\n  prop :subtitle, String, default: \"\"\n  \n  # Nullable property\n  prop :image_url, _Nilable(String)\n  \n  # Property with validation\n  prop :size, _Union(:small, :medium, :large), default: :medium\n  \n  # Boolean property (creates predicate method)\n  prop :featured, _Boolean, default: false\nend\n```\n\n### Post-Initialization Hooks\n\nVident provides a hook for performing actions after component initialization:\n\n```ruby\nclass MyComponent \u003c Vident::ViewComponent::Base\n  prop :data, Hash, default: -\u003e { {} }\n  \n  def after_component_initialize\n    @processed_data = process_data(@data)\n  end\n  \n  private\n  \n  def process_data(data)\n    # Your initialization logic here\n    data.transform_values(\u0026:upcase)\n  end\nend\n```\n\n**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`.\n\n### Built-in Properties\n\nEvery Vident component includes these properties:\n\n- `element_tag` - The HTML tag for the root element (default: `:div`)\n- `id` - The component's DOM ID (auto-generated if not provided)\n- `classes` - Additional CSS classes\n- `html_options` - Hash of HTML attributes\n\n### Root Element Rendering\n\nThe `root_element` helper method renders your component's root element with all configured attributes:\n\n```ruby\n# In your component class\ndef root_element_classes\n  [\"card\", featured? ? \"card-featured\" : nil]\nend\n\nprivate\n\ndef root_element_attributes\n  {\n    html_options: { role: \"article\", \"aria-label\": title }\n  }\nend\n```\n\n```erb\n\u003c%# In your template %\u003e\n\u003c%= root_element do %\u003e\n  \u003ch2\u003e\u003c%= title %\u003e\u003c/h2\u003e\n  \u003cp\u003e\u003c%= subtitle %\u003e\u003c/p\u003e\n\u003c% end %\u003e\n```\n\n## Component DSL\n\n### ViewComponent Integration\n\n```ruby\nclass MyComponent \u003c Vident::ViewComponent::Base\n  # Component code\nend\n\n# Or with an application base class\nclass ApplicationComponent \u003c Vident::ViewComponent::Base\n  # Shared configuration\nend\n\nclass MyComponent \u003c ApplicationComponent\n  # Component code\nend\n```\n\n### Phlex Integration\n\n```ruby\nclass MyComponent \u003c Vident::Phlex::HTML\n  def view_template\n    root do\n      h1 { \"Hello from Phlex!\" }\n    end\n  end\nend\n```\n\n## Stimulus Integration\n\nVident provides comprehensive Stimulus.js integration to add interactivity to your components.\n\n### Declarative Stimulus DSL\n\nUse the `stimulus` block for clean, declarative configuration:\n\n```ruby\nclass ToggleComponent \u003c Vident::ViewComponent::Base\n  prop :expanded, _Boolean, default: false\n  \n  stimulus do\n    # Define actions the controller responds to\n    actions :toggle, :expand, :collapse\n    \n    # Define targets for DOM element references\n    targets :button, :content\n    \n    # Define static values\n    values animation_duration: 300\n    \n    # Define dynamic values using procs (evaluated in component context)\n    values item_count: -\u003e { @items.count }\n    values current_state: proc { expanded? ? \"open\" : \"closed\" }\n    \n    # Map values from component props\n    values_from_props :expanded\n    \n    # Define CSS classes for different states\n    classes expanded: \"block\",\n            collapsed: \"hidden\",\n            transitioning: \"opacity-50\"\n  end\nend\n```\n\n### Dynamic Values and Classes with Procs\n\nThe Stimulus DSL supports dynamic values and classes using procs or lambdas that are evaluated in the component instance context:\n\n```ruby\nclass DynamicComponent \u003c Vident::ViewComponent::Base\n  prop :items, _Array(Hash), default: -\u003e { [] }\n  prop :loading, _Boolean, default: false\n  prop :user, _Nilable(User)\n  \n  stimulus do\n    # Mix static and dynamic values in a single call\n    values(\n      static_config: \"always_same\",\n      item_count: -\u003e { @items.count },\n      loading_state: proc { @loading ? \"loading\" : \"idle\" },\n      user_role: -\u003e { @user\u0026.role || \"guest\" },\n      api_endpoint: -\u003e { Rails.application.routes.url_helpers.api_items_path }\n    )\n    \n    # Mix static and dynamic classes\n    classes(\n      base: \"component-container\",\n      loading: -\u003e { @loading ? \"opacity-50 cursor-wait\" : \"\" },\n      size: proc { @items.count \u003e 10 ? \"large\" : \"small\" },\n      theme: -\u003e { current_user\u0026.dark_mode? ? \"dark\" : \"light\" }\n    )\n    \n    # Dynamic actions and targets\n    actions -\u003e { @loading ? [] : [:click, :submit] }\n    targets -\u003e { @expanded ? [:content, :toggle] : [:toggle] }\n  end\n  \n  private\n  \n  def current_user\n    @current_user ||= User.current\n  end\nend\n```\n\nProcs have access to instance variables, component methods, and Rails helpers.\n\n**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:\n\n```ruby\nstimulus do\n  # Single proc returns a single value (even if it's an array)\n  actions -\u003e { @expanded ? [:click, :submit] : :click }\n  \n  # Multiple procs provide multiple values\n  actions -\u003e { @can_edit ? :edit : nil },\n          -\u003e { @can_delete ? :delete : nil },\n          :cancel  # static value\n  \n  # This results in: [:edit, :delete, :cancel] (assuming both conditions are true)\nend\n```\n\n### Scoped Custom Events\n\nVident provides helper methods to generate scoped event names for dispatching custom events that are unique to your component:\n\n```ruby\nclass MyComponent \u003c Vident::ViewComponent::Base\n  stimulus do\n    # Define an action that responds to a scoped event\n    actions -\u003e { [stimulus_scoped_event_on_window(:data_loaded), :handle_data_loaded] }\n  end\n  \n  def handle_click\n    # Dispatch a scoped event from JavaScript\n    # This would generate: \"my-component:dataLoaded\"\n    puts stimulus_scoped_event(:data_loaded)\n    \n    # For window events, this generates: \"my-component:dataLoaded@window\" \n    puts stimulus_scoped_event_on_window(:data_loaded)\n  end\nend\n\n# Available as both class and instance methods:\nMyComponent.stimulus_scoped_event(:data_loaded)      # =\u003e \"my-component:dataLoaded\"\nMyComponent.new.stimulus_scoped_event(:data_loaded)  # =\u003e \"my-component:dataLoaded\"\n```\n\nThis is useful for:\n- Dispatching events from Stimulus controllers to communicate between components\n- Creating unique event names that won't conflict with other components\n- Setting up window-level event listeners with scoped names\n\n### Manual Stimulus Configuration\n\nFor more control, configure Stimulus attributes manually:\n\n```ruby\nclass CustomComponent \u003c Vident::ViewComponent::Base\n  private\n  \n  def root_element_attributes\n    {\n      element_tag: :article,\n      stimulus_controllers: [\"custom\", \"analytics\"],\n      stimulus_actions: [\n        [:click, :handleClick],\n        [:custom_event, :handleCustom]\n      ],\n      stimulus_values: {\n        endpoint: \"/api/data\",\n        refresh_interval: 5000\n      },\n      stimulus_targets: {\n        container: true\n      }\n    }\n  end\nend\n```\n\nor you can use tag helpers to generate HTML with Stimulus attributes:\n\n```erb\n  \u003c%= content_tag(:input, type: \"text\", class: \"...\", data: {**greeter.stimulus_target(:name)}) %\u003e\n  \u003c%= content_tag(:button, @cta, class: \"...\", data: {**greeter.stimulus_action([:click, :greet])}) do %\u003e\n    \u003c%= @cta %\u003e\n  \u003c% end %\u003e\n  \u003c%= content_tag(:span, class: \"...\", data: {**greeter.stimulus_target(:output)}) %\u003e\n\n  \u003c%# OR use the vident tag helper  %\u003e\n\n  \u003c%= greeter.tag(:input, stimulus_target: :name, type: \"text\", class: \"...\") %\u003e\n  \u003c%= greeter.tag(:button, stimulus_action: [:click, :greet], class: \"...\") do %\u003e\n    \u003c%= @cta %\u003e\n  \u003c% end %\u003e\n  \u003c%= greeter.tag(:span, stimulus_target: :output, class: \"...\") %\u003e\n```\n\nor in your Phlex templates:\n\n```ruby\nroot_element do |greeter|\n  input(type: \"text\", data: {**greeter.stimulus_target(:name)}, class: %(...))\n  trigger_or_default(greeter)\n  greeter.tag(:span, stimulus_target: :output, class: \"ml-4 #{greeter.class_list_for_stimulus_classes(:pre_click)}\") do\n    plain %( ... )\n  end\nend\n```\n\nor directly in the ViewComponent template (eg with ERB) using the `as_stimulus_*` helpers\n\n```erb\n  \u003c%# HTML embellishment approach, most familiar to working with HTML in ERB, but is injecting directly into open HTML tags... %\u003e\n  \u003cinput type=\"text\"\n         \u003c%= greeter.as_stimulus_targets(:name) %\u003e\n         class=\"...\"\u003e\n  \u003cbutton \u003c%= greeter.as_stimulus_actions([:click, :greet]) %\u003e\n          class=\"...\"\u003e\n    \u003c%= @cta %\u003e\n  \u003c/button\u003e\n  \u003cspan \u003c%= greeter.as_stimulus_targets(:output) %\u003e class=\"...\"\u003e\u003c/span\u003e\n```\n\n\n### Stimulus Helpers in Templates\n\nVident provides helper methods for generating Stimulus attributes:\n\n```erb\n\u003c%= render root do |component| %\u003e\n  \u003c!-- Create a target --\u003e\n  \u003cdiv \u003c%= component.as_target(:content) %\u003e\u003e\n    Content here\n  \u003c/div\u003e\n  \n  \u003c!-- Create an action --\u003e\n  \u003cbutton \u003c%= component.as_action(:click, :toggle) %\u003e\u003e\n    Toggle\n  \u003c/button\u003e\n  \n  \u003c!-- Use the tag helper --\u003e\n  \u003c%= component.tag :div, stimulus_target: :output, class: \"mt-4\" do %\u003e\n    Output here\n  \u003c% end %\u003e\n  \n  \u003c!-- Multiple targets/actions --\u003e\n  \u003cinput \u003c%= component.as_targets(:input, :field) %\u003e \n         \u003c%= component.as_actions([:input, :validate], [:change, :save]) %\u003e\u003e\n\u003c% end %\u003e\n```\n\n### Stimulus Outlets\n\nConnect components via Stimulus outlets:\n\n\n\n\n### Stimulus Controller Naming\n\nVident automatically generates Stimulus controller names based on your component class:\n\n- `ButtonComponent` → `button-component`\n- `Admin::UserCardComponent` → `admin--user-card-component`\n- `MyApp::WidgetComponent` → `my-app--widget-component`\n\n### Working with Child Components\n\nSetting Stimulus configuration between parent and child components:\n\n```ruby\nclass ParentComponent \u003c Vident::ViewComponent::Base\n  renders_one :a_nested_component, ButtonComponent\n  \n  stimulus do\n    actions :handleTrigger\n  end\nend\n```\n\n```erb\n\u003c%= root_element do |parent| %\u003e\n  \u003c% parent.with_a_nested_component(\n    text: \"Click me\",\n    stimulus_actions: [\n      parent.stimulus_action(:click, :handleTrigger)\n    ]\n  ) %\u003e\n\u003c% end %\u003e\n```\n\nThis creates a nested component that once clicked triggers the parent components `handleTrigger` action.\n\n## Other Features\n\n### Custom Element Tags\n\nChange the root element tag dynamically:\n\n```ruby\nclass LinkOrButtonComponent \u003c Vident::ViewComponent::Base\n  prop :url, _Nilable(String)\n  \n  private\n  \n  def root_element_attributes\n    {\n      element_tag: url? ? :a : :button,\n      html_options: {\n        href: url,\n        type: url? ? nil : \"button\"\n      }.compact\n    }\n  end\nend\n```\n\n### Intelligent Class Management\n\nVident intelligently merges CSS classes from multiple sources:\n\n```ruby\nclass StyledComponent \u003c Vident::ViewComponent::Base\n  prop :variant, Symbol, default: :default\n  \n  private\n  \n  # Classes on the root element\n  def root_element_classes\n    [\"base-class\", variant_class]\n  end\n  \n  def variant_class\n    case @variant\n    when :primary then \"text-blue-600 bg-blue-100\"\n    when :danger then \"text-red-600 bg-red-100\"\n    else \"text-gray-600 bg-gray-100\"\n    end\n  end\nend\n```\n\nUsage:\n```erb\n\u003c!-- All classes are intelligently merged --\u003e\n\u003c%= render StyledComponent.new(\n  variant: :primary,\n  classes: \"rounded-lg shadow\"\n) %\u003e\n\u003c!-- Result: class=\"base-class text-blue-600 bg-blue-100 rounded-lg shadow\" --\u003e\n```\n\n### Tailwind CSS Integration\n\nVident includes built-in support for Tailwind CSS class merging when the `tailwind_merge` gem is available:\n\n```ruby\nclass TailwindComponent \u003c Vident::ViewComponent::Base\n  prop :size, Symbol, default: :medium\n  \n  private\n  \n  def root_element_classes\n    # Conflicts with size_class will be resolved automatically\n    \"p-2 text-sm #{size_class}\"\n  end\n  \n  def size_class\n    case @size\n    when :small then \"p-1 text-xs\"\n    when :large then \"p-4 text-lg\"\n    else \"p-2 text-base\"\n    end\n  end\nend\n```\n\n### Component Caching\n\nEnable fragment caching for expensive components:\n\n```ruby\nclass ExpensiveComponent \u003c Vident::ViewComponent::Base\n  include Vident::Caching\n  \n  with_cache_key :to_h  # Cache based on all attributes\n  # or\n  with_cache_key :id, :updated_at  # Cache based on specific attributes\nend\n```\n\n```erb\n\u003c% cache component.cache_key do %\u003e\n  \u003c%= render component %\u003e\n\u003c% end %\u003e\n```\n\n\n## Testing\n\nVident components work seamlessly with testing frameworks that support ViewComponent or Phlex.\n\n## Development\n\n### Running Tests\n\n```bash\n# Run all tests\nbin/rails test\n```\n\n### Local Development\n\n```bash\n# Clone the repository\ngit clone https://github.com/stevegeek/vident.git\ncd vident\n\n# Install dependencies\nbundle install\n\n# Run the dummy app\ncd test/dummy\nrails s\n```\n\n## Contributing\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/my-new-feature`)\n3. Write tests for your changes\n4. Commit your changes (`git commit -am 'Add new feature'`)\n5. Push to the branch (`git push origin feature/my-new-feature`)\n6. Create a Pull Request\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](LICENSE.txt).\n\n## Credits\n\nVident is maintained by [Stephen Ierodiaconou](https://github.com/stevegeek).\n\nSpecial thanks to the ViewComponent and Phlex communities for their excellent component frameworks that Vident builds upon.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevegeek%2Fvident","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstevegeek%2Fvident","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevegeek%2Fvident/lists"}