Grouping labels & fields
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).
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>
I personally prefer the second one
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:
- it would be useful for the input to "inherit" some options from the containing group (for instance if the group has the class
.form-groupyou may want the input to have.form-group-input) - 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
- when implementing the hint, we need to add the hint element ID in an
aria-describedbyattribute of the input (by the way, #field_id can be used for this) - 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:
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
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?
Related to #127