@@ -39,6 +44,16 @@
+
diff --git a/docs/reactive-object.html b/docs/reactive-object.html
index 8e83405..9480799 100644
--- a/docs/reactive-object.html
+++ b/docs/reactive-object.html
@@ -19,6 +19,11 @@
+
+ handlebars-list.coffee
+
+
+
reactive-array-test.coffee
@@ -39,6 +44,16 @@
+
+ reactive-list-test.coffee
+
+
+
+
+ reactive-list.coffee
+
+
+
reactive-object-test.coffee
diff --git a/example/client/main.html b/example/client/main.html
new file mode 100644
index 0000000..3812e64
--- /dev/null
+++ b/example/client/main.html
@@ -0,0 +1,21 @@
+
+
Reactive Extra Examples
+
+
+
+ {{> list }}
+
+
+
+
+ {{#each list}}
+
+
+
+ {{/each}}
+
+ Reverse
+ Add
+ Remove
+ Sort
+
\ No newline at end of file
diff --git a/example/client/main.js b/example/client/main.js
new file mode 100644
index 0000000..15b9686
--- /dev/null
+++ b/example/client/main.js
@@ -0,0 +1,24 @@
+MyList = new ReactiveList('a', 'b', 'c');
+
+Template.list.list = function () {
+ return MyList;
+};
+
+Template.list.events({
+ 'click #list-reverse': function(event) {
+ event.preventDefault();
+ MyList.reverse();
+ },
+ 'click #list-add': function(event) {
+ event.preventDefault();
+ MyList.push((new Date()).toLocaleTimeString());
+ },
+ 'click #list-remove': function(event) {
+ event.preventDefault();
+ MyList.pop();
+ },
+ 'click #list-sort': function(event) {
+ event.preventDefault();
+ MyList.sort();
+ }
+});
\ No newline at end of file
diff --git a/example/packages/reactive-extra/lib/handlebars-list.js b/example/packages/reactive-extra/lib/handlebars-list.js
new file mode 100644
index 0000000..fd62e42
--- /dev/null
+++ b/example/packages/reactive-extra/lib/handlebars-list.js
@@ -0,0 +1,185 @@
+// Generated by CoffeeScript 1.6.2
+(function() {
+ var HandlebarsEach, SparkListObserve, findParentOfType, makeRange;
+
+ HandlebarsEach = Handlebars._default_helpers.each;
+
+ Handlebars._default_helpers.each = function(arg, options) {
+ var elseFunc, itemFunc;
+
+ if (!(arg && arg instanceof ReactiveList)) {
+ return HandlebarsEach.call(this, arg, options);
+ }
+ itemFunc = function(item) {
+ return Spark.labelBranch((item && item._id) || Spark.UNIQUE_LABEL, function() {
+ return Spark.setDataContext(item, Spark.isolate(_.bind(options.fn, null, item)));
+ });
+ };
+ elseFunc = function() {
+ if (options.inverse) {
+ return Spark.isolate(options.inverse);
+ } else {
+ return '';
+ }
+ };
+ return SparkListObserve(arg, itemFunc, elseFunc);
+ };
+
+ Spark._ANNOTATION_LIST_OBSERVE = "list_observe";
+
+ Spark._ANNOTATION_LIST_OBSERVE_ITEM = "list_observe_item";
+
+ SparkListObserve = function(observable, itemFunc, elseFunc) {
+ var callbacks, cleanup, handle, html, itemArr, later, maybeAnnotate, notifyParentsRendered, observerCallbacks, outerRange, renderer, stopped;
+
+ elseFunc = elseFunc || function() {
+ return '';
+ };
+ callbacks = {};
+ observerCallbacks = {};
+ _.each(["addedAt", "changedAt", "removedAt", "movedTo"], function(name) {
+ return observerCallbacks[name] = function() {
+ return callbacks[name].apply(null, arguments);
+ };
+ });
+ itemArr = [];
+ _.extend(callbacks, {
+ addedAt: function(val, idx) {
+ return itemArr[idx] = {
+ liveRange: null,
+ value: val
+ };
+ }
+ });
+ handle = observable.observe(observerCallbacks);
+ renderer = Spark._currentRenderer.get();
+ maybeAnnotate = renderer ? _.bind(renderer.annotate, renderer) : function(html) {
+ return html;
+ };
+ html = '';
+ outerRange = null;
+ if (itemArr.length === 0) {
+ html = elseFunc();
+ } else {
+ _.each(itemArr, function(elt) {
+ return html += maybeAnnotate(itemFunc(elt.value), Spark._ANNOTATION_LIST_OBSERVE_ITEM, function(range) {
+ elt.liveRange = range;
+ });
+ });
+ }
+ stopped = false;
+ cleanup = function() {
+ handle.stop();
+ return stopped = true;
+ };
+ html = maybeAnnotate(html, Spark._ANNOTATION_LIST_OBSERVE, function(range) {
+ if (!range) {
+ cleanup();
+ return;
+ }
+ outerRange = range;
+ outerRange.finalize = cleanup;
+ });
+ if (!renderer) {
+ cleanup();
+ return html;
+ }
+ notifyParentsRendered = function() {
+ var walk;
+
+ walk = outerRange;
+ while ((walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk))) {
+ walk.rendered.call(walk.landmark);
+ }
+ };
+ later = function(func) {
+ Deps.afterFlush(function() {
+ if (!stopped) {
+ func();
+ }
+ });
+ };
+ _.extend(callbacks, {
+ addedAt: function(val, idx) {
+ return later(function() {
+ var frag, range;
+
+ frag = Spark.render(_.bind(itemFunc, null, val));
+ DomUtils.wrapFragmentForContainer(frag, outerRange.containerNode());
+ range = makeRange(Spark._ANNOTATION_LIST_ITEM, frag);
+ if (itemArr.length === 0) {
+ Spark.finalize(outerRange.replaceContents(frag));
+ } else {
+ itemArr[idx - 1].liveRange.insertAfter(frag);
+ }
+ return itemArr[idx] = {
+ liveRange: range,
+ value: val
+ };
+ });
+ },
+ removedAt: function(val, idx) {
+ return later(function() {
+ var frag;
+
+ if (itemArr.length === 1) {
+ frag = Spark.render(elseFunc);
+ DomUtils.wrapFragmentForContainer(frag, outerRange.containerNode());
+ Spark.finalize(outerRange.replaceContents(frag));
+ } else {
+ Spark.finalize(itemArr[idx].liveRange.extract());
+ }
+ itemArr.splice(idx, 1);
+ return notifyParentsRendered();
+ });
+ },
+ movedTo: function(val, fromIdx, toIdx) {
+ return later(function() {
+ var elt, frag;
+
+ elt = (itemArr.splice(fromIdx, 1))[0];
+ frag = elt.liveRange.extract();
+ if (toIdx in itemArr) {
+ itemArr[toIdx].liveRange.insertBefore(frag);
+ } else {
+ itemArr[toIdx - 1].liveRange.insertAfter(frag);
+ }
+ itemArr.splice(toIdx, 0, elt);
+ return notifyParentsRendered();
+ });
+ },
+ changedAt: function(val, idx) {
+ return later(function() {
+ var elt;
+
+ elt = itemArr[idx];
+ if (!elt) {
+ throw new Error("Unknown item at index: " + idx);
+ }
+ elt.value = val;
+ return Spark.renderToRange(elt.liveRange, _.bind(itemFunc, null, elt.value));
+ });
+ }
+ });
+ return html;
+ };
+
+ findParentOfType = function(type, range) {
+ while (true) {
+ range = range.findParent();
+ if (!(range && range.type !== type)) {
+ break;
+ }
+ }
+ return range;
+ };
+
+ makeRange = function(type, start, end, inner) {
+ var range;
+
+ range = new LiveRange(Spark._TAG, start, end, inner);
+ range.type = type;
+ return range;
+ };
+
+}).call(this);
diff --git a/example/packages/reactive-extra/lib/reactive-array.js b/example/packages/reactive-extra/lib/reactive-array.js
index 8531cf5..7603587 100644
--- a/example/packages/reactive-extra/lib/reactive-array.js
+++ b/example/packages/reactive-extra/lib/reactive-array.js
@@ -35,6 +35,33 @@
return this._list.slice();
};
+ ReactiveArray.prototype.reverse = function() {
+ Array.prototype.reverse.apply(this._list);
+ for (left = 0, right = this._list.length - 1; left < right; left += 1, right -= 1) {
+ if (left === right) { continue; }
+ if (this._listDeps[left]) { this._listDeps[left].changed(); }
+ if (this._listDeps[right]) { this._listDeps[right].changed(); }
+ };
+ this._listValueDep.changed();
+ return this;
+ };
+
+ ReactiveArray.prototype.sort = function() {
+ var dep, i, orgList, _i, _len, _ref;
+
+ orgList = this._list.slice();
+ Array.prototype.sort.apply(this._list, arguments);
+ _ref = this._listDeps;
+ for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
+ dep = _ref[i];
+ if (dep && orgList[i] !== this._list[i]) {
+ dep.changed();
+ }
+ }
+ this._listValueDep.changed();
+ return this;
+ };
+
ReactiveArray.prototype.indexOf = function(searchElement, fromIndex) {
var i, idx, _base, _i, _ref, _ref1;
@@ -115,7 +142,7 @@
};
ReactiveArray.prototype.clone = function() {
- return ReactiveArray.wrap(this._list);
+ return this.constructor.wrap(this._list);
};
ReactiveArray.prototype.equals = function(obj) {
@@ -177,11 +204,13 @@
ReactiveArray.prototype._indexSet = function(i, val) {
var _ref;
- this._list[i] = val;
- if ((_ref = this._listDeps[i]) != null) {
- _ref.changed();
+ if (this._list[i] !== val) {
+ this._list[i] = val;
+ if ((_ref = this._listDeps[i]) != null) {
+ _ref.changed();
+ }
+ this._listValueDep.changed();
}
- this._listValueDep.changed();
return val;
};
@@ -226,24 +255,6 @@
};
});
- _.each(['reverse', 'sort'], function(m) {
- return ReactiveArray.prototype[m] = function() {
- var dep, i, orgList, _i, _len, _ref;
-
- orgList = this._list.slice();
- Array.prototype[m].apply(this._list, arguments);
- _ref = this._listDeps;
- for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
- dep = _ref[i];
- if (dep && orgList[i] !== this._list[i]) {
- dep.changed();
- }
- }
- this._listValueDep.changed();
- return this;
- };
- });
-
_.each(['concat', 'slice'], function(m) {
return ReactiveArray.prototype[m] = function() {
var rtn;
@@ -251,7 +262,7 @@
rtn = Array.prototype[m].apply(this._list, arguments);
this._listLengthDep.depend();
this._listValueDep.depend();
- return ReactiveArray.wrap(rtn);
+ return this.constructor.wrap(rtn);
};
});
@@ -277,7 +288,7 @@
rtn = _[m].call(null, this._list, iteratorProxy, thisArg);
this._listLengthDep.depend();
this._listValueDep.depend();
- return ReactiveArray.wrap(rtn);
+ return this.constructor.wrap(rtn);
};
});
diff --git a/example/packages/reactive-extra/lib/reactive-list-test.js b/example/packages/reactive-extra/lib/reactive-list-test.js
new file mode 100644
index 0000000..a155a38
--- /dev/null
+++ b/example/packages/reactive-extra/lib/reactive-list-test.js
@@ -0,0 +1,312 @@
+// Generated by CoffeeScript 1.6.2
+(function() {
+ Tinytest.add("ReactiveList - added/addedAt", function(test) {
+ var addedAtX, addedIdx, addedVal, addedX, callbacks, handle, invalidCall, invalidX, list;
+
+ invalidX = 0;
+ invalidCall = function() {
+ return invalidX++;
+ };
+ addedAtX = 0;
+ addedX = 0;
+ addedVal = 'init';
+ addedIdx = 0;
+ callbacks = {
+ added: function(val) {
+ test.equal(val, addedVal);
+ return addedX++;
+ },
+ addedAt: function(val, idx) {
+ test.equal(val, addedVal);
+ test.equal(idx, addedIdx);
+ return addedAtX++;
+ },
+ changed: invalidCall,
+ changedAt: invalidCall,
+ removed: invalidCall,
+ removedAt: invalidCall,
+ movedTo: invalidCall
+ };
+ list = new ReactiveList('init');
+ handle = list.observe(callbacks);
+ test.equal(invalidX, 0);
+ test.equal(addedX, 1);
+ test.equal(addedAtX, 1);
+ addedVal = 'push';
+ addedIdx = 1;
+ list.push(addedVal);
+ test.equal(invalidX, 0);
+ test.equal(addedX, 2);
+ test.equal(addedAtX, 2);
+ addedVal = 'unshift';
+ addedIdx = 0;
+ list.unshift(addedVal);
+ test.equal(invalidX, 0);
+ test.equal(addedX, 3);
+ test.equal(addedAtX, 3);
+ addedVal = 'splice1';
+ addedIdx = 1;
+ list.splice(1, 0, 'splice1');
+ test.equal(invalidX, 0);
+ test.equal(addedX, 4);
+ test.equal(addedAtX, 4);
+ addedVal = 'splice2';
+ addedIdx = 3;
+ list.splice(-1, 0, 'splice2');
+ test.equal(invalidX, 0);
+ test.equal(addedX, 5);
+ return test.equal(addedAtX, 5);
+ });
+
+ Tinytest.add("ReactiveList - remove/removeAt", function(test) {
+ var callbacks, handle, invalidCall, invalidX, list, removedAtX, removedIdx, removedVal, removedX;
+
+ invalidX = -14;
+ invalidCall = function() {
+ return invalidX++;
+ };
+ removedAtX = 0;
+ removedX = 0;
+ removedVal = null;
+ removedIdx = 0;
+ callbacks = {
+ removed: function(val) {
+ test.equal(val, removedVal);
+ return removedX++;
+ },
+ removedAt: function(val, idx) {
+ test.equal(val, removedVal);
+ test.equal(idx, removedIdx);
+ return removedAtX++;
+ },
+ changed: invalidCall,
+ changedAt: invalidCall,
+ added: invalidCall,
+ addedAt: invalidCall,
+ movedTo: invalidCall
+ };
+ list = new ReactiveList('shift', 'keep1', 'splice1', 'keep2', 'splice2', 'keep3', 'pop');
+ handle = list.observe(callbacks);
+ test.equal(invalidX, 0, 'observer');
+ test.equal(removedX, 0, 'observer');
+ test.equal(removedAtX, 0, 'observer');
+ removedVal = 'pop';
+ removedIdx = 6;
+ list.pop();
+ test.equal(invalidX, 0, 'pop');
+ test.equal(removedX, 1, 'pop');
+ test.equal(removedAtX, 1, 'pop');
+ removedVal = 'shift';
+ removedIdx = 0;
+ list.shift();
+ test.equal(invalidX, 0);
+ test.equal(removedX, 2);
+ test.equal(removedAtX, 2);
+ removedVal = 'splice1';
+ removedIdx = 1;
+ list.splice(1, 1);
+ test.equal(invalidX, 0);
+ test.equal(removedX, 3);
+ test.equal(removedAtX, 3);
+ removedVal = 'splice2';
+ removedIdx = 2;
+ list.splice(-2, 1);
+ test.equal(invalidX, 0);
+ test.equal(removedX, 4);
+ return test.equal(removedAtX, 4);
+ });
+
+ Tinytest.add("ReactiveList - splice", function(test) {
+ var addedAtX, addedRun, addedX, callbacks, changedAtX, changedRun, changedX, list, movedRun, movedX, removedAtX, removedRun, removedX;
+
+ addedX = addedAtX = -7;
+ changedX = changedAtX = 0;
+ removedX = removedAtX = 0;
+ movedX = 0;
+ changedRun = [
+ {
+ val: '2',
+ newVal: 'replacement1',
+ idx: 1
+ }, {
+ val: '4',
+ newVal: 'replacement2',
+ idx: 3
+ }, {
+ val: '5',
+ newVal: 'replacement3',
+ idx: 4
+ }, {
+ val: '3',
+ newVal: 'replacement4',
+ idx: 2
+ }, {
+ val: 'replacement4',
+ newVal: 'replacement5',
+ idx: 2
+ }
+ ];
+ addedRun = [
+ {
+ val: '3.5',
+ idx: 3
+ }
+ ];
+ removedRun = [
+ {
+ val: '3.5',
+ idx: 3
+ }
+ ];
+ movedRun = [];
+ callbacks = {
+ added: function(val) {
+ var eq;
+
+ if (addedX >= 0) {
+ eq = addedRun[addedX];
+ test.equal(val, eq.val, 'added - val: ' + addedX);
+ }
+ return addedX++;
+ },
+ addedAt: function(val, idx) {
+ var eq;
+
+ if (addedAtX >= 0) {
+ eq = addedRun[addedAtX];
+ test.equal(val, eq.val, 'addedAt - val: ' + addedAtX);
+ test.equal(idx, eq.idx, 'addedAt - idx: ' + addedAtX);
+ }
+ return addedAtX++;
+ },
+ changed: function(val, oldVal) {
+ var eq;
+
+ eq = changedRun[changedX];
+ test.equal(val, eq.newVal, 'changed - val: ' + changedX);
+ test.equal(oldVal, eq.val, 'changed - oldVal: ' + changedX);
+ return changedX++;
+ },
+ changedAt: function(val, oldVal, idx) {
+ var eq;
+
+ eq = changedRun[changedAtX];
+ test.equal(val, eq.newVal, 'changedAt - val: ' + changedAtX);
+ test.equal(oldVal, eq.val, 'changedAt - oldVal: ' + changedAtX);
+ test.equal(idx, eq.idx, 'changedAt - idx: ' + changedAtX);
+ return changedAtX++;
+ },
+ removed: function(val) {
+ var eq;
+
+ eq = removedRun[removedX];
+ test.equal(val, eq.val, 'removed - val: ' + removedX);
+ return removedX++;
+ },
+ removedAt: function(val, idx) {
+ var eq;
+
+ eq = removedRun[removedAtX];
+ test.equal(val, eq.val, 'removedAt - val: ' + removedAtX);
+ test.equal(idx, eq.idx, 'removedAt - idx: ' + removedAtX);
+ return removedAtX++;
+ },
+ movedTo: function(val, idx) {
+ var eq;
+
+ eq = movedRun[movedX];
+ test.equal(val, eq.val, 'movedTo - val: ' + movedX);
+ test.equal(idx, eq.idx, 'movedTo - idx: ' + movedX);
+ return movedX++;
+ }
+ };
+ list = new ReactiveList('1', '2', '3', '4', '5', '6', '7');
+ list.observe(callbacks);
+ test.equal(addedX, 0, 'added: observe');
+ test.equal(addedAtX, 0, 'addedAt: observe');
+ test.equal(removedX, 0, 'removedAt: observe');
+ test.equal(removedAtX, 0, 'removedAt: observe');
+ test.equal(changedX, 0, 'changed: observe');
+ test.equal(changedAtX, 0, 'changedAt: observe');
+ test.equal(movedX, 0, 'moved: observe');
+ list.splice(1, 1, changedRun[0].newVal);
+ test.equal(addedX, 0, 'added: run 1');
+ test.equal(addedAtX, 0, 'addedAt: run 1');
+ test.equal(removedX, 0, 'removedAt: run 1');
+ test.equal(removedAtX, 0, 'removedAt: run 1');
+ test.equal(changedX, 1, 'changed: run 1');
+ test.equal(changedAtX, 1, 'changedAt: run 1');
+ test.equal(movedX, 0, 'moved: run 1');
+ list.splice(3, 2, changedRun[1].newVal, changedRun[2].newVal);
+ test.equal(addedX, 0, 'added: run 2');
+ test.equal(addedAtX, 0, 'addedAt: run 2');
+ test.equal(removedX, 0, 'removedAt: run 2');
+ test.equal(removedAtX, 0, 'removedAt: run 2');
+ test.equal(changedX, 3, 'changed: run 2');
+ test.equal(changedAtX, 3, 'changedAt: run 2');
+ test.equal(movedX, 0, 'moved: run 2');
+ list.splice(2, 1, changedRun[3].newVal, addedRun[0].val);
+ test.equal(addedX, 1, 'added: run 3');
+ test.equal(addedAtX, 1, 'addedAt: run 3');
+ test.equal(removedX, 0, 'removedAt: run 3');
+ test.equal(removedAtX, 0, 'removedAt: run 3');
+ test.equal(changedX, 4, 'changed: run 3');
+ test.equal(changedAtX, 4, 'changedAt: run 3');
+ test.equal(movedX, 0, 'moved: run 3');
+ list.splice(2, 2, changedRun[4].newVal);
+ test.equal(addedX, 1, 'added: run 4');
+ test.equal(addedAtX, 1, 'addedAt: run 4');
+ test.equal(removedX, 1, 'removedAt: run 4');
+ test.equal(removedAtX, 1, 'removedAt: run 4');
+ test.equal(changedX, 5, 'changed: run 4');
+ test.equal(changedAtX, 5, 'changedAt: run 4');
+ return test.equal(movedX, 0, 'moved: run 4');
+ });
+
+ Tinytest.add("ReactiveList - reverse", function(test) {
+ var arr, arrReversed, list;
+
+ arr = ['1', '2', '3', '4', '5', '6', '7'];
+ arrReversed = arr.slice().reverse();
+ list = ReactiveList.wrap(arr);
+ list.observe({
+ movedTo: function(val, fromIdx, toIdx) {
+ arr.splice(fromIdx, 1);
+ return arr.splice(toIdx, 0, val);
+ }
+ });
+ test.equal(list.reverse(), ReactiveList.wrap(arrReversed));
+ test.equal(arr, arrReversed);
+ list.pop();
+ arr.pop();
+ arrReversed.pop();
+ arrReversed.reverse();
+ test.equal(list.reverse(), ReactiveList.wrap(arrReversed));
+ return test.equal(arr, arrReversed);
+ });
+
+ Tinytest.add("ReactiveList - sort", function(test) {
+ var arr, arrSorted, arrs, list, _i, _len, _results;
+
+ arrs = [['1', '7', '3', '4', '2', '6', '8', '5'], ['1', '2', '3', '4', '5', '6', '7'].reverse(), ['1', '7', '3', '7', '4', '2', '6', '8', '5'], ['1', '7', '3', '7', '7', '4', '7', '2', '6', '8', '5'], ['d', 'a', 'c', 'b', 'z', 'y', 'y']];
+ _results = [];
+ for (_i = 0, _len = arrs.length; _i < _len; _i++) {
+ arr = arrs[_i];
+ arrSorted = arr.slice().sort();
+ list = ReactiveList.wrap(arr);
+ list.observe({
+ movedTo: function(docVal, fromIdx, toIdx) {
+ var val;
+
+ val = (arr.splice(fromIdx, 1))[0];
+ test.equal(val, docVal);
+ return arr.splice(toIdx, 0, val);
+ }
+ });
+ test.equal(list.sort(), ReactiveList.wrap(arrSorted));
+ _results.push(test.equal(arr, arrSorted));
+ }
+ return _results;
+ });
+
+}).call(this);
diff --git a/example/packages/reactive-extra/lib/reactive-list.js b/example/packages/reactive-extra/lib/reactive-list.js
new file mode 100644
index 0000000..097368b
--- /dev/null
+++ b/example/packages/reactive-extra/lib/reactive-list.js
@@ -0,0 +1,276 @@
+// Generated by CoffeeScript 1.6.2
+(function() {
+ var LiveHandler,
+ __hasProp = {}.hasOwnProperty,
+ __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
+
+ this.ReactiveList = (function(_super) {
+ __extends(ReactiveList, _super);
+
+ function ReactiveList() {
+ this._definePrivateProperty('_handlers', []);
+ ReactiveList.__super__.constructor.apply(this, arguments);
+ }
+
+ ReactiveList.prototype.observe = function(callbacks) {
+ var handle, i, _i, _ref;
+
+ handle = new LiveHandler(callbacks);
+ this._handlers.push(handle);
+ for (i = _i = 0, _ref = this._list.length; _i < _ref; i = _i += 1) {
+ this._trigger('added', this._list[i], i);
+ }
+ return handle;
+ };
+
+ ReactiveList.prototype.pop = function() {
+ var rtn;
+
+ rtn = ReactiveList.__super__.pop.apply(this, arguments);
+ this._trigger('removed', rtn, this._list.length);
+ return rtn;
+ };
+
+ ReactiveList.prototype.push = function() {
+ var i, orgLength, rtn, _i, _ref;
+
+ orgLength = this._list.length;
+ rtn = ReactiveList.__super__.push.apply(this, arguments);
+ for (i = _i = orgLength, _ref = this._list.length; _i < _ref; i = _i += 1) {
+ this._trigger('added', this._list[i], i);
+ }
+ return rtn;
+ };
+
+ ReactiveList.prototype.shift = function() {
+ var rtn;
+
+ rtn = ReactiveList.__super__.shift.apply(this, arguments);
+ this._trigger('removed', rtn, 0);
+ return rtn;
+ };
+
+ ReactiveList.prototype.unshift = function() {
+ var i, orgLength, rtn, _i, _ref;
+
+ orgLength = this._list.length;
+ rtn = ReactiveList.__super__.unshift.apply(this, arguments);
+ for (i = _i = 0, _ref = this._list.length - orgLength; _i < _ref; i = _i += 1) {
+ this._trigger('added', this._list[i], i);
+ }
+ return rtn;
+ };
+
+ ReactiveList.prototype.splice = function() {
+ var addAmount, changedAmount, i, idx, orgList, rmAmount, rtn, _i, _j, _k, _l, _ref, _ref1;
+
+ orgList = this._list.slice();
+ rtn = ReactiveList.__super__.splice.apply(this, arguments);
+ idx = arguments[0];
+ if (idx < 0) {
+ idx = orgList.length + idx;
+ }
+ rmAmount = arguments.length > 1 ? arguments[1] : orgList.length - idx;
+ if (arguments.length > 2) {
+ addAmount = arguments.length - 2;
+ if (rmAmount > 0) {
+ changedAmount = rmAmount > addAmount ? addAmount : rmAmount;
+ for (i = _i = 0; _i < changedAmount; i = _i += 1) {
+ this._trigger('changed', this._list[idx], orgList[idx], idx);
+ idx++;
+ }
+ addAmount = addAmount - changedAmount;
+ rmAmount = rmAmount - changedAmount;
+ }
+ if ((rmAmount - addAmount) > 0) {
+ for (i = _j = 0, _ref = rmAmount - addAmount; _j < _ref; i = _j += 1) {
+ this._trigger('removed', orgList[idx + i], idx + i);
+ }
+ } else if ((rmAmount - addAmount) < 0) {
+ for (i = _k = 0, _ref1 = addAmount - rmAmount; _k < _ref1; i = _k += 1) {
+ this._trigger('added', this._list[idx + i], idx + i);
+ }
+ }
+ } else if (rmAmount > 0) {
+ for (i = _l = 0; _l < rmAmount; i = _l += 1) {
+ this._trigger('removed', orgList[idx + i], idx + i);
+ }
+ }
+ return rtn;
+ };
+
+ ReactiveList.prototype.reverse = function() {
+ var array, length;
+
+ ReactiveList.__super__.reverse.apply(this, arguments);
+ array = this._list;
+ length = this._list.length;
+ for (left = 0, right = length - 1; left < right; left += 1, right -= 1) {
+ if (right === left) { continue; }
+ this._trigger('movedTo', array[left], right, left);
+ this._trigger('movedTo', array[right], left+1, right);
+ };
+ return this;
+ };
+
+ ReactiveList.prototype.sort = function() {
+ var currentPosition, finalPosition, lastMove, length, move, moves, org, skip, _i, _len;
+
+ org = this._list.slice();
+ ReactiveList.__super__.sort.apply(this, arguments);
+ if (!this._hasActiveTrigger('movedTo')) {
+ return this;
+ }
+ length = this._list.length;
+ moves = [];
+ currentPosition = 0;
+ while (currentPosition < length) {
+ finalPosition = this._list.indexOf(org[currentPosition]);
+ if (currentPosition + 1 === finalPosition) {
+ while (org[currentPosition + 1] === this._list[finalPosition + 1]) {
+ finalPosition++;
+ currentPosition++;
+ }
+ if (org[currentPosition] === this._list[finalPosition]) {
+ finalPosition++;
+ currentPosition++;
+ }
+ finalPosition = this._list.indexOf(org[currentPosition]);
+ }
+ if (org[currentPosition] === org[currentPosition + 1]) {
+ while (org[currentPosition - 1] === this._list[finalPosition]) {
+ finalPosition++;
+ }
+ if (org[currentPosition] === this._list[finalPosition]) {
+ finalPosition++;
+ }
+ finalPosition = this._list.indexOf(org[currentPosition], finalPosition);
+ }
+ move = {
+ from: currentPosition,
+ to: finalPosition
+ };
+ skip = finalPosition === -1 || lastMove && lastMove.to === move.to && lastMove.from === move.from;
+ if (!skip && finalPosition !== currentPosition) {
+ moves.push(move);
+ lastMove = move;
+ org.splice(move.to, 0, (org.splice(move.from, 1))[0]);
+ if (finalPosition < currentPosition) {
+ currentPosition = finalPosition;
+ } else {
+ currentPosition--;
+ }
+ }
+ currentPosition++;
+ }
+ for (_i = 0, _len = moves.length; _i < _len; _i++) {
+ move = moves[_i];
+ this._trigger('movedTo', this._list[move.to], move.from, move.to);
+ }
+ return this;
+ };
+
+ ReactiveList.prototype.typeName = function() {
+ return 'reactive-list';
+ };
+
+ ReactiveList.prototype.equals = function(obj) {
+ return (obj != null) && obj instanceof ReactiveList && _.isEqual(obj._list, this._list);
+ };
+
+ ReactiveList.prototype._trigger = function(evt) {
+ var args, evtArgs, evtAt, evtAtArgs, handler, i, self, trigger, _ref;
+
+ self = this;
+ args = _.toArray(arguments).slice(1);
+ if (evt === 'movedTo') {
+ trigger = function(callbacks) {
+ if (evt in callbacks) {
+ return callbacks[evt].apply(self, args);
+ }
+ };
+ } else {
+ evtArgs = args.slice(0, -1);
+ evtAt = evt + 'At';
+ evtAtArgs = args;
+ trigger = function(callbacks) {
+ if (evt in callbacks) {
+ callbacks[evt].apply(self, evtArgs);
+ }
+ if (evtAt in callbacks) {
+ return callbacks[evtAt].apply(self, evtAtArgs);
+ }
+ };
+ }
+ _ref = this._handlers;
+ for (i in _ref) {
+ handler = _ref[i];
+ if (!(i in this._handlers)) {
+ continue;
+ }
+ if (handler.stopped) {
+ delete this._handlers[i];
+ continue;
+ }
+ trigger(handler.callbacks);
+ }
+ };
+
+ ReactiveList.prototype._hasActiveTrigger = function(evt) {
+ return _.any(this._handlers, function(handler) {
+ return !handler.stopped && evt in handler.callbacks;
+ });
+ };
+
+ ReactiveList.prototype._indexSet = function(idx, val) {
+ var org, rtn;
+
+ rtn = val;
+ if (this._list[idx] !== val) {
+ org = list[idx];
+ rtn = ReactiveList.__super__._indexSet.apply(this, arguments);
+ this._trigger('changed', this._list[idx], org, idx);
+ }
+ return rtn;
+ };
+
+ return ReactiveList;
+
+ })(ReactiveArray);
+
+ ReactiveList.wrap = function(arr) {
+ var obj;
+
+ obj = new ReactiveList;
+ obj._list = _.toArray(arr);
+ obj._syncIndexProxies(true);
+ return obj;
+ };
+
+ EJSON.addType('reactive-list', function(jsonObj) {
+ return ReactiveList.wrap(jsonObj);
+ });
+
+ LiveHandler = (function() {
+ function LiveHandler(callbacks) {
+ var self;
+
+ self = this;
+ this.stopped = false;
+ this.callbacks = callbacks;
+ if (Deps.active) {
+ Deps.onInvalidate(function() {
+ return self.stop();
+ });
+ }
+ }
+
+ LiveHandler.prototype.stop = function() {
+ return this.stopped = true;
+ };
+
+ return LiveHandler;
+
+ })();
+
+}).call(this);
diff --git a/example/packages/reactive-extra/package.js b/example/packages/reactive-extra/package.js
index c808342..6f1f32d 100644
--- a/example/packages/reactive-extra/package.js
+++ b/example/packages/reactive-extra/package.js
@@ -6,13 +6,20 @@ var path = Npm.require("path");
Package.on_use(function(api) {
// Required packages
api.use(["deps", "ejson", "underscore"], ["client", "server"]);
+ api.use(["templating"], ["client"]);
// Server and client side code
api.add_files([
path.join("lib","reactive-object.js"),
path.join("lib","reactive-dictionary.js"),
- path.join("lib","reactive-array.js")
+ path.join("lib","reactive-array.js"),
+ path.join("lib","reactive-list.js")
], ["client", "server"]);
+
+ // Client side code
+ api.add_files([
+ path.join("lib","handlebars-list.js")
+ ], ["client"]);
});
Package.on_test(function(api) {
@@ -23,6 +30,7 @@ Package.on_test(function(api) {
api.add_files([
path.join("lib","reactive-dictionary-test.js"),
path.join("lib","reactive-object-test.js"),
- path.join("lib","reactive-array-test.js")
+ path.join("lib","reactive-array-test.js"),
+ path.join("lib","reactive-list-test.js")
], ["client", "server"]);
});
diff --git a/lib/handlebars-list.js b/lib/handlebars-list.js
new file mode 100644
index 0000000..fd62e42
--- /dev/null
+++ b/lib/handlebars-list.js
@@ -0,0 +1,185 @@
+// Generated by CoffeeScript 1.6.2
+(function() {
+ var HandlebarsEach, SparkListObserve, findParentOfType, makeRange;
+
+ HandlebarsEach = Handlebars._default_helpers.each;
+
+ Handlebars._default_helpers.each = function(arg, options) {
+ var elseFunc, itemFunc;
+
+ if (!(arg && arg instanceof ReactiveList)) {
+ return HandlebarsEach.call(this, arg, options);
+ }
+ itemFunc = function(item) {
+ return Spark.labelBranch((item && item._id) || Spark.UNIQUE_LABEL, function() {
+ return Spark.setDataContext(item, Spark.isolate(_.bind(options.fn, null, item)));
+ });
+ };
+ elseFunc = function() {
+ if (options.inverse) {
+ return Spark.isolate(options.inverse);
+ } else {
+ return '';
+ }
+ };
+ return SparkListObserve(arg, itemFunc, elseFunc);
+ };
+
+ Spark._ANNOTATION_LIST_OBSERVE = "list_observe";
+
+ Spark._ANNOTATION_LIST_OBSERVE_ITEM = "list_observe_item";
+
+ SparkListObserve = function(observable, itemFunc, elseFunc) {
+ var callbacks, cleanup, handle, html, itemArr, later, maybeAnnotate, notifyParentsRendered, observerCallbacks, outerRange, renderer, stopped;
+
+ elseFunc = elseFunc || function() {
+ return '';
+ };
+ callbacks = {};
+ observerCallbacks = {};
+ _.each(["addedAt", "changedAt", "removedAt", "movedTo"], function(name) {
+ return observerCallbacks[name] = function() {
+ return callbacks[name].apply(null, arguments);
+ };
+ });
+ itemArr = [];
+ _.extend(callbacks, {
+ addedAt: function(val, idx) {
+ return itemArr[idx] = {
+ liveRange: null,
+ value: val
+ };
+ }
+ });
+ handle = observable.observe(observerCallbacks);
+ renderer = Spark._currentRenderer.get();
+ maybeAnnotate = renderer ? _.bind(renderer.annotate, renderer) : function(html) {
+ return html;
+ };
+ html = '';
+ outerRange = null;
+ if (itemArr.length === 0) {
+ html = elseFunc();
+ } else {
+ _.each(itemArr, function(elt) {
+ return html += maybeAnnotate(itemFunc(elt.value), Spark._ANNOTATION_LIST_OBSERVE_ITEM, function(range) {
+ elt.liveRange = range;
+ });
+ });
+ }
+ stopped = false;
+ cleanup = function() {
+ handle.stop();
+ return stopped = true;
+ };
+ html = maybeAnnotate(html, Spark._ANNOTATION_LIST_OBSERVE, function(range) {
+ if (!range) {
+ cleanup();
+ return;
+ }
+ outerRange = range;
+ outerRange.finalize = cleanup;
+ });
+ if (!renderer) {
+ cleanup();
+ return html;
+ }
+ notifyParentsRendered = function() {
+ var walk;
+
+ walk = outerRange;
+ while ((walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk))) {
+ walk.rendered.call(walk.landmark);
+ }
+ };
+ later = function(func) {
+ Deps.afterFlush(function() {
+ if (!stopped) {
+ func();
+ }
+ });
+ };
+ _.extend(callbacks, {
+ addedAt: function(val, idx) {
+ return later(function() {
+ var frag, range;
+
+ frag = Spark.render(_.bind(itemFunc, null, val));
+ DomUtils.wrapFragmentForContainer(frag, outerRange.containerNode());
+ range = makeRange(Spark._ANNOTATION_LIST_ITEM, frag);
+ if (itemArr.length === 0) {
+ Spark.finalize(outerRange.replaceContents(frag));
+ } else {
+ itemArr[idx - 1].liveRange.insertAfter(frag);
+ }
+ return itemArr[idx] = {
+ liveRange: range,
+ value: val
+ };
+ });
+ },
+ removedAt: function(val, idx) {
+ return later(function() {
+ var frag;
+
+ if (itemArr.length === 1) {
+ frag = Spark.render(elseFunc);
+ DomUtils.wrapFragmentForContainer(frag, outerRange.containerNode());
+ Spark.finalize(outerRange.replaceContents(frag));
+ } else {
+ Spark.finalize(itemArr[idx].liveRange.extract());
+ }
+ itemArr.splice(idx, 1);
+ return notifyParentsRendered();
+ });
+ },
+ movedTo: function(val, fromIdx, toIdx) {
+ return later(function() {
+ var elt, frag;
+
+ elt = (itemArr.splice(fromIdx, 1))[0];
+ frag = elt.liveRange.extract();
+ if (toIdx in itemArr) {
+ itemArr[toIdx].liveRange.insertBefore(frag);
+ } else {
+ itemArr[toIdx - 1].liveRange.insertAfter(frag);
+ }
+ itemArr.splice(toIdx, 0, elt);
+ return notifyParentsRendered();
+ });
+ },
+ changedAt: function(val, idx) {
+ return later(function() {
+ var elt;
+
+ elt = itemArr[idx];
+ if (!elt) {
+ throw new Error("Unknown item at index: " + idx);
+ }
+ elt.value = val;
+ return Spark.renderToRange(elt.liveRange, _.bind(itemFunc, null, elt.value));
+ });
+ }
+ });
+ return html;
+ };
+
+ findParentOfType = function(type, range) {
+ while (true) {
+ range = range.findParent();
+ if (!(range && range.type !== type)) {
+ break;
+ }
+ }
+ return range;
+ };
+
+ makeRange = function(type, start, end, inner) {
+ var range;
+
+ range = new LiveRange(Spark._TAG, start, end, inner);
+ range.type = type;
+ return range;
+ };
+
+}).call(this);
diff --git a/lib/reactive-array-test.js b/lib/reactive-array-test.js
index 4b88eac..5142f2c 100644
--- a/lib/reactive-array-test.js
+++ b/lib/reactive-array-test.js
@@ -107,7 +107,15 @@
test.equal(bracketX, 3);
test.equal(lengthX, 5);
test.equal(indexOfX, 5, 'sort changes all (not really but i\'m lazy)');
- return test.equal(lastIndexOfX, 6, 'sort changes all (not really but i\'m lazy)');
+ test.equal(lastIndexOfX, 6, 'sort changes all (not really but i\'m lazy)');
+ arr.unshift('drink');
+ Deps.flush();
+ test.equal(lengthX, 6);
+ test.equal(bracketX, 4);
+ test.equal(arr.shift(), 'drink');
+ Deps.flush();
+ test.equal(lengthX, 7);
+ return test.equal(bracketX, 5);
});
Tinytest.add("ReactiveArray - Sort/Reverse", function(test) {
diff --git a/lib/reactive-array.js b/lib/reactive-array.js
index ed54788..7603587 100644
--- a/lib/reactive-array.js
+++ b/lib/reactive-array.js
@@ -35,6 +35,33 @@
return this._list.slice();
};
+ ReactiveArray.prototype.reverse = function() {
+ Array.prototype.reverse.apply(this._list);
+ for (left = 0, right = this._list.length - 1; left < right; left += 1, right -= 1) {
+ if (left === right) { continue; }
+ if (this._listDeps[left]) { this._listDeps[left].changed(); }
+ if (this._listDeps[right]) { this._listDeps[right].changed(); }
+ };
+ this._listValueDep.changed();
+ return this;
+ };
+
+ ReactiveArray.prototype.sort = function() {
+ var dep, i, orgList, _i, _len, _ref;
+
+ orgList = this._list.slice();
+ Array.prototype.sort.apply(this._list, arguments);
+ _ref = this._listDeps;
+ for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
+ dep = _ref[i];
+ if (dep && orgList[i] !== this._list[i]) {
+ dep.changed();
+ }
+ }
+ this._listValueDep.changed();
+ return this;
+ };
+
ReactiveArray.prototype.indexOf = function(searchElement, fromIndex) {
var i, idx, _base, _i, _ref, _ref1;
@@ -115,7 +142,7 @@
};
ReactiveArray.prototype.clone = function() {
- return ReactiveArray.wrap(this._list);
+ return this.constructor.wrap(this._list);
};
ReactiveArray.prototype.equals = function(obj) {
@@ -177,11 +204,13 @@
ReactiveArray.prototype._indexSet = function(i, val) {
var _ref;
- this._list[i] = val;
- if ((_ref = this._listDeps[i]) != null) {
- _ref.changed();
+ if (this._list[i] !== val) {
+ this._list[i] = val;
+ if ((_ref = this._listDeps[i]) != null) {
+ _ref.changed();
+ }
+ this._listValueDep.changed();
}
- this._listValueDep.changed();
return val;
};
@@ -198,7 +227,7 @@
})();
- _.each(['pop', 'push', 'shift', 'splice', 'unshift'], function(m) {
+ _.each(['pop', 'push'], function(m) {
return ReactiveArray.prototype[m] = function() {
var rtn;
@@ -208,12 +237,13 @@
};
});
- _.each(['reverse', 'sort'], function(m) {
+ _.each(['shift', 'splice', 'unshift'], function(m) {
return ReactiveArray.prototype[m] = function() {
- var dep, i, orgList, _i, _len, _ref;
+ var dep, i, orgList, rtn, _i, _len, _ref;
orgList = this._list.slice();
- Array.prototype[m].apply(this._list, arguments);
+ rtn = Array.prototype[m].apply(this._list, arguments);
+ this._syncIndexProxies();
_ref = this._listDeps;
for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
dep = _ref[i];
@@ -221,8 +251,7 @@
dep.changed();
}
}
- this._listValueDep.changed();
- return this;
+ return rtn;
};
});
@@ -233,7 +262,7 @@
rtn = Array.prototype[m].apply(this._list, arguments);
this._listLengthDep.depend();
this._listValueDep.depend();
- return ReactiveArray.wrap(rtn);
+ return this.constructor.wrap(rtn);
};
});
@@ -259,7 +288,7 @@
rtn = _[m].call(null, this._list, iteratorProxy, thisArg);
this._listLengthDep.depend();
this._listValueDep.depend();
- return ReactiveArray.wrap(rtn);
+ return this.constructor.wrap(rtn);
};
});
diff --git a/lib/reactive-list-test.js b/lib/reactive-list-test.js
new file mode 100644
index 0000000..a155a38
--- /dev/null
+++ b/lib/reactive-list-test.js
@@ -0,0 +1,312 @@
+// Generated by CoffeeScript 1.6.2
+(function() {
+ Tinytest.add("ReactiveList - added/addedAt", function(test) {
+ var addedAtX, addedIdx, addedVal, addedX, callbacks, handle, invalidCall, invalidX, list;
+
+ invalidX = 0;
+ invalidCall = function() {
+ return invalidX++;
+ };
+ addedAtX = 0;
+ addedX = 0;
+ addedVal = 'init';
+ addedIdx = 0;
+ callbacks = {
+ added: function(val) {
+ test.equal(val, addedVal);
+ return addedX++;
+ },
+ addedAt: function(val, idx) {
+ test.equal(val, addedVal);
+ test.equal(idx, addedIdx);
+ return addedAtX++;
+ },
+ changed: invalidCall,
+ changedAt: invalidCall,
+ removed: invalidCall,
+ removedAt: invalidCall,
+ movedTo: invalidCall
+ };
+ list = new ReactiveList('init');
+ handle = list.observe(callbacks);
+ test.equal(invalidX, 0);
+ test.equal(addedX, 1);
+ test.equal(addedAtX, 1);
+ addedVal = 'push';
+ addedIdx = 1;
+ list.push(addedVal);
+ test.equal(invalidX, 0);
+ test.equal(addedX, 2);
+ test.equal(addedAtX, 2);
+ addedVal = 'unshift';
+ addedIdx = 0;
+ list.unshift(addedVal);
+ test.equal(invalidX, 0);
+ test.equal(addedX, 3);
+ test.equal(addedAtX, 3);
+ addedVal = 'splice1';
+ addedIdx = 1;
+ list.splice(1, 0, 'splice1');
+ test.equal(invalidX, 0);
+ test.equal(addedX, 4);
+ test.equal(addedAtX, 4);
+ addedVal = 'splice2';
+ addedIdx = 3;
+ list.splice(-1, 0, 'splice2');
+ test.equal(invalidX, 0);
+ test.equal(addedX, 5);
+ return test.equal(addedAtX, 5);
+ });
+
+ Tinytest.add("ReactiveList - remove/removeAt", function(test) {
+ var callbacks, handle, invalidCall, invalidX, list, removedAtX, removedIdx, removedVal, removedX;
+
+ invalidX = -14;
+ invalidCall = function() {
+ return invalidX++;
+ };
+ removedAtX = 0;
+ removedX = 0;
+ removedVal = null;
+ removedIdx = 0;
+ callbacks = {
+ removed: function(val) {
+ test.equal(val, removedVal);
+ return removedX++;
+ },
+ removedAt: function(val, idx) {
+ test.equal(val, removedVal);
+ test.equal(idx, removedIdx);
+ return removedAtX++;
+ },
+ changed: invalidCall,
+ changedAt: invalidCall,
+ added: invalidCall,
+ addedAt: invalidCall,
+ movedTo: invalidCall
+ };
+ list = new ReactiveList('shift', 'keep1', 'splice1', 'keep2', 'splice2', 'keep3', 'pop');
+ handle = list.observe(callbacks);
+ test.equal(invalidX, 0, 'observer');
+ test.equal(removedX, 0, 'observer');
+ test.equal(removedAtX, 0, 'observer');
+ removedVal = 'pop';
+ removedIdx = 6;
+ list.pop();
+ test.equal(invalidX, 0, 'pop');
+ test.equal(removedX, 1, 'pop');
+ test.equal(removedAtX, 1, 'pop');
+ removedVal = 'shift';
+ removedIdx = 0;
+ list.shift();
+ test.equal(invalidX, 0);
+ test.equal(removedX, 2);
+ test.equal(removedAtX, 2);
+ removedVal = 'splice1';
+ removedIdx = 1;
+ list.splice(1, 1);
+ test.equal(invalidX, 0);
+ test.equal(removedX, 3);
+ test.equal(removedAtX, 3);
+ removedVal = 'splice2';
+ removedIdx = 2;
+ list.splice(-2, 1);
+ test.equal(invalidX, 0);
+ test.equal(removedX, 4);
+ return test.equal(removedAtX, 4);
+ });
+
+ Tinytest.add("ReactiveList - splice", function(test) {
+ var addedAtX, addedRun, addedX, callbacks, changedAtX, changedRun, changedX, list, movedRun, movedX, removedAtX, removedRun, removedX;
+
+ addedX = addedAtX = -7;
+ changedX = changedAtX = 0;
+ removedX = removedAtX = 0;
+ movedX = 0;
+ changedRun = [
+ {
+ val: '2',
+ newVal: 'replacement1',
+ idx: 1
+ }, {
+ val: '4',
+ newVal: 'replacement2',
+ idx: 3
+ }, {
+ val: '5',
+ newVal: 'replacement3',
+ idx: 4
+ }, {
+ val: '3',
+ newVal: 'replacement4',
+ idx: 2
+ }, {
+ val: 'replacement4',
+ newVal: 'replacement5',
+ idx: 2
+ }
+ ];
+ addedRun = [
+ {
+ val: '3.5',
+ idx: 3
+ }
+ ];
+ removedRun = [
+ {
+ val: '3.5',
+ idx: 3
+ }
+ ];
+ movedRun = [];
+ callbacks = {
+ added: function(val) {
+ var eq;
+
+ if (addedX >= 0) {
+ eq = addedRun[addedX];
+ test.equal(val, eq.val, 'added - val: ' + addedX);
+ }
+ return addedX++;
+ },
+ addedAt: function(val, idx) {
+ var eq;
+
+ if (addedAtX >= 0) {
+ eq = addedRun[addedAtX];
+ test.equal(val, eq.val, 'addedAt - val: ' + addedAtX);
+ test.equal(idx, eq.idx, 'addedAt - idx: ' + addedAtX);
+ }
+ return addedAtX++;
+ },
+ changed: function(val, oldVal) {
+ var eq;
+
+ eq = changedRun[changedX];
+ test.equal(val, eq.newVal, 'changed - val: ' + changedX);
+ test.equal(oldVal, eq.val, 'changed - oldVal: ' + changedX);
+ return changedX++;
+ },
+ changedAt: function(val, oldVal, idx) {
+ var eq;
+
+ eq = changedRun[changedAtX];
+ test.equal(val, eq.newVal, 'changedAt - val: ' + changedAtX);
+ test.equal(oldVal, eq.val, 'changedAt - oldVal: ' + changedAtX);
+ test.equal(idx, eq.idx, 'changedAt - idx: ' + changedAtX);
+ return changedAtX++;
+ },
+ removed: function(val) {
+ var eq;
+
+ eq = removedRun[removedX];
+ test.equal(val, eq.val, 'removed - val: ' + removedX);
+ return removedX++;
+ },
+ removedAt: function(val, idx) {
+ var eq;
+
+ eq = removedRun[removedAtX];
+ test.equal(val, eq.val, 'removedAt - val: ' + removedAtX);
+ test.equal(idx, eq.idx, 'removedAt - idx: ' + removedAtX);
+ return removedAtX++;
+ },
+ movedTo: function(val, idx) {
+ var eq;
+
+ eq = movedRun[movedX];
+ test.equal(val, eq.val, 'movedTo - val: ' + movedX);
+ test.equal(idx, eq.idx, 'movedTo - idx: ' + movedX);
+ return movedX++;
+ }
+ };
+ list = new ReactiveList('1', '2', '3', '4', '5', '6', '7');
+ list.observe(callbacks);
+ test.equal(addedX, 0, 'added: observe');
+ test.equal(addedAtX, 0, 'addedAt: observe');
+ test.equal(removedX, 0, 'removedAt: observe');
+ test.equal(removedAtX, 0, 'removedAt: observe');
+ test.equal(changedX, 0, 'changed: observe');
+ test.equal(changedAtX, 0, 'changedAt: observe');
+ test.equal(movedX, 0, 'moved: observe');
+ list.splice(1, 1, changedRun[0].newVal);
+ test.equal(addedX, 0, 'added: run 1');
+ test.equal(addedAtX, 0, 'addedAt: run 1');
+ test.equal(removedX, 0, 'removedAt: run 1');
+ test.equal(removedAtX, 0, 'removedAt: run 1');
+ test.equal(changedX, 1, 'changed: run 1');
+ test.equal(changedAtX, 1, 'changedAt: run 1');
+ test.equal(movedX, 0, 'moved: run 1');
+ list.splice(3, 2, changedRun[1].newVal, changedRun[2].newVal);
+ test.equal(addedX, 0, 'added: run 2');
+ test.equal(addedAtX, 0, 'addedAt: run 2');
+ test.equal(removedX, 0, 'removedAt: run 2');
+ test.equal(removedAtX, 0, 'removedAt: run 2');
+ test.equal(changedX, 3, 'changed: run 2');
+ test.equal(changedAtX, 3, 'changedAt: run 2');
+ test.equal(movedX, 0, 'moved: run 2');
+ list.splice(2, 1, changedRun[3].newVal, addedRun[0].val);
+ test.equal(addedX, 1, 'added: run 3');
+ test.equal(addedAtX, 1, 'addedAt: run 3');
+ test.equal(removedX, 0, 'removedAt: run 3');
+ test.equal(removedAtX, 0, 'removedAt: run 3');
+ test.equal(changedX, 4, 'changed: run 3');
+ test.equal(changedAtX, 4, 'changedAt: run 3');
+ test.equal(movedX, 0, 'moved: run 3');
+ list.splice(2, 2, changedRun[4].newVal);
+ test.equal(addedX, 1, 'added: run 4');
+ test.equal(addedAtX, 1, 'addedAt: run 4');
+ test.equal(removedX, 1, 'removedAt: run 4');
+ test.equal(removedAtX, 1, 'removedAt: run 4');
+ test.equal(changedX, 5, 'changed: run 4');
+ test.equal(changedAtX, 5, 'changedAt: run 4');
+ return test.equal(movedX, 0, 'moved: run 4');
+ });
+
+ Tinytest.add("ReactiveList - reverse", function(test) {
+ var arr, arrReversed, list;
+
+ arr = ['1', '2', '3', '4', '5', '6', '7'];
+ arrReversed = arr.slice().reverse();
+ list = ReactiveList.wrap(arr);
+ list.observe({
+ movedTo: function(val, fromIdx, toIdx) {
+ arr.splice(fromIdx, 1);
+ return arr.splice(toIdx, 0, val);
+ }
+ });
+ test.equal(list.reverse(), ReactiveList.wrap(arrReversed));
+ test.equal(arr, arrReversed);
+ list.pop();
+ arr.pop();
+ arrReversed.pop();
+ arrReversed.reverse();
+ test.equal(list.reverse(), ReactiveList.wrap(arrReversed));
+ return test.equal(arr, arrReversed);
+ });
+
+ Tinytest.add("ReactiveList - sort", function(test) {
+ var arr, arrSorted, arrs, list, _i, _len, _results;
+
+ arrs = [['1', '7', '3', '4', '2', '6', '8', '5'], ['1', '2', '3', '4', '5', '6', '7'].reverse(), ['1', '7', '3', '7', '4', '2', '6', '8', '5'], ['1', '7', '3', '7', '7', '4', '7', '2', '6', '8', '5'], ['d', 'a', 'c', 'b', 'z', 'y', 'y']];
+ _results = [];
+ for (_i = 0, _len = arrs.length; _i < _len; _i++) {
+ arr = arrs[_i];
+ arrSorted = arr.slice().sort();
+ list = ReactiveList.wrap(arr);
+ list.observe({
+ movedTo: function(docVal, fromIdx, toIdx) {
+ var val;
+
+ val = (arr.splice(fromIdx, 1))[0];
+ test.equal(val, docVal);
+ return arr.splice(toIdx, 0, val);
+ }
+ });
+ test.equal(list.sort(), ReactiveList.wrap(arrSorted));
+ _results.push(test.equal(arr, arrSorted));
+ }
+ return _results;
+ });
+
+}).call(this);
diff --git a/lib/reactive-list.js b/lib/reactive-list.js
new file mode 100644
index 0000000..097368b
--- /dev/null
+++ b/lib/reactive-list.js
@@ -0,0 +1,276 @@
+// Generated by CoffeeScript 1.6.2
+(function() {
+ var LiveHandler,
+ __hasProp = {}.hasOwnProperty,
+ __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
+
+ this.ReactiveList = (function(_super) {
+ __extends(ReactiveList, _super);
+
+ function ReactiveList() {
+ this._definePrivateProperty('_handlers', []);
+ ReactiveList.__super__.constructor.apply(this, arguments);
+ }
+
+ ReactiveList.prototype.observe = function(callbacks) {
+ var handle, i, _i, _ref;
+
+ handle = new LiveHandler(callbacks);
+ this._handlers.push(handle);
+ for (i = _i = 0, _ref = this._list.length; _i < _ref; i = _i += 1) {
+ this._trigger('added', this._list[i], i);
+ }
+ return handle;
+ };
+
+ ReactiveList.prototype.pop = function() {
+ var rtn;
+
+ rtn = ReactiveList.__super__.pop.apply(this, arguments);
+ this._trigger('removed', rtn, this._list.length);
+ return rtn;
+ };
+
+ ReactiveList.prototype.push = function() {
+ var i, orgLength, rtn, _i, _ref;
+
+ orgLength = this._list.length;
+ rtn = ReactiveList.__super__.push.apply(this, arguments);
+ for (i = _i = orgLength, _ref = this._list.length; _i < _ref; i = _i += 1) {
+ this._trigger('added', this._list[i], i);
+ }
+ return rtn;
+ };
+
+ ReactiveList.prototype.shift = function() {
+ var rtn;
+
+ rtn = ReactiveList.__super__.shift.apply(this, arguments);
+ this._trigger('removed', rtn, 0);
+ return rtn;
+ };
+
+ ReactiveList.prototype.unshift = function() {
+ var i, orgLength, rtn, _i, _ref;
+
+ orgLength = this._list.length;
+ rtn = ReactiveList.__super__.unshift.apply(this, arguments);
+ for (i = _i = 0, _ref = this._list.length - orgLength; _i < _ref; i = _i += 1) {
+ this._trigger('added', this._list[i], i);
+ }
+ return rtn;
+ };
+
+ ReactiveList.prototype.splice = function() {
+ var addAmount, changedAmount, i, idx, orgList, rmAmount, rtn, _i, _j, _k, _l, _ref, _ref1;
+
+ orgList = this._list.slice();
+ rtn = ReactiveList.__super__.splice.apply(this, arguments);
+ idx = arguments[0];
+ if (idx < 0) {
+ idx = orgList.length + idx;
+ }
+ rmAmount = arguments.length > 1 ? arguments[1] : orgList.length - idx;
+ if (arguments.length > 2) {
+ addAmount = arguments.length - 2;
+ if (rmAmount > 0) {
+ changedAmount = rmAmount > addAmount ? addAmount : rmAmount;
+ for (i = _i = 0; _i < changedAmount; i = _i += 1) {
+ this._trigger('changed', this._list[idx], orgList[idx], idx);
+ idx++;
+ }
+ addAmount = addAmount - changedAmount;
+ rmAmount = rmAmount - changedAmount;
+ }
+ if ((rmAmount - addAmount) > 0) {
+ for (i = _j = 0, _ref = rmAmount - addAmount; _j < _ref; i = _j += 1) {
+ this._trigger('removed', orgList[idx + i], idx + i);
+ }
+ } else if ((rmAmount - addAmount) < 0) {
+ for (i = _k = 0, _ref1 = addAmount - rmAmount; _k < _ref1; i = _k += 1) {
+ this._trigger('added', this._list[idx + i], idx + i);
+ }
+ }
+ } else if (rmAmount > 0) {
+ for (i = _l = 0; _l < rmAmount; i = _l += 1) {
+ this._trigger('removed', orgList[idx + i], idx + i);
+ }
+ }
+ return rtn;
+ };
+
+ ReactiveList.prototype.reverse = function() {
+ var array, length;
+
+ ReactiveList.__super__.reverse.apply(this, arguments);
+ array = this._list;
+ length = this._list.length;
+ for (left = 0, right = length - 1; left < right; left += 1, right -= 1) {
+ if (right === left) { continue; }
+ this._trigger('movedTo', array[left], right, left);
+ this._trigger('movedTo', array[right], left+1, right);
+ };
+ return this;
+ };
+
+ ReactiveList.prototype.sort = function() {
+ var currentPosition, finalPosition, lastMove, length, move, moves, org, skip, _i, _len;
+
+ org = this._list.slice();
+ ReactiveList.__super__.sort.apply(this, arguments);
+ if (!this._hasActiveTrigger('movedTo')) {
+ return this;
+ }
+ length = this._list.length;
+ moves = [];
+ currentPosition = 0;
+ while (currentPosition < length) {
+ finalPosition = this._list.indexOf(org[currentPosition]);
+ if (currentPosition + 1 === finalPosition) {
+ while (org[currentPosition + 1] === this._list[finalPosition + 1]) {
+ finalPosition++;
+ currentPosition++;
+ }
+ if (org[currentPosition] === this._list[finalPosition]) {
+ finalPosition++;
+ currentPosition++;
+ }
+ finalPosition = this._list.indexOf(org[currentPosition]);
+ }
+ if (org[currentPosition] === org[currentPosition + 1]) {
+ while (org[currentPosition - 1] === this._list[finalPosition]) {
+ finalPosition++;
+ }
+ if (org[currentPosition] === this._list[finalPosition]) {
+ finalPosition++;
+ }
+ finalPosition = this._list.indexOf(org[currentPosition], finalPosition);
+ }
+ move = {
+ from: currentPosition,
+ to: finalPosition
+ };
+ skip = finalPosition === -1 || lastMove && lastMove.to === move.to && lastMove.from === move.from;
+ if (!skip && finalPosition !== currentPosition) {
+ moves.push(move);
+ lastMove = move;
+ org.splice(move.to, 0, (org.splice(move.from, 1))[0]);
+ if (finalPosition < currentPosition) {
+ currentPosition = finalPosition;
+ } else {
+ currentPosition--;
+ }
+ }
+ currentPosition++;
+ }
+ for (_i = 0, _len = moves.length; _i < _len; _i++) {
+ move = moves[_i];
+ this._trigger('movedTo', this._list[move.to], move.from, move.to);
+ }
+ return this;
+ };
+
+ ReactiveList.prototype.typeName = function() {
+ return 'reactive-list';
+ };
+
+ ReactiveList.prototype.equals = function(obj) {
+ return (obj != null) && obj instanceof ReactiveList && _.isEqual(obj._list, this._list);
+ };
+
+ ReactiveList.prototype._trigger = function(evt) {
+ var args, evtArgs, evtAt, evtAtArgs, handler, i, self, trigger, _ref;
+
+ self = this;
+ args = _.toArray(arguments).slice(1);
+ if (evt === 'movedTo') {
+ trigger = function(callbacks) {
+ if (evt in callbacks) {
+ return callbacks[evt].apply(self, args);
+ }
+ };
+ } else {
+ evtArgs = args.slice(0, -1);
+ evtAt = evt + 'At';
+ evtAtArgs = args;
+ trigger = function(callbacks) {
+ if (evt in callbacks) {
+ callbacks[evt].apply(self, evtArgs);
+ }
+ if (evtAt in callbacks) {
+ return callbacks[evtAt].apply(self, evtAtArgs);
+ }
+ };
+ }
+ _ref = this._handlers;
+ for (i in _ref) {
+ handler = _ref[i];
+ if (!(i in this._handlers)) {
+ continue;
+ }
+ if (handler.stopped) {
+ delete this._handlers[i];
+ continue;
+ }
+ trigger(handler.callbacks);
+ }
+ };
+
+ ReactiveList.prototype._hasActiveTrigger = function(evt) {
+ return _.any(this._handlers, function(handler) {
+ return !handler.stopped && evt in handler.callbacks;
+ });
+ };
+
+ ReactiveList.prototype._indexSet = function(idx, val) {
+ var org, rtn;
+
+ rtn = val;
+ if (this._list[idx] !== val) {
+ org = list[idx];
+ rtn = ReactiveList.__super__._indexSet.apply(this, arguments);
+ this._trigger('changed', this._list[idx], org, idx);
+ }
+ return rtn;
+ };
+
+ return ReactiveList;
+
+ })(ReactiveArray);
+
+ ReactiveList.wrap = function(arr) {
+ var obj;
+
+ obj = new ReactiveList;
+ obj._list = _.toArray(arr);
+ obj._syncIndexProxies(true);
+ return obj;
+ };
+
+ EJSON.addType('reactive-list', function(jsonObj) {
+ return ReactiveList.wrap(jsonObj);
+ });
+
+ LiveHandler = (function() {
+ function LiveHandler(callbacks) {
+ var self;
+
+ self = this;
+ this.stopped = false;
+ this.callbacks = callbacks;
+ if (Deps.active) {
+ Deps.onInvalidate(function() {
+ return self.stop();
+ });
+ }
+ }
+
+ LiveHandler.prototype.stop = function() {
+ return this.stopped = true;
+ };
+
+ return LiveHandler;
+
+ })();
+
+}).call(this);
diff --git a/package.js b/package.js
index c808342..6f1f32d 100644
--- a/package.js
+++ b/package.js
@@ -6,13 +6,20 @@ var path = Npm.require("path");
Package.on_use(function(api) {
// Required packages
api.use(["deps", "ejson", "underscore"], ["client", "server"]);
+ api.use(["templating"], ["client"]);
// Server and client side code
api.add_files([
path.join("lib","reactive-object.js"),
path.join("lib","reactive-dictionary.js"),
- path.join("lib","reactive-array.js")
+ path.join("lib","reactive-array.js"),
+ path.join("lib","reactive-list.js")
], ["client", "server"]);
+
+ // Client side code
+ api.add_files([
+ path.join("lib","handlebars-list.js")
+ ], ["client"]);
});
Package.on_test(function(api) {
@@ -23,6 +30,7 @@ Package.on_test(function(api) {
api.add_files([
path.join("lib","reactive-dictionary-test.js"),
path.join("lib","reactive-object-test.js"),
- path.join("lib","reactive-array-test.js")
+ path.join("lib","reactive-array-test.js"),
+ path.join("lib","reactive-list-test.js")
], ["client", "server"]);
});
diff --git a/smart.json b/smart.json
index 8212d5b..c3ec1b8 100644
--- a/smart.json
+++ b/smart.json
@@ -3,7 +3,7 @@
"description": "Providing reactive classes",
"homepage": "https://github.com/boekkooi/reactive-extra",
"author": "Warnar Boekkooi
",
- "version": "0.0.2",
+ "version": "0.0.3",
"git": "https://github.com/boekkooi/reactive-extra.git",
"packages": {}
}
\ No newline at end of file
diff --git a/src/handlebars-list.coffee b/src/handlebars-list.coffee
new file mode 100644
index 0000000..4dd52bc
--- /dev/null
+++ b/src/handlebars-list.coffee
@@ -0,0 +1,143 @@
+# # Handlebars each override
+HandlebarsEach = Handlebars._default_helpers.each
+Handlebars._default_helpers.each = (arg, options) ->
+ # Only use our implementation when the arg is a ReactiveList
+ return HandlebarsEach.call(this, arg, options) unless arg and arg instanceof ReactiveList
+
+ # Item & else functions (stolen from [templating/deftemplate.js](https://github.com/meteor/meteor/blob/master/packages/templating/deftemplate.js)
+ itemFunc = (item) ->
+ Spark.labelBranch (item && item._id) || Spark.UNIQUE_LABEL, () ->
+ Spark.setDataContext item, Spark.isolate(_.bind(options.fn, null, item))
+ elseFunc = () -> if options.inverse then Spark.isolate(options.inverse) else ''
+
+ # Call our curstom observe based SparkArrayList
+ SparkListObserve arg, itemFunc, elseFunc
+
+# # Spark listObserve
+Spark._ANNOTATION_LIST_OBSERVE = "list_observe";
+Spark._ANNOTATION_LIST_OBSERVE_ITEM = "list_observe_item";
+
+# Render a object with a observe function using Spark
+# *Most of this code is ripped from [Spark.list](https://github.com/meteor/meteor/blob/master/packages/spark/spark.js#L899)*
+SparkListObserve = (observable, itemFunc, elseFunc) ->
+ elseFunc = elseFunc || () -> return ''
+
+ # Create a level of indirection around our observable callbacks so we can change them later
+ callbacks = {}
+ observerCallbacks = {}
+ _.each ["addedAt", "changedAt", "removedAt", "movedTo"], (name) ->
+ observerCallbacks[name] = ->
+ callbacks[name].apply null, arguments
+
+ # Create liverange stubs for the current contents of the observable
+ itemArr = []
+ _.extend callbacks,
+ addedAt: (val, idx) ->
+ itemArr[idx] = { liveRange: null, value: val }
+
+ handle = observable.observe observerCallbacks
+
+ # Get the renderer, if any
+ renderer = Spark._currentRenderer.get();
+ maybeAnnotate = if renderer then _.bind(renderer.annotate, renderer) else (html) -> html
+
+ # Render the initial contents.
+ # If we have a renderer, create a range around each item as well as around the list, and save them off for later.
+ html = ''
+ outerRange = null
+ if itemArr.length == 0
+ html = elseFunc()
+ else
+ _.each itemArr, (elt) ->
+ html += maybeAnnotate itemFunc(elt.value), Spark._ANNOTATION_LIST_OBSERVE_ITEM, (range) ->
+ elt.liveRange = range
+ return
+
+ stopped = false
+ cleanup = () ->
+ handle.stop()
+ stopped = true
+
+ html = maybeAnnotate html, Spark._ANNOTATION_LIST_OBSERVE, (range) ->
+ if !range
+ cleanup()
+ return
+ outerRange = range
+ outerRange.finalize = cleanup
+ return
+
+ # No renderer? Then we have no way to update the returned html and we can close the observer.
+ if !renderer
+ cleanup()
+ return html
+
+ notifyParentsRendered = ->
+ walk = outerRange
+ walk.rendered.call walk.landmark while (walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk))
+ return
+
+ later = (func) ->
+ Deps.afterFlush () ->
+ # Spark uses withEventGuard let's just have this won't brake
+ func() unless stopped
+ return
+ return
+
+ # The DOM update callbacks.
+ _.extend callbacks,
+ addedAt: (val, idx) ->
+ later ->
+ frag = Spark.render(_.bind(itemFunc, null, val))
+ DomUtils.wrapFragmentForContainer frag, outerRange.containerNode()
+ range = makeRange(Spark._ANNOTATION_LIST_ITEM, frag)
+ if itemArr.length == 0
+ Spark.finalize outerRange.replaceContents(frag)
+ else
+ itemArr[idx-1].liveRange.insertAfter frag
+ itemArr[idx] = { liveRange: range, value: val }
+
+ removedAt: (val, idx) ->
+ later ->
+ if itemArr.length == 1
+ frag = Spark.render(elseFunc)
+ DomUtils.wrapFragmentForContainer frag, outerRange.containerNode()
+ Spark.finalize outerRange.replaceContents(frag)
+ else
+ Spark.finalize itemArr[idx].liveRange.extract()
+ itemArr.splice idx, 1
+ notifyParentsRendered()
+
+ movedTo: (val, fromIdx, toIdx) ->
+ later ->
+ elt = (itemArr.splice fromIdx, 1)[0]
+ frag = elt.liveRange.extract()
+ if toIdx of itemArr
+ itemArr[toIdx].liveRange.insertBefore frag
+ else
+ itemArr[toIdx-1].liveRange.insertAfter frag
+ itemArr.splice toIdx, 0, elt
+ notifyParentsRendered()
+
+ changedAt: (val, idx) ->
+ later ->
+ elt = itemArr[idx]
+ throw new Error("Unknown item at index: " + idx) unless elt
+ elt.value = val
+ Spark.renderToRange elt.liveRange, _.bind(itemFunc, null, elt.value)
+
+ return html
+
+# ## findParentOfType
+# Ripped from [Spark.list](https://github.com/meteor/meteor/blob/master/packages/spark/spark.js#L77)*
+findParentOfType = (type, range) ->
+ loop
+ range = range.findParent()
+ break unless range and range.type isnt type
+ range
+
+# ## makeRange
+# Ripped from [Spark.list](https://github.com/meteor/meteor/blob/master/packages/spark/spark.js#L63)*
+makeRange = (type, start, end, inner) ->
+ range = new LiveRange(Spark._TAG, start, end, inner)
+ range.type = type
+ range
\ No newline at end of file
diff --git a/src/reactive-array.coffee b/src/reactive-array.coffee
index ac955d2..ab90a2d 100644
--- a/src/reactive-array.coffee
+++ b/src/reactive-array.coffee
@@ -1,5 +1,4 @@
-# I wish i could use http://wiki.ecmascript.org/doku.php?id=harmony:proxies
-
+# ## *Reactive Array*
class @ReactiveArray
constructor: () ->
# the actual array that we proxy to
@@ -38,6 +37,33 @@ class @ReactiveArray
@_listValueDep.depend()
return @_list.slice()
+ # ### *Mutator methods*
+ # ---------------------------------------
+ # Optimized mutator methods for reactivity
+ reverse: () ->
+ # Implement a custom array sort could be usefull based on http://jsperf.com/js-array-reverse-vs-while-loop/9
+ # but this works for small array's i still trust array.reverse a bit better
+ Array.prototype.reverse.apply @_list
+ `for (left = 0, right = this._list.length - 1; left < right; left += 1, right -= 1) {
+ if (left === right) { continue; }
+ if (this._listDeps[left]) { this._listDeps[left].changed(); }
+ if (this._listDeps[right]) { this._listDeps[right].changed(); }
+ }`
+ @_listValueDep.changed()
+ return @
+
+ # #### sort
+ # Sorts the elements of an array in place and returns the array.
+ sort: () ->
+ orgList = @_list.slice()
+ Array.prototype.sort.apply @_list, arguments
+
+ # Find the changed values and trigger there dependencies
+ for dep, i in @_listDeps when dep && orgList[i] != @_list[i]
+ dep.changed()
+ @_listValueDep.changed()
+ return @
+
# ### *Accessor methods*
# ---------------------------------------
# Optimized accessor methods for reactivity
@@ -129,7 +155,7 @@ class @ReactiveArray
# #### clone
# *[EJSON::clone](http://docs.meteor.com/#ejson_type_clone)*
clone: () ->
- ReactiveArray.wrap @_list
+ @constructor.wrap @_list
# #### equals
# *[EJSON::equals](http://docs.meteor.com/#ejson_type_equals)*
@@ -188,9 +214,10 @@ class @ReactiveArray
# ### _indexSet
_indexSet: (i, val) ->
- @_list[i] = val
- @_listDeps[i]?.changed()
- @_listValueDep.changed()
+ if @_list[i] != val
+ @_list[i] = val
+ @_listDeps[i]?.changed()
+ @_listValueDep.changed()
val
# #### _definePrivateProperty
@@ -224,16 +251,6 @@ _.each ['shift', 'splice', 'unshift'], (m) ->
dep.changed() for dep, i in @_listDeps when dep && orgList[i] != @_list[i]
rtn
-_.each ['reverse','sort'], (m) ->
- ReactiveArray.prototype[m] = () ->
- orgList = @_list.slice()
- Array.prototype[m].apply @_list, arguments
-
- # Find the changed values and trigger there dependencies
- dep.changed() for dep, i in @_listDeps when dep && orgList[i] != @_list[i]
- @_listValueDep.changed()
- return @
-
# #### *Accessor methods*
# Create Accessor proxy methods
_.each ['concat','slice'], (m) ->
@@ -241,7 +258,7 @@ _.each ['concat','slice'], (m) ->
rtn = Array.prototype[m].apply @_list, arguments
@_listLengthDep.depend()
@_listValueDep.depend()
- ReactiveArray.wrap rtn
+ @constructor.wrap rtn
_.each ['join','toString'], (m) ->
ReactiveArray.prototype[m] = () ->
@@ -263,7 +280,7 @@ _.each ['filter', 'map'], (m) ->
@_listLengthDep.depend()
@_listValueDep.depend()
- ReactiveArray.wrap rtn
+ @constructor.wrap rtn
# Create iteration proxy methods for `reduce`, `reduceRight` that are using [underscore.js](http://underscorejs.org/)
_.each ['reduce', 'reduceRight'], (m) ->
diff --git a/src/reactive-list-test.coffee b/src/reactive-list-test.coffee
new file mode 100644
index 0000000..0cf885d
--- /dev/null
+++ b/src/reactive-list-test.coffee
@@ -0,0 +1,275 @@
+Tinytest.add "ReactiveList - added/addedAt", (test) ->
+ invalidX = 0
+ invalidCall = () ->
+ invalidX++
+
+ addedAtX = 0
+ addedX = 0
+ addedVal = 'init'
+ addedIdx = 0
+ callbacks =
+ added: (val) ->
+ test.equal val, addedVal
+ addedX++
+ addedAt: (val, idx) ->
+ test.equal val, addedVal
+ test.equal idx, addedIdx
+ addedAtX++
+ changed: invalidCall
+ changedAt: invalidCall
+ removed: invalidCall
+ removedAt: invalidCall
+ movedTo: invalidCall
+
+ list = new ReactiveList('init')
+
+ # observe: test initial added event's
+ handle = list.observe callbacks
+ test.equal invalidX, 0
+ test.equal addedX, 1
+ test.equal addedAtX, 1
+
+ # push: test added
+ addedVal = 'push'
+ addedIdx = 1
+ list.push addedVal
+ test.equal invalidX, 0
+ test.equal addedX, 2
+ test.equal addedAtX, 2
+
+ # unshift: test added
+ addedVal = 'unshift'
+ addedIdx = 0
+ list.unshift addedVal
+ test.equal invalidX, 0
+ test.equal addedX, 3
+ test.equal addedAtX, 3
+
+ # splice: test added
+ addedVal = 'splice1'
+ addedIdx = 1
+ list.splice 1, 0, 'splice1'
+ test.equal invalidX, 0
+ test.equal addedX, 4
+ test.equal addedAtX, 4
+
+ addedVal = 'splice2'
+ addedIdx = 3
+ list.splice -1, 0, 'splice2'
+ test.equal invalidX, 0
+ test.equal addedX, 5
+ test.equal addedAtX, 5
+
+Tinytest.add "ReactiveList - remove/removeAt", (test) ->
+ invalidX = -14
+ invalidCall = () ->
+ invalidX++
+
+ removedAtX = 0
+ removedX = 0
+ removedVal = null
+ removedIdx = 0
+ callbacks =
+ removed: (val) ->
+ test.equal val, removedVal
+ removedX++
+ removedAt: (val, idx) ->
+ test.equal val, removedVal
+ test.equal idx, removedIdx
+ removedAtX++
+ changed: invalidCall
+ changedAt: invalidCall
+ added: invalidCall
+ addedAt: invalidCall
+ movedTo: invalidCall
+
+ list = new ReactiveList('shift','keep1','splice1','keep2','splice2','keep3','pop')
+
+ handle = list.observe callbacks
+ test.equal invalidX, 0, 'observer'
+ test.equal removedX, 0, 'observer'
+ test.equal removedAtX, 0, 'observer'
+
+ # pop
+ removedVal = 'pop'
+ removedIdx = 6
+ list.pop()
+ test.equal invalidX, 0, 'pop'
+ test.equal removedX, 1, 'pop'
+ test.equal removedAtX, 1, 'pop'
+
+ # shift
+ removedVal = 'shift'
+ removedIdx = 0
+ list.shift()
+ test.equal invalidX, 0
+ test.equal removedX, 2
+ test.equal removedAtX, 2
+
+ # splice: test added
+ removedVal = 'splice1'
+ removedIdx = 1
+ list.splice 1, 1
+ test.equal invalidX, 0
+ test.equal removedX, 3
+ test.equal removedAtX, 3
+
+ removedVal = 'splice2'
+ removedIdx = 2
+ list.splice -2, 1
+ test.equal invalidX, 0
+ test.equal removedX, 4
+ test.equal removedAtX, 4
+
+Tinytest.add "ReactiveList - splice", (test) ->
+ addedX = addedAtX = -7
+ changedX = changedAtX = 0
+ removedX = removedAtX = 0
+ movedX = 0
+
+ changedRun = [
+ { val: '2', newVal: 'replacement1', idx: 1 } # run 1
+ { val: '4', newVal: 'replacement2', idx: 3 } # run 2
+ { val: '5', newVal: 'replacement3', idx: 4 }
+
+ { val: '3', newVal: 'replacement4', idx: 2 } # run 3
+ { val: 'replacement4', newVal: 'replacement5', idx: 2 } # run 4
+ ]
+ addedRun = [
+ { val: '3.5', idx: 3 } # run 3
+ ]
+ removedRun = [
+ { val: '3.5', idx: 3 } # run 4
+ ]
+ movedRun = []
+
+ callbacks =
+ added: (val) ->
+ if addedX >= 0
+ eq = addedRun[addedX]
+ test.equal val, eq.val, 'added - val: ' + addedX
+ addedX++
+ addedAt: (val, idx) ->
+ if addedAtX >= 0
+ eq = addedRun[addedAtX]
+ test.equal val, eq.val, 'addedAt - val: ' + addedAtX
+ test.equal idx, eq.idx, 'addedAt - idx: ' + addedAtX
+ addedAtX++
+ changed: (val, oldVal) ->
+ eq = changedRun[changedX]
+ test.equal val, eq.newVal, 'changed - val: ' + changedX
+ test.equal oldVal, eq.val, 'changed - oldVal: ' + changedX
+ changedX++
+ changedAt: (val, oldVal, idx) ->
+ eq = changedRun[changedAtX]
+ test.equal val, eq.newVal, 'changedAt - val: ' + changedAtX
+ test.equal oldVal, eq.val, 'changedAt - oldVal: ' + changedAtX
+ test.equal idx, eq.idx, 'changedAt - idx: ' + changedAtX
+ changedAtX++
+ removed: (val) ->
+ eq = removedRun[removedX]
+ test.equal val, eq.val, 'removed - val: ' + removedX
+ removedX++
+ removedAt: (val, idx) ->
+ eq = removedRun[removedAtX]
+ test.equal val, eq.val, 'removedAt - val: ' + removedAtX
+ test.equal idx, eq.idx, 'removedAt - idx: ' + removedAtX
+ removedAtX++
+ movedTo: (val, idx) ->
+ eq = movedRun[movedX]
+ test.equal val, eq.val, 'movedTo - val: ' + movedX
+ test.equal idx, eq.idx, 'movedTo - idx: ' + movedX
+ movedX++
+
+ list = new ReactiveList('1','2','3','4','5','6','7')
+
+ # Attach callbacks
+ list.observe callbacks
+ test.equal addedX, 0, 'added: observe'
+ test.equal addedAtX, 0, 'addedAt: observe'
+ test.equal removedX, 0, 'removedAt: observe'
+ test.equal removedAtX, 0, 'removedAt: observe'
+ test.equal changedX, 0, 'changed: observe'
+ test.equal changedAtX, 0, 'changedAt: observe'
+ test.equal movedX, 0, 'moved: observe'
+
+ # splice
+ list.splice 1, 1, changedRun[0].newVal
+ test.equal addedX, 0, 'added: run 1'
+ test.equal addedAtX, 0, 'addedAt: run 1'
+ test.equal removedX, 0, 'removedAt: run 1'
+ test.equal removedAtX, 0, 'removedAt: run 1'
+ test.equal changedX, 1, 'changed: run 1'
+ test.equal changedAtX, 1, 'changedAt: run 1'
+ test.equal movedX, 0, 'moved: run 1'
+
+ # splice
+ list.splice 3, 2, changedRun[1].newVal, changedRun[2].newVal
+ test.equal addedX, 0, 'added: run 2'
+ test.equal addedAtX, 0, 'addedAt: run 2'
+ test.equal removedX, 0, 'removedAt: run 2'
+ test.equal removedAtX, 0, 'removedAt: run 2'
+ test.equal changedX, 3, 'changed: run 2'
+ test.equal changedAtX, 3, 'changedAt: run 2'
+ test.equal movedX, 0, 'moved: run 2'
+
+ # splice
+ list.splice 2, 1, changedRun[3].newVal, addedRun[0].val
+ test.equal addedX, 1, 'added: run 3'
+ test.equal addedAtX, 1, 'addedAt: run 3'
+ test.equal removedX, 0, 'removedAt: run 3'
+ test.equal removedAtX, 0, 'removedAt: run 3'
+ test.equal changedX, 4, 'changed: run 3'
+ test.equal changedAtX, 4, 'changedAt: run 3'
+ test.equal movedX, 0, 'moved: run 3'
+
+ # splice
+ list.splice 2, 2, changedRun[4].newVal
+ test.equal addedX, 1, 'added: run 4'
+ test.equal addedAtX, 1, 'addedAt: run 4'
+ test.equal removedX, 1, 'removedAt: run 4'
+ test.equal removedAtX, 1, 'removedAt: run 4'
+ test.equal changedX, 5, 'changed: run 4'
+ test.equal changedAtX, 5, 'changedAt: run 4'
+ test.equal movedX, 0, 'moved: run 4'
+
+Tinytest.add "ReactiveList - reverse", (test) ->
+ arr = ['1','2','3','4','5','6','7']
+ arrReversed = arr.slice().reverse()
+ list = ReactiveList.wrap(arr)
+
+ list.observe
+ movedTo: (val, fromIdx, toIdx) ->
+ arr.splice fromIdx, 1
+ arr.splice toIdx, 0, val
+
+ test.equal list.reverse(), ReactiveList.wrap(arrReversed)
+ test.equal arr, arrReversed
+
+ list.pop()
+ arr.pop()
+ arrReversed.pop()
+ arrReversed.reverse()
+ test.equal list.reverse(), ReactiveList.wrap(arrReversed)
+ test.equal arr, arrReversed
+
+Tinytest.add "ReactiveList - sort", (test) ->
+ arrs = [
+ ['1','7','3','4','2','6','8','5']
+ ['1','2','3','4','5','6','7'].reverse()
+ ['1','7','3','7','4','2','6','8','5']
+ ['1','7','3','7','7','4','7','2','6','8','5'],
+ ['d', 'a', 'c', 'b', 'z', 'y', 'y']
+ ]
+ for arr in arrs
+ arrSorted = arr.slice().sort()
+ list = ReactiveList.wrap(arr)
+
+ list.observe
+ movedTo: (docVal, fromIdx, toIdx) ->
+ val = (arr.splice fromIdx, 1)[0]
+ test.equal val, docVal
+ arr.splice toIdx, 0, val
+
+ test.equal list.sort(), ReactiveList.wrap(arrSorted)
+ test.equal arr, arrSorted
diff --git a/src/reactive-list.coffee b/src/reactive-list.coffee
new file mode 100644
index 0000000..28c9df3
--- /dev/null
+++ b/src/reactive-list.coffee
@@ -0,0 +1,246 @@
+# ## *Reactive List*
+# Represents a reactive list extended from a ReactiveArray.
+# A `ReactiveList` adds the observe function.
+#
+# Example
+#
+# ```javascript
+# list = new ReactiveList('first');
+# list.observer({
+# added: function(newDoc) { console.log("added", newDoc) }
+# });
+# list.push('second');
+# ```
+class @ReactiveList extends ReactiveArray
+ constructor: () ->
+ # A array of active lineHandlers
+ @_definePrivateProperty '_handlers', []
+
+ super
+
+ # ### observer
+ # Based on [cursor.observe](http://docs.meteor.com/#observe)
+ observe: (callbacks) ->
+ handle = new LiveHandler callbacks
+ @_handlers.push handle
+ @_trigger 'added', @_list[i], i for i in [0...@_list.length] by 1
+ handle
+
+ # ### *Mutator methods*
+ # ---------------------------------------
+ # Optimized mutator methods for reactivity
+
+ # #### pop
+ # Removes the last element from an array and returns that element.
+ pop: () ->
+ rtn = super
+ @_trigger 'removed', rtn, @_list.length
+ rtn
+
+ # ####
+ # Adds one or more elements to the end of an array and returns the new length of the array.
+ push: () ->
+ orgLength = @_list.length
+ rtn = super
+ # Fire event, added, addedAt
+ @_trigger 'added', @_list[i], i for i in [orgLength...@_list.length] by 1
+ rtn
+
+ # #### shift
+ # Removes the first element from an array and returns that element.
+ shift: () ->
+ rtn = super
+ @_trigger 'removed', rtn, 0
+ rtn
+
+ # #### unshift
+ # Adds one or more elements to the front of an array and returns the new length of the array.
+ unshift: () ->
+ orgLength = @_list.length
+ rtn = super
+ # Fire event, added, addedAt
+ @_trigger 'added', @_list[i], i for i in [0...(@_list.length-orgLength)] by 1
+ rtn
+
+ # #### splice
+ # Adds and/or removes elements from an array.
+ splice: () ->
+ orgList = @_list.slice()
+ rtn = super
+
+ # start index
+ idx = arguments[0]
+ idx = orgList.length + idx if idx < 0
+ rmAmount = if arguments.length > 1 then arguments[1] else orgList.length - idx
+ # Elements where added/changed
+ if arguments.length > 2
+ addAmount = arguments.length - 2
+ if rmAmount > 0
+ changedAmount = if rmAmount > addAmount then addAmount else rmAmount
+ for i in [0...changedAmount] by 1
+ @_trigger 'changed', @_list[idx], orgList[idx], idx
+ idx++
+ addAmount = addAmount - changedAmount
+ rmAmount = rmAmount - changedAmount
+
+ if (rmAmount-addAmount) > 0
+ @_trigger 'removed', orgList[idx + i], idx + i for i in [0...rmAmount-addAmount] by 1
+ else if (rmAmount-addAmount) < 0
+ # only elements where added
+ @_trigger 'added', @_list[idx + i], idx + i for i in [0...addAmount-rmAmount] by 1
+ else if rmAmount > 0
+ @_trigger 'removed', orgList[idx + i], idx + i for i in [0...rmAmount] by 1
+
+ rtn
+
+ # #### reverse
+ # Reverses an array in place. The first array element becomes the last and the last becomes the first.
+ reverse: () ->
+ super
+ array = @_list;
+ length = @_list.length
+ `for (left = 0, right = length - 1; left < right; left += 1, right -= 1) {
+ if (right === left) { continue; }
+ this._trigger('movedTo', array[left], right, left);
+ this._trigger('movedTo', array[right], left+1, right);
+ }`
+ return @
+
+ # #### sort
+ # Sorts the elements of an array in place and returns the array.
+ sort: () ->
+ org = @_list.slice()
+ super
+
+ return @ if !@_hasActiveTrigger 'movedTo'
+
+ # Create a list of moves that results in @_list
+ length = @_list.length
+ moves = []
+ currentPosition = 0
+ while currentPosition < length
+ finalPosition = @_list.indexOf(org[currentPosition])
+
+ # A movement of one won't happen
+ if currentPosition+1 == finalPosition
+ # Look forward maybe we have a group here
+ while org[currentPosition+1] == @_list[finalPosition+1]
+ finalPosition++
+ currentPosition++
+ if org[currentPosition] == @_list[finalPosition]
+ finalPosition++
+ currentPosition++
+ finalPosition = @_list.indexOf(org[currentPosition])
+ # This is my evil way of detecting duplicates
+ if org[currentPosition] == org[currentPosition+1]
+ while org[currentPosition-1] == @_list[finalPosition]
+ finalPosition++
+ if org[currentPosition] == @_list[finalPosition]
+ finalPosition++
+ finalPosition = @_list.indexOf(org[currentPosition], finalPosition)
+
+ move =
+ from: currentPosition
+ to: finalPosition
+ skip = finalPosition == -1 || lastMove && lastMove.to == move.to && lastMove.from == move.from
+
+ if !skip && finalPosition != currentPosition
+ moves.push move
+ lastMove = move
+ org.splice move.to, 0, (org.splice move.from, 1)[0]
+
+ if finalPosition < currentPosition
+ currentPosition = finalPosition
+ else
+ currentPosition--
+ currentPosition++
+
+ for move in moves
+ this._trigger 'movedTo', @_list[move.to], move.from, move.to
+
+ return @
+
+ # ### *EJSON Functions*
+ # ---------------------------------------
+ # These are overrides from [ReactiveArray](reactive-array.html)
+ #
+ # #### typeName
+ # *[EJSON::typeName](http://docs.meteor.com/#ejson_type_typeName)*
+ typeName: () ->
+ 'reactive-list'
+
+ # #### equals
+ # *[EJSON::equals](http://docs.meteor.com/#ejson_type_equals)*
+ #
+ # **obj** object to compare
+ equals: (obj) ->
+ return obj? &&
+ obj instanceof ReactiveList &&
+ _.isEqual obj._list, @_list
+
+ # ### *Internal Functions*
+ # ---------------------------------------
+ #
+ # #### _trigger
+ # trigger a event to all observeables
+ _trigger: (evt) ->
+ self = @
+ args = _.toArray(arguments).slice(1)
+ if evt == 'movedTo'
+ trigger = (callbacks) ->
+ callbacks[evt].apply self, args if evt of callbacks
+ else
+ evtArgs = args.slice(0, -1)
+ evtAt = evt + 'At'
+ evtAtArgs = args
+ trigger = (callbacks) ->
+ callbacks[evt].apply self, evtArgs if evt of callbacks
+ callbacks[evtAt].apply self, evtAtArgs if evtAt of callbacks
+
+ for i, handler of @_handlers when i of @_handlers
+ if handler.stopped
+ delete @_handlers[i]
+ continue
+ trigger handler.callbacks
+ return
+
+ _hasActiveTrigger: (evt) ->
+ _.any @_handlers, (handler) ->
+ return !handler.stopped && evt of handler.callbacks
+
+ # ### _indexSet
+ _indexSet: (idx, val) ->
+ rtn = val
+ if @_list[idx] != val
+ org = list[idx]
+ rtn = super
+ @_trigger 'changed', @_list[idx], org, idx
+ rtn
+
+# ### *Helper methods*
+# ---------------------------------------
+# #### wrap
+# Method for wrapping a array
+ReactiveList.wrap = (arr) ->
+ obj = new ReactiveList
+ obj._list = _.toArray arr
+ obj._syncIndexProxies(true)
+ obj
+
+# ## EJSON add ReactiveArray
+# *[EJSON.addType](https://docs.meteor.com/#ejson_add_type)*
+EJSON.addType 'reactive-list', (jsonObj) ->
+ ReactiveList.wrap jsonObj
+
+class LiveHandler
+ constructor: (callbacks) ->
+ self = @
+ @stopped = false
+ @callbacks = callbacks
+
+ if (Deps.active)
+ Deps.onInvalidate () ->
+ self.stop()
+
+ stop: () ->
+ @stopped = true