51

I'm using the chosen.js plugin http://harvesthq.github.com/chosen/ with jQuery to allow the user to select multiple options from a select. However, I now want to be able to let them create values that aren't already present - any idea how to go about this?

EDIT: something similar to SO's own tag selection/creation bar would be close to what I'm after

Preferably without changing or editing the plugin, but will do if required.

The code: HTML:

<p>Select something</p>
<select name="theSelect[]" multiple="multiple">
    <option value="First Option">First Option</option>
    <option value="Second Option">Second Option</option>
</select>

Javascript:

$(function(){
    $('select').chosen();
});

So if a user were to type in "Third Option", i'd like to add that to the list and have it selected. The value and display name are / will be the same so that's not a concern

MBaas
  • 7,248
  • 6
  • 44
  • 61
totallyNotLizards
  • 8,489
  • 9
  • 51
  • 85

10 Answers10

28

According to the documentation you can try doing something like this:

$('select').append('<option>test</option>');
$('select').trigger('liszt:updated');

As Tony stated in the comments below:

"Starting with version 1.0 which the trigger is now "chosen:updated". See harvesthq.github.io/chosen/#change-update-events"

jeffreydev
  • 1,722
  • 13
  • 31
  • 2
    that's one peice of the puzzle, but what I also need is to be able to fire that when a user types something that isn't already in the list. i'm grinding through the source now :( – totallyNotLizards Sep 12 '11 at 08:58
  • 1
    or is it a better idea to let them type whatever they want and check it on submit? I notice that seems to be what SO does – totallyNotLizards Sep 12 '11 at 08:58
  • 3
    marking this as accepted because I got around the problem by having a text box to type in a new value, and on submitting that I used these commands to refresh chosen accordingly – totallyNotLizards Sep 12 '11 at 12:31
  • 4
    Starting with version 1.0 which the trigger is now "chosen:updated". See http://harvesthq.github.io/chosen/#change-update-events – Tony Aug 07 '13 at 21:31
  • I know this is ancient old but I amusing Chosen as a Tag selector. To limit the tags available I have a separate popover that manages Adding available tags and edit/delete old ones. As new tags are added in the popover I will update Chosen to show it as a new Tag option to select. Here is an image of a MIlestone version of the popver I did http://i.imgur.com/HVQdyYI.png – JasonDavis Jun 19 '15 at 13:04
20

I stumbled upon this looking for the same ideas. Seems like its a pretty popular feature request, and a couple of forks have implemented it. Looks like it'll be merged into the master branch soon enough.

+1 for this particular pull which worked a charm: https://github.com/harvesthq/chosen/pull/166

You can view Koenpunt's fork here: https://github.com/koenpunt/chosen

Galaxy
  • 3,370
  • 3
  • 29
  • 41
  • Let me try and understand this...I download the fork and replace my current copy of chosen with that. Then I use the snippet to add the option to my element that is in the pull link you posted? – Michael Mallett Jan 25 '13 at 11:21
  • 1
    Or you just clone directly the merged version: https://github.com/koenpunt/chosen – CC. Feb 28 '13 at 21:45
  • 2
    I am a bit confused with this. Right now I'm using chosen in a lot of places within my site. If I get the source code from the last link and then replace the source code of the one I'm using for my site, will it work? I mean, do regular chosen updates reflect in the merged/forked one ? – Zubzob Aug 08 '13 at 15:07
  • Hahahaha don't hold your breath on any pull requests getting accepted, they've got a huge backlog of pull requests and no one's made a commit since late April. Chosen is apparently very low on their priority list right now. – Isochronous Jul 09 '15 at 16:25
  • Option adding is not something Harvest wants as a feature of Chosen, because Chosen is designed to be only a visual/UX enhancement of a select, not a functional one. But throughout the years I try to keep my fork up-to-date with the main repo. – Koen. Feb 08 '16 at 21:23
12

Here's a simple way that I did it:

$(".search-field").find("input").live( "keydown", function (evt) {
    var stroke;
    stroke = (_ref = evt.which) != null ? _ref : evt.keyCode;
    if (stroke == 9) { // 9 = tab key
        $('#tags').append('<option value="' + $(this).val() + '" selected="selected">' + $(this).val() + '</option>');
        $('#tags').trigger('chosen:updated');
    }
});
Ultimation
  • 1,059
  • 9
  • 18
10

I was just trying to solve the same problem. I wound up modifying the source code a bit. Here's the new keyup_checker function. Take a look at case 13:

AbstractChosen.prototype.keyup_checker = function(evt) {
  var stroke, _ref;
  stroke = (_ref = evt.which) != null ? _ref : evt.keyCode;
  this.search_field_scale();
  switch (stroke) {
    case 8:
      if (this.is_multiple && this.backstroke_length < 1 && this.choices > 0) {
        return this.keydown_backstroke();
      } else if (!this.pending_backstroke) {
        this.result_clear_highlight();
        return this.results_search();
      }
      break;
    case 13:
      evt.preventDefault();
      if (this.results_showing) {
        if (!this.is_multiple || this.result_highlight) {
          return this.result_select(evt);
        }  

        $(this.form_field).append('<option>' + $(evt.target).val() + '</option>');
        $(this.form_field).trigger('liszt:updated');
        this.result_highlight = this.search_results.find('li.active-result').last();
        return this.result_select(evt);
      } 
      break;
    case 27:
      if (this.results_showing) this.results_hide();
      return true;
    case 9:
    case 38:
    case 40:
    case 16:
    case 91:
    case 17:
      break;
    default:
      return this.results_search();
  }
};
Samo
  • 8,202
  • 13
  • 58
  • 95
6

I know this not the answer, but an alternate solution.

I was searching for the on-the-fly adding part and found that https://select2.org/tagging provides the same thing as chosen + other stuffs like "Tagging".

dana
  • 17,267
  • 6
  • 64
  • 88
sk8terboi87 ツ
  • 3,396
  • 3
  • 34
  • 45
4

You could just attach an event to the input text box to listen for a particular character code. After that add the option and trigger the update on the dropdown.

 var dropDown = $('select.chosen');
 dropDown.parent().find('.chzn-container .chzn-search input[type=text]').keydown( function (evt) {
           var stroke, _ref, target, list;
           // get keycode
           stroke = (_ref = evt.which) != null ? _ref : evt.keyCode;
           target = $(evt.target);               
           // get the list of current options
           list = target.parents('.chzn-container').find('.chzn-choices li.search-choice > span').map(function () { return $(this).text(); }).get();
           if (stroke === 9 || stroke === 13) {
              var value = $.trim(target.val());
              // if the option does not exists
              if ($.inArray(value,list) < 0) {
                 var option = $('<option>');
                 option.text(value).val(value).appendTo(dropDown);
                 option.attr('selected','selected');
                 // add the option and set as selected
              }
              // trigger the update event
              dropDown.trigger("liszt:updated");
              return true;
           }
        });
leogdion
  • 2,332
  • 1
  • 19
  • 21
  • I got this working but I had to make a couple tweaks to the jquery .find() selectors. The first one had to be '.chzn-container .chzn-search input[type=text]' and the second needed to be '.chzn-drop .chzn-results li.active-result'. I'm guessing that maybe chosen moved things around a bit but after that update this worked for me. Thank you so much. – Steve Hiner Jul 24 '13 at 19:50
  • I also had to change the final line to return true so the list would close. Otherwise the item gets added to the list but the list stays open. – Steve Hiner Jul 24 '13 at 21:02
  • @Steve Hiner, I made the change accordingly. Thanks – leogdion Jul 30 '13 at 17:30
  • The one downside of this which I haven't yet tried hard to solve is that the code as-is depends on the tab or return key to add it to the list. If the user clicks on something then the value is lost. It confuses users if they fill it in and click the submit button and their text is gone. I've done it a few times myself. I suppose I might be able to capture that in the blur event on the textbox. – Steve Hiner Aug 15 '13 at 21:13
  • I found that while this code worked, it was not without problems. For instance, you can't really edit an existing entry nor type a similar entry because it automatically selects the entry from the list. I ultimately switched to Twitter's typeahead and it has worked great for my purposes. – Steve Hiner Apr 01 '14 at 20:20
  • Could you add a link for future reference to Twitter typeahead? Thanks – leogdion Apr 02 '14 at 13:33
1

An update to leogdion's answer which works with later versions of chosen:

        var dropDown = $('#select_chosen');
        // Make the chosen drop-down dynamic. If a given option is not in the list, the user can still add it
        dropDown.parent().find('.chosen-container .search-field input[type=text]').keydown( 
            function (evt) {
               var stroke, _ref, target, list;
               // get keycode
               stroke = (_ref = evt.which) != null ? _ref : evt.keyCode;
               // If enter or tab key
               if (stroke === 9 || stroke === 13) {
                   target = $(evt.target);               
                   // get the list of current options
                   chosenList = target.parents('.chosen-container').find('.chosen-choices li.search-choice > span').map(function () { return $(this).text(); }).get();
                   // get the list of matches from the existing drop-down
                   matchList = target.parents('.chosen-container').find('.chosen-results li').map(function () { return $(this).text(); }).get();
                   // highlighted option
                   highlightedList = target.parents('.chosen-container').find('.chosen-results li.highlighted').map(function () { return $(this).text(); }).get();
                   // Get the value which the user has typed in
                   var newString = $.trim(target.val());
                   // if the option does not exists, and the text doesn't exactly match an existing option, and there is not an option highlighted in the list
                   if ($.inArray(newString,matchList) < 0 && $.inArray(newString,chosenList) < 0 && highlightedList.length == 0) {
                     // Create a new option and add it to the list (but don't make it selected)
                     var newOption = '<option value="' + newString + '">' + newString + '</option>';
                     $("#select").prepend(newOption);
                     // trigger the update event
                     $("#select").trigger("chosen:updated");
                     // tell chosen to close the list box
                     $("#select").trigger("chosen:close");
                     return true;
                  } 
                  // otherwise, just let the event bubble up
                  return true;
               }
            }
        )
3nochroot
  • 101
  • 1
  • 6
1

I have updated the code from 3nochroot once more. Now I have only one selector to find the multiselect input.

$(document).ready(function() {
$(".js-choicelist").chosen({
    //config comes here
}).parent().find('.chosen-container .search-field input[type=text]').keydown(
    function (evt) {
        var stroke, _ref, target, list;
        // get keycode
        stroke = (_ref = evt.which) != null ? _ref : evt.keyCode;
        // If enter or tab key
        if (stroke === 9 || stroke === 13) {
            target = $(evt.target);
            // get the list of current options
            chosenList = target.parents('.chosen-container').find('.chosen-choices li.search-choice > span').map(function () { return $(this).text(); }).get();
            // get the list of matches from the existing drop-down
            matchList = target.parents('.chosen-container').find('.chosen-results li').map(function () { return $(this).text(); }).get();
            // highlighted option
            highlightedList = target.parents('.chosen-container').find('.chosen-results li.highlighted').map(function () { return $(this).text(); }).get();
            // Get the value which the user has typed in
            var newString = $.trim(target.val());
            // if the option does not exists, and the text doesn't exactly match an existing option, and there is not an option highlighted in the list
            if ($.inArray(newString,matchList) < 0 && $.inArray(newString,chosenList) < 0 && highlightedList.length == 0) {
                // Create a new option and add it to the list (but don't make it selected)
                var newOption = '<option value="' + newString + '">' + newString + '</option>';
                var choiceSelect = target.parents('.select-multiple').find('select');
                choiceSelect.prepend(newOption);
                // trigger the update event
                choiceSelect.trigger("chosen:updated");
                // tell chosen to close the list box
                choiceSelect.trigger("chosen:close");
                return true;
            }
            // otherwise, just let the event bubble up
            return true;
        }
    }
)

})

Rawburner
  • 1,387
  • 11
  • 12
0

Update of leogdion answer for multiple select (gist)

  $(".chosen-select-with-add-new").chosen({
    no_results_text: "Click Enter or Tab to add new option",
    width: '100%'
  }).parent().find('.chosen-container .search-field input[type=text]').keydown(function (evt) {
    // get keycode
    const stroke = evt.which != null ? evt.which : evt.keyCode;
    // If enter or tab key
    if (stroke === 9 || stroke === 13) {
      const target = $(evt.target);
      // get the list of current options
      const chosenList = target.parents('.chosen-container').find('.chosen-choices li.search-choice > span').map(function () { return $(this).text(); }).get();
      // get the list of matches from the existing drop-down
      const matchList = target.parents('.chosen-container').find('.chosen-results li').map(function () { return $(this).text(); }).get();
      // highlighted option
      const highlightedList = target.parents('.chosen-container').find('.chosen-results li.highlighted').map(function () { return $(this).text(); }).get();
      // Get the value which the user has typed in
      const newString = $.trim(target.val());
      // if the option does not exists, and the text doesn't exactly match an existing option, and there is not an option highlighted in the list

      if ($.inArray(newString, matchList) < 0 && $.inArray(newString,chosenList) < 0 && highlightedList.length == 0) {
        // Create a new option and add it to the list (but don't make it selected)
        const newOption = '<option value="' + newString + '" selected="selected">' + newString + '</option>';
        const choiceSelect = target.parents('.chosen-container').siblings('.chosen-select-with-add-new');
        choiceSelect.append(newOption);
        // trigger the update event
        choiceSelect.trigger("chosen:updated");
        // tell chosen to close the list box
        choiceSelect.trigger("chosen:close");
        return true;
      }
      // otherwise, just let the event bubble up
      return true;
    }
  })

Example usage on rails (slim)

  .tag-list
    label.control-label.h5 Tag list
    = select_tag :tag_list, options_for_select(ActsAsTaggableOn::Tag.order('taggings_count desc').pluck(:name), @publication.tags.map(&:name)), multiple: true, data: { placeholder: 'north, east, south, west' }, class: 'chosen-select-with-add-new'
srghma
  • 4,770
  • 2
  • 38
  • 54
0

I tried several solutions given here and elsewhere, however none worked in chosen.js 1.8.5 (jQuery: 3.3.1) and so I ended up with the following since I didn't want to use a fork that might not always be up-to-date to the master branch:

For the case that I might not want any .chosen-select to allow new values, I added a new class .chosen-newValuesAllowed. I set an event handler on this class where CTRL + I adds the new value if it is not yet present. The focus on the input field does not get lost afterwards. In my example, I check the innerHTML of the since @value actually contains database ids and therefore the new value, which is a string in my example that would be processed by the server later, could never be found in @value. If you want to check @value, please see the comment inside the snippet. The code handles single and multiple selects.

$(document).on("keydown", ".chosen-container.chosen-newValuesAllowed input", function(e) { 
if (e.ctrlKey === true && e.keyCode === 73) { // CTRL + I
    e.preventDefault();
    var newValue = $(this).val();
    if (newValue) {            
        try {
            // only add if there is no option having the content/text of "input" yet!
            // instead of filter() for the content of <option> you can check on its @value by: find("option[val='...']") 
            var $selectElement = $(e.target).closest("div.chosen-container").prev(); // the previous sibling should be the <select>. If not, grab it some other way, e.g. via @id
            if (!$selectElement.find("option").filter(function () { return $(this).html() === newValue; }).length) {
                if (!$selectElement.attr("multiple")) { // unselect for single-select 
                    $selectElement.val('');
                }
                $selectElement.append('<option val="' + newValue + '" selected>' + newValue + '</option>');
                $selectElement.trigger('chosen:updated');    
            }
        } catch(error) {
            // pass
        }
        e.target.focus();
    }        
    return false; 
}
});

Another solution would be to call the triggerable function chosen:no_results if new values should only be added if there explicitly is no result:

$(".chosen-select.chosen-newValuesAllowed").on("chosen:no_results", function(e, data){
       var newValue = data.chosen.get_search_text();
       ...
 });
meistermuh
  • 393
  • 3
  • 11