Search code examples
angularjsjquery-select2metro-ui-css

Dependent drop-down using Select2 (with MetroUI CSS) in AngularJS is not working


Select2 with MetroUI CSS works well with AngularJS. But when I try using the same in dependent drop down lists I face the issue that view of child drop-down list doesn't get updated accordingly to change in parent drop-down list.

Here is a Stack Snippet demonstrating the issue I am seeing.

(function () {
    "use strict";
    var app = angular.module("app", []);

    app.controller("AppCtrl", AppCtrl);

    function AppCtrl() {
        var vm = this;

        vm.ContextData = {
            selectParents: [{ "id": 1, "name": "item 1" },
                { "id": 2, "name": "item 2" },
                { "id": 3, "name": "item 3" },
                { "id": 4, "name": "item 4" }],
            selectChildren: [{ "id": 1, "parentId": 1, "name": "1 based on item1" },
                { "id": 2, "parentId": 1, "name": "2 based on item1" },
                { "id": 3, "parentId": 1, "name": "3 based on item1" },
                { "id": 4, "parentId": 1, "name": "4 based on item1" },
                { "id": 5, "parentId": 1, "name": "5 based on item1" },
                { "id": 6, "parentId": 1, "name": "6 based on item1" },
                { "id": 7, "parentId": 1, "name": "7 based on item1" },
                { "id": 8, "parentId": 2, "name": "8 based on item2" },
                { "id": 9, "parentId": 2, "name": "9 based on item2" },
                { "id": 10, "parentId": 2, "name": "10 based on item2" },
                { "id": 11, "parentId": 2, "name": "11 based on item2" },
                { "id": 12, "parentId": 2, "name": "12 based on item2" },
                { "id": 13, "parentId": 3, "name": "13 based on item3" },
                { "id": 14, "parentId": 3, "name": "14 based on item3" },
                { "id": 15, "parentId": 3, "name": "15 based on item3" },
                { "id": 16, "parentId": 3, "name": "16 based on item3" },
                { "id": 17, "parentId": 4, "name": "17 based on item4" },
                { "id": 18, "parentId": 4, "name": "18 based on item4" }]
        };            
    }

})();
<body ng-app="app" ng-controller="AppCtrl as vm">
    <div class="flex-grid">
        <div class="row">
            <div class="cell colspan2 margin10">
                <div class="input-control full-size" data-role="select" data-placeholder="Select a parent" data-allow-clear="true">
                    <select class="full-size" style="display:none" ng-model="vm.selectedParent" ng-options="item.name for item in vm.ContextData.selectParents">
                        <option value=""></option>                        
                    </select>
                </div>
            </div>
            <div class="cell colspan2 margin10">
                <div class="input-control full-size" data-role="select" data-placeholder="Select a child" data-allow-clear="true">
                    <select class="full-size" style="display:none" ng-model="vm.selectedChild" ng-options="item.name for item in vm.ContextData.selectChildren | filter:{parentId:vm.selectedParent.id}">
                        <option value=""></option>
                    </select>
                </div>
            </div>
        </div>
    </div>
    <div class="flex-grid margin10">
        <div class="row">
            <div class="cell colspan2">
                Selected Parent : 
            </div>
            <div class="cell colspan3">
                {{vm.selectedParent}}
            </div>
        </div>
        <div class="row">
            <div class="cell colspan2">
                selected Child : 
            </div>
            <div class="cell colspan3">
                {{vm.selectedChild}}
            </div>
        </div>
    </div>    
</body>

<link href="https://cdn.rawgit.com/olton/Metro-UI-CSS/master/build/css/metro-responsive.min.css" rel="stylesheet"/>
<link href="https://cdn.rawgit.com/olton/Metro-UI-CSS/master/build/css/metro.min.css" rel="stylesheet"/>

<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.1/js/select2.min.js"></script>
<script src="https://cdn.rawgit.com/olton/Metro-UI-CSS/master/build/js/metro.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.min.js"></script>

Steps to reproduce:

  1. Select 'Item 1' in parent select (parent select's place-holder is 'select a parent')
  2. In "Selected parent" section correct value of selected parent is shown.
  3. Child select is bound with correct child option.
  4. Select any option from child select.
  5. In "Selected child" section correct value of selected child is shown.
  6. In Parent select, select 'Item 4'.
  7. In "Selected parent" section correct value of selected parent is shown.
  8. Child select is bound but the options are not updated and it still shows the options for parent Item1.
  9. Select any item in child select, In "Selected Child" section correct value of child is shown, but child drop down list shows wrong Item!

Hence my issue is that even though child select gets the correct options bound to it, it's option list view doesn't refreshes.

[I tried select2.val(") but still the option view of child dropdown did not refresh.]

Another issue is this: if I clear the selection in Parent select, it doesn't clear out the selection in Child select.


Solution

  • See this codepen.io to get the dependent drop-down list working.

    Key points of solution are:

    1. Give an ID to child drop-down
    2. Then provide an ng-change event to parent drop-down
    3. In ng-change event of parent drop-down, search for the child drop-down via it's ID
    4. Then call select2 functions on child drop-down to empty the selected value and html
    5. Do not forget to trigger change event on child drop-down.

    so it would be something like this in the ng-change event (childSelect2 is ID of child drop-down):

    var ele = angular.element('#childSelect2');            
    ele.html("").val("").trigger("change");
    

    Also note that on clicking the cross mark in parent drop-down, we are clearing the child drop-down as for any change we are calling ele.html("").val("").trigger("change");

    and this is expected because if there is no selection in parent then it has no meaning to have selection in child. but with this we also need to clear out the ng-model of child drop-down so for that in ng-change function set the model of child drop-down to null (vm.selectedChild is ng-model for child drop-down)

    vm.selectedChild = null;
    

    though to have this any effect I found out that this setting of child model to null should be before calling ele.html("").val("").trigger("change"); hence I have made it the first statement in ng-change event. [I do not know why this placement-order is important, but it worked only after this change :)]

    Hope the answer helps other and save their time.

    quick reference: HTML Code:

    <body ng-app="app" ng-controller="AppCtrl as vm">
        <div class="flex-grid">
            <div class="row">
                <div class="cell colspan2 margin10">
                    <div class="input-control full-size" data-role="select" data-placeholder="Select a parent" data-allow-clear="true">
                        <select class="full-size" style="display:none" ng-model="vm.selectedParent" ng-options="item.name for item in vm.ContextData.selectParents" ng-change="vm.SetSelect2()">
                            <option value=""></option>                        
                        </select>
                    </div>
                </div>
                <div class="cell colspan2 margin10">
                    <div class="input-control full-size" data-role="select" data-placeholder="Select a child" data-allow-clear="true">
                        <select class="full-size" style="display:none" ng-model="vm.selectedChild" ng-options="item.name for item in vm.ContextData.selectChildren | filter:{parentId:vm.selectedParent.id}" id="childSelect2">
                            <option value=""></option>
                        </select>
                    </div>
                </div>
            </div>
        </div>
        <div class="flex-grid margin10">
            <div class="row">
                <div class="cell colspan2">
                    Selected Parent : 
                </div>
                <div class="cell colspan3">
                    {{vm.selectedParent}}
                </div>
            </div>
            <div class="row">
                <div class="cell colspan2">
                    selected Child : 
                </div>
                <div class="cell colspan3">
                    {{vm.selectedChild}}
                </div>
            </div>
        </div>    
    </body>
    

    JS Code (angular Module and controller):

    (function () {
        "use strict";
        var app = angular.module("app", []);
    
        app.controller("AppCtrl", AppCtrl);
    
        function AppCtrl() {
            var vm = this;
    
            vm.ContextData = {
                selectParents: [{ "id": 1, "name": "item 1" },
                    { "id": 2, "name": "item 2" },
                    { "id": 3, "name": "item 3" },
                    { "id": 4, "name": "item 4" }],
                selectChildren: [{ "id": 1, "parentId": 1, "name": "1 based on item1" },
                    { "id": 2, "parentId": 1, "name": "2 based on item1" },
                    { "id": 3, "parentId": 1, "name": "3 based on item1" },
                    { "id": 4, "parentId": 1, "name": "4 based on item1" },
                    { "id": 5, "parentId": 1, "name": "5 based on item1" },
                    { "id": 6, "parentId": 1, "name": "6 based on item1" },
                    { "id": 7, "parentId": 1, "name": "7 based on item1" },
                    { "id": 8, "parentId": 2, "name": "8 based on item2" },
                    { "id": 9, "parentId": 2, "name": "9 based on item2" },
                    { "id": 10, "parentId": 2, "name": "10 based on item2" },
                    { "id": 11, "parentId": 2, "name": "11 based on item2" },
                    { "id": 12, "parentId": 2, "name": "12 based on item2" },
                    { "id": 13, "parentId": 3, "name": "13 based on item3" },
                    { "id": 14, "parentId": 3, "name": "14 based on item3" },
                    { "id": 15, "parentId": 3, "name": "15 based on item3" },
                    { "id": 16, "parentId": 3, "name": "16 based on item3" },
                    { "id": 17, "parentId": 4, "name": "17 based on item4" },
                    { "id": 18, "parentId": 4, "name": "18 based on item4" }]
            };
    
            vm.SetSelect2 = function () {
                ////debugger;
                vm.selectedChild = null;
                var ele = angular.element('#childSelect2');
                //ele.html("");
                //ele.val("").trigger("change");  
    
                ele.html("").val("").trigger("change");
            }
        }
    
    })();