Building shadcn Form Builders in Rails

Custom Form Builders in Ruby on Rails

For shadcn on rails, I wanted to create a custom form builder that would plug my shadcn form inputs and controls into ActiveModel so that the error validations and other features native to Rails forms would work.

Creating the Custom Form Builder

First, let's create a new form builder. We'll call it ShadcnFormBuilder, and it will inherit from ActionView::Helpers::FormBuilder.

class ShadcnFormBuilder < ActionView::Helpers::FormBuilder
  def text_field(method, options = {})
    options[:class] << " error" if @object.errors[method].any?
    @template.render_input(
      name: "#{object_name}[#{method}]",
      id: "#{object_name}_#{method}",
      value: @object.send(method),
      type: "text", **options
    )
  end
end

In the text_field method, we first check if there are any validation errors for the given field (method). If there are, we add the "error" CSS class to the field.

The important part here is maintaining the conventions of Rails, like naming the form fields correctly using "#{object_name}[#{method}]" and giving it the appropriate ID using "#{object_name}_#{method}"

Just to be clear, with the form builder methods, you get access to some important objects that help you build your custom form.

Form Builder Objects

The form builder objects I'm using above are:

@object which represents the underlying object that was passed to form_for or form_with.

@template which represents the ActionView instance of the rendering engine. You need to call any of the ActionView methods you would normally, like text_field_tag, or in our case our custom method supplied by our Components::InputHelper render_input.

object_name is just a shortcut method that gives you the class of the @object, useful for generating HTML attributes.

Next, we call the render_input helper method on the @template object, passing the necessary parameters to it.

Creating Helper Methods

The render_input helper method is where we'll define the HTML for our text field. It lives in a helper module, Components::InputHelper.

module Components::InputHelper
  def render_input(name:, label: false, id: nil, type: :text, value: nil, **options)
    # define and apply CSS classes
    # ...

    render partial: "components/ui/input", locals: {
      type:,
      label:,
      name:,
      value:,
      id:,
      options: options
    }
  end
end

The render_input method is responsible for rendering the form field's HTML. It receives various options, formats the CSS classes and other attributes, and then renders a partial (_input.html.erb) that contains the actual HTML for the form field.

The Form Field Partial

Our _input.html.erb file looks like this:

<%= text_field_tag name, value, type: type, id: id,
              class: options[:class],
              placeholder: options[:placeholder],
              autocomplete: options[:autocomplete],
              autocapitalize: options[:autocapitalize],
              autocorrect: options[:autocorrect],
              disabled: options[:disabled],
              required: options[:required],
              readonly: options[:readonly],
              data: options[:data] %>

Here, we use the Rails text_field_tag helper to generate the HTML for our form field. We pass all the necessary attributes to it.

Using the Custom Form Builder

To use our custom form builder, we've created two additional helper methods: render_form_with and render_form_for. These methods simply call the corresponding Rails form helper methods (form_with and form_for), but they also pass the builder: ShadcnFormBuilder option, which tells Rails to use our custom form builder.

module Components::FormsHelper
  def render_form_with(**opts)
    form_with(**opts.merge(builder: ShadcnFormBuilder)) do |form|
      yield form
    end
  end

  def render_form_for(obj, **opts)
    form_for(obj, **opts.merge(builder: ShadcnFormBuilder), html: opts) do |form|
      yield form
    end
  end
end

Now, when we want to create a form in our views, we can use the render_form_for helper:

<%= render_form_for(@user) do |f| %>
  <%= f.text_field :username %>
<% end %>

Conclusion

Building a custom form builder in Rails gives you the flexibility to define your own form fields' HTML and to manipulate form fields based on certain conditions (like the presence of validation errors). This example demonstrated how you could create a simple custom form builder that gives you control over the form fields' CSS classes and HTML attributes. From here, you could extend your form builder to add more field types, apply more custom logic, and further customize your form fields.

Did you find this article valuable?

Support code.avi.nyc by becoming a sponsor. Any amount is appreciated!