Skip to content

Instantly share code, notes, and snippets.

@rexso
Last active October 1, 2015 09:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rexso/db3df9058df9273f0deb to your computer and use it in GitHub Desktop.
Save rexso/db3df9058df9273f0deb to your computer and use it in GitHub Desktop.
JavaScript class extending Array with event observers
(function() {
"use strict";
function toArray(array) {
return Array.prototype.slice.call(array);
}
function watchProperty(context, property, callback, thisArg) {
var path = property, descriptor, writable, getter, setter, value;
property.split(".").forEach(function(segment, index, array) {
if(context) {
if(++index < array.length) {
context = context[seg];
} else {
if((descriptor = Object.getOwnPropertyDescriptor(context, (property = segment)))) {
getter = descriptor.get || null;
setter = descriptor.set || null;
value = descriptor.value;
writable = descriptor.writable;
}
}
}
});
if(context && ((!getter || setter) || writable)) {
if(delete context[property]) {
Object.defineProperty(context, property, {
configurable: true,
enumerable: true,
get: function() {
return getter ? getter() : value;
},
set: function(val) {
var oldVal = getter ? getter() : value;
(setter && setter(val));
return callback.call((thisArg || context), property, (setter ? val : (value = val)), oldVal);
}
});
return true;
}
}
return false;
}
function unwatchProperty(context, property) {
property.split(".").forEach(function(segment, index, array) {
if(context) {
if(++index < array.length) {
context = context[segment];
} else {
property = segment;
}
}
});
var desc, getter, setter, value;
if((desc = Object.getOwnPropertyDescriptor(context, property)) && (getter = desc.get) && (setter = desc.set)) {
value = context[property];
delete context[property];
context[property] = value;
}
}
function ObserverArray() {
var length = 0, callbacks = {}, observeElements = true;
Object.defineProperties(this, {
// Add callbacks property used internally to track event callbacks
callbacks: {
get: function() {
return callbacks;
},
set: function(value) {
if(typeof value != "object") {
throw new TypeError("ObserverArray.callbacks requires an object");
} else {
callbacks = {};
for(var c in value) {
if(value.hasOwnProperty(c) && typeof value[c] == "function") {
callbacks[c] = value[c];
}
}
}
}
},
// Add copy property used to return a copy of the array
copy: {
get: function() {
return new ObserverArray().assign(this);
}
},
// Add observeElements property used to toggle element observation
observeElements: {
get: function() {
return observeElements;
},
set: function(value) {
if(typeof value != "boolean") {
throw new TypeError("ObserverArray.observeElements requires a boolean");
} else if(value != observeElements) {
observeElements = value;
this.update();
}
}
},
// Redefine length property
length: {
get: function() {
return length;
},
set: function(value) {
if(typeof value != "number" || value < 0 || value > 0xFFFFFFFF) {
throw new RangeError("Invalid array length");
} else if(value != length) {
length = value;
this.emit("length");
}
}
}
});
// Push all arguments to array
toArray(arguments).forEach(function(arg) {
this.push(arg);
}, this);
};
// Inherit Array prototype
ObserverArray.prototype = new Array;
// Override Array prototype methods changing the array
[ "pop", "push", "reverse", "shift", "sort", "splice", "unshift" ].forEach(function(method) {
ObserverArray.prototype[method] = function() {
var args = arguments, len = this.length, ret, desc;
this.emit(method, function() {
ret = Array.prototype[method].apply(this, args);
});
this.update();
return ret;
};
});
// Override Array prototype methods returning a new Array
[ "filter", "map", "slice" ].forEach(function(method) {
ObserverArray.prototype[method] = function() {
return new ObserverArray().assign(Array.prototype[method].apply(toArray(this), arguments));
};
});
// Override Array concat method to return an ObserverArray
ObserverArray.prototype.concat = function() {
var arr = new ObserverArray(), args = [];
toArray(arguments).forEach(function(arg) {
args.push(toArray(arg));
});
return arr.assign(Array.prototype.concat.apply(toArray(this), args));
};
// Add assign method used to assign values from an existing array
ObserverArray.prototype.assign = function(array) {
if(!(array instanceof Array)) {
throw new TypeError("ObserverArray.assign requires an array");
}
if(array instanceof ObserverArray) {
this.observeElements = array.observeElements;
this.callbacks = array.callbacks;
}
this.emit("assign", function() {
Array.prototype.splice.apply(this, [0, this.length].concat(toArray(array)));
});
this.update();
return this;
}
// Add emit method used to invoke callback functions
ObserverArray.prototype.emit = function(event, method, index) {
var ret = this;
// Set event to emit if not a string
(typeof event != "string" && (event = "emit"));
// Prevent nested emits if method is defined
if(typeof method == "function") {
this.emit = function(){};
ret = method.call(this);
delete this.emit;
}
// Invoke change callback for all events if defined
if(typeof this.callbacks.change == "function") {
this.callbacks.change.call(this, event, index);
}
// Invoke event specific callback if defined
if(event != "change" && typeof this.callbacks[event] == "function") {
this.callbacks[event].call(this, event, index);
}
return ret;
};
// Add on method used to define new callback functions
ObserverArray.prototype.on = function(event, callback) {
if(typeof event != "string") {
throw new TypeError("Invalid event name: " + event);
} else {
this.callbacks[event] = (typeof callback == "function" ? callback : null);
}
};
// Add update method used to update element observers
ObserverArray.prototype.update = function() {
function onElementChange(index) {
this.emit("change", null, index);
}
var desc;
// Add or remove event listeners to elements
if(this.observeElements) {
this.forEach(function(elem, index) {
if((desc = Object.getOwnPropertyDescriptor(this, index)) && !desc.get && !desc.set) {
watchProperty(this, index.toString(), onElementChange);
}
}, this);
} else {
this.forEach(function(elem, index) {
if((desc = Object.getOwnPropertyDescriptor(this, index)) && desc.get && desc.set) {
unwatchProperty(this, index.toString());
}
}, this);
}
return this;
};
if(typeof module == "object") {
module.exports = ObserverArray;
}
if(typeof window == "object") {
window.ObserverArray = ObserverArray;
}
return ObserverArray;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment