Skip to content

Instantly share code, notes, and snippets.

@walterdavis
Last active February 15, 2022 20:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save walterdavis/386c86b05d7e134c7fed689e32fe0d78 to your computer and use it in GitHub Desktop.
Save walterdavis/386c86b05d7e134c7fed689e32fe0d78 to your computer and use it in GitHub Desktop.
A combo box, in this setting, is a `<select>` that offers the ability to edit its contents, directly within the current form.

For example, the list of options is "Foo", "Bar", and "Baz". At the bottom of the list (always at the bottom) the user sees "Add..." as a new option. When she selects that option, the <select> disappears, replaced in place with a focused <input type="text"> ready to accept a new option. It's important to note that this pattern is only usable in cases where the input is stored as a string, and you want to enforce a limited but extensible vocabulary of options. A possible example of this is colors, where you don't want to just let people enter anything, otherwise you would end up with "Gray" and "Grey" and possibly "grey".

app/javascripts/controllers/combo_controller.js

import { Controller } from "stimulus";

export default class extends Controller {
  static targets = ['picker'];

  connect() {
    const elm = this.pickerTarget, add = 'Add...';
    const text_field = '<input type="text" id="' + elm.id + '" name="' + elm.name + '" class="' + elm.className + '">';

    elm.options[elm.options.length] = new Option(add, add);
    elm.addEventListener('change', function(evt){
      let val = this.options[this.selectedIndex].value;
      if (val == add){
        let restore = elm.cloneNode(true);
        elm.parentNode.insertAdjacentHTML('beforeend', text_field);
        let replacement = elm.nextSibling;
        elm.remove();
        replacement.focus();
        replacement.addEventListener('blur', function(evt){
          if(this.value == ''){
            replacement.replaceWith(restore);
          }
        });
      }
    });
  }
}

app/helpers/forms_helper.rb

module FormsHelper
  def combo_box(form, attr, identifier = :to_s, value = :to_s, collection = nil)
    collection ||= form.object.class.unscoped.select(attr).distinct(attr).order(attr).pluck(attr).compact

    form.collection_select attr, collection, identifier, value, {
                                   prompt: true, 
                                   wrapper: {
                                     data: { controller: 'combo' }
                                   }
                                 }, data: { target: 'combo.picker' }
  end
end

app/views/titles/_form.html.erb

<%= bootstrap_form_with model: @title, local: true do |form| %>
...
<%= combo_box form, :publisher %>
...
<%- end -%>

(optional) app/models/title.rb

...
  def publisher=(val)
    val = nil if val.blank?
    super
  end
...

This is needed if you want to ensure that the empty string doesn't creep into your list of pre-filled values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment