view_component-contrib

repository·master·Indexed 19 days ago

https://github.com/palkan/view_component-contrib

A meta-gem providing extensions, patterns, and development tools for the ViewComponent library. It features an extended sidecar pattern for component organization, boilerplate reduction for component previews via ViewComponentContrib::Preview::Base, and a StyleVariants system for managing CSS classes (optimized for TailwindCSS). It also includes tools for namespaced I18n, Stimulus controller registration, and CSS isolation using postcss-modules.

Tokens
9.1K
Snippets
32
Records
42
Agent score
64%

What's inside view_component-contrib

  1. Inherit style variants using different strategies

    master

    When a component inherits from a parent that has StyleVariants defined, you can choose how the child's style block interacts with the parent's variants using the strategy: option in the variants method:

    1. override (default): The child's variants completely replace the parent's variants.
    2. merge: Performs a deep merge. All parent variant keys are preserved unless explicitly overwritten by the child.
    3. extend: Performs a shallow merge. All parent variants are kept, but if a key matches, the entire variant object for that key is replaced by the child's version.
    # Using merge strategy
    class Child::Component < Parent::Component
      style do
        variants(strategy: :merge) do
          size { lg { "text-larger" } }
        end
      end
    end
  2. Organize components using the extended sidecar pattern

    master

    The extended sidecar pattern moves all component-related files (Ruby logic, templates, previews, CSS, and JS) into a single directory. This avoids directory bloat and removes the need for _component suffixes in filenames.

    Example structure:

    app/frontend/components/
      example/
        component.rb
        component.html
        preview.rb
        index.css
        index.js

    To use this structure, add the components folder to your Rails autoload paths:

    config.autoload_paths << Rails.root.join("app", "frontend", "components")
  3. Define dependent or compound styles

    master

    To handle complex styling where one variant depends on another (e.g., a primary theme that looks different when the size is lg), you have two options:

    1. Ruby Blocks: Pass the current variant values as block arguments to the variant definition for dynamic logic.
    2. compound directive: Use a declarative approach to define styles that apply only when a specific combination of variant values is met.
    # Option 1: Ruby Blocks
    style do
      variants {
        size { sm { "text-sm" }; lg { "text-lg" } }
        theme {
          primary do |size:, **|
            %w[bg-blue-500 text-white].tap do
              _1 << "uppercase" if size == :lg
            end
          end
        }
      }
    end
    
    # Option 2: Compound directive
    style do
      variants {
        size { sm { "text-sm" }; lg { "text-lg" } }
        theme { primary { %w[bg-blue-500 text-white] } }
      }
      compound(size: :lg, theme: :primary) { %w[uppercase] }
    end
  4. Wrap components with ViewComponentContrib::WrapperComponent

    master

    To wrap a component in custom HTML (e.g., for positioning) while respecting the component's #render? logic, use ViewComponentContrib::WrapperComponent. This ensures the wrapper only renders if the inner component's #render? method returns true.

    Standard Usage: Pass the component instance to ViewComponentContrib::WrappedComponent.new and use the block to define the wrapper HTML. Access the inner component via wrapper.component.

    <%= render ViewComponentContrib::WrappedComponent.new(Example::Component.new) do |wrapper| %>
      <div class="col-md-auto mb-4">
        <%= wrapper.component %>
      </div>
    <% end %>
  5. Manage CSS classes with Style Variants

    master

    Since v0.2.0, ViewComponentContrib::StyleVariants allows you to define a schema for CSS classes (ideal for TailwindCSS) within your component class. You define base classes, variants (like color, size, or disabled), and defaults.

    In your template, use the #style method to compile the classes based on the current component state.

    Key behaviors:

    • Boolean variants: Passing true or false automatically maps to yes or no variant keys.
    • Nil values: Passing nil triggers the use of the defined defaults.
    • Multiple sets: You can define multiple style sets (e.g., style :image do ... end) and call them specifically via style(:image, ...).
    • Manual classes: Use the special class: key to append additional classes: style(size:, class: 'extra-class').
    class ButtonComponent < ViewComponent::Base
      include ViewComponentContrib::StyleVariants
    
      style do
        base { %w[font-medium bg-blue-500 text-white rounded-full] }
        variants {
          color {
            primary { %w[bg-blue-500 text-white] }
            secondary { %w[bg-purple-500 text-white] }
          }
          size {
            sm { "text-sm" }
            md { "text-base" }
            lg { "px-4 py-3 text-lg" }
          }
          disabled {
            yes { "opacity-75" }
          }
        }
        defaults { {size: :md, color: :primary} }
      end
    
      def initialize(size: nil, color: nil, disabled: false)
        @size = size
        @color = color
        @disabled = disabled
      end
    end
    <button class="<%= style(size:, color:, disabled: true) %>">Click me</button>
  6. Isolate CSS using postcss-modules

    master

    To avoid global CSS collisions, use postcss-modules to generate unique, scoped class names. This allows you to use local names in your CSS and reference them in Ruby.

    1. Install: yarn add postcss-modules.
    2. Configure PostCSS: Set up generateScopedName to follow a convention: c-{identifier}-{name}. The identifier should match the Stimulus controller name.

    PostCSS Config Example:

    module.exports = {
      plugins: {
        'postcss-modules': {
          generateScopedName: (name, filename, _css) => {
            const matches = filename.match(/\/app\/frontend\/components\/?(.*)\/index.css$/);
            if (!matches) return name;
            const identifier = matches[1].replace("/", "--");
            return `c-${identifier}-${name}`;
          },
          getJSON: () => {}
        }
      }
    }
    1. Ruby Helper: Implement a class_for method in your ApplicationViewComponent to construct the scoped class name.
    class ApplicationViewComponent
      private
    
      def identifier
        @identifier ||= self.class.name.sub("::Component", "").underscore.split("/").join("--")
      end
    
      def class_for(name, from: identifier)
        "c-" + from + "-" + name
      end
    end

    Usage in Template:

    <div class="<%= class_for("container") %>">
      <p class="<%= class_for("body") %>"><%= text %></p>
    </div>
  7. Use namespaced I18n for ViewComponents

    master

    Instead of using isolated localization files, you can use a namespacing convention to manage translations centrally. By default, translations should be placed under the <locale>.view_components.<component_scope> key in your YAML files.

    Example YAML structure:

    en:
      view_components:
        login_form:
          submit: "Log in"
        nav:
          user_info:
            login: "Log in"
            logout: "Log out"

    Usage in templates: Use the standard t(".key") syntax within your component templates. The helper will automatically resolve the path based on the component's scope.

    Setup: If you are using ViewComponentContrib::Base, translation support is included automatically. If you are inheriting directly from ViewComponent::Base, you must include the ViewComponentContrib::TranslationHelper module.

    <!-- login_form/component.html.erb -->
    <button type="submit"><%= t(".submit") %></button>
    
    <!-- nav/user_info/component.html.erb -->
    <a href="/logout"><%= t(".logout") %></a>
  8. Enable sidecar previews via initializer

    master

    By default, ViewComponent requires preview files to have a _preview.rb suffix. To allow sidecar previews (where the file is simply named preview.rb within the component folder), patch the ViewComponent::Preview class in an initializer:

    ActiveSupport.on_load(:view_component) do
      ViewComponent::Preview.extend ViewComponentContrib::Preview::Sidecarable
    end
  9. Organize component assets (JS/CSS) with Vite or Webpack

    master

    This pattern uses a sidecar directory structure where each component has its own index.css and index.js.

    Structure:

    components/
      example/
        component.html
        component.rb
        index.css
        index.js

    Implementation:

    1. components/example/index.js should import the local CSS: import "./index.css".
    2. A root entrypoint (e.g., components/index.js) must glob and import all component entrypoints.

    Vite Example:

    import.meta.glob("./**/index.js").forEach((path) => {
      const mod = await import(path);
      mod.default();
    });

    Webpack Example:

    const context = require.context(".", true, /index.js$/);
    context.keys().forEach(context);
  10. Use dry-initializer for declarative component initialization

    master

    To move from imperative #initialize methods to a declarative style, you can integrate dry-initializer.

    1. Add extend Dry::Initializer to your base component class.
    2. Use the option method in your components to define parameters and default values.
    class ApplicationViewComponent
      extend Dry::Initializer
    end
    
    class FlashAlert::Component < ApplicationViewComponent
      option :type, default: proc { "success" }
      option :duration, default: proc { 3000 }
      option :body
    end
  11. Use the interactive generator to set up view_component-contrib

    master

    The easiest way to integrate view_component-contrib is to run the interactive Rails template generator. This command performs the following actions:

    • Installs the view_component-contrib gem.
    • Configures view_component paths.
    • Adds ApplicationViewComponent and ApplicationViewComponentPreview base classes.
    • Configures your testing framework (RSpec or Minitest).
    • Adds a custom generator to create components.

    Note: If your application includes lib/ in its autoload paths, ensure you ignore the generated lib/generators folder.

    • In Rails 7.1+, use the ignore option in config.autoload_lib.
    • In older versions, use Rails.autoloaders.main.ignore(...).
    rails app:template LOCATION="https://railsbytes.com/script/zJosO5"