Last active
October 1, 2015 09:26
-
-
Save rexso/db3df9058df9273f0deb to your computer and use it in GitHub Desktop.
JavaScript class extending Array with event observers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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