Rails+TailwindCSS: adding "required" to form labels (and how to find the solution by yourself)
Tags: rails, tailwindcss, form, i18n
Published: 2020-07-10 13:44:26

---
canonical_url: https://paraside.in/required-form-label-with-rails-tailwindcss/
---
This post addresses a simple need: adding a "required" text next to any form field label that is… required.

But I wanted a solution:
* using only TailwindCSS/TailwindUI existing classes
* that can be reused easily
* internationalization friendly (because we're using a word - required - and not the "*" sign as you often see)
* that doesn't reinvent the wheel in some way
* that looks like this

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4isnogc6vypl829kl0k1.png)


# For the coder in a hurry, my solution

The gist of it is `label_builder.translation`.

```slim
= f.label :email, required: true, class: "block text-xxs uppercase text-gray-500" do |label_builder|
  = label_builder.translation
  span.ml-2.normal-case.text-orange-400.text-xxs.font-semibold= t("required")
  = f.email_field :email, autocomplete: false, required: true, class: "form-input mt-1 block w-full text-field focus:shadow-outline focus:border-green-300", placeholder: "richard@piedpiper.com"
```

Note: I use the [Slim](https://github.com/slim-template/slim) template language instead of ERB; it's easier to read so I trust that even if you've never heard of it you won't be lost.

# For the curious mind, how I got there and why

*FAIR WARNING: the rest is not your typical technical article. You already have the solution above, the rest is about how I found the solution. So it's more a story than a guide.*

Here I was setting up a simple profile form, adding the proper `required: true` attributes and looking at it when I realized: **how the hell would a user know which fields are required and which aren't?**

Like anyone, anytime I see a form, my mind looks for the easy way out and tries to figure out how to hit that submit button as quickly as possible.
This bland form triggered my fill or flight response and I knew I had to do something about it.

## Starting point

With a bit of TailwindCSS, here is what it was looking like.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1eyy0fn26yk5ozi1knph.png)

and the code

```slim
= f.label :email, required: true, class: "block text-xxs uppercase text-gray-500"
= f.email_field :email, autocomplete: false, required: true, class: "form-input mt-1 block w-full text-field focus:shadow-outline focus:border-green-300", placeholder: "richard@piedpiper.com"
```

It's as basic as it gets: a label and an email_field with some classes.
I will skip the TailwindCSS explanations, just know that "form-input" comes from a [plugin](https://tailwindcss-custom-forms.netlify.app).

Goal check:
* ✅ using only TailwindCSS/TailwindUI existing classes
* ❌ that can be reused easily
* ❌ internationalization friendly
* ❌ that doesn't reinvent the wheel in some way
* ❌ that looks like the cover image

## Approach n°1: Add a "*" after the email

Often, forms signal that a field is required by appending an asterisk to the label title, which can be done with a bit of CSS.

```css
  .required:after {
    content:" *";
  }
```

I went to the [TailwindCSS docs](https://tailwindcss.com/docs/installation) and couldn't find anything ready-made to handle this.
Sure, I could add it to my stylesheet files, but I already had another idea in mind.
I played a little bit with [Basecamp](https://basecamp.com) recently as I watched the [On Writing Software (well?)](https://www.youtube.com/playlist?list=PL9wALaIpe0Py6E_oHCgTrD6FvFETwJLlx) videos by [DHH](http://twitter.com/dhh). And it reminded me that using the word "required" instead of a simple "*" sign is great to improve clarity.

## Approach n°2: Appeal to humanity

This confusing title is a bad pun inspired the method we'll be using in this approach: `Model.human_attribute_name`.

Since `label` can take a block to render, it's easy to come up with a first solution.

So I went from
```slim
= f.label :email, required: true, class: "block text-xxs uppercase text-gray-500"
```
to
```slim
= f.label :email, required: true, class: "block text-xxs uppercase text-gray-500" do
  | Email
  span.ml-2.normal-case.text-orange-400.text-xxs.font-semibold required
```

OK. It looks like the final result, but **we're not quite there yet, because both "Email" and "required" are hardcoded**.

Goal check:
* ✅ using only TailwindCSS/TailwindUI existing classes
* ❌ that can be reused easily
* ❌ internationalization friendly
* ❌ that doesn't reinvent the wheel in some way
* ✅ that looks like the cover image

The "required" text doesn't need much talking about. We just have to replace it with `= t("required")` and add a translation somewhere, probably in `config/locals/en.yml` since it will be pretty generic.
```yml
en:
  required: required
```

But what about the email? I needed a solution that would be a bit more generic.
The [standard way to look up translations](https://guides.rubyonrails.org/i18n.html#translations-for-active-record-models) for any attribute is `Model.human_attribute_name(:attribute)`, so I did just that.

```slim
= f.label :email, required: true, class: "block text-xxs uppercase text-gray-500" do
  / 👍 Not horrible
  = f.object.class.c(:email)
  span.ml-2.normal-case.text-orange-400.text-xxs.font-semibold= t("required")
  = f.email_field :email, autocomplete: false, required: true, class: "form-input mt-1 block w-full text-field focus:shadow-outline focus:border-green-300", placeholder: "richard@piedpiper.com"
```

At this point, I am relying on common techniques. A little bit unsatisfying but it gets the job done.

What did I find unsatisfying, you may wonder?

Generally speaking, rails take care of many intricacies you might not think of. When you're replicating part of a method (in our case the part that takes a symbol - :email - and turns it into text - Email -), it's highly likely you're forgetting edge cases or oversimplifying.
It's like using a steering wheel versus ropes tied to the wheels. Sure, it works, you have a direct grip on things, but there's a reason we built an intermediary thingy.

In particular, by doing our own thing instead of relying on rails' wisdom, we're missing out on lazy lookup cleverness.
And in our case, by using `Model.human_attribute_name` directly, we are reinventing the wheel.

Goal check:
* ✅ using only TailwindCSS/TailwindUI existing classes
* ✅ that can be reused easily
* ✅ internationalization friendly
* ❌ that doesn't reinvent the wheel in some way
* ✅ that looks like the cover image

## Final approach

Where to go from there? I knew that the `label` helper was handling the translation at some level, and I wanted to know if you could tap into it.

There was two way to deal with this. The easy way, and the way I did it because… I got carried away.

### Let's start with the laborious (but still interesting) way

The easiest way to know how the `label` helper handles the translation is to look at the source code 
Using [Dash](https://kapeli.com/dash), I opened the rails source code and found [this](https://github.com/rails/rails/blob/b738f1930f3c82f51741ef7241c1fee691d7deb2/actionview/lib/action_view/helpers/form_helper.rb#L2252)

```ruby
def label(method, text = nil, options = {}, &block)
  @template.label(@object_name, method, text, objectify_options(options), &block)
end
```

I clicked the label call and looked at the definitions found by Github's code navigation (I only discovered recently that Github lets you do that, so I'm mentioning it here in case you didn't know about).

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/g5dzkaz8ga9tsg0misky.png)

The method's signature of the [first row](https://github.com/rails/rails/blob/a0e0f0263902896a7aacf703bcab35bee16bdaf8/actionview/lib/action_view/helpers/form_helper.rb#L1118) seems to match the one used before, so I followed it and found this, which doesn't do much:

```ruby
def label(object_name, method, content_or_options = nil, options = nil, &block)
    Tags::Label.new(object_name, method, self, content_or_options, options).render(&block)
end
```

From there, I followed `Tags::Label` and finally arrived on the [LabelBuilder](https://github.com/rails/rails/blob/a0e0f0263902896a7aacf703bcab35bee16bdaf8/actionview/lib/action_view/helpers/tags/label.rb#L6)
Here I started to skim through the code, but I didn't have to go far because, at the very top, I saw a promising `def translation`.

I went back to the code, added a parameter to the block call that I felt should be named label_builder, and called its translation method.

```slim
= f.label :email, required: true, class: "block text-xxs uppercase text-gray-500" do |label_builder|
  / ✨ True magic
  = label_builder.translation
  span.ml-2.normal-case.text-orange-400.text-xxs.font-semibold= t("required")
  = f.email_field :email, autocomplete: false, required: true, class: "form-input mt-1 block w-full text-field focus:shadow-outline focus:border-green-300", placeholder: "richard@piedpiper.com"
```

This translation is closer to the template engine than the previous solution.

### The easy way

Before we go further, here is the easy way to find that same method in less time.

You just have to a) use the console

```slim
= f.label :email, required: true, class: "block text-xxs uppercase text-gray-500" do |label_builder|
  - console
```

b) load the page in your browser, wait for the console to appear, and type `label_builder.methods` to return the list of the names of methods of our label_builder: `:translation` is the very first one.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/u22smpon0xpra2yqk1ml.png)

Why didn't I think of that? Force of habits mostly.
I've found a lot of solutions recently by reading code so it was my first instinct. Also, I didn't think it would be that easy, ie that the `LabelBuilder` would just expose the translation.
But I must admit that the prospect of reading code and learning a thing or two because of it was also appealing. I find that it's always worth my time.

### Why is it better and what did we learn?

Remember when I told you that we were missing out on lazy lookup cleverness?
You can reveal it using [i18n-debug](https://github.com/fphilipe/i18n-debug).

The previous solution using `human_attribute_name` looks for this key:
```bash
en.activerecord.attributes.contact.email
```

Our new solution first looks for one specific to labels and only falls back on the model one if it's nil.
```bash
en.helpers.label.contact.email
en.activerecord.attributes.contact.email
```

This is way more satisfying 😌 and the end of the original article.

I wanted to not only share my solution but also the tools I use as well as a way to go beyond duck-taping (which to me is any approach up to - and including - the `human_attribute_name` approach).

At this point you know how to add a "required" text to form labels:
* ✅ using only TailwindCSS/TailwindUI existing classes
* ✅ that can be reused easily
* ✅ internationalization friendly
* ✅ that doesn't reinvent the wheel in some way
* ✅ that looks like the cover image


# For the Stakhanovites, going further with a Form Builder

I didn't plan to go that far initially, but I couldn't resist once I thought about it 😬

If you're going to do that often, you'll probably want to start [customizing form builders](https://guides.rubyonrails.org/form_helpers.html#customizing-form-builders) in order to make all of this even more reusable, write less code and get even closer to vanilla rails.

Indeed, with a form builder we can simply write something like this:

```slim
= f.label :email, required: true, class: "block text-xxs uppercase text-gray-500", required_class: "ml-2 normal-case text-orange-400 text-xxs font-semibold"
= f.email_field :email, autocomplete: false, required: true, class: "form-input mt-1 block w-full text-field focus:shadow-outline focus:border-green-300", placeholder: "richard@piedpiper.com"
```

If you compare it to our starting point, you'll see that there is only one difference: an extra `required_class`. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/j03a8ptbymo67f37whm2.png)

Now, this is 😎.

What would that form builder look like? Here's my take on this. It's the first form builder I write, so if you have more experienced and notice something weird please do tell.

```ruby
# app/form_builders/requiring_form_builder.rb
class RequiringFormBuilder < ActionView::Helpers::FormBuilder
  def label(method, text = nil, options = {}, &block)
    text_is_options = text.is_a?(Hash)
    required = text_is_options ? text[:required] : options[:required]
    if required
      required_class = text_is_options ? text.delete(:required_class) : options.delete(:required_class)
      super(method, text, options) do |label_builder|
        @template.concat label_builder.translation
        @template.concat @template.content_tag(:span, I18n.t("required", scope: :helpers), class: required_class)
        @template.concat(@template.capture(label_builder, &block)) if block_given?
      end
    else
      super(method, text, options, &block)
    end
  end
end
```

There are two other things required for this to work.

First, the form_with declaration must declare the builder:
```slim
= form_with model: @user, builder: RequiringFormBuilder do |f|
```

Second, you might have noticed that I use `I18n.t("required", scope: :helpers)` and not `I18n.t("required")` as we did before because it seems more appropriate to put that under the `helpers` namespace (in my case in a file called `config/locals/helpers/en.yml`). So you need to move that in the proper translation file, or at the minimum to change
```yaml
en:
  required: required
```
into
```yaml
en:
  helpers:
    required: required
```

I won't go into the details, but I'll point to two parts in the rails code that helped me write this code.
* https://github.com/rails/rails/blob/a0e0f0263902896a7aacf703bcab35bee16bdaf8/actionview/lib/action_view/helpers/tags/label.rb#L30 because text can, in fact, be the options
* https://github.com/rails/rails/blob/477fae3eb3d3b3bfdbe28586fecb8578c0be4721/actionview/lib/action_view/helpers/form_helper.rb#L1924 because we don't want to break block calling (`do |label_builder|`)

With this form builder, we can now simply add a `required_class` to any required label field.

If you enjoyed this article, you can follow me on Twitter [@sowenjub](https://twitter.com/sowenjub).