3

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.

Community
  • 1
  • 1
Wei Hao
  • 2,756
  • 9
  • 27
  • 40

2 Answers2

5

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.

Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • Hi, I was trying to do the same stuff showing it in a nested table format where we can show or hide childs. Can you help me to do it? http://stackoverflow.com/questions/18008897/knockout-mapping-for-nested-json – Okky Aug 02 '13 at 05:12
2

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/

Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • Your JSON data has fixed keys (Text and Children), but I need to allow for variable keys as well. – Wei Hao Jun 22 '12 at 08:48
  • @Wei: It is not clear from your question how those variable keys would look like (I could guess, but I don't want to). How is knockout supposed to know how to apply the template if your key names are changing? Besides, I consider it a code smell if key names in a data object are subject to change. If key names are variable, they are *data* and not keys. – Tomalak Jun 22 '12 at 09:02
  • You have a point there. Well, they are variable in the sense that the incoming JSON data could have key names like "Name", "Date", "Foo", "Bar" etc, but we have no idea of knowing what it could be until the JSON object comes in. The commonality is that knockout just needs to display all the values inside the JSON object. I too wish the keys can be fixed, but due to business logic there can be all kinds of key names coming in. – Wei Hao Jun 22 '12 at 09:17
  • @Wei: See my other answer. I've kept that answer because I feel it is the right approach to solve such a problem. Since you can't do anything about your JSON, it does not apply to you. But others might find it helpful. – Tomalak Jun 22 '12 at 09:57