Skip to content

Custom inputs examples

Dillon Lustick edited this page Jun 27, 2020 · 10 revisions

Custom inputs examples

Week Day Input

In order to get an input with localized week days:

# app/inputs/wday_input.rb
class WdayInput < SimpleForm::Inputs::Base
  def input(wrapper_options)
    @builder.select(attribute_name, I18n.t(:"date.day_names").each_with_index.to_a)
  end
end

Then, use it in your form:

<%= timetable.input :wday, :as => :wday %>

Date Time Picker for Twitter Bootstrap 3 using the DateTimePicker JS Library

## app/inputs/date_time_picker_input.rb
class DateTimePickerInput < SimpleForm::Inputs::Base
  def input(wrapper_options)
    template.content_tag(:div, class: 'input-group date form_datetime') do
      template.concat @builder.text_field(attribute_name, input_html_options)
      template.concat span_remove
      template.concat span_table
    end
  end

  def input_html_options
    super.merge({class: 'form-control', readonly: true})
  end

  def span_remove
    template.content_tag(:span, class: 'input-group-addon') do
      template.concat icon_remove
    end
  end

  def span_table
    template.content_tag(:span, class: 'input-group-addon') do
      template.concat icon_table
    end
  end

  def icon_remove
    "<i class='glyphicon glyphicon-remove'></i>".html_safe
  end

  def icon_table
    "<i class='glyphicon glyphicon-th'></i>".html_safe
  end

end

To use add this to an appropriate coffee script file

$(document).ready ->
  $('.form_datetime').datetimepicker({
    autoclose: true,
    todayBtn: true,
    pickerPosition: "bottom-left",
    format: 'mm-dd-yyyy hh:ii'
  });

call on simple form input with

f.input :my_date, as: :date_time_picker

Text Array Input

Input that submits an array of strings. Starts with one blank input and clones it when the Add button is clicked. Remove button is hidden with JS if it is the only entry.

# app/inputs/array_input.rb
class ArrayInput < SimpleForm::Inputs::StringInput
  def input(wrapper_options = nil)
    input_html_options[:type] ||= input_type
    existing_values = object.public_send(attribute_name)
    existing_values.push(nil) if existing_values.blank?

    template.content_tag(:div, class: 'text-array', id: "#{object_name}_#{attribute_name}") do
      Array(existing_values).map do |array_el|
        template.concat build_row(array_el)
      end

      template.concat add_button
    end
  end

  def input_type
    :text
  end

  private

  def build_row(val)
    template.content_tag(:div, class: 'text-array__row') do
      template.concat @builder.text_field(nil,
        input_html_options.merge(value: val, name: "#{object_name}[#{attribute_name}][]"))
      template.concat remove_button
    end
  end

  def add_button
    '<button class="text-array__add" href="#">Add</button>'.html_safe
  end

  def remove_button
    '<button class="text-array__remove" href="#">Remove</button>'.html_safe
  end
end

Add coffeescript for the buttons:

$(document).ready ->
  addRow = (ev) ->
    ev.preventDefault()

    $nearest_row = $(this).prev()
    # Show remove button in case it was hidden
    $nearest_row.find('.text-array__remove').show()

    $new_row = $nearest_row.clone()
    $new_row.find('input').val("")

    # Need to set click handler on newly created button
    $remove_button = $new_row.find('.text-array__remove')
    $remove_button.click removeRow

    $new_row.insertBefore(this)

  removeRow = (ev) ->
    ev.preventDefault()

    this.parentElement.remove()
    hideRemoveButton()

  # Need to hide button so they can't remove the only row, otherwise there would be nothing to clone.
  hideRemoveButton = ->
    $('.text-array').each (i, el) ->
      $rows = $(el).children('.text-array__row')
      if $rows.length == 1
        $rows.find('.text-array__remove').hide()

  $('.text-array__add').click addRow
  $('.text-array__remove').click removeRow
  hideRemoveButton()

Make sure the controller params list it as an array:

def user_params
  params.require(:user).permit(:name, :age, { my_array: [] })
end

Between inputs

This example uses MetaSearch which is now deprecated but still provides a good example of custom inputs. This input actually generates two input fields, one for the lower boundary (greater than or equal to) and one for the upper (less than or equal to). For example, searching for users between the ages of 21 and 30.

# app/inputs/between_input.rb
class BetweenInput < SimpleForm::Inputs::Base
  def input(wrapper_options)
    field1 = @builder.number_field(:"#{attribute_name}_gteq", input_html_options)
    field2 = @builder.number_field(:"#{attribute_name}_lteq", input_html_options)

    # Be aware for I18n: translate the "and" here
    (field1 << @builder.label(:"#{attribute_name}_lteq", 'and', class: 'separator') << field2).html_safe
  end

  # Make the label be for the first of the two fields
  def label_target
    :"#{attribute_name}_gteq"
  end
end
- # app/views/users/_search.html.haml
= simple_form_for @user_search do |form|
  = form.input :age, as: :between, label: "Age between"

Predefined collection

Rather than using a select input and specifying the collection, a custom collection input type can bundle that collection and have the added benefit of including any associated input_html you care to use.

class CustomCollectionInput < SimpleForm::Inputs::CollectionSelectInput
  def input(wrapper_options)
    collection = Collection::LIST

    label_method = :to_s
    value_method = :to_s

    @builder.collection_select(
      attribute_name, collection, value_method, label_method,
      input_options, input_html_options
    )
  end

  def input_html_classes
    super.push('custom-css-class chosen-selector')
  end

end

Call in a form with

  <%= f.input :column_name, as: :custom_collection %>

Custom CollectionRadioButtonsInput or CollectionCheckBoxesInput

When inheriting from these classes you need to override input_type so the builder can find the correct input method. Otherwise you will see an error similar to: undefined method 'collection_custom_checkboxes' for #<#SimpleForm::FormBuilder>

class CustomCheckboxesInput < SimpleForm::Inputs::CollectionCheckBoxesInput
  # simple_form looks for the method "collection_#{input_type}" on the form,
  # can be either check_boxes or radio_buttons
  def input_type
    'check_boxes'
  end
end