I wrote this code because I was often annoyed that I couldn't get the parent object. This class allows you to traverse the parent-child relationship between objects, in both directions. It also preserves prototypes.
For example:
let myTree = new Tree({a:{b:{k:2}}});
myTree.a.b; // { k: 2 }
myTree.a === myTree.a.b.__parent__; // true
It also enables you to add more properties:
myTree.c = {d:{l:5}}; // {d: {l: 5}}
myTree.c.__parent__ === myTree; // true
You can also move a branch:
myTree.a.b.__parent__ = myTree.c; // {d: {l: 5}}
myTree.c.b.k; // 2
myTree.a.b; // undefined
You can even move it to another tree:
let anotherTree = new Tree({});
myTree.a.l = 3;
myTree.a.l; // 3
myTree.a.__parent__ = anotherTree;
anotherTree.a; // {l: 3}
myTree.a; // undefined
anotherTree.a.__parent__ === anotherTree; // true
Now, you may be wondering, how do you get a human readable and console-friendly version of this tree?
myTree.__raw__();
Note: only properties that are objects are able to give you the parent object.
My code:
const Tree = (function(){
const TARGET = Symbol('__target__'),
HANDLER = Symbol('__handler__'),
PROXY = Symbol('__proxy__'),
{ assign, defineProperty, entries, setPrototypeOf } = Object,
convert=( obj )=>{
let res = new Branch(obj);
entries(obj).forEach(([key, value], _) => {
if( ({}).hasOwnProperty.call(obj, key) ) {
if(typeof value === 'object') {
res[key] = convert(value);
defineProperty(res[key], '__parent__', {
value: res[PROXY],
configurable: false,
writable: true
});
} else {
res[key] = value.constructor(value);
}
}
});
return res;
},
getKey = (obj, val) => {
return entries(obj).filter(([key, value], _) => { return value[TARGET] === val; })[0][0];
};
let genHandler = (_target) => {
return (function(){
let res = {
set: (target, prop, value) => {
if( ['__parent__'].includes(prop) ) {
if( typeof value === 'object' && (value.__istree__ || value.__isbranch__) && value !== target.__parent__) {
const key = getKey(target.__parent__, target);
if(target.__parent__[key]) {
delete target.__parent__[key];
}
value[key] = target;
return value;
} else {
throw TypeError('Cannot assign __parent__ to a non-tree value');
}
}
if(typeof value === 'object') {
value = convert(value);
defineProperty(value, '__parent__', {
value: target[PROXY],
configurable: false,
writable: true
});
}
target[prop] = value;
return value;
},
setProxy: (val) => {
res.proxy = val;
},
get: (target, prop) => {
if( prop === '__raw__' ) {
return __raw__.bind(target);
}
if( prop === TARGET ) {
return _target;
}
if( prop === HANDLER ) {
return res;
}
if( prop === PROXY ) {
return res.proxy;
}
return target[prop];
}
};
return res;
})()
};
/**
* Get the raw value of the tree, without all that proxy stuff.
* @returns {Object} The raw object. Please not that objects will not be the same instances.
* @memberof Tree#
*/
function __raw__() {
let res = setPrototypeOf({}, this.__proto__);
entries(this).forEach( ([key, value], _) => {
if( {}.hasOwnProperty.call(this, key )) {
if( typeof value === 'object') {
res[key] = __raw__(value[TARGET]);
} else {
res[key] = value;
}
}
});
return res;
}
/**
* A class that enables navigation from child properties to parent. WIP - currently figuring out how to make new properties.
* For all purposes this functions as intended, but it doesn't print well in the console. It even perserves prototypes.
* @property {(Branch|*)} * Properties.
* @property {(Tree|Branch)} __parent__ The parent element. This can be changed to move the object to another tree or branch.
*/
class Tree {
/**
* Constructs a new Tree instance.
* @constructs Tree
* @param {Object} obj The object to convert to a tree.
* @throws {TypeError} You probably passed it a primitive.
*/
constructor(obj) {
let conf = { __istree__: true },
res = new Proxy(setPrototypeOf(conf, obj.__proto__), genHandler(conf));
Object.defineProperty(res[HANDLER], 'proxy', {
value: res,
protected: true
});
if( typeof obj !== 'object') {
throw TypeError('Tree expects an object');
} else {
for( let key in obj) {
let value = obj[key];
if(typeof value === 'object') {
res[key] = convert(value);
defineProperty(res[key], '__parent__', {
value: res[PROXY],
configurable: false
writable: true,
});
} else {
res[key] = value.constructor(value);
};
};
};
return res;
}
}
class Branch {
constructor(obj) {
let conf = { __isbranch__: true },
res = new Proxy(setPrototypeOf(conf, obj.__proto__), genHandler(conf));
Object.defineProperty(res[HANDLER], 'proxy', {
value: res,
protected: true
});
return res;
}
}
return Tree;
})();