Advanced form components with Alpine and Rails

Advanced form components with Alpine and Rails

Make hidden inputs really valuable

Sometime ago I posted article about alpine.js usage with Ruby on Rails app and also another article about ViewComponents

Today I want to show you how can you implement really advanced form(and not just) input components. By advanced I mean something like custom drop-downs, toggles, or anything other that you can't create/style with regular inputs

The idea is to use Alpine.js together with hidden inputs which would hold your input state and then wrap it into ViewComponent, so it could be reused in any of your forms

Implementation

As an example I use dropdown from screenshot above. This is tailwind styled component, but it is a good example of more advanced component.

This is how it would look like in simplest implementation

# custom_dropdown_component.rb
class CustomDropdownComponent < ViewComponent::Base
  def initialize(options:, selected_option: nil, form:, field_name:)
    @options = options
    @selected_option = selected_option
    @form = form
    @field_name = field_name
  end

  private

  attr_reader :options, :selected_option, :form, :field_name
end

# custom_dropdown_component.html.erb

<div x-data="{ open: false, selectedOption: '<%= selected_option %>' }" class="relative pointer-events-auto w-[28.125rem] text-[0.8125rem] leading-5 text-slate-700">
  <div class="font-semibold text-slate-900">Assigned to</div>
  <div class="relative mt-1">
  <button @click="open = !open" type="button" class="w-full bg-white border border-gray-300 rounded-md shadow-sm px-4 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
    <span x-text="selectedOption || 'Select an option'"></span>
    <span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
      <!-- Heroicon name: solid/selector -->
      <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
        <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
      </svg>
    </span>
  </button>
  </div>

  <div x-show="open" @click.away="open = false" class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
    <% options.each do |option| %>
      <div @click="selectedOption = '<%= option[:value] %>'; open = false" class="cursor-pointer select-none relative py-2 pl-3 pr-9 hover:bg-indigo-600 hover:text-white">
        <%= option[:value] %>
      </div>
    <% end %>
  </div>
  <%= form.hidden_field field_name, 'x-model': "selectedOption" %>

  <div class="mt-2 text-slate-900">Current selected value inside hidden_field: <span x-text="selectedOption"></span></div>
</div>

# Your template

<%= form_with do |f| %> # There can be your form for your model
  <%= render(CustomDropdownComponent.new(form: f, field_name: :test, options: [{id: 1, value: 'John Doe'}, {id: 2, value: 'Jane Doe'}, { id: 3, value: 'Tobias Luetke'}]))%>
<% end %>

And final result would look like below

All the magic happens thanks to this:

<%= form.hidden_field field_name, 'x-model': "selectedOption" %>

where x-model is responsible to setting proper value to hidden_field upon any action from page. So when you click on any of dropdown elements

<div @click="selectedOption = '<%= option[:value] %>'; open = false"><%= option[:value]</div> we have @click handler which sets `selectedOption` to interpolated value of our option.

You can have any interface you prefer, either hash or specific DropdownItem class which has needed methods for your case.

Conclusion

With this approach you can have any form inputs you can come up with or find online. For example for tailwind there are few really cool UI kits which you can use right away. This can save time on your UI, especially if you work on MVP and you'd prefer to work on core features rather than UI

By using ViewComponent you also make your UI more reusable, since all form inputs can be used across all your forms not just one

For more interesting content follow me on X where I post daily