/// module formsjs { export class Flatten { /** * Return a (1-dimensional) array of keys representing an object. * *

For example, {foo: {bar: 'baz'}} will become flattened into '['foo', 'foo.bar']. * *

Arrays can also be flattened. * Their flattened keys will take the form of 'myArray[0]' and 'myArray[0].myNestedProperty'. */ public static flatten(object:any):Array { var keys:Array = []; var queue:Array = [{ object: object, prefix: null }]; while (true) { if (queue.length === 0) { break; } var data:any = queue.pop(); var prefix:string = data.prefix ? data.prefix + '.' : ''; if (typeof data.object === 'object') { for (var prop in data.object) { var path:string = prefix + prop; var value:any = data.object[prop]; keys.push(path); queue.push({ object: value, prefix: path }); } } } return keys; } /** * Returns the property value of the flattened key or undefined if the property does not exist. * *

For example, the key 'foo.bar' would return "baz" for the object {foo: {bar: "baz"}}. * The key 'foo[1].baz' would return 2 for the object {foo: [{bar: 1}, {baz: 2}]}. */ public static read(flattenedKey:string, object:any):any { var keys:Array = flattenedKey.split(/[\.\[\]]/); while (keys.length > 0) { var key:any = keys.shift(); // Keys after array will be empty if (!key) { continue; } // Convert array indices from strings ('0') to integers (0) if (key.match(/^[0-9]+$/)) { key = parseInt(key); } // Short-circuit if the path being read doesn't exist if (!object.hasOwnProperty(key)) { return undefined; } object = object[key]; } return object; } /** * Writes a value to the location specified by a flattened key and creates nested structure along the way as needed. * *

For example, writing "baz" to the key 'foo.bar' would result in an object {foo: {bar: "baz"}}. * Writing 3 to the key 'foo[0].bar' would result in an object {foo: [{bar: 3}]}. */ public static write(value:any, flattenedKey:string, object:any):void { var currentKey:any; var keyIndexStart = 0; for (var charIndex = 0, length = flattenedKey.length; charIndex < length; charIndex++) { var character = flattenedKey.charAt(charIndex); switch(character) { case '[': currentKey = flattenedKey.substring(keyIndexStart, charIndex); this.createPropertyIfMissing_(currentKey, object, Array); break; case ']': currentKey = flattenedKey.substring(keyIndexStart, charIndex); currentKey = parseInt(currentKey); // Convert index from string to int // Special case where we're targeting this object in the array if (charIndex === length - 1) { object[currentKey] = value; } else { // If this is the first time we're accessing this Array key we may need to initialize it. if (!object[currentKey] && charIndex < length - 1) { switch(flattenedKey.charAt(charIndex + 1)) { case '[': object[currentKey] = []; break; case '.': object[currentKey] = {}; break; } } object = object[currentKey]; } break; case '.': currentKey = flattenedKey.substring(keyIndexStart, charIndex); // Don't do anything with empty keys that follow Array indices (e.g. anArray[0].aProp) if (currentKey) { this.createPropertyIfMissing_(currentKey, object, Object); } break; default: continue; // Continue to iterate... break; } keyIndexStart = charIndex + 1; if (currentKey) { object = object[currentKey]; } } if (keyIndexStart < flattenedKey.length) { currentKey = flattenedKey.substring(keyIndexStart, flattenedKey.length); object[currentKey] = value; } } /** * Helper method for initializing a missing property. * * @throws Error if unrecognized property specified * @throws Error if property already exists of an incorrect type */ private static createPropertyIfMissing_(key:string, object:any, propertyType:any):void { switch(propertyType) { case Array: if (!object.hasOwnProperty(key)) { object[key] = []; } else if (!(object[key] instanceof Array)) { throw Error('Property already exists but is not an Array'); } break; case Object: if (!object.hasOwnProperty(key)) { object[key] = {}; } else if (typeof object[key] !== 'object') { throw Error('Property already exists but is not an Object'); } break; default: throw Error('Unsupported property type'); break; } } } }