Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cascading parent child select boxes from dynamic JSON data

I've created some chained select-boxes dynamically from JSON data which I will receive from server. The chaining/cascading works in a way that every select-box is a named object with following properties:

  1. Parent Attribute: Name of object which is the parent of this select-box object.
  2. Options: Array of option objects, where each object contains: (a) Option Value (b) Parent Option Value - The parent select-box value with which the current value is mapped. (c) Option ID.

  3. Selected Option: An object with two properties: (a) Currently selected value (b) ID of currently selected value.

I am creating select-boxes using ng-repeat in "option" tag, or using ng-option in "select" tag, and then I am using a custom filter where I filter the retrieved options (2) by matching the parent option value (2 > b) of the option values (2 > a) with the "currently selected value" (3 > a) of its parent object. Basically doing many-to-one mapping from child option values to selected parent value using a custom filter.

enter image description here

I am able to correctly map the parent-child select-boxes, but the issue is when I change the parent select-box value, the "selected option value" of its child object doesn't update (the child selected item doesn't grab the first item in the filtered list, causing the grandchild drop-down to not update.)[1]

Is there any way that if parent value changes, the child select-box (and the subsequent children/grandchildren) is initialized with the first option value instead of the current blank value?

Here is the working plunker. (ng-repeat implementation). Will really appreciate any help.

Here is the another plunker with ng-options implementation.

HTML (ng-repeat):

<div ng-repeat="selection in vm.selectionData">
    <div ng-repeat="(key, attribute) in selection.attributes">
      <span>{{key}}</span>
      <select class="form-control" ng-model="attribute.selectedOption.name">
        <option ng-repeat="option in attribute.options | optionFilter : selection.attributes[attribute.parentAttr]">{{option.name}}</option>
      </select>
    </div>
</div>

HTML (ng-options):

<div ng-repeat="selection in vm.selectionData">
    <div ng-repeat="(key, attribute) in selection.attributes">
      <span>{{key}}</span>
      <select ng-model="attribute.selectedOption" ng-options="attribute.name for attribute in (attribute.options | optionFilter : selection.attributes[attribute.parentAttr]) track by attribute.id">
      </select>
    </div>        
</div>

JS:

myApp.filter('optionFilter', function() {
  return function(items, parent) {
    var result = [];
    if (parent) {
      for (var i = 0; i < items.length; i++) {
        console.log(items[0].parent, parent.selectedOption.name);
        if (items[i].parent === parent.selectedOption.name) {
          result.push(items[i]);
        }
      }
      return result;
    } else {
      return items;
    }
  }
});

myApp.controller("MyCtrl", function($scope) {

  this.selectionData = [{
    selectionType: "Geography",
    attributes: {
      country: {
        parentAttr: "none",
        options: [{
          name: "India",
          parent: "None",
          id: 1
        }, {
          name: "Bangladesh",
          parent: "None",
          id: 2
        }, {
          name: "Afganistan",
          parent: "None",
          id: 3
        }],
        selectedOption: {
          name: "India",
          id: 1
        }
      },
      state: {
        parentAttr: "country",
        options: [{
          name: "Rajasthan",
          parent: "India",
          id: 1
        }, {
          name: "Haryana",
          parent: "India",
          id: 2
        }, {
          name: "Dhaka",
          parent: "Bangladesh",
          id: 3
        }, {
          name: "Kabul",
          parent: "Afganistan",
          id: 4
        }],
        selectedOption: {
          name: "Rajasthan",
          id: 1
        }
      },
      city: {
        parentAttr: "state",
        options: [{
          name: "Kota",
          parent: "Rajasthan",
          id: 1
        }, {
          name: "Sirsa",
          parent: "Haryana",
          id: 2
        }, {
          name: "Alwar",
          parent: "Rajasthan",
          id: 3
        }, {
          name: "Gurgaon",
          parent: "Haryana",
          id: 4
        }, {
          name: "Kabul",
          parent: "Kabul",
          id: 5
        },{
          name: "Dhaka",
          parent: "Dhaka",
          id: 6
        }
        ],
        selectedOption: {
          name: "Kota",
          id: 1
        }
      }
    },
  }];

});

References:

  • Cascading select/dropdowns
like image 479
Rishabh Avatar asked Oct 30 '22 22:10

Rishabh


1 Answers

After struggling for some time, I came up with a solution (though not sure if it conforms to best practices). In my custom filter which returns the filtered options based on parent object's selected option value, I am additionally sending current select box object. Then,

  1. The filter first checks whether the parent object exists or not, else it returns all options.
  2. If parent exists, then it loops through all available options of current select-box object.
  3. If current select-box options' parent value matches the parent object's selected option value, then it pushes the filtered option values in result array, only if parent object's selected option is not null (parent object's selected option can go null when grandparent value is changed and parent does not grab the resulted filtered options, leading to blank first option in parent select-box). This will temporarily cause the current-select box to be completely empty, as the parent object's selected option is null.
  4. Then, it checks whether the current select-box object's selected option is null. If so, it assigns the first element (object) of result array to the selected option object, and returns the result array. Since the filter will run for all select-boxes (grandparent, parent and child), it will set the first element in parent's filtered array as the parent object's selected option. Now when the parent object's selected option is no longer null, the current select-box will show filtered options (from result array) and first element of result array will be assigned to the selected option of current select-box.

Working demo here. Please share if there is a better solution. Below is the custom filter:

myApp.filter('optionFilter', function() {
  return function(items, parent, self) {
    var result = [];
    if (parent) {
      for (var i = 0; i < items.length; i++) {
        if (parent.selectedOption !== null && items[i].parentValue === parent.selectedOption.value) {
          result.push(items[i]);
        }
      }
      if (self.selectedOption === null) {
        self.selectedOption = result[0];
      }
      return result;
    } else {
      return items;
    }
  }
});

HTML:

<div ng-repeat="(key, item) in data">
    <span>{{key}}</span>
    <select ng-model="item.selectedOption" ng-options="option.value for option in (item.availableOptions | optionFilter : data[item.parent] : item) track by option.id">
    </select>
</div>

Data:

this.data = {
  Country: {
    parent: "None",
    availableOptions: [{
      value: "United States",
      parentValue: "None",
      id: 1
    }, {
      value: "China",
      parentValue: "None",
      id: 2
    }, {
      value: "India",
      parentValue: "None",
      id: 3
    }],
    selectedOption: {
      value: "United States",
      parentValue: "None",
      id: 1
    }
  },
  State: {
    parent: "Country",
    availableOptions: [{
      value: "California",
      parentValue: "United States",
      id: 1
    }, {
      value: "Shanghai",
      parentValue: "China",
      id: 2
    }, {
      value: "Delhi",
      parentValue: "India",
      id: 3
    }],
    selectedOption: {
      value: "California",
      parentValue: "United States",
      id: 1
    }
  },
  City: {
    parent: "State",
    availableOptions: [{
      value: "Greenfield",
      parentValue: "California",
      id: 1
    }, {
      value: "Shanghai",
      parentValue: "Shanghai",
      id: 2
    }, {
      value: "New Delhi",
      parentValue: "Delhi",
      id: 3
    }],
    selectedOption: {
      value: "Greenfield",
      parentValue: "California",
      id: 1
    }
  }
};
like image 121
Rishabh Avatar answered Nov 12 '22 12:11

Rishabh