Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

adds ObservableArray

behaves just like an Array, has all the methods, but can also use .set/get/observe.
  • Loading branch information...
commit 063c19647d22ebcb93a9ae8042b6582db10e2c15 1 parent bd554ae
@seanmonstar authored
View
21 lib/shipyard/class/Observable.js
@@ -39,19 +39,24 @@ var Observable = module.exports = new Class({
return getter;
}
} else {
- return this.__data[key];
+ return this.__get(key);
}
},
+ __get: function __get(key) {
+ // getting unknown property
+ return this.__data[key];
+ },
+
get: func.overloadGetter(function get(key) {
- return this._get(key);
+ return this._get(String(key));
}),
_set: function set(key, value) {
// We've got 3 ways that .set can set content.
// 1. A match from a previous use of .defineSetter.
// 2. The property already exists in this.
- // 3. Stored in __data.
+ // 3. Stored in __data (by method __set).
var old = this.get(key);
var setter = this.constructor.lookupSetter(key);
if (setter) {
@@ -64,7 +69,7 @@ var Observable = module.exports = new Class({
this[key] = value;
}
} else {
- this.__data[key] = value;
+ this.__set(key, value);
}
if (old !== value) {
this.emit('propertyChange', key, value, old);
@@ -72,8 +77,13 @@ var Observable = module.exports = new Class({
return this;
},
+ __set: function __set(key, val) {
+ // setting unknown property
+ this.__data[key] = val;
+ },
+
set: func.overloadSetter(function set(key, value) {
- this._set(key, value);
+ this._set(String(key), value);
}),
unset: function unset(key) {
@@ -86,6 +96,7 @@ var Observable = module.exports = new Class({
observe: function observe(prop, handler, goDeep) {
// goDeep default is true.
+ prop = String(prop); // normalize numeric keys into strings
goDeep = goDeep === undefined ? true : !!goDeep;
if (goDeep) {
this._deepObserve(prop);
View
142 lib/shipyard/class/ObservableArray.js
@@ -0,0 +1,142 @@
+var Class = require('./Class'),
+ Observable = require('./Observable'),
+ array = require('../utils/array');
+
+var SLICE = Array.prototype.slice;
+
+/*
+ * ObservableArray
+ * ==============
+ *
+ * An instanceof Observable, so property changes will fire
+ * propertyChange events, and you can .observe() properties (a specific
+ * index, or length?). Perhaps you need to bind to the first element in
+ * the array, always. That can be done with `arr.observe('0', fn)`.
+ *
+ * More likely, you'll want to know when the array as a whole changes.
+ * You could just listen for propertyChange event, but then you lose out
+ * on anything extra that .observe does, just as deep observing. Plus,
+ * it makes people have to use 2 different APIs, which is no fun.
+ *
+ * TODO:
+ * So, there should be a property that you can observe that essentially
+ * means "observe the entire freaking thing, batteries included."
+ *
+ * arr.observe('[]', fn) ?
+ * arr.observe('this', fn) ?
+ *
+ * For performance reasons, it'd be good if this property could pass
+ * only the values that changed. This way, something like a ListView can
+ * know exactly which children need to be re-rendered, instead of
+ * re-rendering the whole enchilda...
+ *
+ */
+var ObservableArray = module.exports = new Class({
+
+ Extends: Observable,
+
+ length: 0,
+
+ initialize: function ObservableArray(/* array, or args... */) {
+ var arr;
+ if (arguments.length > 1) {
+ arr = array.from(arguments);
+ } else if (arguments.length === 1) {
+ arr = array.from(arguments[0]);
+ }
+ this.parent(arr);
+ },
+
+ __set: function __set(key, val) {
+ var index = parseInt(key, 10);
+ if (!isNaN(index)) {
+ this[index] = val;
+ if (index + 1 > this.get('length')) {
+ this.set('length', index + 1);
+ }
+ } else {
+ this.parent(key, val);
+ }
+ }
+
+});
+
+// mutators
+// push, pop, shift, unshift, splice, reverse, sort
+
+ObservableArray.implement({
+
+ push: function push() {
+ array.from(arguments).forEach(function(val) {
+ this.set(this.get('length'), val);
+ }, this);
+ return this.get('length');
+ },
+
+ pop: function pop() {
+ if (this.get('length') > 0) {
+ var index = this.get('length') - 1;
+ var ret = this[index];
+ this.set(index, undefined);
+ this.set('length', index);
+ return ret;
+ }
+ },
+
+ shift: function shift() {
+ if (this.get('length') > 0) {
+ var ret = this[0];
+ this.set(SLICE.call(this, 1));
+ this.set('length', this.get('length') - 1);
+ return ret;
+ }
+ },
+
+ unshift: function unshift() {
+ var args = array.from(arguments);
+ args = args.concat(array.from(this));
+ this.set(args);
+ return this.get('length');
+ },
+
+ sort: function sort(fn) {
+ var arr = array.from(this);
+ arr.sort(fn);
+ this.set(arr);
+ },
+
+ reverse: function reverse() {
+ var arr = array.from(this);
+ arr.reverse();
+ this.set(arr);
+ },
+
+ splice: function splice(index, howMany /*. args...*/) {
+ throw new Error('Not Implemented');
+ }
+
+});
+
+
+
+// accessors
+var accessors = ['indexOf', 'lastIndexOf', 'join'];
+accessors.forEach(function(method) {
+ ObservableArray.implement(method, Array.prototype[method]);
+});
+
+
+// iterators
+// if these return a new Array, wrap it in ObservableArray
+var iterators = ['forEach', 'some', 'every', 'filter', 'map', 'slice', 'concat'];
+iterators.forEach(function(method) {
+ ObservableArray.implement(method, function() {
+ var ret = Array.prototype[method].apply(this, arguments);
+ if (ret && ret.length) {
+ return new ObservableArray(ret);
+ } else {
+ return ret;
+ }
+ });
+});
+
View
2  lib/shipyard/utils/Accessor.js
@@ -40,7 +40,7 @@ module.exports = function(singular, plural) {
return this[accessors][key];
}
for (var l = this[matchers].length; l--; l) {
- var matcher = this[matchers][l], matched = key.match(matcher.regexp);
+ var matcher = this[matchers][l], matched = String(key).match(matcher.regexp);
if (matched && (matched = matched.slice(1))) {
if (matcher.type === 'function') {
return function() {
View
3  lib/shipyard/utils/function.js
@@ -7,7 +7,8 @@ exports.noop = function noop() {};
// Allows fn(params) -> fn(key, value) for key, value in params
exports.overloadSetter = function(fn) {
return function overloadedSetter(keyOrObj, value) {
- if (typeOf(keyOrObj) !== 'string') {
+ var type = typeOf(keyOrObj);
+ if (type !== 'string' && type !== 'number') {
for (var key in keyOrObj) {
fn.call(this, key, keyOrObj[key]);
}
View
59 test/unit/class/obs_array.js
@@ -0,0 +1,59 @@
+var Observable = require('../../../lib/shipyard/class/Observable'),
+ ObservableArray = require('../../../lib/shipyard/class/ObservableArray');
+
+module.exports = {
+ 'ObservableArray': function(it, setup) {
+
+ it('should be an instance of Observable', function(expect) {
+ var arr = new ObservableArray();
+ expect(arr).toBeAnInstanceOf(Observable);
+ });
+
+ it('should be array-like', function(expect) {
+ var arr = new ObservableArray(1, 2, 3);
+ expect(arr.length).toBe(3);
+ expect(arr[0]).toBe(1);
+ });
+
+ it('should wrap a native Array', function(expect) {
+ var arr = new ObservableArray(['a', 'b', 'c', 'd']);
+ expect(arr.length).toBe(4);
+ expect(arr[2]).toBe('c');
+ });
+
+ it('should have all Array methods', function(expect) {
+ var arr = new ObservableArray();
+
+ expect(arr.indexOf(3)).toBe(-1);
+
+ expect(arr.push(3)).toBe(1);
+ expect(arr.indexOf(3)).toBe(0);
+
+ expect(arr.unshift('foo')).toBe(2);
+
+ expect(arr.shift()).toBe('foo');
+ expect(arr.length).toBe(1);
+
+ expect(arr.pop()).toBe(3);
+ expect(arr.length).toBe(0);
+ });
+
+ it('should have observable indices', function(expect) {
+ var spy = this.createSpy();
+ var arr = new ObservableArray();
+
+ // first in list
+ arr.observe('0', spy);
+ arr.push('a');
+ expect(spy).toHaveBeenCalled();
+
+ var spy2 = this.createSpy();
+ arr.observe('0', spy2);
+ arr.push('b');
+
+ expect(spy2).not.toHaveBeenCalled();
+ arr.shift();
+ expect(spy2).toHaveBeenCalled();
+ });
+ }
+};
Please sign in to comment.
Something went wrong with that request. Please try again.