2
\$\begingroup\$

My server performs a query to get all country data with Eloquent relationships:

$country = Countries::with([
'description',
'provinces.Districts',
'provinces.Districts.communes',
'provinces.Districts.communes.villages'])
->where('status', 1)->get()->toArray();

Using the JavaScript below, I'm filtering the villages based on the locations already selected.

$(document).on('change', '#country_of_birth,#province_of_birth,#district_of_birth,#district_of_birth,#commune_of_birth', function (e) {
    "use strict";
    var data = {};
    var provinces = $('#province_of_birth'),
        district = $('#district_of_birth'),
        commune = $('#commune_of_birth'),
        village = $('#village_of_birth');
    var pro = '<option value="">-</option>',distrs = '<option value="">-</option>',comms = '<option value="">-</option>', vills = '<option value="">-</option>';

    var countryId = parseInt($("select[name='country_of_birth']").select2("data").element[0].dataset['id']);
    if ($(this).is('#country_of_birth')) {

        provinces.select2('val', '');
        district.select2('val', '');
        commune.select2('val', '');
        village.select2('val', '');

        $.each(country, function (inx, vals) {

            if (parseInt(vals.id) === countryId) {

                $.each(vals.provinces, function (inx, prov) {

                    data = prov.districts;
                    pro += '<option value="' + prov.prov_gis + '" data-id="' + prov.id + '"> ' + prov.eng_name + ' </option>';
                });
            }
        });

        provinces.empty().append(pro);

    }
    if ($(this).is('#province_of_birth')) {

        var provinceId = parseInt($("select[name='province_of_birth']").select2("data").element[0].dataset['id']);
        district.select2('val', '');
        commune.select2('val', '');
        village.select2('val', '');

        $.each(country, function (inx, countr) {
            if (parseInt(countr.id) !== countryId)return;
            $.each(countr.provinces, function (inx, prov) {
                $.each(prov.districts, function (inx, distr) {
                    if (parseInt(distr.prov_id) !== provinceId)return;
                    distrs += '<option value="' + distr.distr_gis + '" data-id="' + distr.id + '"> ' + distr.eng_name + ' </option>';
                })
            });
        });
        district.empty().append(distrs);
    }
    if ($(this).is('#district_of_birth')) {

        var districId = parseInt($("select[name='district_of_birth']").select2("data").element[0].dataset['id']);
        village.select2('val', '');

        $.each(country, function (inx, countr) {
            if (parseInt(countr.id) !== countryId) return;
            $.each(countr.provinces, function (inx, prov) {

                $.each(prov.districts, function (inx, distr) {
                    $.each(distr.communes, function (inx, comm) {

                        if (parseInt(comm.distr_id) !== districId)return;
                        comms += '<option value="' + comm.comm_gis + '" data-id="' + comm.id + '"> ' + comm.en_name +' </option>';
                    });
                })
            });
        });
        commune.empty().append(comms);
    }
    if ($(this).is('#commune_of_birth')) {

        var commId = parseInt($("select[name='commune_of_birth']").select2("data").element[0].dataset['id']);

        $.each(country, function (inx, countr) {
            if (parseInt(countr.id) !== countryId)return;
            $.each(countr.provinces, function (inx, prov) {

                $.each(prov.districts, function (inx, distr) {
                    $.each(distr.communes, function (inx, comm) {
                        $.each(comm.villages, function (inx, vill) {

                            if (parseInt(vill.comm_id) !== commId)return;

                            vills += '<option value="' + vill.vill_gis + '"> ' + vill.en_name + ' </option>';
                        });
                    });
                })
            });
        });
        village.empty().append(vills);
    }
});

It works well, but I am concerned about its performance. I want to revise it or use a Javascript builtin method instead of looping through the data repeatedly.

\$\endgroup\$
2
  • \$\begingroup\$ You could benefit heavily by using some data-binding framework such as Knockout JS or Angular JS. I'm not sure about performance improvements, but the code will be much simpler and easier to maintain. \$\endgroup\$ Commented Nov 11, 2016 at 16:48
  • \$\begingroup\$ I'm using Jquery. \$\endgroup\$ Commented Nov 11, 2016 at 16:56

1 Answer 1

4
\$\begingroup\$

There are some major concerns with this approach.

First, why rebuild all the DOM elements every time there is a change? Your universe of different countries, provinces, districts, etc. is static right? You could either render all DOM elements needed for the page up front (probably the simplest approach), or if you find this is too much bandwidth to download on initial page load, lazily load different options on the tree as the user's selections warrant. But in any case, once the element is added to the DOM, there really is no reason to ever overwrite the DOM element. You should then just be working with DOM manipulation (hiding and showing to filter options, setting/unsetting which options are selected at each level, etc.).

Second, you have added a lot of complexity by having the entire selection tree handled in a single event handler. What you really have here is a recursion problem. The central action that is taking place when a selection is made at any level in the tree, is that all descendant nodes in the tree need to recalculate. Let's think about how we might be able to solve this problem using recursion.

Finally, you really should take advantage of caching your jQuery collections, so you don't have to traverse the DOM using selectors every time the change event handler is fired.

I will show an example of how this might all fit together. Note that this example assumes all DOM elements are loaded up front (and probably all but root elements are hidden). Here I am keeping it simple and just using order of <select> elements on the page to determine tree hierarchy. You could decouple the parent-child behavior from HTML structure using data attributes, but I won't do that here for simplicity sake. Instead, I will only focus on using data attributes to relate options to their parent option within "parent" select.

Let's assume simple HTML structure like this:

<form id="selection_tree_container">
    <select id="country" name="country" class="tree_select">
        <option class="tree_option_default" selected="selected" value="">Please Select Country</option>
        <option class="tree_option" value="United States">United States</option>
        <option class="tree_option" value="Canada">Canada</option>
        ...
    </select>
    <select id="province" name="province" class="tree_select">
        <option class="tree_option" selected="selected" value="">Please Select Province</option>
        <option class="tree_option" value="Alabama" data-parent="United States"
>Alabama</option>
        <option class="tree_option" value="Alaska" data-parent="United States">Alaska</option>
        ...
        <option class="tree_option" value="Alberta" data-parent="Canada"
>Alberta</option>
        <option class="tree_option" value="British Columbia" data-parent="Canada">British Columbia</option>
        ...
    </select>
    ...
</form> 

Then we want to define a "class" in javascript for this selection tree. Here we assume this class can use jQuery as a dependency and that we will focus on caching the jQuery collections on the concrete object produced from the class prototype. This class could (and probably should) live in its own file that could be included on pages that need this functionality. To continue to to think about re-use as we design this class, we also give some configuration options (along with defaults) that let user specify different id's and class names that the class can use to set itself up.

// class definition of instance properties and constructor
function selectionTree(config) {
    this.config = {}
    this.tree = {};

    // object constructor
    function init(config) {
        // extend default config with passed config
        var config = $.extend(selectionTree.defaultConfig, config);
        this.config = config;

        // let's traverse DOM to set up this object up front
        // again you could store some of these jQuery collections
        // on public object properties if you found a use case to do so
        var $container = this.$container = $('#' + config.containerId);
        var $selects = this.$container.find('.' + config.selectClass);

        // iterate the selects
        var tree = {};

        $selects.each( (idx, el) => {
            // set key on collections map
            var id = el.id;
            tree[id] = {};

            // determine child select for this select
            var childIdx = idx + 1;
            if (childIdx < $selects.length) {
                // this is not bottom of tree
                // cache jQuery selector result for child select element
                // we now have parent-child relationship established
                tree[id].$childSelect= $(
                    '#' + $selects.get(childIdx).attr('id')
                );
            } else {
                // we are at last select in hierarchy
                tree[id].$childSelect = null;
            }

            // set options for this select id
            // cache jQuery collection for options related to this select
            var $options = $(el).find('.' + config.optionClass);
            tree[id].$options = $options;

            var optionsByParentValue = {}

            $options.each( (optIdx, optEl) => {
                var parentValue = $(optEl).data('parent');
                if (
                    parentValue !== undefined &&
                    parentValue in optionsByParentValue === false
                ) {
                    optionsByParentValue[parentValue] = $options.filter(
                        '[data-parent="' + parentValue + '"]'
                    );  
                };
            });
            tree[id].optionsByParentValue = optionsByParentValue;
        });

        // set tree info on object
        this.tree = tree;
    }

    init(config);
}

// static properties on class    
selectionTree.defaultConfig = {
    containerId: 'selection_tree_container',
    selectClass: 'tree_select',
    optionClass: 'tree_option'
}

// prototype class method
selectionTree.prototype.applyChange = function(el) {
    // not shown - perhaps validate here that select element is passed

    var selectId = el.id;
    var currentNode = this.tree[selectId];
    var value = $(el).val();

    // now let's operate against child select
    // we always need to reset the child select (if present) to default value
    // and then trigger change event on this child to recursively propagate
    // this change down the tree.
    var $child = currentNode.$childSelect;

    // do nothing if no child select
    if($child === null) return;

    $child.val('').trigger('change');

    if(value === '') {
        // value was set back to default ("Select ...") state
        // we should hide the child
        $child.hide();
    } else {
        // we need to filter the child based on this selection
        var childId = $child.attr('id');
        var childNode = this.tree[childId];
        // hide all child node options
        childNode.$options.hide();

        // show child options filtered on current value of parent select
        childNode.optionsByParentValue[value].show();
    }
}

This code above is actually not too far off from how one might write a jQuery plug-in, which might be a nice avenue to pursue.

Now to the code on the page. You would need to implement something like the following:

$(document).ready(function() {
    // instantiate tree object
    // in this case using default config
    var selectionTree = new selectionTree();

    // attach change event handler
    $('#' + selectionTree.config.containerId).on(
        'change',
        '.' + selectionTree.config.selectClass,
        function() {
            selectionTree.applyChange(this);
        }
    );
}
\$\endgroup\$
1
  • \$\begingroup\$ I've testing it get some errors i will post when I ready \$\endgroup\$ Commented Nov 22, 2016 at 11:02

Not the answer you're looking for? Browse other questions tagged or ask your own question.