Monday, September 23, 2013

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 = {}));

Knockout ES5 track nested objects

See updated version on github:
https://github.com/CarlosOnline/knockout.es5.mapping

/* Knockout ES5 track nested objects: TypeScript version.
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.
  • Does not handle null valued variables.  Cannot deduce the type from these variables.
*/

interface KnockoutStatic {
    es5: {
        computed: Function;
        track: Function;
    };
}
 
module koES5 {
    function getType(x) {
        if ((x) && (typeof (x) === "object")) {
            if (x.constructor === Date) return "date";
            if (x.constructor === Array) return "array";
        }
        return typeof x;
    }
 
    export class Track {
        mapped = [];
 
        constructor(private rootObject: any) {
            this.track(rootObject);
            this.clearAllMapped();
        }
 
        track(source, name: string = null) {
            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((item) => {
                    this.makeComputed(source, item.name, item.fn);
                });
            }
        }
 
        private name(value) {
            var xtor = value.__proto__.constructor;
            return xtor !== undefined && xtor.name != undefined ? xtor.name : "";
        }
 
        private isTrackable(value) {
            var xtor = value.__proto__.constructor;
            return xtor.name === "Object";
        }
 
        private isTrackableField(key: string) {
            return key != "__ko_mapping__";
        }
 
        private isMapped(value: any) {
            return (value.__tracked__ === true);
        }
 
        private setMapped(value: any) {
            if (this.isMapped(value))
                return;
            value.__tracked__ = true;
            this.mapped.push(value);
        }
 
        private clearAllMapped() {
            this.mapped.forEach((value) => {
                delete value["__tracked__"];
            });
            this.mapped.unshift();
        }
 
        private isComputed(fn: Function) {
            return (fn["__ko_es5_computed__"] === true);
        }
 
        private makeComputed(container: any, name: string, fn: Function) {
            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__"];
        }
    }
 
    export function track(root: any) {
        new Track(root);
    }
 
    export function computed(fn: Function, name: string = null) {
        fn["__ko_es5_computed__"] = true;
        if (name || false) {
            fn["__ko_es5_computed_name__"] = true;
        }
        return fn;
    }
 
    ko.es5 = {
        computed: koES5.computed,
        track: koES5.track,
    };
}