13

Here is the problem,

I actually have to manage objects that can contain other objects defined in db. So, for example, I have 5 kind of boxes. A red box, a green box, a blue box, a yellow box and a black box.

Each box can contain one box, that can also contain a box, and so on.

What I receive is this kind of object :

{
    "id":1,
    "type":"black",
    "box":
    {
        "id":8,
        "type":"red",
        "box":
        {
            "id":15,
            "type":"green",
            "box":null
        }
    }
}

So this example is : a black box, containing a red box, containing an empty green box. (black -> red -> green -> empty)

There are conditions :

  • The black box can only contain blue, green and red,
  • The red box can only contain green and yellow,
  • The yellow box can contain nothing,
  • The other boxes (green & blue) can contain anything

What I need to do is some kind of "box set editor", I receive a box object, that is complex or not (meaning that it can have only one box level, or several). I have to represent it in a list of select boxes, so, for the example I wrote it will show this :

<select name="LEVEL_1">
        <option value="0">NONE</option>
        <option selected value="1">black</option>
        <option value="8">red</option>
        <option value="15">green</option>
        <option value="3">blue</option>
        <option value="10">yellow</option>
    </select>
<br/>
    <select name="LEVEL_2">
        <option value="0">NONE</option>
        <option selected value="8">red</option>
        <option value="15">green</option>
        <option value="3">blue</option>
    </select>
<br/>
    <select name="LEVEL_3">
        <option value="0">NONE</option>
        <option selected value="15">green</option>
        <option value="10">yellow</option>
    </select>
<br/>
    <select name="LEVEL_4">
        <option selected value="0">NONE</option>
        <option value="15">green</option>
        <option value="8">red</option>
        <option value="3">blue</option>
        <option value="10">yellow</option>
        <option value="1">black</option>
    </select>

This has to be achieved with AngularJS.

The whole example is coming on a table, so the boxes are displayed as a table this way :

<table>
  <thead style="font-weight:bold;">
    <tr style="background-color:lightblue;">
      <td>Id</td>
      <td>Type</td>
      <td>Contains (sum)</td>
    </tr>
  </thead>
  <tbody>
    <tr ng-click="setCurrentBox();" style="background-color:lightgreen;">
      <td>1</td>
      <td>black</td>
      <td>2 boxes</td>
    </tr>
  </tbody>
</table>

Notice the ng-click part. The setCurrentBox() function is defined in the controller, and it sets, as $scope.currentBox, the box object received from the "BoxService".

Click on the row will call the BoxService, retrieve the json object for the selected box (completely! with the contained boxes into it, as written at top of the thread), and assign it to the $scope.currentBox variable.

Change a box selection value should "empty" the next possible choice (set "none" as selected and add the possible choices as options) and, if there are child boxes then just erase them (change black choice to red in my example black->red->green->empty would give red->empty (None -selected- and green and yellow options).

In my case, I only have direct access to $scope.currentBox. And the boxes that "currentBox" contains are properties. So, somehow I think I should do some kind of if object.box!=null then read box... But I'm a bit lost about it...

Well, I don't know if I'm clear enough in my problem definition, here is a short fiddle that should "show where I want to get" in this kind of "Russian Dolls" problematic...

http://jsfiddle.net/z267dquk/2/

Update 1 : http://jsfiddle.net/0js7q638/

thanks for reading / help



Update 2 : Here is an example of what I exactly mean as my question/What I'd like to make/What I miss does not seem to be clear.

Concrete example - START SITUATION :

Box object :

Box 0 (black one)
contains Box 1 (red one)
contains Box 2 (green one)
contains Box 3 (green one)
contains Box 4 (green one) 
contains nothing (yet)

When user selects box 0 in the table he gets this object :

{
"id":"1",
"type":"black",
"box":{
    "id":"8",
    "type":"red",
    "box":{
        "id":"15",
        "type":"green",
        "box":{
            "id":"15",
            "type":"green",
            "box":{
                "id":"15",
                "type":"green",
                "box":null
            }
        }
    }
}
}

This object has to be displayed into editable select boxes as follows :

Box 0 (all box colors choices available here!): 
    <!--This select contains all possible choices since it is the very first choice possible, no dependency-->
        <select name="box0">
            <option value="">NO CHOICE</option>
            <option selected value="1">black</option>
            <option value="8">red</option>
            <option value="15">green</option>
            <option value="3">blue</option>
            <option value="10">yellow</option>
        </select>
    <br/>Box 1 (contained in box 0 box property) : 
    <!--This select contains only boxes choices that a black box can get (since it depends of box 0 value)-->
        <select name="box1">
            <option value="">NO CHOICE</option>
            <option selected value="8">red</option>
            <option value="15">green</option>
            <option value="3">blue</option>
        </select>        
    <br/>Box 2 (contained in box 1 box property) : 
    <!--This select contains only boxes choices that a red box can get (since it depends of box 1 value)-->
        <select name="box2">
            <option value="">NO CHOICE</option>
            <option selected value="15">green</option>
            <option value="10">yellow</option>
        </select>        
    <br/>Box 3 (contained in box 2 box property) : 
    <!--This select contains only boxes choices that a green box can get (since it depends of box 2 value)-->
        <select name="box3">
            <option value="">NO CHOICE</option>
            <option value="1">black</option>
            <option value="8">red</option>
            <option selected value="15">green</option>
            <option value="3">blue</option>
            <option value="10">yellow</option>
        </select>        
    <br/>Box 4 (contained in box 3 box property) : 
    <!--This select contains only boxes choices that a green box can get (since it depends of box 3 value)-->
        <select name="box4">
            <option value="">NO CHOICE</option>
            <option value="1">black</option>
            <option value="8">red</option>
            <option selected value="15">green</option>
            <option value="3">blue</option>
            <option value="10">yellow</option>
        </select>        
    <br/>Box 5 (empty box ready to be filled in box 4 property) : 
    <!--This select contains only boxes choices that a green box can get (since it depends of box 4 value)-->
    <!--This select has default selected value set as null since box4 box property is not set (box 4 box property is not a box, box 4 contains nothing)-->
        <select name="box5">
            <option value="" selected>NO CHOICE</option>
            <option value="1">black</option>
            <option value="8">red</option>
            <option value="15">green</option>
            <option value="3">blue</option>
            <option value="10">yellow</option>
        </select>        

CONCRETE EXAMPLE : USER ACTION 1 :

If user sets box 2 as NO CHOICE OR YELLOW (since yellow box cannot contain any box), then the current box object should look like this :

{
    "id":"1",
    "type":"black",
    "box":{
        "id":"8",
        "type":"red",
        "box":{
            "id":"15",
            "type":"green",
            "box":null
        }
    }
}

And the HTML part should turn like this :

Box 0 (all box colors choices available here!): 
    <!--This select contains all possible choices since it is the very first choice possible, no dependency-->
        <select name="box0">
            <option value="">NO CHOICE</option>
            <option selected value="1">black</option>
            <option value="8">red</option>
            <option value="15">green</option>
            <option value="3">blue</option>
            <option value="10">yellow</option>
        </select>
    <br/>Box 1 (contained in box 0 box property) : 
    <!--This select contains only boxes choices that a black box can get (since it depends of box 0 value)-->
        <select name="box1">
            <option value="">NO CHOICE</option>
            <option selected value="8">red</option>
            <option value="15">green</option>
            <option value="3">blue</option>
        </select>        
    <br/>Box 2 (contained in box 1 box property) : 
    <!--This select contains only boxes choices that a red box can get (since it depends of box 1 value)-->
        <select name="box2">
            <option selected value="">NO CHOICE</option>
            <option value="15">green</option>
            <option value="10">yellow</option>
        </select>        

CONCRETE EXAMPLE : USER ACTION 1 :

If user sets box 1 as BLUE, then the current box object should look like this :

{
    "id":"1",
    "type":"black",
    "box":{
        "id":"3",
        "type":"blue",
        "box":null
    }
}

And the HTML part should turn like this :

Box 0 (all box colors choices available here!): 
    <!--This select contains all possible choices since it is the very first choice possible, no dependency-->
        <select name="box0">
            <option value="">NO CHOICE</option>
            <option selected value="1">black</option>
            <option value="8">red</option>
            <option value="15">green</option>
            <option value="3">blue</option>
            <option value="10">yellow</option>
        </select>
    <br/>Box 1 (contained in box 0 box property) : 
    <!--This select contains only boxes choices that a black box can get (since it depends of box 0 value)-->
        <select name="box1">
            <option value="">NO CHOICE</option>
            <option value="8">red</option>
            <option value="15">green</option>
            <option selected value="3">blue</option>
        </select>        
    <br/>Box 2 (contained in box 1 box property) : 
    <!--This select contains only boxes choices that a blue box can get (since it depends of box 1 value)-->
        <select name="box2">
            <option selected value="">NO CHOICE</option>
            <option value="15">green</option>
            <option value="8">red</option>
            <option value="3">blue</option>
            <option value="10">yellow</option>
            <option value="1">black</option>
        </select>        

Note that I can get the possible choices for a box, or all possible choices for any box from the BoxService. This HAS TO come from BoxService. This data can be huge, it is small in this example, but this can be a long list of objects that can be contained in another one.

Hope this example can make my question more clear.

Thanks for reading

Mi-Creativity
  • 9,554
  • 10
  • 38
  • 47
Julo0sS
  • 2,096
  • 5
  • 28
  • 52
  • 1
    So, after the update it's seems that you have what that you want. What is missing? – Mosh Feu Dec 09 '15 at 05:54
  • @MoshFeu Well, after update the select boxes system does not interact. Need to make it dynamic and kinda fast. When select box one is changed, then following boxes should disappear and only one child item should stay with no choice selected but corresponding options into it. This is the main thing missing. Choice 3 depends on 2 that depends on 1... Then, there is also the queries problem. What would be the best way to solve this? Retrieve the possible options together client side and get options for select in array, or require from $http...? Many things missing tbh.. – Julo0sS Dec 09 '15 at 08:03
  • Really seems to be for homework to me. – yunzen Dec 14 '15 at 11:00
  • not for homework, just to get something working with nested objects and be able to use this on several projects which seems to me more insteresting than a "homework" – Julo0sS Dec 14 '15 at 13:21

3 Answers3

2

Try this example: http://jsfiddle.net/kevalbhatt18/0js7q638/1/

Using checkInnerObject function it will return count of 'box' see in example

function checkInnerObject(obj) {
    var i = 0;
    var arg = Array.prototype.slice.call(arguments, 1);
    start: while (obj) {
        if (obj.hasOwnProperty(arg)) {
            obj = obj[arg];
            i = i + 1;
            continue start;
        }
     }
        return i - 1;
}

checkInnerObject(OBJECT,'key you want to find');

UPDATE:


Eample : http://jsfiddle.net/kevalbhatt18/0js7q638/5/

Keval Bhatt
  • 6,224
  • 2
  • 24
  • 40
  • Thanks for your answer, but this is not the only problem... Problem is to get the thing dynamic. When user clicks on a row, it retrieves a box, containing or not another box, which contains (or not) another box, and so on... 0 to N boxes. This has to be displayed as a set of select boxes, ordered from parent (0) to last child box (N). When he changes for example choice in select 2 (box 2) the box 3 becomes empty (no choice) and has options corresponding to box 2 choice possibilities. And boxes after 3 are erased. Think I am not clear in my question, sorry :/ – Julo0sS Dec 09 '15 at 13:36
  • @Julo0sS its same thing and its dynamic only you can pass n number of nested object to this function it will give you the result count . In your code you will respond with same json so it will give you same result, even you are passing same id 1.This function will work only on object not in array if you need any help will help you – Keval Bhatt Dec 10 '15 at 04:25
  • @Julo0sS see this fiddle. Click on type it will give you count of box value. So from example you will get idea how it will work http://jsfiddle.net/kevalbhatt18/0js7q638/5/ – Keval Bhatt Dec 10 '15 at 04:51
  • I will have a look on it with your function right now, I open a chat room on it – Julo0sS Dec 10 '15 at 07:56
1

Your question is quite lengthy so pardon me if my answer below doesn't satisfies all your conditions. Saying that, I think if you don't have to create this kind of object(Russian doll) then we don't have to worry much.

Do this:-

var reallyLengthyBoxObj = {
"id":"1",
"type":"black",
"box":{
    "id":"8",
    "type":"red",
    "box":{
        "id":"15",
        "type":"green",
        "box":{
            "id":"15",
            "type":"green",
            "box":{
                "id":"15",
                "type":"green",
                "box":null
            }
        }
    }
}
}

$scope.boxObjArr = [],
    $scope.selectedBoxes = {};
    i = 0;
function recurseMe(boxObj){
   i++;
   $scope.selectedBoxes["level"+i] = null;
   var  obj = {};
   obj.id = boxObj.id;
   obj.type = boxObj.type;
   obj.level = i;
   try{
      var haskeys = Object.keys(boxObj.box);
      obj.isParent = true;
      $scope.boxObjArr.push(obj);
      recurseMe(boxObj.box);
   }catch(e){
      obj.isParent = false;
      $scope.boxObjArr.push(obj);
      return;
   }
}

recurseMe(reallyLengthyBoxObj);

This way you will get an array with all the boxes and their levels. Now I assume that duplicate ids won't come(and shouldn't come) from your server. Else our logic will grow.

Now you have 2 things ready - $scope.boxObjArr and $scope.selectedBoxes.

write this in html :

<div ng-repeat="(key,value) in selectedBoxes">
   <select ng-model="value" ng-if="key=='level1' || selectedBoxes[key.slice(0,key.length-1)+(key.slice(-1)-1)] != null">
     <option ng-repeat="box in boxObjArr" ng-show="key=="level1" || box.level < selectedBoxes[key.slice(0,key.length-1)+(key.slice(-1)-1)].level">
     </option>
   </select>
</div>

The javascript part is done and working. Not sure if I have done any mistake in HTML portion. But I think you have got the idea on how and why to form $scope.selectedBoxes and $scope.boxObjArr.

Hope it solves your problem in shortest way possible.

Thanks

Mi-Creativity
  • 9,554
  • 10
  • 38
  • 47
Vineet 'DEVIN' Dev
  • 1,183
  • 1
  • 10
  • 35
1

Building upon your JSFiddle code, I think I got it working the way you want:

var app = angular.module('myApp', []);

app.controller('BoxController', ['$scope', 'BoxService', function($scope, BoxService) {
  $scope.currentBox = {};
  $scope.currentSelection = [];
  $scope.currentOptions = [];
  $scope.defaultOptions = [{
    "id": 1,
    "type": "black"
  }, {
    "id": 8,
    "type": "red"
  }, {
    "id": 15,
    "type": "green"
  }, {
    "id": 10,
    "type": "yellow"
  }, {
    "id": 3,
    "type": "blue"
  }];
  
  // This object maps each box's ID to its length. For example,
  // `boxLengths['1'] = 2` means that box with ID '1' contains 2 boxes.
  $scope.boxLengths = {};
  
  $scope.setCurrentBox = function(id) {
    BoxService.getBoxItem(id, function(box) {
      $scope.currentBox = box;
      
      // Convert the box from a tree structure into a flat array `data`
      BoxService.getBoxesAsTab(box, function(data) {
        $scope.currentSelection = data;
        $scope.currentOptions = [];
        
        // We now know the current box contains `data.length - 1` boxes
        // (subtract 1 so we don't count the first box in the `data` array)
        $scope.boxLengths[id] = data.length - 1;
        
        angular.forEach(data, function(item, index) {
          BoxService.getBoxOptions(item.type, function(options) {
            $scope.currentOptions[index] = options;
          });
        });
      });
    });
  };
  
  // This gets called whenever a `<select>` box changes value
  $scope.updateSelection = function(index, choiceId) {
    // Truncate the arrays down to the element at the specified `index`
    // http://stackoverflow.com/a/6928247/5249519
    $scope.currentSelection.length = index + 1;
    $scope.currentOptions.length = index + 1;
    
    // If the user selects "NO CHOICE", then `choiceId` will be `null`
    if (choiceId === null) {
      // Update the number of boxes that the current box contains
      // (subtract 1 so we don't count the first box in the array).
      // NOTE: If the user selects "NO CHOICE" for the 1st choice,
      // then `$scope.currentBox.id` would be `null` at this point,
      // but I'm not sure what you want to do in that case...
      $scope.boxLengths[$scope.currentBox.id] = $scope.currentSelection.length - 1;
      
      // Update the appropriate object reference in the chain
      if (index === -1) {
        $scope.currentBox = null;
      } else {
        $scope.currentSelection[index].box = null;
      }
      
      // Stop here and return
      return;
    }
    
    // Otherwise, create the next item in the chain
    var nextItem = {
      id: choiceId,
      type: '',
      box: null
    };
    
    // Given the `id`, find the corresponding `type` name in the `defaultOptions` array
    for (var i = 0; i < $scope.defaultOptions.length; i++) {
      if ($scope.defaultOptions[i].id === nextItem.id) {
        nextItem.type = $scope.defaultOptions[i].type;
        break;
      }
    }
    
    // Update the appropriate object reference in the chain
    if (index === -1) {
      $scope.currentBox = nextItem;
    } else {
      $scope.currentSelection[index].box = nextItem;
    }
    
    // Add the `nextItem` to the `currentSelection` array
    $scope.currentSelection.push(nextItem);
    
    // Get the options for the `nextItem` and add them to the `currentOptions` array
    BoxService.getBoxOptions(nextItem.type, function(options) {
      $scope.currentOptions.push(options);
    });
    
    // Update the number of boxes that the current box contains
    // (subtract 1 so we don't count the first box in the array)
    $scope.boxLengths[$scope.currentBox.id] = $scope.currentSelection.length - 1;
  };
}]);

app.directive('editForm', function() {
  return {
    restrict: 'E',
    template:
      '1st choice :                                                                 ' +
      '<select ng-model="currentBox.id"                                             ' +
      '        ng-options="obj.id as obj.type for obj in defaultOptions"            ' +
      '        ng-change="updateSelection(-1, currentBox.id)">                      ' +
      '  <option value="">NO CHOICE</option>                                        ' +
      '</select>                                                                    ' +
      '<div class="editor" ng-repeat="item in currentSelection">                    ' +
      '  <br/><br/>Choice {{$index}} :                                              ' +
      '  <div> Id : <label>{{item.id}}</label></div>                                ' +
      '  <div> Type : <label>{{item.type}}</label></div>                            ' +
      '  <div class="boxes" style="border:1px solid red;">                          ' +
      '    Box :                                                                    ' +
      '    <select ng-model="item.box.id"                                           ' +
      '            ng-options="obj.id as obj.type for obj in currentOptions[$index]"' +
      '            ng-change="updateSelection($index, item.box.id)">                ' +
      '      <option value="">NO CHOICE</option>                                    ' +
      '    </select>                                                                ' +
      '  </div>                                                                     ' +
      '</div>                                                                       '
  };
});

//This is the http service supposed to retrieve boxes data. HARDCODED for the example
app.factory('BoxService', ['$http', function($http) {
  return {
    getBoxItem: function(id, callback) {
      callback({
        "id": 1,
        "type": "black",
        "box": {
          "id": 8,
          "type": "red",
          "box": {
            "id": 15,
            "type": "green",
            "box": null
          }
        }
      });
    },
    getBoxesAsTab: function(box, callback) {
      var boxesArray = [];
      var currentBox = box;
      
      while (currentBox) {
        boxesArray.push(currentBox);
        currentBox = currentBox.box;
      }
      
      callback(boxesArray);
    },
    getBoxOptions: function(type, callback) {
      if (type === 'black') {
        callback([{
          'id': 8,
          'type': 'red'
        }, {
          'id': 3,
          'type': 'blue'
        }, {
          'id': 15,
          'type': 'green'
        }]);
      } else if (type === 'red') {
        callback([{
          'id': 15,
          'type': 'green'
        }, {
          'id': 10,
          'type': 'yellow'
        }]);
      } else if (type === 'blue') {
        callback([{
          'id': 1,
          'type': 'black'
        }, {
          'id': 8,
          'type': 'red'
        }, {
          'id': 15,
          'type': 'green'
        }, {
          'id': 10,
          'type': 'yellow'
        }, {
          'id': 3,
          'type': 'blue'
        }]);
      } else if (type === 'green') {
        callback([{
          'id': 1,
          'type': 'black'
        }, {
          'id': 8,
          'type': 'red'
        }, {
          'id': 15,
          'type': 'green'
        }, {
          'id': 10,
          'type': 'yellow'
        }, {
          'id': 3,
          'type': 'blue'
        }]);
      } else {
        callback([]);
      }
    }
  };
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

<div ng-app="myApp" ng-controller="BoxController">
  <p>Click on the table row (green line) to set "pre-defined" (hardcoded) data</p>
  <table class='table'>
    <thead>
      <tr style="border:1px solid black;">
        <td style="border:1px solid black;">id</td>
        <td style="border:1px solid black;">type</td>
        <td style="border:1px solid black;">contains</td>
      </tr>
    </thead>
    <tbody>
      <tr ng-click="setCurrentBox('1');" style="background-color:lightgreen;">
        <td>1</td>
        <td>Black</td>
        <td ng-bind="boxLengths['1']"></td>
      </tr>
    </tbody>
  </table>
  <edit-form></edit-form>
  <br/>
  <br/>
  <br/> CURRENT BOX : {{currentBox}}
  <br/> CURRENT SELECTION : {{currentSelection}}
  <br/> CURRENT OPTIONS : {{currentOptions}}
</div>

I know you said you prefer to work with the boxes in a tree structure, rather than an array of boxes, but you need a flat array in order to use ng-repeat. Anyway, once you have the box, it's easy to convert from a tree structure to a flat array; I've modified your BoxService.getBoxesAsTab function to do that by copying over the object references into a new array:

getBoxesAsTab: function(box, callback) {
  var boxesArray = [];
  var currentBox = box;

  while (currentBox) {
    boxesArray.push(currentBox);
    currentBox = currentBox.box;
  }

  callback(boxesArray);
}

Hope that helps. Let me know if you have any questions. Thanks!


UPDATE: I updated my above code with the following changes:

  • Each <select> now has a default "NO CHOICE" option, which should work as expected. Note: ng-options lets you have "a single hard-coded <option> element" to serve as the null option.
  • The "contains" value (in the table) is now initialized and updated dynamically as you change your selections. Of course, it won't update anymore if you change the "1st choice" to something other than "black".
  • I simplified the editForm template HTML by removing the ng-init and using item.box.id directly in ng-model and ng-change instead.
  • I added more code comments to explain the code more clearly.

Just to clarify: When the user changes a <select> box value, the $scope.updateSelection function is called. Among other things, the function updates $scope.currentSelection (flat array of boxes), but it also updates item.box (reference to the next box in the chain) as needed, so you should see $scope.currentBox (box in tree structure) updated as well. This works because ultimately, both $scope.currentSelection and $scope.currentBox contain references to the same box objects in memory.

Hope that helps. Thanks.

DynamicDispatch
  • 421
  • 4
  • 12
  • Hey! nice answer, soon accepted ;) Got almost the same solution http://jsfiddle.net/xx0arx0j/ . Now trying to make it work in opposite sense (from child to parent instead of parent to child, meaning object "is contained" in box property). Let me know if you have time to help pls... – Julo0sS Dec 14 '15 at 08:16
  • @Julo0sS, Thanks! Just glad I could help. Sorry, need to go to the doctor this week and other family issues, so I won't have time. But the opposite sense should be similar, just reverse all the logic. For example, I would probably do `$scope.currentSelection.reverse()` to reverse the array in-place. Then, in the `$scope.updateSelection` function, maybe change `$scope.currentSelection[index].box = nextItem` into `nextItem.box = $scope.currentSelection[index]`. Reverse stuff like that. Hope that helps. – DynamicDispatch Dec 15 '15 at 04:22