Note: before you attempt to manually wire up a super_select field, note that Super Scaffolding will automatically do that for your models. See Super Scaffolding docs, section 4, for an example. And make sure Super Scaffolding doesn't automatically do what you're trying to do.

Examples for the super_select Field Partial

The super_select partial provides a wonderful default UI (in contrast to the vanilla browser experience for select boxes, which is horrible) with optional search and multi-select functionality out-of-the-box. It invokes the Select2 library to provide you these features.

Define Available Buttons via Localization Yaml

If you invoke the field partial in app/views/account/some_class_name/_form.html.erb like so:

<%= render 'shared/fields/super_select', form: form, method: :response_behavior %>

You can define the available options in config/locales/en/some_class_name.en.yml like so:

en:
  some_class_name:
    fields:
      response_behavior:
        name: &response_behavior Response Behavior
        label: When should this object respond to new submissions?
        heading: Responds
        choices:
          immediate: Immediately
          after_10_minutes: After a 10 minute delay
          disabled: Doesn't respond

Specify Available Choices Inline

Although it's recommended to define any static list of choices in the localization Yaml file (so your application remains easy to translate into other languages), you can also specify these choices using the choices option from the underlying select form field helper:

<%= render 'shared/fields/super_select', form: form, method: :response_behavior,
  choices: [['Immediately', 'immediate'],
  ['After a 10 minute delay', 'after_10_minutes'],
  ["Doesn't respond", 'disabled']] %>

Generate Choices Programmatically

You can generate the available buttons using a collection of database objects by passing the options option like so:

<%= render 'shared/fields/super_select', form: form, method: :category_id,
  choices: Category.all.map { |category| [category.label_string, category.id] } %>

Allowing Multiple Option Selections

Here is an example allowing multiple team members to be assigned to a (hypothetical) Project model:

<%= render 'shared/fields/super_select', form: form, method: :membership_ids,
  choices: @project.valid_memberships.map { |membership| [membership.name, membership.id] },
  html_options: {multiple: true} %>

The html_options key is just inherited from the underlying Rails select form field helper.

Allowing Search

Here is the same example, with search enabled:

<%= render 'shared/fields/super_select', form: form, method: :membership_ids,
  choices: @project.valid_memberships.map { |membership| [membership.name, membership.id] },
  html_options: {multiple: true}, other_options: {search: true} %>

Overriding Browser Time Zone

When using super-select with Time Zone options, passing use_browser_time_zone: false will override the automatic setting of the timezone value from the browser.

Here is the an example setting the selected value to @user.time_zone which will not be overridden by the browser time zone.

<%= render 'shared/fields/super_select', form: f, method: :time_zone,
    choices: time_zone_options_for_select(@user.time_zone, nil, ActiveSupport::TimeZone),
    other_options: {search: true, required: true, use_browser_time_zone: false } %>

Accepting New Entries

Here is an example allowing a new option to be entered by the user:

<%= render 'shared/fields/super_select', form: form, method: :delay_minutes,
  choices: %w(1 5 10 30).map { |value| [value, value] },
  other_options: {accepts_new: true} %>

Note: this will set the option value (which will be submitted to the server) to the entered text.

To handle the new entry's text on the server, use ensure_backing_models_on.

ensure_backing_models_on validates an id: or multiple ids: against a passed Active Record relation, and yields for each missing id so you can create backing models. Like this:

if strong_params[:category_id]
  strong_params[:category_id] = ensure_backing_models_on(current_team.categories, id: strong_params[:category_id]) do |scope, id|
    scope.find_or_create_by(name: id)
  end
end

In case our form had multiple: true, we could have used ids: instead:

if strong_params[:category_ids]
  strong_params[:category_ids] = ensure_backing_models_on(current_team.categories, ids: strong_params[:category_ids]) do |scope, id|
    scope.find_or_create_by(name: id)
  end
end

Note, if you need to constrain the collection further you could pass any extra scope, e.g. current_team.categories.not_archived.

Events

All events dispatched from the super_select partial are Select2's jQuery events re-dispatched as native DOM events with the following caveats:

  1. The native DOM event name is pre-pended with $
  2. The original jQuery event is passed through under event.detail.event
Select2 event name DOM event name
change $change
select2:closing $select2:closing
select2:close $select2:close
select2:opening $select2:opening
select2:open $select2:open
select2:selecting $select2:selecting
select2:select $select2:select
select2:unselecting $select2:unselecting
select2:unselect $select2:unselect
select2:clearing $select2:clearing
select2:clear $select2:clear

For example, the view template for catching the $change events in a parent dependent-form-fields using a Stimulus controller with a single updateDependentFields method would look like this:

<div data-controller="dependent-form-fields">
  <div data-action="$change->dependent-form-fields#updateDependentFields">
    <%= render 'shared/fields/super_select', form: form, method: :category_id,
      choices: Category.all.map { |category| [category.label_string, category.id] } %>
  </div>
  <div>
    <!-- This is the dependent field that would get updated when the previous one changes -->
    <%= render 'shared/fields/super_select', form: form, method: :category_id,
      choices: Category.all.map { |category| [category.label_string, category.id] },
      html_options: { data: { 'dependent-form-fields-target': 'dependentField' }} %>
  </div>
</div>

And this is an example of what the dependent-form-fields Stimulus controller would look like.


// dependent_form_fields_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "dependentField" ]

  updateDependentFields(event) {
    const originalSelect2Event = event.detail.event
    console.log(`catching event ${event.type}`, originalSelect2Event)

    this.dependentFieldTargets.forEach((dependentField) => {
      // update dependentField based on value found in originalSelect2Event.target.value
    })
  }
}

Options

Select2 has different options available which you can check here.

You can pass these options to the super select partial like so:

<%= render 'shared/fields/super_select', method: :project,
  select2_options: {
    allowClear: true,
    placeholder: 'Your Custom Placeholder'
  }
%>

*Passing options like this doesn't allow JS callbacks or functions to be used, so you must extend the Stimulus controller and add options to the optionsOverride getter if you want to do so.

Dynamically Updating Form Fields

If you'd like to:

  • modify other fields based on the value of your super_select, or
  • modify your super_select based on the value of other fields

See Dynamic Forms and Dependent Fields.