Mason G. Zhwiti
Mason G. Zhwiti

Reputation: 6540

Using Knockout mapping for complex JSON

Most of Knockout seems very intuitive. One thing that is strange to me though is how the mapping plugin works. I was expecting/hoping I would be able to feed it JSON from an ajax call, and have a sort of "dynamic" view model that is available to reference in my HTML.

The description of the mapping plugin even makes it sound like this is how it works:

"If your data structures become more complex (e.g. they contain children or contain arrays) this becomes very cumbersome to handle manually. What the mapping plugin allows you to do is create a mapping from the regular JavaScript object (or JSON structure) to an observable view model."

But it appears you actually need to define the view model first in your code, and then you can populate it after the fact using the mapping plugin and some JSON data. Is this right?

A concrete example of what I was trying to do.

I am trying to use Knockout with Solr (a search engine that returns JSON search results). The skeleton structure of the JSON data returned by Solr is:

  {
      "responseHeader": {
          "status": 0,
          "QTime": 0,
          "params": {
              "facet": "true",
              "facet.field": "System",
              "q": "testphrase",
              "rows": "1",
              "version": "2.2"
          }
      },
      "response": {
          "numFound": 0,
          "start": 0,
          "maxScore": 0.0,
          "docs": []
      },
      "facet_counts": {
          "facet_queries": {},
          "facet_fields": {
              "System": []
          },
          "facet_dates": {},
          "facet_ranges": {}
      },
      "highlighting": {}
  }

In fact, that's the structure I am feeding into my mapped view model when I first set it up.

Just so you understand a bit about how the JSON data comes back from Solr: The response.docs array contains an array of hashes, where the hash data is comprised of key/values for your indexed document data. Each hash in the array is one document being returned in your search results.

That part seems to map over just fine.

The "highlighting" part of the JSON is what causes me problems. When I try to reference the highlighting fields in my HTML, I get ReferenceErrors. Here's an example of what the highlighting field might look like in the JSON:

"highlighting": {
    "2-33-200": {
        "Title": ["1992 <b>Toyota</b> Camry 2.2L CV Boots"]
    },
    "2-28-340": {
        "Title": ["2003 <b>Toyota</b> Matrix 2.0L Alignment"]
    },
    "2-31-2042": {
        "Title": ["1988 <b>Toyota</b> Pickup 2.4L Engine"]
    }
}

I have a foreach in my HTML that tries to parse through each response.docs element, and if the highlighting part of the object contains a match for that document's Id field, I want to substitute the highlighted Title rather than the default Title. (In the code below, "Results" is the name of the viewmodel I am mapping the JSON over to.)

<div id="search-results" data-bind="foreach: Results.response.docs">
    <div data-bind="attr: { id: 'sr-' + Id }" class="search-result">
        <h3 class="title"><a data-bind="html: (($root.Results.highlighting[Id]['Title'] != undefined) ? $root.Results.highlighting[Id]['Title'] : Title), attr: {href: Link}"></a></h3>
        <span class="date" data-bind="text: DateCreated"></span>
        <span class="snippet" data-bind="html: Snippet"></span>
    </div>
</div>

When I try to use this, I always get this error:

Uncaught Error: Unable to parse bindings.
Message: TypeError: Cannot read property 'Title' of undefined;
Bindings value: html: (($root.Results.highlighting[Id]['Title'] != undefined)  ? $root.Results.highlighting[Id]['Title'] : Title), attr: {href: Link}

I've tried variations on how I am referencing the data, but I just can't seem to access it.

Edit I'm making a bit of headway. In my mapping definition, I now specify "highlighting" like this:

"highlighting": ko.observable({})

Rather than just setting highlighting to {}. Now I am at least able to peer a bit into the highlighting data when I do my mapping. Yet I'm still seeing strange errors.

I've simplified my test HTML code to just spit out the highlighting data for each search result:

<div id="search-results" data-bind="foreach: Results.response.docs">
    <pre data-bind="text: JSON.stringify(ko.toJS($root.Results.highlighting()[Id()]), null, 2)"></pre>
</div>

This returns multiple <pre> tags now that look like this:

{
  "Title": [
    "1992 <b>Toyota</b> Camry 2.2L CV Boots"
  ]
}

However, if I change that HTML code to this:

<pre data-bind="text: $root.Results.highlighting()[Id()]['Title']"></pre>

I continue to get errors like this:

Message: TypeError: Cannot read property 'Title' of undefined;
Bindings value: text: $root.Results.highlighting()[Id()]['Title']

Makes no sense to me! My previous test shows that the data available does contain a "Title" key, why can't I access that data?

Edit I created a jsfiddle, but of course... it works as expected. I am unable to reproduce my issue on jsfiddle. :-(

Edit OK I am making some headway here, but am still very confused as to what is going on. First I changed my debugging HTML to this:

<div id="search-results" data-bind="foreach: Results.response.docs">
    <pre data-bind="text: console.log($root.Results.highlighting()[Id()])"></pre>
</div>

I then submitted my ajax call, and I noticed in the Chrome console this output:

undefined
undefined
> Object

So for some reason, the foreach loop is looping over 3 Results.response.docs, and the first two are not mapping to anything in my highlights() object, so they are returning undefined -- and that's why my attempt to pull the .Title property was failing.

To confirm this, I wrapped a ko if: $root.Results.highlighting()[Id()] around that block, and was finally able to access the .Title property during the foreach loop without a JS error.

This still leaves me with the question of why/how there are 3 Results.response.docs objects being looped over. Perhaps the foreach binding is being run 3 times, and the first 2 times the highlighting object is empty, and the third time, it's finally filled in? But I am having a hard time figuring out why that would be.

Another possible clue: if I trigger the ajax call a second time, without reloading the page, I can see that those 3 "passes" all return a valid Object each time in the console log. So instead of two undefineds and an Object, it's three Objects all in a row.

On my HTML output, though, I only see one row of data. So that seems to prove that it's not looping over 3 elements, but is in fact being run 3 times. The question remains... WHY?

Upvotes: 3

Views: 5948

Answers (1)

madcapnmckay
madcapnmckay

Reputation: 15984

The mapping plugin is working as you expect. Your problem is simply that you are expecting the plugin to create observables at every level in the object. This is not how the plugin works. It will only create observables for "leaf" properties. So in your case $root.Results.highlighting is not created as an observable. The id properties on docs are however created as observables so the solution is.

$root.Results.highlighting[Id()]

I believe you may have been confused because one of your fiddles was assigning self.Results twice which made it appear to work one way when in fact the problem was being masked.

Here is the working version

http://jsfiddle.net/madcapnmckay/UaBKe/

Hope this helps.

Upvotes: 3

Related Questions