Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grouping labels & fields #36

Open
Tracked by #5
Spone opened this issue Jul 7, 2021 · 7 comments
Open
Tracked by #5

Grouping labels & fields #36

Spone opened this issue Jul 7, 2021 · 7 comments
Labels
discussion Let's discuss ideas and features
Milestone

Comments

@Spone
Copy link
Collaborator

Spone commented Jul 7, 2021

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 Spone added the discussion Let's discuss ideas and features label Jul 7, 2021
@Spone Spone mentioned this issue Jul 7, 2021
25 tasks
@Spone
Copy link
Collaborator Author

Spone commented Jul 9, 2021

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 Spone modified the milestones: v0.2, v0.3 Sep 9, 2021
@nicolas-brousse
Copy link
Member

I personally prefer the second one

@Spone
Copy link
Collaborator Author

Spone commented Nov 15, 2021

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 🙏

@tmaier
Copy link
Contributor

tmaier commented Dec 19, 2021

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

@Spone
Copy link
Collaborator Author

Spone commented Jan 3, 2022

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
Copy link
Collaborator Author

Spone commented Feb 2, 2023

Related to #127

@woller
Copy link

woller commented Jun 26, 2024

I've been playing around with a slot-based solution for this. There are some downsides as the syntax is not exactly the same as using a proper FormBuilder.

# frozen_string_literal: true

class Form::GroupComponent < ViewComponent::Form::FieldComponent
  renders_one :input, ->(input, options = {}) do
    "Form::#{input.to_s.camelize}FieldComponent".constantize.new(
      form,
      object_name,
      method_name,
      default_input_options.merge(options)
    )
  end

  def show_label?
    options[:hide_label] != true
  end

  def hint
    options[:hint] # || t(some.translation.key.default)
  end

  def default_input_options
    return {} unless hint.present?

    {aria: {describedby: hint_id}}
  end

  def hint_id
    form.field_id(method_name, :hint)
  end
end
<fieldset>
  <% if show_label? %>
    <label for="<%= method_name %>" class="block text-sm font-medium leading-6 text-gray-900"><%= label_text %></label>
  <% end %>
  <div class="mt-2">
    <div class="relative rounded-md shadow-sm">
      <%= input %>

      <% if method_errors? %>
        <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
          <%= render IconComponent.new(name: :circle_exclamation, scheme: :alert) %>
        </div>
      <% end %>
    </div>

    <%= form.hint method_name, hint, id: hint_id %>
    <%= form.error_message method_name %>
  </div>
</fieldset>
<%= form.group :email, hint: "What is the email you signed up with?" do |group| %>
  <%= group.with_input :email, autocomplete: "email", class: "w-full" %>
<% end %>
<fieldset>
  <label for="email" class="block text-sm font-medium leading-6 text-gray-900">Email</label>
  <div class="mt-2">
    <div class="relative rounded-md shadow-sm">
      <input aria-describedby="admin_email_hint" autocomplete="email"
        class="w-full block rounded-md border-0 shadow-sm py-1.5 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 text-slate-900 ring-slate-300 placeholder:text-slate-400 focus:ring-slate-500"
        type="email" name="admin[email]" id="admin_email">

    </div>

    <div skip_default_ids="false" allow_method_names_outside_object="true" id="admin_email_hint"
      object="#<Admin:0x0000000170f4ac88>" class="mt-1 text-sm text-gray-500">What is the email you signed up with?
    </div>

  </div>
</fieldset>

It needs some more work (I have no idea, why it outputs an Admin instance), but I think there is some potential.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Let's discuss ideas and features
Projects
None yet
Development

No branches or pull requests

4 participants