ViewComponentProps

A ViewComponent extension that adds a prop DSL to components: defaults, fallbacks, required props, casting, enum validation, custom validators, and a pluggable caster registry.

In Rails apps, a Railtie patches ViewComponent::Base during boot (enabled by default). Outside Rails, the gem patches on require. Inherit as usual and pass a props hash to new.

Requirements

  • Ruby >= 3.0
  • ViewComponent >= 3.0, < 5.0
  • ActiveSupport >= 6.0, < 9.0
  • ActiveModel >= 6.0, < 9.0

Rails is supported via a Railtie but not required; the gem auto-installs into ViewComponent::Base on require when Rails is not loaded.

Installation

Add this line to your application's Gemfile:

gem "view_component-props"

And then execute:

bundle install

Usage

class ButtonComponent < ViewComponent::Base
  CLASS = %w[
    px-9
    py-3
    text-lg
    font-medium
    tracking-wider
    ring-0
  ].freeze

  prop :label, required: true
  prop :shape, cast: :symbol, enum: %i[pill rounded rectangle], default: :pill
  prop :disabled, cast: :boolean, default: false

  def before_render
    @class = cn(CLASS, {
      "rounded-full" => @props[:shape] == :pill,
      "rounded-xl" => @props[:shape] == :rounded,
      "rounded-none" => @props[:shape] == :rectangle,
    }, @props[:class])
  end

  def call
    tag.button(@props[:label], class: @class, disabled: @props[:disabled])
  end
end

render ButtonComponent.new(label: "Save", shape: :rounded)

Resolved props are available as props (or @props), a frozen hash you can read with either string or symbol keys. The original input, before any casting or defaults are applied, is available as raw_props (or @raw_props).

Once props are resolved, #after_initialize runs, so you can override it for any setup that depends on props.

Options

Each prop accepts:

Option Purpose
default: Value (or callable) used when the key is missing from the input.
fallback: Value (or callable) used when the resolved value is nil.
required: Raises RequiredPropError when the resolved value is nil.
cast: Coerces the value. A built-in caster name or any callable.
enum: Restricts the value to a list of allowed values.
validate: Callable that must return truthy for the value to be accepted.
description: Free-form string for documentation and tooling.

Defaults and fallbacks can also be callables, evaluated in the context of the component instance. Use raw_props to read other props from the constructor input:

class AvatarComponent < ViewComponent::Base
  prop :user, required: true
  prop :alt, default: -> { "#{raw_props[:user].name}'s avatar" }
end

Casters

Built-in casters: :integer, :float, :string, :symbol, :boolean, :array, :hash, :decimal, :date, :datetime.

A nil value is never cast and stays nil. Use default: or fallback: when you need a value for a missing or nil prop.

Register custom casters in the configuration block:

ViewComponentProps.configure do |config|
  config.register_caster(:slug) do |value|
    value.to_s.parameterize
  end
end

class HeadingComponent < ViewComponent::Base
  prop :anchor, cast: :slug
end

Built-in casters can be overridden the same way by registering the same key again in configure.

Or pass a lambda inline:

prop :code, cast: ->(value) { value.to_s.upcase }

Strict props

class FormFieldComponent < ViewComponent::Base
  reject_undefined_props!

  prop :name, required: true
  prop :label
end

FormFieldComponent.new(name: "email", typo: true)
# => raises ViewComponentProps::UnknownPropsError

Custom base classes

ViewComponentProps.install!(MyComponentBase) applies the same patch to another class (idempotent). Useful if your app uses a shared component superclass that does not inherit from ViewComponent::Base.

install! no-ops when the target already includes Definable through any ancestor, so calling it on a subclass of ViewComponent::Base does nothing (the patch is already inherited). You only need it for base classes outside the ViewComponent::Base hierarchy.

Configuration

Rails

Disable auto-install in config/application.rb:

config.view_component_props.auto_include = false

When enabled (the default), ViewComponent::Base is patched after initializers load so config/initializers can configure the gem first.

Initializer

ViewComponentProps.configure do |config|
  config.reject_undefined_props = true

  config.register_caster(:slug) do |value|
    value.to_s.parameterize
  end
end

When reject_undefined_props is true, every component rejects unknown prop keys by default. Override per class with reject_undefined_props! or permit_undefined_props!.

Non-Rails

Auto-install runs on require when configuration.auto_include is true (the default). Because it is read at require time, you must configure it before requiring the gem for it to have any effect:

require "view_component_props/configuration"
ViewComponentProps.configure { |config| config.auto_include = false }

require "view_component_props"
ViewComponentProps.install!

Configuring auto_include after require "view_component_props" cannot reverse the install that already happened. Note this configuration.auto_include switch is consulted only outside Rails; in Rails the equivalent switch is config.view_component_props.auto_include, handled by the Railtie.

Development

After checking out the repo, run:

bin/setup

Run the test suite:

bundle exec rspec

Run the linter:

bundle exec rubocop

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/kinnell/view_component-props.

License

The gem is available as open source under the terms of the MIT License.