knockout nested observables using view controller

109 views Asked by At

We are refactoring our client code and implementing viewmodels. I'd like to keep our viewmodels as dumb as possible, having them be solely data representations.

We'll use a view controller and pub/sub to get fresh data for the vm as required, and just push the data into the viewmodel in a one-way data hierarchy model akin to the way components communicate in Vue.

For flat viewmodel properties this approach works fine, using a 'Props' function, but for a case of nested observables like address, I lose the observable (of course).

var model = function() {
  var self = this;
  self.name = ko.observable();
  self.occupation= ko.observable();
  self.address = ko.observable({
    street: ko.observable('Streetname'),
    zip: ko.observable('Zipcode')
  });

  self.doUpdate = function() {
      self.props({name: 'Tom', address: {street:'NewStreet'}});
  };
  self.props = function(data) {
    var viewmodel = self;
    for (p in data) {
      if (self[p]) {
        self[p](data[p]);
      }
    }
  };
}

ko.applyBindings(new model());

I can't pass in

self.props({name: 'Tom', address: {street:ko.observable('NewStreet')}});

because I have to assume we'll just be getting a data structure from a service or other module, and it's the viewmodel's job to manage what are observables or not.

The alternative I thought of was just using the ko mapping functionality, but that requires a little more intelligence in my Props function, where I would do something like

if(self['mapping_' + p]){
  //If self.mapping_address() exists, use that to 
  //create mapped observables...
}else if(self[p]){}...

I'd be comfortable with this, but it seems a little kludgey. Is there a better way to accomplish maintaining nested observables when passing in hierarchical data from a service or controller?

1

There are 1 answers

0
Sam On

I take it you have your own ajax(get/post) implementation wrapped in a little library, what we do is we have our own xhrObject(jqXHR) that returns a promise, before we resolve the deferred of that promise we can choose if we change the properties to observables or not so that the viewmodel implementation doesn't have to deal with converting each api call.

Here's a little thingy to get you on your way, hope it helps

//fakedata
var $stub = {
  id: 'foo',
  name: 'bar',
  complex: {
    name: 'test'
  }
};
//fakeapi
var $fakeAsync = function(api, result) {
  var dfd = $.Deferred(function() {
    setTimeout(function() {
      dfd.resolve(result);
    }, 100);
  });
  return dfd.promise();
};
//ajaxlib
var $ajaxlib = new function() {
  var self = this;
  //normal json object
  self.getAsJson = function(api) {
    return $fakeAsync(api, $stub);
  };
  //everything changed to observables before returning
  self.getAsObservable = function(api) {
    var dfd = $.Deferred();
    $fakeAsync(api, $stub).done(function(result) {
      var propNames = [];
      for (var prop in result) {
        propNames.push(prop);
      }
      mappedResult = ko.mapping.fromJS(result);
      $.each(propNames, function(index, propName) {
        if (_.isObject(mappedResult[propName]) && !_.isFunction(mappedResult[propName])) {
          var obj = mappedResult[propName];
          mappedResult[propName] = ko.observable(obj);
        }
      });

      dfd.resolve(mappedResult);
    });
    return dfd;
  };
};
//viewmodel
ko.applyBindings(() => {
  var self = this;

  self.json = ko.observable();
  self.obse = ko.observable();

  self.init = function() {
    $ajaxlib.getAsJson('/api/fake/1').done((result) => {
      self.json(result)
    });
    $ajaxlib.getAsObservable('/api/fake/1').done((result) => {
      self.obse(result)
    });
  };
  self.init();
});
div {
  padding: 5px;
  border: 1px solid #555;
  margin: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<div>
  <!-- ko with: json -->
  <!-- ko with: complex -->
  json: <span data-bind="text: name"></span>
  <br />(isObservable: <span data-bind="text: ko.isObservable(name)"></span>)
  <br /><input type="text" data-bind="textInput: name" />
  <!-- /ko -->
  <!-- /ko -->
</div>
<div>
  <!-- ko with: obse -->
  <!-- ko with: complex -->
  observable: <span data-bind="text: name"></span>
  <br />(isObservable: <span data-bind="text: ko.isObservable(name)"></span>)
  <br /><input type="text" data-bind="textInput: name" />
  <!-- /ko -->
  <!-- /ko -->
</div>