Tuesday, March 14, 2023
HomeRuby On RailsTidy views with appropriately DRY styling

Tidy views with appropriately DRY styling


Just lately I shared a few of my struggles writing kinds in standalone JavaScript UI frameworks.
Types are:

  • an inevitable actuality in net improvement
  • cruft that we have to seize information
  • issues that enable us to really present worth to customers

Re-inventing the wheel on primary considerations like validation, storing and composing kind information,
and persisting it means we’re shedding time and vitality we’d moderately dedicate to extra invaluable options.
It’s unhappy after we quit the elegant instruments Rails offers us to rapidly construct out kinds.

Equally, I’ve discovered utilizing trendy styling options can really feel cumbersome with Rails kinds. Tailwind is nice, particularly
in a component-based UI. There’s a clear translation from parts to partials / views, however I’ve by no means discovered the
proper method to consider how we construct kinds.

Prior to now, I’ve been responsible of copy-pasting from different kinds within the venture and tweaking to fulfill a
new requirement. That’s the trail of least resistance while you don’t have an honest technique in place or are
unfamiliar with the place the precise place to make an abstraction. It additionally results in dangerous design decisions by breeding
inconsistency throughout groups. Each kind drifts ever so barely away from the unique, like a nasty recreation of phone.
Discovering the precise method to compose these kinds is hard!

I don’t suppose I’m alone there. All kinds of gems attempt to assist easy out that have and make kinds
really feel extra ‘component-y’. Easy kind is a good instance! Should you’ve
by no means tried these gems, they could attraction to you!

These days, I’ve been making an attempt to interact my curiosity in regards to the instruments I exploit earlier than leaping to make use of a dependency.
Dependencies aren’t dangerous by any means! We simply wish to be very acutely aware of what tradeoff we’re making
by together with them. Typically it’s well worth the tradeoff, however by working straight to them, I
restrict my alternative to go deeper in my understanding of the stack I exploit most frequently.

Queue an amazing concept from Justin Searls. He’s proven me methods to lean on Rails defaults for constructing kinds in a method that I
suppose hits a candy spot between views and Tailwind styling. With a small little bit of exploration of the Rails internals,
we’d have the ability to free our kinds from leaning on one other dependency.

Be aware: I’ll be writing .erb fashion syntax with out their requisite <%= %> && <% %> tags, so that they format extra kindly in markdown. Please forgive me :`)

Should you’ve additionally tried styling your Rails app with Tailwind, you might need
made a kind that appears like this:

# In our bakery present view, someplace
form_with mannequin: @bakery do |f|
  f.label :bakery_name, "Bakery Identify", class: "block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4"
  f.text_field :identify, class: "mt-1 block w-full rounded-md shadow-sm focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
  f.label :bakery_email, "Bakery E mail", class: "block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4"
  f.text_field :e mail, class: "mt-1 block w-full rounded-md shadow-sm focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
  f.label :bakery_country, "Bakery Nation", class: "block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4"
  f.text_field :nation, class: "mt-1 block w-full rounded-md shadow-sm focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
  f.label :bakery_open, "Open?", class: "block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4"
  f.check_box :open, disabled: true
finish

The ideas of Tailwind and ActionView can really feel opposed to one another.

The form_with helper offers us a reusable construction for creating kinds. We straight work together
with mannequin attributes, with a simple DSL to construct kinds for an underlying object. Rails is opinionated about
the behaviour, so we all know what to do, with sufficient flexibility to customise the HTML output.

We get so many issues totally free. Validations in opposition to the mannequin are tied to the matching subject on the shape. We will simply
move error messages to the view. The shape is aware of the place to submit itself and methods to bundle up its kind information. And all
the opposite issues.

Tailwind offers us a concise CSS DSL, blended with an inline first method to styling that matches the componentized
world of net improvement. Tailwind is closely opinionated about making use of fashion inline together with your HTML,
with loads of advantages. No extra deserted CSS lessons. Keep away from abstractions that evolve, develop, bloat,
and deteriorate. Cell-first styling, simple theming, darkish mode, and so forth.

After we use these two instruments collectively, we will wind up with a little bit of a multitude.
The instance code above is a working example. Even a easy kind with just a few attributes has us copy-pasting the identical
styling to all of them. It looks like we’re doing one thing mistaken. The styling is so moist that we lose sight of the
easy kind syntax that makes us love Rails, however we have to maintain our styling inline with our HTML.

How can we reconcile this?

We will begin by leaning on app-wide theme styling, letting Tailwind care for these considerations at our config layer.
Using a Tailwind theme for issues like our model colours is the best way.

If we slip and think about using @apply so as to add customized lessons to our kind components, we break conference.


# no, dangerous. Do not be tricked!

form_with mannequin: @bakery, class: 'bakery_form' do |f|
  f.label :bakery_name, "Bakery Identify", class: "form-label"
  f.text_field :identify, class: "form-text-input"
  f.label :bakery_email, "Bakery E mail", class: "form-label"
  f.text_field :e mail, class: "form-text-input"
  f.label :bakery_country, "Bakery Nation", class: "form-label"
  f.text_field :nation, class: "form-text-input"
  f.label :bakery_open, "Open?", class: "form-label"
  f.check_box :open, disabled: true, class: "form-check-box"
finish

We see this DRY fashion and really feel higher. It’s a extra snug learn if we’re used to conventional CSS approaches.

If we fall for this entice, actuality will smack us because it all the time does.

What occurs when we have to prolong a single enter fashion, one time, on one kind? That one-off will get forgotten and
regresses someplace down the road.

What occurs if bakery_form kinds get reused? As soon as it makes a single leap to a different kind, it’ll make 3 extra earlier than
you blink. Then we’ve got a CSS class to take care of throughout a number of kinds and contexts.

Altering international CSS is terrifying on massive initiatives.

Inevitably, these abstractions push us away from what’s superior about Tailwind. There are an entire lot of causes
Tailwind tells us to keep away from the
@apply abstraction. Worry not!

Tailwind additionally offers steerage on managing
duplication of styling, knowledge akin to “tolerate some quantity of repetition over untimely abstraction”.
They coach us to dry out our inline styling in two methods; loop intelligently, and lean on partials.
When potential, we must always map over our assortment of issues to be rendered. If that doesn’t match, lean into partials
to share styling throughout contexts.

Types fall right into a difficult center floor the place neither of those decisions solves our drawback with out points.

Looping by mannequin attributes to generate an enter assortment creates interdependency between every enter.
It’s additionally onerous to reuse kind partials. My Bakery mannequin may be very totally different from my Cake mannequin, however I most likely need
similarity in the best way the kinds look.

There’s a greater method to lean into Rails kinds with Tailwind in an anticipated and concise method; Kind builders.

The Rails Gods give us a method to make Customized Kind Builders,
and @searls pointed me in the direction of a artistic tie-in to using them successfully whereas styling kinds throughout an
software.

TL;DR:

We will make a small seam within the FormBuilder Rails gives to us to use kinds, then bounce proper again to the default
performance.

I’ve received a pattern app in case you’d want I present you the code.

There’s a precept I believe pairs properly with the shape builder method:

Wrap with construction, fill with fashion.

The pattern repo’s “completed product” has some intelligent optimizations. Should you’re snug
with inheritance, metaprogramming, and a small little bit of recursion, I believe you’ll be set to hop in and grok the precept.

If these issues are new to you, otherwise you’d like a refresher, no worries! I’ve received you! I’ll element the most straightforward
implementation of this builder and incrementally work in the direction of these intelligent optimizations step-by-step within the sections
under.

Rails lets us outline a customized Kind Builder to alter what occurs after we name form_with in a view.

We’ll make a customized builder whose solely job is to insert our styling.

Then the builder will level us straight again to Rails magic land, the place we get validation, submission,
information wrangling, and all the opposite issues totally free from the form_with helper.

First, I would like you to recollect the first guideline that’s going to assist us stability Tailwind and Rails
greatest practices.

Wrap with construction, fill with fashion.
We wish to do format styling in our view, wrapping our form_with components.
The shape builder ought to fill in our look styling.

There are 4 issues we have to get began:

  • Tailwind configuration, so it sees kinds on the builder
  • Find out how to name your kind builder
  • Implementing the FormBuilder
  • Leveraging the builder alongside our kind

The pattern app and the examples under are based mostly on an Adventurer mannequin with a easy edit view.

├── app
│ ├── helpers
│ │ └── application_helper.rb
│ ├── lib
│ │ └── form_builders
│ │     └── tailwind_form_builder.rb
│ └── views
│     └── adventurers
│       └── present.html.erb
└── tailwind.config.js

tailwind.config.js

First, we wish to configure Tailwind to examine for styling within the kind builder.

Discover your tailwind.config.js, and embrace:

module.exports = {
  content material: [
    './app/views/**/*.html.erb', // you probably have all these ones!
    './app/helpers/**/*.rb',
    './app/assets/stylesheets/**/*.css',
    './app/javascript/**/*.js',
    './app/lib/form_builders/**/*.rb',  // Add this one! Or, whatever directory you'd like to store your builder
  ]
}

The 1st step is completed!

There are two major methods Rails expects a customized builder to be invoked. The precise place relies on what abstraction is acceptable to your venture.

Each form_with helper takes an elective parameter builder, which is the place we will move a builder class.

Within the instance app, our present view invokes it this fashion:

<%= form_with mannequin: @adventurer, builder: MyFormBuilder do |f| %>

One other place you may take into account invoking your builder is as a project-wide default:

# app/helpers/application_helper.rb

module ApplicationHelper
  ActionView::Base.default_form_builder = MyFormBuilder
finish

Should you use the default, form_with doesn’t have to be given a builder possibility.

Which method must you go? As all the time, it relies upon!

Should you’re constructing greenfield or have only a few kind views, setting a default makes loads of sense!
You will get a uniform look on each kind you construct and arrange an efficient sample to customise each as you go.

Should you’ve received a lot of pre-existing Rails views / kinds you’re wrangling, it might be preferable to go view-by-view,
manually passing your builder possibility. Drying up your kinds this fashion can assist untangle construction from look
styling. It might additionally reveal if there are a number of “varieties” of kinds in your area you’d like to visualise in a different way.

Working in the direction of a default implementation is perhaps an honest purpose, or it’s possible you’ll must maintain working with just a few totally different
kind styling choices.

Regardless, the implementation of our first customized builder would be the identical!

In Working Successfully With Legacy Code, Michael Feathers describes the idea of a code seam.

A seam is a spot the place two components of a system work together, and we will introduce some new behaviour.
We’re going to create our personal seam within the
FormBuilder api.

First, we outline our builder and inherit from the default FormBuilder supplied by ActionView.

# /app/lib/form_builders/tailwind_form_builder.rb
module FormBuilders
  class TailwindFormBuilder < ActionView::Helpers::FormBuilder
  finish
finish

I’ve opted to namespace this instance, however you don’t must.

Our final purpose is to provide the form_with block entry to every of the enter subject strategies.

For instance:

form_with mannequin: @adventurer do |f|
  f.label :adventurer_name, "Adventurer Identify", class: "block text-gray-500
 font-bold md:text-right mb-1 md:mb-0 pr-4"
  f.text_field :identify, class: "mt-1 block w-full rounded-md shadow-sm
 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
  f.label :adventurer_city, "Adventurer Metropolis", class: "block text-gray-500
 font-bold md:text-right mb-1 md:mb-0 pr-4"
  f.text_field :metropolis, class: "mt-1 block w-full rounded-md shadow-sm
 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
finish

The simplest first step to drying this up is to outline a text_field technique on our customized builder that does all of the
styling.

# /app/lib/form_builders/tailwind_form_builder.rb
module FormBuilders
  class TailwindFormBuilder < ActionView::Helpers::FormBuilder
    def text_field(technique, choices={})
	  default_style = "mt-1 block w-full rounded-md shadow-sm focus:ring
        focus:ring-indigo-200 focus:ring-opacity-50"


	  tremendous(technique, choices.merge({class: default_style}))
    finish
  finish
finish

What’s occurring right here?

We’re shifting our Tailwind class string into the text_field technique and out of our view.

Then we leverage our inheritance powers to name the text_field technique that Rails already gives for us on the
default ActionView::Helpers::FormBuilder, by invoking tremendous.

Now we will clear up our view!

form_with mannequin: @adventurer do |f|
  f.label :adventurer_name, "Adventurer Identify", class: "block text-gray-500
 font-bold md:text-right mb-1 md:mb-0 pr-4"
  f.text_field :identify
  f.label :adventurer_city, "Adventurer Metropolis", class: "block text-gray-500
 font-bold md:text-right mb-1 md:mb-0 pr-4"
  f.text_field :metropolis
finish

All of our textual content fields get the identical styling utilized.

We will wash-rinse-repeat with the label too!

# /app/lib/form_builders/tailwind_form_builder.rb
module FormBuilders
  class TailwindFormBuilder < ActionView::Helpers::FormBuilder
	def text_field(technique, choices = {})
      default_style = "mt-1 block w-full rounded-md shadow-sm
        focus:ring focus:ring-indigo-200 focus:ring-opacity-50"

        tremendous(technique, choices.merge({class: default_style}))
      finish

    def label(technique, textual content = nil, choices = {})
      default_style = "block text-gray-500 font-bold md:text-right
        mb-1 md:mb-0 pr-4"

      tremendous(technique, textual content, choices.merge({class: default_style}))
    finish
  finish
finish

Be aware that Labels have a barely totally different set of parameters than text_field. Now our view is tidy:

form_with mannequin: @adventurer do |f|
  f.label :adventurer_name, "Adventurer Identify"
  f.text_field :identify
  f.label :adventurer_city, "Adventurer Metropolis"
  f.text_field :metropolis
finish

This may look like an inexpensive place to cease. Sadly, if we do cease right here, we’re making extra issues.

Our kind is now depending on the TailwindFormBuilder to create all of the styling. It’s not unreasonable to think about
we may find yourself with a builder per kind. At this stage, we’re including indirection and complexity for much less profit than
simply copy-pasting our styling. To make issues worse, we’re making each text_field co-dependent.

Adjustments to 1 have an effect on all of them.

So, we should maintain going!

Our present instance has two fundamental dependencies we have to overcome:

  • All the styling is within the builder technique. We will’t modify or modify every text_field
  • We’ve got to put in writing a technique for each enter kind we wish to be styled

If we may modify every enter subject in isolation, our builder code would turn into extra reusable.

The second dependency may appear small, however there are 24 enter varieties. Fortuitously, 17 of them act precisely
like a text_field. We will handle loads of enter varieties in the identical method.

For something distinctive in its behaviours, like a choose, or a check_box, we’ll nonetheless lean on manually defining
our strategies. These distinctive fields have their very own api to work together with.

Breaking the primary dependency strikes us to reusable code, and breaking the second helps our builder be concise.

Let’s begin with permitting modifications to particular person text_field calls. We’ll see how the wrap with construction,
fill with fashion
sample in motion.

Later we’ll embrace that behaviour on all 17 text-like fields.

What makes the 2 kinds totally different? What’s the identical between them?

At its base, a kind is only a assortment of inputs that every level to a selected attribute we wish to report.
Whatever the underlying mannequin, there are solely so some ways to seize the info on the web page.

Enter components have loads of overlap. We wish all of our inputs to have a constant look to them so that they’re
intuitive. They need to all have the identical font, border styling, coloration, and so on.

Types will seem in several components of the person stream. A kind with 20 inputs must be organized
otherwise than a 3-input kind. Format is essential for stream and value, so every wants management of its personal
construction.

Look needs to be the identical throughout totally different kinds, whereas the format ought to shift to accommodate
the knowledge being captured. Splitting up these two considerations offers constant branding with dry
CSS lessons, with the flexibleness to construction the format on every kind independently.

We intuitively use views this fashion. We count on a partial to carry the identical form wherever it’s rendered.
A partial innately has duty for construction. Giving the FormBuilder duty for look styling
lets us pair FormBuilders with partial views to hit the candy spot of dry code and performance.
The enter fields seem constantly the identical whatever the kind however can be formed to suit a selected kind’s want.

Our text_field technique is presently doing each:

# /app/lib/form_builders/tailwind_form_builder.rb
module FormBuilders
  class TailwindFormBuilder < ActionView::Helpers::FormBuilder
    def text_field(technique, choices={})
      default_style = "mt-1 block w-full rounded-md shadow-sm focus:ring
        focus:ring-indigo-200 focus:ring-opacity-50"

      tremendous(technique, choices.merge({class: default_style}))
    finish
  finish
finish

Our construction styling:
mt-1 block w-full

and look styling:
rounded-md shadow-sm focus:ring focus:ring-indigo-200 focus:ring-opacity-50

Our subsequent change: depart the looks styling right here, however carry our construction styling again as much as the view.

Our text_field interface permits us to move in a mannequin attribute and a hash of all our choices.

Right here’s what our view ought to seem like:

form_with mannequin: @adventurer do |f|
  f.text_field :identify, class: "mt-1 block w-full"
finish

On this instance, we’re calling for a subject tied to the identify attribute on the @adventurer. All the things handed
after identify will get bundled right into a hash.

To permit styling to be dealt with from each the view and the builder degree, we have to isolate the class handed
from the view with our construction styling. Then we have to mix it with the looks styling declared in our builder.

# /app/lib/form_builders/tailwind_form_builder.rb
module FormBuilders
  class TailwindFormBuilder < ActionView::Helpers::FormBuilder
    def text_field(technique, choices = {})
      style_options, custom_options =
        partition_custom_opts(choices)

      fashion = "rounded-md shadow-sm focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
        + " #{style_options[:class]}"

      tremendous(technique, choices.merge({class: fashion}))
    finish

    CUSTOM_OPTS = [:class].freeze
    def partition_custom_opts(opts)
      opts.partition  CUSTOM_OPTS.embrace?(okay) .map(&:to_h)
    finish
  finish
finish

Partitioning out the :class from our different choices offers us entry to do exactly that.
I’ve opted to make use of a template literal, so we’ve got some security if no class is given from the view.

Our ensuing HTML within the browser has each the view-layer construction styling matched with the
look styling from the builder:

<enter class="rounded-md shadow-sm focus:ring focus:ring-indigo-200 focus:ring-opacity-50 mt-1 block w-full"
       kind="textual content" worth="Gandalf the Gray" identify="adventurer[name]" id="adventurer_name">

One dependency damaged!

If we apply an identical change to our labels, it permits our view to handle the construction solely:

<%= form_with mannequin: @adventurer do |f| %>
    <div class="md:flex md:w-full md:justify-between md:items-center mb-6">
      <%= f.text_field :identify, class: "min-w-1/2" %>
      <%= f.label :identify, class: "mb-1 md:mb-0 pr-4" %>
    </div>
<% finish %>

The pattern repo does some intelligent enterprise to bundle up label creation inside text_field and different helpers.
Subsequent, we’ll make modifications, so we’re not manually writing all 17 text_like subject helpers. That permits us to
deal with labels inside the builder.

Onto making a concise implementation of all 17 text_like fields!

Should you’re snug with ruby metaprogramming, that is the half the place we’re shamelessly stealing from the
Rails supply for producing these strategies.
Should you’re much less snug, let’s speak in regards to the function metaprogramming serves right here and the way we transfer our
implementation towards using it.

Below the hood, the ActionView::Helpers::FormBuilder makes use of metaprogramming to maintain
itself tidy. The complete record of enter varieties
is right here in case you’d like to have a look at all of them.
The ActionView FormBuilder bundles the way it defines all of the strategies that behave like a text_field
and, on initialization, generates an occasion technique
to deal with every kind of enter.

Our builder can mimic this behaviour so as to add our styling to all of the text-like fields, then name for a similar technique
on the ActionView FormBuilder we’re inheriting from to make use of the underlying performance.

We’re going to regulate our implementation to:

  • use class_eval to generate strategies for us
  • embrace two paths by each technique; one for making use of styling, one for pointing again to the default behaviour

Right here’s the large hop towards that new method:

module FormBuilders
  class TailwindFormBuilder < ActionView::Helpers::FormBuilder

    field_helpers - [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field].every do |field_method|
      class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
        def #{field_method}(technique, choices = {})
          if choices.delete(:tailwindified)
            tremendous
          else
            text_like_field(#{field_method.examine}, technique, choices)
          finish
        finish
      RUBY_EVAL
    finish

    def text_like_field(field_method, object_method, choices = {})
      style_options, custom_options =
        partition_custom_opts(choices)

      fashion = "rounded-md shadow-sm focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
      + " #{style_options[:class]}"

      ship(field_method, object_method, {
        class: fashion,
      }.compact.merge(opts).merge({ tailwindified: true }))
    finish
  finish
finish

What modified? Right here we:

  • name for field_helpers, which comes from ActionView::Helpers::FormBuilder
  • subtract just a few strategies from field_helpers that don’t behave like a text_field
  • add our class_eval loop, which handles our metaprogramming
  • rename our text_field technique to text_like_field to point out we’re dealing with many differing kinds
  • modify text_like_field to name ship as a substitute of tremendous
  • introduce an possibility referred to as tailwindifed to regulate our stream

class_evals, how do they work?

This class_eval technique is sort of lifted straight from the Rails supply. Thanks, Rails gods.

By tapping into the metaprogramming facet of Ruby, we will create all of the strategies that act like a text_field
in a dry method. We’re creating a technique for every text_like subject that is aware of methods to apply styling, then level to
the default Rails behaviour for that enter kind.

field_helpers is outlined on the guardian class, ActionView::Helpers::FormBuilder. Our particular choice right here

field_helpers - [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field]

creates an array of all of the field_helpers that act the identical method as our text_field enter does.

Then we loop by our record and generate a technique for every on our builder.
When an occasion of the TailwindFormBuilder is established, the item in reminiscence will seem like this:

module FormBuilders
  class TailwindFormBuilder < ActionView::Helpers::FormBuilder
    # the category eval loop is right here

    # def text_like_field ...

	def text_field(technique, choices={})
      if choices.delete(:tailwindified)
        tremendous
      else
        text_like_field(:text_field, technique, choices)
      finish
	finish

	def email_field(technique, choices={})
      if choices.delete(:tailwindified)
        tremendous
      else
        text_like_field(:email_field, technique, choices)
      finish
    finish
	# and so forth, for the entire different 15 text-like subject strategies
  finish
finish

We’ve captured our earlier code that partitions the class possibility and combines it with our default styling to reuse
that performance for each text-like subject. We’re additionally letting every textual content subject check with the default Rails behaviour
by calling tremendous within the technique. For instance, ActionView::Helpers::FormBuilder has an email_field we will invoke by
calling tremendous in an email_field technique on our personal builder.

The place does tailwindified come into play?

I pulled a quick one on you. We’re doing a little gentle recursion. Every technique has two flows. The primary invocation
will add our styling and embrace a brand new key within the choices hash referred to as tailwindified.

As soon as that styling is utilized, it requires the unique performance on the default builder by calling tremendous.

The stream goes like this:

  • We create a text_field within the view
  • Our customized builder’s text_field technique known as
  • The text_field technique calls text_like_field
  • text_like_field applies our styling and provides the tailwindified possibility
  • text_like_field makes use of ship to name text_field once more on our personal TailwindFormBuilder
  • This time, text_field calls tremendous, asking for the default Rails text_field behaviour and passing the choices alongside.

By abstracting the identify of the enter kind, and the 2 pathways, we’re in a position to tie into the entire enter fields
that behave equally to text_input, fashion them, and maintain our FormBuilder pretty concise.

This is perhaps the place you determine to get off the practice! We’ve received the tie into FormBuilders down.
We will leverage the wrap with construction, fill with fashion sample I’ve been encouraging, and get a
respectable quantity of worth in doing a little easy drying up of our Tailwind.

That mentioned, I’ve received a pair extra ideas for tactics you may lean into each this sample and a few intelligent tie-ins to
Rails magic. Validation errors and labels!

The explanation I like this method is we get to make use of our anticipated Rails API. If a person fails a kind
validation, we wish to present them which subject was incorrect with styling and probably labels.

One main benefit of ActionView is attending to work together elegantly with our mannequin and its built-in validations.

Listed below are a pair jumps that can carry our styling a bit nearer to the pattern app and provides us a pleasant
seam to answer mannequin errors.

module FormBuilders
  class TailwindFormBuilder < ActionView::Helpers::FormBuilder

    # no modifications to the opposite stuff

    def text_like_field(field_method, object_method, choices = {})
      style_options, custom_options =
        partition_custom_opts(choices)

      fashion = "rounded-md shadow-sm focus:ring focus:ring-indigo-200 focus:ring-opacity-50"

      lessons = apply_style_classes(fashion, style_options, object_method)

      ship(field_method, object_method, {
        class: lessons,
      }.compact.merge(opts).merge({ tailwindified: true }))
    finish

    def apply_style_classes(lessons, style_options, object_method = nil)
      lessons + border_color_classes(object_method) + " #{style_options[:class]}"
    finish

    def border_color_classes(object_method)
      if errors_for(object_method).current?
        " border-2 border-red-400 focus:border-rose-200"
      else
        " border border-gray-300 focus:border-yellow-700"
      finish
    finish

    def errors_for(object_method)
      return until @object.current? && object_method.current?

      @object.errors[object_method]
    finish
  finish
finish

What modified? Now:

  • we retailer our look styling in a variable in text_like_field
  • text_like_field leans on one other technique for composing the entire fashion string
  • apply_style_classes took over mixing construction styling from the view into the looks styling
    our builder gives
  • a brand new border_color_classes technique provides a crimson border to the enter if errors are current on the mannequin attribute
  • a helper technique to examine for errors on the item

Rails magic! Should you’re utilizing a flash[:error] in your kind view, this helps seize the styling on the shape
to point out customers the place your message relates.

Should you poke on the pattern venture, it’s possible you’ll be aware that text_like_field returns two issues: labels + subject.
FormBuilder strategies are solely required to return HTML that attaches to our view, so we will bundle as many collectively
as we’d like. The Rails docs
use computerized labeling for instance for customized kind builders.

We will present wrap with construction, fill with fashion therapy to our label technology by making small changes:

  • Enable text_like_field to partition out label choices, much like our inline class
  • Regulate text_like_field to return each a label and the enter ingredient.

Partition

# /app/lib/form_builders/tailwind_form_builder.rb
module FormBuilders
  class TailwindFormBuilder < ActionView::Helpers::FormBuilder
    # ... the opposite issues right here

    CUSTOM_OPTS = [:class, :label].freeze
    def partition_custom_opts(opts)
      opts.partition  CUSTOM_OPTS.embrace?(okay) .map(&:to_h)
    finish
  finish
finish

The partition can seize each class and label from the choices hash, handed from the view.

Programmatic labels

Now we will generate some labels from the choices hash:

module FormBuilders
  class TailwindFormBuilder < ActionView::Helpers::FormBuilder
    # ... no modifications to the opposite issues!

    def text_like_field(field_method, object_method, choices = {})
      custom_opts, opts = partition_custom_opts(choices)

      fashion = "rounded-md shadow-sm focus:ring focus:ring-indigo-200 focus:ring-opacity-50"

      lessons = apply_style_classes(fashion, custom_opts, object_method)

      subject = ship(field_method, object_method, {
        class: lessons,
        title: errors_for(object_method)&.be part of(" ")
      }.compact.merge(opts).merge({tailwindified: true}))

      label = tailwind_label(object_method, custom_opts[:label], choices)

      label + subject
    finish

    def tailwind_label(object_method, label_options, field_options)
      textual content, label_opts = if label_options.current?
        [label_options[:text], label_options.besides(:textual content)]
      else
        [nil, {}]
      finish

      label_classes = label_opts[:class] || "block text-gray-500 font-bold"
      label_classes += " text-yellow-800 darkish:text-yellow-400" if field_options[:disabled]
      label(object_method, textual content, {
        class: label_classes
      }.merge(label_opts.besides(:class)))
    finish
  finish
finish

What modified?

  • text_like_field returns each an enter, and a label for the enter
  • we move any label choices supplied to a helper technique, tailwind_label
  • tailwind_label chooses defaults, so we safely do nothing if no label is supplied on the view degree
  • we apply the looks fashion to the label and name for the default label technique

Now our view can present a label for enter fields as an possibility.

<%= form_with mannequin: @adventurer, builder: FormBuilders::TailwindFormBuilder, do |f| %>
  <div class="md:flex md:w-full md:justify-between md:items-center mb-6">
    <%= f.text_field :identify, label: { textual content: "Full Identify" } %>
  </div>
<% finish %>

By itself, this looks like a small sidestep in performance from the f.label method, so I perceive if
it’s not notably interesting to you. One factor it does allow is so as to add error labels to our kind inputs utilizing
the identical API.

module FormBuilders
  class TailwindFormBuilder < ActionView::Helpers::FormBuilder
    # ... no modifications to the opposite issues!

    def text_like_field(field_method, object_method, choices = {})
      custom_opts, opts = partition_custom_opts(choices)

      fashion = "rounded-md shadow-sm focus:ring focus:ring-indigo-200 focus:ring-opacity-50"

      lessons = apply_style_classes(fashion, custom_opts, object_method)

      subject = ship(field_method, object_method, {
        class: lessons,
        title: errors_for(object_method)&.be part of(" ")
      }.compact.merge(opts).merge({tailwindified: true}))

      labels = labels(object_method, custom_opts[:label], choices)

      labels + subject
    finish

    def labels(object_method, label_options, field_options)
      label = tailwind_label(object_method, label_options, field_options)
      error_label = error_label(object_method, field_options)

      @template.content_tag("div", label + error_label, {class: "flex flex-col items-start"})
    finish

    def error_label(object_method, choices)
      if errors_for(object_method).current?
        error_message = @object.errors[object_method].accumulate(&:titleize).be part of(", ")
        tailwind_label(object_method, {textual content: error_message, class: " font-bold text-red-500"}, choices)
      finish
    finish
  finish
finish

General this kinda feels intelligent to me. I don’t love being intelligent. I’ve a powerful
choice for writing specific code and have a excessive tolerance for repetitively typing f.label in my views.

In the end I’ll depart it as an train to you to determine if the cleverness of auto-labeling is a win or not.

A lot of it’s context-dependent.

I’ll additionally depart the caveat that we’d mix look/format considerations with labels. I’m nonetheless reconciling that and
want to do this sample out on some bigger initiatives earlier than I give a last verdict.

Each time I sit down to put in writing, I find yourself with an entire novel. So, thanks for sticking it out.

I’m not going to prescribe the error styling or label technology extensions to your kinds. Your venture will
have its personal distinctive qualities that can decide if these are related to you or not.

I do hope this dive into the best way Rails builds kinds and the way wrap with construction, fill with fashion helps
easy out your expertise constructing kinds. And even evokes you to do some extra cool issues! I haven’t even touched
on issues like Turbo or among the deeper integration between kinds and their underlying fashions. Should you construct a
cool factor with this sample, I’d like to see it!

In the end, I simply need us all to have kinds that serve us, really feel intuitive, and don’t require us to spend extra
time than mandatory on the cruft. This won’t be the sample for you or to your venture, however possibly it’ll assist.

RELATED ARTICLES

Most Popular

Recent Comments