Skip to content

Instantly share code, notes, and snippets.

@ajaxray
Last active November 23, 2022 07:27
Show Gist options
  • Star 65 You must be signed in to star a gist
  • Fork 16 You must be signed in to fork a gist
  • Save ajaxray/187e7c9a00666a7ffff52a8a69b8bf31 to your computer and use it in GitHub Desktop.
Save ajaxray/187e7c9a00666a7ffff52a8a69b8bf31 to your computer and use it in GitHub Desktop.
Making Select2 (4.x) list boxes cascading / dependent. Options of a select2 list box will be loaded/refreshed by ajax based on selection of another select2 list box.
/**
* A Javascript module to loadeding/refreshing options of a select2 list box using ajax based on selection of another select2 list box.
*
* @url : https://gist.github.com/ajaxray/187e7c9a00666a7ffff52a8a69b8bf31
* @auther : Anis Uddin Ahmad <anis.programmer@gmail.com>
*
* Live demo - https://codepen.io/ajaxray/full/oBPbQe/
* w: http://ajaxray.com | t: @ajaxray
*/
var Select2Cascade = ( function(window, $) {
function Select2Cascade(parent, child, url, select2Options) {
var afterActions = [];
var options = select2Options || {};
// Register functions to be called after cascading data loading done
this.then = function(callback) {
afterActions.push(callback);
return this;
};
parent.select2(select2Options).on("change", function (e) {
child.prop("disabled", true);
var _this = this;
$.getJSON(url.replace(':parentId:', $(this).val()), function(items) {
var newOptions = '<option value="">-- Select --</option>';
for(var id in items) {
newOptions += '<option value="'+ id +'">'+ items[id] +'</option>';
}
child.select2('destroy').html(newOptions).prop("disabled", false)
.select2(options);
afterActions.forEach(function (callback) {
callback(parent, child, items);
});
});
});
}
return Select2Cascade;
})( window, $);

Select2 Cascade (for v4.x)

Loadeding/refreshing options of a select2 list box (using ajax) based on selection of another select2 list box.

Check the live demo.

How to use

Create a new instance of Select2Cascade by passing the following 4 things -

  • Parent select2 listbox
  • Child select2 listbox
  • URL to load child items from
  • (OPTIONAL) select2 options

To set the parent selected value in ajax request, keep :parentId: as a placeholder in url. The selected value of parent select2 listbox will replace the :parentId: string in url. For example -
Query string: /path/to/api?type=childType&parent_id=:parentId:
RESTful url: /path/to/api/items/:parentId:/sub-items

The response json should be a flat object of value:label pairs. e,g,

{
  "20150415" : "Chittagong Zila",
  "20190901" : "Comilla Zila",
  "20221601" : "Cox's Bazar Zila",
  "20301401" : "Feni Zila"
}

Otherwisw you have to adjust the way of populating child options (at line 29).

Examples

When #parentList is changed, call to path/to/geocode/district/SELECTED-PARENT/zilla and set the options of #childList from the ajax response.

var options = { width: 'resolve' };
new Select2Cascade($('#district'), $('#zilla'), 'path/to/geocode/district/:parentId:/zilla', options);
new Select2Cascade($('#zilla'), $('#thana'), 'path/to/geocode/zilla/:parentId:/thana', options);

If you want to do something with the response data or selectboxes, you can set (any number of) callbacks to be executed after the child listbox refreshed -

var cascadLoading = new Select2Cascade($('#parent'), $('#child'), 'path/to/api/categories?parent_id=:parentId:');
cascadLoading.then( function(parent, child, items) {
    // Open the child listbox immediately
    child.select2('open');
    // Dump response data
    console.log(items);
})
@m47730
Copy link

m47730 commented Feb 13, 2017

/**
 * A Javascript module to loadeding/refreshing options of a select2 list box using ajax based on selection of another select2 list box.
 *
 * @url : https://gist.github.com/ajaxray/187e7c9a00666a7ffff52a8a69b8bf31
 * @auther : Anis Uddin Ahmad <anis.programmer@gmail.com>
 *
 * Live demo - https://codepen.io/ajaxray/full/oBPbQe/
 * w: http://ajaxray.com | t: @ajaxray
 */
var Select2Cascade = ( function(window, $) {

    function Select2Cascade(parent, child, url, options) {
        var afterActions = [];

        // Register functions to be called after cascading data loading done
        this.then = function(callback) {
            afterActions.push(callback);
            return this;
        };

        parent.select2(options).on("change", function (e) {

            child.prop("disabled", true);
            var _this = this;

            $.getJSON(url.replace(':parentId:', $(this).val()), function(items) {
                var newOptions = '<option value=""></option>';
                for(var id in items) {
                    newOptions += '<option value="'+ id +'">'+ items[id] +'</option>';
                }

                child.select2('destroy').html(newOptions).prop("disabled", false)
                    .select2(options);

                afterActions.forEach(function (callback) {
                    callback(parent, child, items, options);
                });
            });
        });
    }

    return Select2Cascade;

})( window, $);

a simple modification to support select2 options (as theme)

@ajaxray
Copy link
Author

ajaxray commented Feb 21, 2017

@m47730, Very good idea! we can keep the options open for the user.
I am going to add this. Thank you very much 👍

@neumachen
Copy link

@ajaxray how would you set a default value for the child when the value for parent is already selected on load?

@ajaxray
Copy link
Author

ajaxray commented Mar 7, 2017

@magicalbanana I guess, as we are re-generating child values from html <option>s, if you keep the attribute selected with an option, select2 will remain it selected by default.

@nicpot
Copy link

nicpot commented Mar 26, 2017

@ajaxray Thanks for the nice code! I'm using 3 cascaded select2 dropdowns in a bootstrap modal - so I'm re-initializing and destroying the select2 dropdowns in the bootstrap show.bs.modal and hide.bs.modal. Here's what I have:

$("#addressModal").on("hide.bs.modal", function (e) {
    $("#addressProvinceIdId").select2("destroy");
    $("#addressDistrictId").select2("destroy");
    $("#addressMunicipalityId").select2("destroy");
   
    // some other code...
});

$("#addressModal").on("show.bs.modal", function (e) {
    var select2Options = {
        placeholder: "- Select -",
        theme: "bootstrap",
        triggerChange: true,
        allowClear: false,
        dropdownAutoWidth: true,
        minimumResultsForSearch: 50
    };

    $("#addressProvinceId").select2(select2Options);
    $("#addressDistrictId").select2(select2Options);
    $("#addressMunicipalityId").select2(select2Options);

    var provinceCascade = Select2Cascade($("#addressProvinceId"), $("#addressDistrictId"), "/someurl/getdistricts?provinceId=:parentId:", select2Options);
    var districtCascade = Select2Cascade($("#addressDistrictId"), $("#addressMunicipalityId"), "/someurl/getmunicipalities?districtId=:parentId:", select2Options);

    // some other code...
});

I noticed that the Select2Cascade ajax requests would increase as the modal is opened/closed on the same page. So I guess I need to re-init and destroy the Select2Cascade functions as well. What would be the correct way to do this?

@ajaxray
Copy link
Author

ajaxray commented Apr 1, 2017

Hello @nicpot,
As we are binding ONLY Select2's change event for handling the association, it it will automatically unbind it on destroy() call.

@pjam
Copy link

pjam commented Jun 26, 2017

Hi @ajaxray

The placeholder option should be optional or its text should be customizable. Thinking about other languages.

Great piece of code, by the way.

@jazzsnap
Copy link

select2cascade code is interesting for me,
but when i see the live demo, i think the json is not working.

@ajaxray
Copy link
Author

ajaxray commented Sep 28, 2017

@pjam Thanks :)

@ajaxray
Copy link
Author

ajaxray commented Sep 28, 2017

@jazzsnap Thanks. Right you are, the blob was no more there. FIxing ASAP.
Thanks again for informing 👍

@L5eoneill
Copy link

Thanks for the script! A couple of things I tweaked:

  • A new callback for data processing
  • The option builder operates on the same data structure that select2 .ajax expects:
    [{"id": val, "text": textval}, {"id": val, "text": textval},...]
    Because it seemed counterproductive to NOT use the same data structure. The ajax endpoint should not need to be altered to send data to a cascading select2 rather than a regular ajax select2.
function Select2Cascade(parent, child, url, select2Options) {
  var afterActions = [];
  var handleData = [];
  var options = select2Options || {};

  // Register functions to be called after cascading data loading done
  this.then = function(callback) {
    afterActions.push(callback);
    return this;
  };
  // register functions to be called for data translation <-----------------------------
  this.formatData = function(callback) {
    handleData.push(callback);
  }

  parent.select2(select2Options).on("change", function (e) {
    child.prop("disabled", true);
    var _this = this;

    $.getJSON(url.replace(':parentId:', $(this).val()), function(response) {
      var newOptions = '<option value="">-- Select --</option>';
      var newData = response; // <-----------------------------------------
      handleData.forEach(function(callback) {
        newData = callback(child, response);
      });
      newData.forEach(function(item) {
        newOptions += '<option value="'+ item.id +'">'+ item.text +'</option>';
      });
      // end of changes <--------------------------------------------------
      child.select2('destroy').html(newOptions).prop("disabled", false)
           .select2(options);

      afterActions.forEach(function (callback) {
        callback(parent, child, response);
      });
    });
  });
}

For my data, I instantiated the cascade with:

  $('#parent, #child').select2();
  var url = 'blah?lookup=:parentId:';
  var cascader = new Select2Cascade($('#parent'), $('#child'), url, {});
  cascader.then(function(parent, child, items) {
    child.select2('open');
  });
  cascader.formatData(function(child, items) {
    var data = $.map(items, function (item) {
      item.id = item.myId;
      item.text = '(' + item.myId + ') ' + item.description;
      return item;
    });
    return data;
  });
});

@ajaxray
Copy link
Author

ajaxray commented Dec 9, 2017

Hello @L5eoneill ,
Definitely very good addition! 👍
I'll soon update the script and will add the callback for data processing functionality.

About using {"id": val, "text": textval} as default format, I'm not yet sure. The reason behind using {id: "textVal"} was, it's generic output of most of the list APIs. So I didn't want people to make another, custom API only for feeding Select2 list boxes.

@L5eoneill
Copy link

L5eoneill commented Dec 13, 2017

Has anybody had problems making this work in IE11/Edge? The browser quits as soon as it hits the $.getJSON line, with no apparent reason. Stepping in, I find it actually quitting inside Select2's (version 4.0, line 637) listeners[i].apply(this, params);

Our other usages of Select2 do not have any problem, in IE or anywhere.

The only way I can make cascading work in IE is to make the parent NOT be a select2.

UPDATE: my colleague discovered that if you remove child.prop("disabled", true);, then IE/Edge is fine.

@scarroll32
Copy link

Is failing in the pen.

screen shot 2018-09-16 at 15 54 08

@ajaxray
Copy link
Author

ajaxray commented Apr 16, 2019

Hello @seanfcarroll,
Thanks for notifying! I've fixed it.
Actually, I was using https://jsonblob.com/ content as sample API response, which has been expired.
So, now I have made a gist with 2 sample json file and using their raw version as API response.

@scarroll32
Copy link

Thanks @ajaxray !

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