2
votes

I'm using KnockOut mapping fromJS to create my View Model from a JSON object like the following:

{
    "cats": [{
        "name": "fluffy",
        "color": "brown",
        "kittens": [{
            "name": "spot",
            "color": "brown"
        }, {
            "name": "rascal",
            "color": "grey"
        }, {
            "name": "trouble",
            "color": "white"
        }]
    }, {
        "name": "kitty",
        "color": "red",
        "kittens": [{
            "name": "lady",
            "color": "red"
        }, {
            "name": "skat",
            "color": "striped"
        }]
    }]
}

html:

<div data-bind="foreach:cats">
    <span data-bind="text:name"></span>
    <table>
        <tr data-bind="foreach: kittens">
             <td data-bind="text:name"></td>
             <td data-bind="text:color"></td>
             <td><a data-bind="click: $parent:showParentColor" href="#">Parent Color</a></td>
        </tr>
    </table>
</div>

Javascript:

var KittenModel = function (data) {
    ko.mapping.fromJS(data, {}, this);

    // ... various computed values added to this
}

var mapping = {
    'kittens': {
        create: function(options) {
            return new KittenModel(options.data);
        }
    },
    'otherItem': {
        create: function(options) {
             return ('otherStuff');
        }
     }
}

var data = { ... }; // the JSON above

var CatsViewModel = ko.mapping.fromJS(data, mapping);

Question:

Where and how do I put the showParentColor() function so that the the data-bind works in the kitten table? For example:

function showParentColor(cat) {
    alert(cat.color);
}

Thanks!

2

2 Answers

4
votes

You may use one of the following based on your view model hierarchy :

  • $root : This points to the main view model object in the root context.The top most parent context.
  • $parents array : This is an array which contains all your view models.

    • $parents[0] : The parent view model context.(also it’s the same as $parent)

    • $parents[1]: The second parent view model context.(grand parent)

    • $parents[2]: The third parent view model context . (great-grand parent)

    • And so on....


Update : If you want to add a function in CatsViewModel level, you simply add your function to created model.

Example :https://jsfiddle.net/kyr6w2x3/87/

JS:

 CatsViewModel.showParentColor = function(item){
      console.log(item.name());
      console.log(item.color());
    }

View:

<a data-bind="click: $parents[1].showParentColor">

Below is the hierarchy of your model

- CatsViewModel 
    - cats : observableArray
        - name : observable
        - color : observable
        - kittens : observableArray
               - name : observable
               - color : observable
    - showParentColor : function


Alternative :click

Example :http://jsfiddle.net/kyr6w2x3/91/

HTML :

<div data-bind="foreach:cats">
    <span data-bind="text:name"></span>
    <table>
    <tbody data-bind="foreach: kittens">
       <tr>
             <td data-bind="text:name"></td>
             <td data-bind="text:color"></td>
             <td><a data-bind="click: $parent.showParentColor" href="#">Parent Color</a></td>
        </tr>
    </tbody>

    </table>
</div>

JS:

 var data = {
    "cats": [{
        "name": "fluffy",
        "color": "brown",
        "kittens": [{
            "name": "spot",
            "color": "brown"
        }, {
            "name": "rascal",
            "color": "grey"
        }, {
            "name": "trouble",
            "color": "white"
        }]
    }, {
        "name": "kitty",
        "color": "red",
        "kittens": [{
            "name": "lady",
            "color": "red"
        }, {
            "name": "skat",
            "color": "striped"
        }]
    }]
}

var CatsViewModel = function (data){ 
  var self = this;
  self.cats = ko.observableArray($.map(data.cats, function (item) {
        return new CatItemViewModel(item);
    }));
}
var CatItemViewModel = function (data){
  var self = this;
  self.name = ko.observable(data.name);
  self.color = ko.observable(data.color);
  self.kittens = ko.observableArray($.map(data.kittens, function (item)     {
   var newData = Object.assign({}, item, { parent: self.name()});
        return new KittenModel(newData);
   }));
   self.showParentColor = function (item){
      console.log("Parent Name: " , self.name());
      console.log("Name: " , item.name());
      console.log("Color: " , item.color());
   }
}
var KittenModel = function (data) {
  var self = this;
  self.name = ko.observable(data.name);
  self.color = ko.observable(data.color);
  self.parent = ko.observable(data.parent);
}
var vm = new CatsViewModel(data);
ko.applyBindings(vm);
1
votes

Note: I like the other answer's approach better than this one, but did want to present an alternative solution for the sake of having multiple options.

Alternative 1

You could create a "static" method in your KittenModel that logs any cat's color that it's passed:

var KittenModel = function (data) {
  ko.mapping.fromJS(data, {}, this);
};

KittenModel.logCatColor = function(cat) {
  console.log(cat.color);
};

Now, because your view has access to the parent-child structure, you can call this method with any parent you want:

<!-- to log the parent's color -->
<div data-bind="click: logCatColor.bind(null, $parent)"></div>

<!-- knockout automatically passes `$data` as a first argument,
     so you won't need to bind the method to log your own color -->
<div data-bind="click: logCatColor"></div>

An example:

ko.applyBindings({
  name: "parent",
  child: {
    name: "child",
    logName: function(entity) {
      console.log(entity.name);
    }
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<h1 data-bind="text: name"></h1>
<div data-bind="with: child" style="border: 1px solid black">
  <h2 data-bind="text: name">
    </h2>
  <button data-bind="click: logName">
    log my name
  </button>
  <button data-bind="click: logName.bind(null, $parent)">
    log my parent's name
  </button>
</div>

Alternative 2

In the factory function you're passing as an options, you have both data and parent available: options.data holds the item being currently mapped. options.parent in this case will refer to the parent cat. I haven't tested this, but this might work:

'kittens': {
    create: function(options) {
        var dataWithParent = Object.assign({}, 
                               options.data, 
                               { parent: options.parent });

        return new KittenModel(dataWithParent);
    }
},