Monday, September 23, 2013

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,
    };
}

2 comments: