Skip to content

Instantly share code, notes, and snippets.

@ajaxray
Last active June 1, 2025 08:01
Show Gist options
  • 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 <[email protected]>
*
* 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 <[email protected]>
 *
 * 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 !

@maryjos
Copy link

maryjos commented Sep 13, 2024

Just wanted to drop of note of thanks for sharing this code. I had a fairly complex 4-selectbox cascade with different options needed for the first one (that does an Ajax lookup of its own based on the search) vs. the other 3 that populate down from that selection and was pulling my hair out trying to get it working right with Select2, this was exactly the solution I needed!

@krnsptr
Copy link

krnsptr commented Jun 1, 2025

⚠️ Security Issue: Potential XSS Vulnerability in Option Rendering

Hi Anis, thanks for sharing this useful snippet — it’s been a big help in implementing dependent Select2 dropdowns.

I noticed a potential XSS vulnerability in the following part of the code:

option += ">" + item["text"] + "</option>";

If item["text"] comes from untrusted or user-generated data, malicious users can inject <script> tags, leading to a cross-site scripting (XSS) attack.

🔒 Fix suggestion: Use a text-escaping function like:

function escapeHtml(text) {
  return $('<div>').text(text).html();
}

option += ">" + escapeHtml(item["text"]) + "</option>";

Or better yet, use jQuery’s DOM-based element creation:

var $option = $('<option>', {
  value: item.id,
  text: item.text
});

Then append it safely instead of using .html() with raw strings.

I hope this helps improve the snippet and make it safer for broader use.

Thanks again for the great work!

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