Wrapping a (not-so) simple_form

Here is what should be a pretty simple input requirement: produce this:

It's three input fields presented as one and it's part of a larger form. The rest of the form isn't relevant here. We're using Bootstrap 3 and the simple_form gem. This should be easy, right? Well, think again...

What we're after is a Bootstrap input group with three fields in it, the first of which is a dropdown. I'm going to walk through building this up from scratch using simple_form components to learn how to make it work together wth Bootstrap to produce the desired markup.

Bootstrap input group

The Bootstrap input group is described here and a basic example with one dropdown and one text field is shown below:

<div class="row">  
  <div class="col-md-12">
    <div class="input-group">
      <div class="input-group-btn">
        <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Action <span class="caret"></span></button>
        <ul class="dropdown-menu">
          <li><a href="#">Action</a></li>
          <li><a href="#">Another action</a></li>
        </ul>
      </div><!-- /btn-group -->
      <input type="text" class="form-control" aria-label="...">
    </div><!-- /input-group -->
  </div><!-- /.col-md-12 -->
</div><!-- /.row -->  

This looks along the right lines, and the text area can easily be split into two fields by replacing

<input type="text" class="form-control" aria-label="...">  

with

<div class="col-sm-6">  
  <input type="text" class="form-control" aria-label="...">
</div>  
<div class="col-sm-6">  
  <input type="text" class="form-control" aria-label="...">
</div>  

This looks good, except for the gaps (gutters) between on the left and right ends of the fields. Fixing this calls for a little CSS:

.row .no-gutter {                                                                     
  margin-right: 0;
  margin-left: 0;

  > [class^='col-'],
  > [class*=' col-'] {
    padding-right: 0;
    padding-left: 0;
  }
}

CSS inspired by this, this, and this.
Then wrap the above two column divs in

<div class="no-gutter">  
</div>  

which produces a rendering almost in keeping with the requirement.
There are a couple of slight visual concerns: the separating bar between the two fields is double-thickness and the right-hand end of the input group does not have rounded corners.

The double thickness is due to both fields having borders. The solution requires a little CSS to remove the border (actually, keep it but make it invisble to retain alignments). The lack of rounded corners is due to this in the Bootstrap SASS sources. It can be fixed with a little more CSS to reverse the effect. The additional CSS to fix both problems is shown below:

.row .no-gutter {
  > [class^='col-']:last-child,
  > [class*=' col-']:last-child {
    .form-control {
      border-bottom-right-radius: 4px !important;
      border-top-right-radius: 4px !important;
      border-left: 1px solid transparent;
    }
  }
}

And the resulting control looks good:

This is despite the Bootstrap documentation not supporting it:

We do not support multiple form-controls in a single input group.

The next step is to reproduce this effect in ERB using the application's real input controls.

simple_form inputs

I'll start with a single field, This HTML needs to be generated by ERB:

<div class="col-sm-6">  
  <input type="text" class="form-control" aria-label="...">
</div>  

Here's the ERB for a single input text field in a simple_form_for:

<%= f.input :title %>  

and here is the HTML that produces:

<div class="form-group string required user_title">  
  <label class="string required control-label" for="user_title">
    <abbr title="required">*</abbr> Title
  </label>
  <input class="string required form-control" name="user[title]" id="user_title" type="text">
</div>  

There are three components here: an outer wrapping <div>, a <label> and the <input> field, which is what the desired input group needs to include. The above ERB can be written like this:

<%= f.input :title do%>  
  <%= f.input_field :title %>                                                 
<% end %>  

which renders like this:

<div class="form-group string required user_title">  
<label class="string required control-label" for="user_title">  
  <abbr title="required">*</abbr> Title
</label>  
<input class="string required" name="user[title]" id="user_title" type="text">  
</div>  

There is an issue with this version's <input> line - it does't style properly, and that's because it lacks the form-control class. So specify it manually:

<%= f.input :title do%>  
  <%= f.input_field :title, class: 'form-control' %>
<% end %>  

Now it renders the exact HTML as before. This can be reduced to just the outer <div> if no input_field lines are given and the label is disabled:

<%= f.input :nothing, type: :hidden, label: false do %>  
<% end %>  

(the field name can't be dropped but, as there isn't one, a more appropriate name can be given to avoid confusion )

However, I want an overall name label for the input group, so this would be more appropriate here:

<%= f.input :name do %>  
<% end %>  

which produces:

<div class="form-group string required user_name">  
  <label class="string required control-label" for="user_name">
    <abbr title="required">*</abbr> Name
  </label>
</div>  

Now we can add our own content. We want a dropdown button and two input fields. Leaving out the button for now, each of the two input fields can be rendered like this:

<div class="col-sm-6">  
  <%= f.input_field :forename, placeholder: 'forename', class: "form-control" %>
</div>  

I added the placeholder which is displayed in the field when it is empty.

Handling errors

The input_field is just that; the errors need to be displayed separately. They'll be displayed beneath the input group:

<%= f.error :title %>  
<%= f.error :forename %>  
<%= f.error :surname %>  

However, they don't appear in red because that's done by the f.input outer <div>. Recall that it looks like this:

<div class="form-group string required user_name">  
...
</div>  

When there is an error, it changes to this:

<div class="form-group string required user_name has-error">  
...
</div>  

The difference is the addition of a has-error class. This will have to be added by the view logic because it needs to be added if any of the three fields have errors. The view can provide this using a helper method:

def has_errors(properties)  
  (properties & model.errors.keys).empty? ? '' : 'has-error'
end  

which can then be referenced in the view. But this customisation requires the simple_form wrapper used above is replaced with a hand-crafted <div>:

<div class="form-group <%=has_errors %i(forename surname title)%>">  

The form-group class is necessary to ensure consistent layout of the input group with padding, etc, the same as other form fields. The string required user_name classes related to the fake input field on the empty wrapper - they aren't needed (there is no such field) so they have been dropped now that the <div> is hand-crafted.

The above is sufficient wiring for the text fields. The button is a bit more involved.

The button dropdown

The dropdown button field, for the honorific title (Mr, Mrs, etc), is the hard bit. The simple_form f.input with a collection works, rendering a <select> but it isn't rendered with the required Bootstrap look and feel.
The Bootstrap example button has the following HTML:

<div class="input-group-btn">  
  <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Action <span class="caret"></span></button>
  <ul class="dropdown-menu">
    <li><a href="#">Action</a></li>
    <li><a href="#">Another action</a></li>
    <li><a href="#">Something else here</a></li>
    <li role="separator" class="divider"></li>
    <li><a href="#">Separated link</a></li>
  </ul>
</div><!-- /btn-group -->  

A bootstrap-select gem offers a potential solution, however it doesn't fit well with simple_form and the Bootstrap input group due to the HTML it produces. I decided to use similar concepts to implement a picker directly. It works like this:

  • A dropdown is implemented following the HTML of the Bootstrap example
  • A hidden input field is used to include the selection in the form
  • Javascript updates the button text and the hidden input field.

The ERB produces HTML similar to the example; like this:

<div class="input-group-btn btn-group">  
  <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
    <%=title%> <span class="caret"></span>
  </button>
  <%= f.input_field :title, as: :hidden, class: 'dropdown-data' %>    
  <ul class="dropdown-menu">ppen
    <% titles.each do |t| %>
      <li><a href="#"><%=t%></a></li>
    <% end %>
  </ul>
</div><!-- /btn-group -->  

The button displays the currently selected value. When the form is first displayed, it displays a prompt or the current value. A title helper returns the current value of the title or, if blank, a prompt.

def title  
  tp = operation.contract.title
  return tp.blank? ? t('.title_prompt') : tp
end  

The button will appear and operate but nothing happens when a selection is made (this is also true of the Bootstrap example). The following Javascript sorts that:

<%= javascript_tag %Q(  
  $(".dropdown-menu li a").click(function(e){
    var selText = $(this).text();
    var parents = $(this).parents('.input-group');
    parents.find('.dropdown-toggle').html(selText+' <span class="caret"></span>');       
    parents.find('.dropdown-data').val(selText);       
  });
) %>

The result of all the above is a triple-field input that is presented as a single field.