I wrote a small piece of code for observing changes on JSON object. Whenever there is a change in Object, it should trigger all the handlers with simplistic information about data change.
I am looking for improvements in the code. As I am struck with this pattern, could not think of better ideas of it. Please help.
How I am using this:
var reactiveData = new Observer({a: [1,2, {b: 10}]});
reactiveData.observe('a.3', function() {
console.log('Changed');
});
Implementation:
var Observer = (function() {
function Observer(data) {
this.data = data;
this.handlers = {};
}
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function setValue(object, path, value) {
var a = path, o = object;
if(!Array.isArray(path)) {
a = path.split('.');
}
for (var i = 0; i < a.length - 1; i++) {
var n = a[i];
if (n in o) {
o = o[n];
} else {
o[n] = {};
o = o[n];
}
}
o[a[a.length - 1]] = value;
}
function getValue(object, path, getTrace) {
var o = object, a = path, trace = [];
if(!Array.isArray(path)) {
path = path.replace(/\[(\w+)\]/g, '.$1');
path = path.replace(/^\./, '');
a = path.split('.');
} else {
a = path.slice(0);
}
while (a.length) {
var n = a.shift();
if (n in o) {
o = o[n];
if(getTrace) trace.push(o);
} else {
return getTrace ? trace : undefined;
}
}
return getTrace ? trace : o;
}
function triggerHandlers(path, eventData) {
var that = this, handlers = this.handlers;
if(Array.isArray(path)) path = path.join('.');
var pathHandlers = this.handlers[path] || [];
pathHandlers.forEach(function(handler) {
handler(eventData);
});
}
var prototype = {
constructor: Observer
};
prototype.setValue = function(path, value, options) {
options = options || {};
var trace = this.getValue(path, true) || [];
if(!Array.isArray(path)) path = path.split('.');
var data = trace[trace.length-2], d = this.data, p = path.slice(0);
if(data) {
d = data;
p = p.slice(-1);
}
var previousValue = clone(getValue(d, p));
setValue(d, p, value);
// Lets trigger handlers now.
for(var i = 0; i < path.length; i++) {
// var type = Object.prototype.toString.call(trace[i], i);
var where = path.slice(i + 1);
var evData = {
prev: previousValue,
new: value,
where: where
};
if(options.eventData) evData.original = options.eventData;
where = where.length === 0 ? undefined : where;
triggerHandlers.call(this, path.slice(0, i + 1), evData);
}
};
prototype.getValue = function(path, getTrace) {
return getValue(this.data, path, getTrace);
};
prototype.push = function(path, value) {
var that = this, pathRef = getValue(that.data, path), options = {
eventData: {
type: 'push'
}
};
var eventData = options.eventData;
if(pathRef && Array.isArray(pathRef)) {
eventData.position = pathRef.length;
that.setValue(path + '.' + pathRef.length, value, options);
} else if(pathRef === undefined) {
eventData.position = 0;
that.setValue(path, [value], options);
} else {
return false;
}
};
prototype.unshift = function(path, value) {
var pathRef = getValue(this.data, path), setVal = this.setValue, eventData = {
type: 'unshift',
position: 0
};
if(pathRef && Array.isArray(pathRef)) {
pathRef.unshift(value);
this.setValue(path + '.0', value, eventData);
} else if(pathRef === undefined) {
this.setValue(path, [value], eventData);
}
};
prototype.insert = function(path, pos, value) {
var pathRef = getValue(this.data, path), setVal = this.setValue, eventData = {
type: 'insert',
position: pos
};
if(pathRef && Array.isArray(pathRef)) {
pathRef.splice(pos, 0, value);
this.setValue(path + '.' + pos, value, eventData);
} else if(pathRef === undefined) {
this.setValue(path, [value], eventData);
}
};
prototype.remove = function(path, pos) {
var pathRef = getValue(this.data, path), setVal = this.setValue, eventData = {
type: 'delete',
position: pos
};
if(Array.isArray(pathRef)) {
if(pos > 0 && pos < pathRef.length) {
pathRef.splice(pos, 1);
triggerHandlers.call(this, path, eventData);
}
} else if(typeof pathRef === "object") {
delete pathRef[pos];
triggerHandlers.call(this, path, eventData);
}
};
prototype.observe = function(path, handler) {
this.handlers[path] = this.handlers[path] || [];
this.handlers[path][this.handlers[path].length] = handler;
};
prototype.unobserve = function(path, handler) {
var pathHandlers = this.handlers[path] || [];
var idx = pathHandlers.indexOf(handler);
if(idx > -1) {
pathHandlers.splice(idx, 1);
}
};
Observer.prototype = prototype;
return Observer;
})();