3
votes

I'm trying to use Knockout's mapping plugin on a nested JSON object with variable data inside. However, I'm not sure how to get it to display in my HTML. How do I correctly map all the nested JSON objects and display it as, say, a simple string? Here is my code:

JS

var ListModel = function(jsonData) {
  var self = this;
  self.master = ko.mapping.fromJS(jsonData);
}
var listModel = new ListModel(jsonData);
ko.applyBindings(listModel);

HTML

<!-- ko foreach: master -->
  <div data-bind="text: $data"></div> 
<!-- /ko -->

Sample JSON

{"Level 1a":"Hi","Level 1b":{
  "Level 2a":"Hello","Level 2b":{
    "Level 3":"Bye"}
  }
}

Sample Output

Hi
  Hello
    Bye

The main thing I'm trying to do here is to print out the values from all nested levels. The key values and number of nested levels are entirely variable (most of the nested JSON examples I found on SO and online were for fixed keys). Is this possible?

Update: I found the jQuery equivalent, but I still need the Knockout implementation for observables.

2

2 Answers

5
votes

Since your JSON object has variable keys, you must transform it into a fixed, predictable structure first or nested template mapping will not work (knockout is declarative, so you need to know key names beforehand).

Consider the following custom mapping code (no knockout mapping plugin needed):

var ListModel = function(jsonData) {
    var self = this;

    self.master = ko.observableArray([]);

    function nestedMapping(data, level) {
        var key, value, type;

        for (key in data) {
            if (data.hasOwnProperty(key)) {
                if (data[key] instanceof Object) {
                    type = "array";
                    value = ko.observableArray([]);
                    nestedMapping(data[key], value());
                } else {
                    type = "simple";
                    value = ko.observable(data[key]);
                }
                level.push({key: key, type: type, value: value});
            }
        }
    }

    nestedMapping(jsonData, self.master());
}

the function nestedMapping() turns your data structure:

{
    "Level 1a": "Hi",
    "Level 1b": {
        "Level 2a": "Hello",
        "Level 2b": {
            "Level 3": "Bye"
        }
    }
}

into:

[
    {
        "key": "Level 1a",
        "type": "simple",
        "value": "Hi"
    },
    {
        "key": "Level 1b",
        "type": "array",
        "value": [
            {
                "key": "Level 2a",
                "type": "simple",
                "value": "Hello"
            },
            {
                "key": "Level 2b",
                "type": "array",
                "value": [
                    {
                        "key": "Level 3",
                        "type": "simple",
                        "value": "Bye"
                    }
                ]
            }
        ]
    }
]

Now you can create a template like this one:

<script type="text/html" id="nestedTemplate">
  <!-- ko if: type == 'simple' -->
  <div class="name" data-bind="text: value, attr: {title: key}"></div>
  <!-- /ko -->
  <!-- ko if: type == 'array' -->
  <div class="container" data-bind="
    template: {
      name: 'nestedTemplate', 
      foreach: value
    }
  "></div>
  <!-- /ko -->
</script>

See it working: http://jsfiddle.net/nwdhJ/2/

Note a subtle but important point about nestedMapping(). It creates nested observables/observableArrays. But it works with the native array instances (by passing self.master() and value() into the recursion).

This way you avoid needless delay during object construction. Every time you push values to an observableArray it triggers knockout change tracking, but we don't need that. Working with the native array will be considerably faster.

2
votes

Change your JSON data to this (note that the arrays!):

[
  {
    "Text": "Hi",
    "Children": [
      {
        "Text": "Hello",
        "Children": [
          {
            "Text": "Bye"
          }
        ]
      }
    ]
  }
]

and use a self-referential template:

<script type="text/html" id="nestedTemplate">
  <div class="name" data-bind="text: Text"></div>
  <div class="container" data-bind="
    template: {
      name: 'nestedTemplate', 
      foreach: Children
    }
  "></div>
</script>

that you call like this:

<div class="container" data-bind="
  template: {
    name: 'nestedTemplate', 
    foreach: master
  }
"></div>

You can then use CSS to manage indent:

/* indent from second level only */
div.container div.container {
  margin-left: 10px;
}

See it on jsFiddle: http://jsfiddle.net/nwdhJ/1/