Knockout ES5 track nested objects - (JavaScript version)
See updated version on github: https://github.com/CarlosOnline/knockout.es5.mapping
/* Knockout ES5 track nested objects: JavaScript version - compiled via TypeScript. Original: http://carlosonlineprogramming.blogspot.com/2013/09/knockout-es5-track-nested-objects.html
Solves the problem of ko.track not traversing into nested objects.
- Calls ko.es5.track on nested objects: allowing tracking of strings, numbers, & arrays.
- Use ko.es5.computed(function() ...) to mark computed functions. ko.es5.track will call ko.defineProperty on these marked functions, after calling ko.track on the primitive members.
- Excludes nested functions/objects with named constructors. Idea is that TypeScript classes with constructors can call ko.es5.track themselves. This exclusion can be removed.
- Does not traverse into nested classes/objects with named constructors will not be traversed. These constructors should call ko.es5.track themselves.
var koES5; (function (koES5) { function getType(x) { if ((x) && (typeof (x) === "object")) { if (x.constructor === Date) return "date"; if (x.constructor === Array) return "array"; } return typeof x; } var Track = (function () { function Track(rootObject) { this.rootObject = rootObject; this.mapped = []; this.track(rootObject); this.clearAllMapped(); } Track.prototype.track = function (source, name) { if (typeof name === "undefined") { name = null; } var _this = this; if (source == null || this.isMapped(source)) return; if (name == null) name = this.name(source); var keys = []; var computed = []; this.setMapped(source); for (var key in source) { var value = source[key]; var type = getType(value); switch (type) { case "array": case "string": case "number": //console.log(name + "." + key, type); keys.push(key); break; case "function": if (this.isComputed(value)) { //console.log("f> " + name + "." + key, type); computed.push({ name: key, fn: value }); } break; case "object": if (value == null || this.isMapped(value) || !this.isTrackable(value) || !this.isTrackableField(key)) continue; //console.log("o> " + name + "." + key, type); this.track(value, key); break; } } if (keys.length > 0) { ko.track(source, keys); } if (computed.length > 0) { computed.forEach(function (item) { _this.makeComputed(source, item.name, item.fn); }); } }; Track.prototype.name = function (value) { var xtor = value.__proto__.constructor; return xtor !== undefined && xtor.name != undefined ? xtor.name : ""; }; Track.prototype.isTrackable = function (value) { var xtor = value.__proto__.constructor; return xtor.name === "Object"; }; Track.prototype.isTrackableField = function (key) { return key != "__ko_mapping__"; }; Track.prototype.isMapped = function (value) { return (value.__tracked__ === true); }; Track.prototype.setMapped = function (value) { if (this.isMapped(value)) return; value.__tracked__ = true; this.mapped.push(value); }; Track.prototype.clearAllMapped = function () { this.mapped.forEach(function (value) { delete value["__tracked__"]; }); this.mapped.unshift(); }; Track.prototype.isComputed = function (fn) { return (fn["__ko_es5_computed__"] === true); }; Track.prototype.makeComputed = function (container, name, fn) { var nameOverride = fn["__ko_es5_computed_name__"]; if (nameOverride !== undefined && nameOverride !== "") { name = nameOverride; delete fn["__ko_es5_computed_name__"]; } if (name === undefined || name == "") { console.log("Error. Function missing name", fn); return; } ko.defineProperty(container, name, fn); delete fn["__ko_es5_computed__"]; }; return Track; })(); koES5.Track = Track; function track(root) { new Track(root); } koES5.track = track; function computed(fn, name) { if (typeof name === "undefined") { name = null; } fn["__ko_es5_computed__"] = true; if (name || false) { fn["__ko_es5_computed_name__"] = true; } return fn; } koES5.computed = computed; ko.es5 = { computed: koES5.computed, track: koES5.track }; })(koES5 || (koES5 = {}));