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.