view_component-form icon indicating copy to clipboard operation
view_component-form copied to clipboard

Grouping labels & fields

Open Spone opened this issue 4 years ago • 7 comments

A common use case when building forms is the need to group labels and fields, or multiple fields together. Let's discuss these cases.

A label + a field

It's the most common use case. That's for instance what is generated by Rails scaffolding:

  <div class="field">
    <%= form.label :first_name %>
    <%= form.text_field :first_name %>
  </div>

When the field is a check_box or a radio_button, you usually want to invert the label and the input:

  <div class="field">
    <%= form.check_box :accepts_terms %>
    <%= form.label :accepts_terms %>
  </div>

We could have a ViewComponent::Form::GroupComponent for this purpose.

We could also add a label option to some helpers (see #16).

Errors

When a field has errors, it's a good practice to display them next to the field. The errors could be handled by the ViewComponent::Form::GroupComponent.

Hints

Some fields require additional information to help the user. The ViewComponent::Form::GroupComponent could accept a hint option for this. See GOV.UK for an example implementation.

A group of fields (and their labels)

The <fieldset> element is used for this.

<fieldset>
  <legend>Your identity</legend>

  <div class="field">
    <%= form.label :first_name %>
    <%= form.text_field :first_name %>
  </div>

  <div class="field">
    <%= form.label :last_name %>
    <%= form.text_field :last_name %>
  </div>
</fieldset>

Rails does not provide a dedicated helper for this element.

We could have a ViewComponent::Form::FieldsetComponent for this purpose, or reuse the ViewComponent::Form::GroupComponent (but it would make it more complex).

Spone avatar Jul 07 '21 21:07 Spone

Here are some syntax options for groups, which one do you prefer? Or maybe we can implement both?

1. Additional params to the existing Rails helpers

We could pass strings:

<%= f.text_field :first_name, label: "First name", hint: "How should we call you?" %>
<div>
  <label for="user_first_name">First name</label>
  <span>How should we call you?</span>
  <input type="text" id="user_first_name" name"user[first_name]" />
</div>

or booleans... in this case the label and the hint come from the locale files:

<%= f.text_field :first_name, label: true, hint: true %>
# config/locale/en.yml
helpers:
  label:
    user:
      first_name: First name
  hint:
    user:
      first_name: How should we call you?

We can also pass hashes with a :text key. This allows us to add more params later (for instance to add class or position). That's similar to what GOV.UK does.

<%= f.text_field :first_name, label: { text: "First name" }, hint: { text: "How should we call you?" } %>
<%= f.text_field :first_name, label: { text: "First name", class: "my-custom-label" }, hint: { text: "How should we call you?", position: :after_input } %>
<div>
  <label for="user_first_name" class="my-custom-label">First name</label>
  <input type="text" id="user_first_name" name"user[first_name]" />
  <span>How should we call you?</span>
</div>

2. Using a group helper, with a block

<%= f.group :first_name, hint: "How should we call you?" do %>
  <%= f.text_field :first_name %>
<% end %>
<div>
  <label for="user_first_name">First name</label>
  <span>How should we call you?</span>
  <input type="text" id="user_first_name" name"user[first_name]" />
</div>

Spone avatar Jul 09 '21 13:07 Spone

I personally prefer the second one

nicolas-brousse avatar Nov 03 '21 19:11 nicolas-brousse

After discussing it, we'll go for the second option (Using a group helper, with a block).

While working on a first implementation, we hit the following roadblocks:

  1. it would be useful for the input to "inherit" some options from the containing group (for instance if the group has the class .form-group you may want the input to have .form-group-input)
  2. there will be some similar I18n lookup logic in the group (label, hint) and in the input (placeholder), we should try to keep it DRY
  3. when implementing the hint, we need to add the hint element ID in an aria-describedby attribute of the input (by the way, #field_id can be used for this)
  4. when the group contains a collection of checkboxes or radio buttons, we don't want to use a <label> element for the group label, since the labels are already attached to each checkbox / radio

A potential solution to 1. 2. 3. is for the block to receive its own FormBuilder instance as an argument, so instead of:

<%= f.group :first_name, hint: "How should we call you?" do %>
  <%= f.text_field :first_name %>
<% end %>

we would have

<%= f.group :first_name, hint: "How should we call you?" do |g| %>
  <%= g.text_field :first_name %>
<% end %>

Then we could inject the class, placeholder, aria-describedby.

For 4. we may need to create another helper, such as collection_group or maybe pass a param label_tag: :div to group?

Feel free to contribute ideas :pray:

Spone avatar Nov 15 '21 10:11 Spone

Hi, it would be great to have the "primitives" for this in the code... I think having such a group method could be the second step, as the primitives "hints" and "error message" are always useful.

I created something like this. Maybe it is useful for anyone: https://gist.github.com/tmaier/22966c6bddac86e3612c8eddc072b919

tmaier avatar Dec 19 '21 19:12 tmaier

Hi, it would be great to have the "primitives" for this in the code... I think having such a group method could be the second step, as the primitives "hints" and "error message" are always useful.

Hi @tmaier I opened an issue for this (#97) would you like to contribute a PR for this?

Spone avatar Jan 03 '22 13:01 Spone

Related to #127

Spone avatar Feb 02 '23 21:02 Spone