' + Spark.isolate(function() {
+ var div = OnscreenDiv(Spark.render(function () {
+ return '
' + Spark.isolate(function () {
return '' + R.get() + '';
}) + '
';
}));
@@ -88,11 +198,11 @@ Tinytest.add("spark - basic isolate", function (test) {
});
-Tinytest.add("spark - one render", function(test) {
+Tinytest.add("spark - one render", function (test) {
var R = ReactiveVar("foo");
- var frag = WrappedFrag(Meteor.render(function() {
+ var frag = WrappedFrag(Meteor.render(function () {
return R.get();
})).hold();
@@ -117,14 +227,14 @@ Tinytest.add("spark - one render", function(test) {
test.equal(R.numListeners(), 0);
// empty return value should work, and show up as a comment
- frag = WrappedFrag(Meteor.render(function() {
+ frag = WrappedFrag(Meteor.render(function () {
return "";
}));
test.equal(frag.html(), "");
// nodes coming and going at top level of fragment
R.set(true);
- frag = WrappedFrag(Meteor.render(function() {
+ frag = WrappedFrag(Meteor.render(function () {
return R.get() ? "
hello
world
" : "";
})).hold();
test.equal(frag.html(), "
hello
world
");
@@ -141,7 +251,7 @@ Tinytest.add("spark - one render", function(test) {
// more complicated changes
R.set(1);
- frag = WrappedFrag(Meteor.render(function() {
+ frag = WrappedFrag(Meteor.render(function () {
var result = [];
for(var i=0; i
'+
@@ -170,11 +280,11 @@ Tinytest.add("spark - one render", function(test) {
test.equal(WrappedFrag(Meteor.render("foo")).html(), "foo");
});
-Tinytest.add("spark - slow path GC", function(test) {
+Tinytest.add("spark - slow path GC", function (test) {
var R = ReactiveVar(123);
- var div = OnscreenDiv(Meteor.render(function() {
+ var div = OnscreenDiv(Meteor.render(function () {
return "
The number is "+R.get()+".
underlined";
}));
@@ -194,9 +304,9 @@ Tinytest.add("spark - slow path GC", function(test) {
test.equal(R.numListeners(), 0);
});
-Tinytest.add("spark - isolate", function(test) {
+Tinytest.add("spark - isolate", function (test) {
- var inc = function(v) {
+ var inc = function (v) {
v.set(v.get() + 1); };
var R1 = ReactiveVar(0);
@@ -204,11 +314,11 @@ Tinytest.add("spark - isolate", function(test) {
var R3 = ReactiveVar(0);
var count1 = 0, count2 = 0, count3 = 0;
- var frag = WrappedFrag(Meteor.render(function() {
+ var frag = WrappedFrag(Meteor.render(function () {
return R1.get() + "," + (count1++) + " " +
- Spark.isolate(function() {
+ Spark.isolate(function () {
return R2.get() + "," + (count2++) + " " +
- Spark.isolate(function() {
+ Spark.isolate(function () {
return R3.get() + "," + (count3++);
});
});
@@ -241,13 +351,13 @@ Tinytest.add("spark - isolate", function(test) {
R2.set(0);
R3.set(0);
- frag = WrappedFrag(Meteor.render(function() {
+ frag = WrappedFrag(Meteor.render(function () {
var buf = [];
buf.push('');
- buf.push(Spark.isolate(function() {
+ buf.push(Spark.isolate(function () {
var buf = [];
for(var i=0; i'+R3.get()+'
';
}));
}
@@ -296,12 +406,12 @@ Tinytest.add("spark - isolate", function(test) {
frag.release();
// calling isolate() outside of render mode
- test.equal(Spark.isolate(function() { return "foo"; }), "foo");
+ test.equal(Spark.isolate(function () { return "foo"; }), "foo");
// caller violating preconditions
- test.throws(function() {
- Meteor.render(function() {
+ test.throws(function () {
+ Meteor.render(function () {
return Spark.isolate("foo");
});
});
@@ -310,10 +420,10 @@ Tinytest.add("spark - isolate", function(test) {
// unused isolate
var Q = ReactiveVar("foo");
- Meteor.render(function() {
+ Meteor.render(function () {
// create an isolate, in render mode,
// but don't use it.
- Spark.isolate(function() {
+ Spark.isolate(function () {
return Q.get();
});
return "";
@@ -328,9 +438,9 @@ Tinytest.add("spark - isolate", function(test) {
// nesting
var stuff = ReactiveVar(true);
- var div = OnscreenDiv(Meteor.render(function() {
- return Spark.isolate(function() {
- return "x"+(stuff.get() ? 'y' : '') + Spark.isolate(function() {
+ var div = OnscreenDiv(Meteor.render(function () {
+ return Spark.isolate(function () {
+ return "x"+(stuff.get() ? 'y' : '') + Spark.isolate(function () {
return "hi";
});
});
@@ -347,19 +457,19 @@ Tinytest.add("spark - isolate", function(test) {
var num1 = ReactiveVar(false);
var num2 = ReactiveVar(false);
var num3 = ReactiveVar(false);
- var numset = function(n) {
- _.each([num1, num2, num3], function(v, i) {
+ var numset = function (n) {
+ _.each([num1, num2, num3], function (v, i) {
v.set((i+1) === n);
});
};
numset(1);
- var div = OnscreenDiv(Meteor.render(function() {
- return Spark.isolate(function() {
+ var div = OnscreenDiv(Meteor.render(function () {
+ return Spark.isolate(function () {
return (num1.get() ? '1' : '')+
- Spark.isolate(function() {
+ Spark.isolate(function () {
return (num2.get() ? '2' : '')+
- Spark.isolate(function() {
+ Spark.isolate(function () {
return (num3.get() ? '3' : '')+'x';
});
});
@@ -482,7 +592,7 @@ Tinytest.add("spark - data context", function (test) {
Tinytest.add("spark - tables", function (test) {
var R = ReactiveVar(0);
- var table = OnscreenDiv(Meteor.render(function() {
+ var table = OnscreenDiv(Meteor.render(function () {
var buf = [];
buf.push("");
for(var i=0; i| "+(i+1)+" | ");
@@ -547,7 +657,7 @@ Tinytest.add("spark - tables", function (test) {
div.node().appendChild(DomUtils.htmlToFragment(""));
R.set(3);
div.node().getElementsByTagName("tr")[0].appendChild(Meteor.render(
- function() {
+ function () {
var buf = [];
for(var i=0; i"+(i+1)+"");
@@ -563,4 +673,303 @@ Tinytest.add("spark - tables", function (test) {
div.kill();
Meteor.flush();
test.equal(R.numListeners(), 0);
-});
\ No newline at end of file
+});
+
+var eventmap = function (/*args*/) {
+ // support event_buf as final argument
+ var event_buf = null;
+ if (arguments.length && _.isArray(arguments[arguments.length-1])) {
+ event_buf = arguments[arguments.length-1];
+ arguments.length--;
+ }
+ var events = {};
+ _.each(arguments, function (esel) {
+ var etyp = esel.split(' ')[0];
+ events[esel] = function (evt) {
+ if (evt.type !== etyp)
+ throw new Error(etyp+" event arrived as "+evt.type);
+ (event_buf || this).push(esel);
+ };
+ });
+ return events;
+};
+
+Tinytest.add("spark - event handling", function (test) {
+ var event_buf = [];
+ var getid = function (id) {
+ return document.getElementById(id);
+ };
+
+ var div;
+
+ var chunk = function (htmlFunc, options) {
+ var html = Spark.isolate(htmlFunc);
+ options = options || {};
+ if (options.events)
+ html = Spark.attachEvents(options.events, html);
+ if (options.event_data)
+ html = Spark.setDataContext(options.event_data, html);
+ return html;
+ };
+
+ var render = function (htmlFunc, options) {
+ return Spark.render(function () {
+ return chunk(htmlFunc, options);
+ });
+ };
+
+
+ // clicking on a div at top level
+ event_buf.length = 0;
+ div = OnscreenDiv(render(function () {
+ return 'Foo
';
+ }, {events: eventmap("click"), event_data:event_buf}));
+ clickElement(getid("foozy"));
+ test.equal(event_buf, ['click']);
+ div.kill();
+ Meteor.flush();
+
+ // selector that specifies a top-level div
+ event_buf.length = 0;
+ div = OnscreenDiv(render(function () {
+ return 'Foo
';
+ }, {events: eventmap("click div"), event_data:event_buf}));
+ clickElement(getid("foozy"));
+ test.equal(event_buf, ['click div']);
+ div.kill();
+ Meteor.flush();
+
+ // selector that specifies a second-level span
+ event_buf.length = 0;
+ div = OnscreenDiv(render(function () {
+ return 'Foo
';
+ }, {events: eventmap("click span"), event_data:event_buf}));
+ clickElement(getid("foozy").firstChild);
+ test.equal(event_buf, ['click span']);
+ div.kill();
+ Meteor.flush();
+
+ // replaced top-level elements still have event handlers
+ // even if replaced by an isolate above the handlers in the DOM
+ var R = ReactiveVar("p");
+ event_buf.length = 0;
+ div = OnscreenDiv(render(function () {
+ return chunk(function () {
+ return '<'+R.get()+' id="foozy">Hello'+R.get()+'>';
+ });
+ }, {events: eventmap("click"), event_data:event_buf}));
+ clickElement(getid("foozy"));
+ test.equal(event_buf, ['click']);
+ event_buf.length = 0;
+ R.set("div"); // change tag, which is sure to replace element
+ Meteor.flush();
+ clickElement(getid("foozy")); // still clickable?
+ test.equal(event_buf, ['click']);
+ event_buf.length = 0;
+ R.set("p");
+ Meteor.flush();
+ clickElement(getid("foozy"));
+ test.equal(event_buf, ['click']);
+ event_buf.length = 0;
+ div.kill();
+ Meteor.flush();
+
+ // bubbling from event on descendent of element matched
+ // by selector
+ event_buf.length = 0;
+ div = OnscreenDiv(render(function () {
+ return 'Foo'+
+ 'Bar
';
+ }, {events: eventmap("click span"), event_data:event_buf}));
+ clickElement(
+ getid("foozy").firstChild.firstChild.firstChild);
+ test.equal(event_buf, ['click span']);
+ div.kill();
+ Meteor.flush();
+
+ // bubbling order (for same event, same render node, different selector nodes)
+ event_buf.length = 0;
+ div = OnscreenDiv(render(function () {
+ return 'Foo'+
+ 'Bar
';
+ }, {events: eventmap("click span", "click b"), event_data:event_buf}));
+ clickElement(
+ getid("foozy").firstChild.firstChild.firstChild);
+ test.equal(event_buf, ['click b', 'click span']);
+ div.kill();
+ Meteor.flush();
+
+ // "bubbling" order for handlers at same level
+ event_buf.length = 0;
+ div = OnscreenDiv(render(function () {
+ return chunk(function () {
+ return chunk(function () {
+ return 'Hello';
+ }, {events: eventmap("click .c"), event_data:event_buf});
+ }, {events: eventmap("click .b"), event_data:event_buf});
+ }, {events: eventmap("click .a"), event_data:event_buf}));
+ clickElement(getid("foozy"));
+ test.equal(event_buf, ['click .c', 'click .b', 'click .a']);
+ event_buf.length = 0;
+ div.kill();
+ Meteor.flush();
+
+ // stopPropagation doesn't prevent other event maps from
+ // handling same node
+ event_buf.length = 0;
+ div = OnscreenDiv(render(function () {
+ return chunk(function () {
+ return chunk(function () {
+ return 'Hello';
+ }, {events: eventmap("click .c"), event_data:event_buf});
+ }, {events: {"click .b": function (evt) {
+ event_buf.push("click .b"); evt.stopPropagation();}}});
+ }, {events: eventmap("click .a"), event_data:event_buf}));
+ clickElement(getid("foozy"));
+ test.equal(event_buf, ['click .c', 'click .b', 'click .a']);
+ event_buf.length = 0;
+ div.kill();
+ Meteor.flush();
+
+ // stopImmediatePropagation DOES
+ event_buf.length = 0;
+ div = OnscreenDiv(render(function () {
+ return chunk(function () {
+ return chunk(function () {
+ return 'Hello';
+ }, {events: eventmap("click .c"), event_data:event_buf});
+ }, {events: {"click .b": function (evt) {
+ event_buf.push("click .b");
+ evt.stopImmediatePropagation();}}});
+ }, {events: eventmap("click .a"), event_data:event_buf}));
+ clickElement(getid("foozy"));
+ test.equal(event_buf, ['click .c', 'click .b']);
+ event_buf.length = 0;
+ div.kill();
+ Meteor.flush();
+
+ // bubbling continues even with DOM change
+ event_buf.length = 0;
+ R = ReactiveVar(true);
+ div = OnscreenDiv(render(function () {
+ return chunk(function () {
+ return ''+(R.get()?'abcd':'')+'
';
+ }, {events: { 'click span': function () {
+ event_buf.push('click span');
+ R.set(false);
+ Meteor.flush(); // kill the span
+ }, 'click div': function (evt) {
+ event_buf.push('click div');
+ }}});
+ }));
+ // click on span
+ clickElement(getid("foozy"));
+ test.expect_fail(); // doesn't seem to work in old IE
+ test.equal(event_buf, ['click span', 'click div']);
+ event_buf.length = 0;
+ div.kill();
+ Meteor.flush();
+
+ // "deep reach" from high node down to replaced low node.
+ // Tests that attach_secondary_events actually does the
+ // right thing in IE. Also tests change event bubbling
+ // and proper interpretation of event maps.
+ event_buf.length = 0;
+ R = ReactiveVar('foo');
+ div = OnscreenDiv(render(function () {
+ return '';
+ }, { events: eventmap('change b', 'change input'), event_data:event_buf }));
+ R.set('bar');
+ Meteor.flush();
+ // click on input
+ clickElement(div.node().getElementsByTagName('input')[0]);
+ event_buf.sort(); // don't care about order
+ test.equal(event_buf, ['change b', 'change input', 'click input']);
+ event_buf.length = 0;
+ div.kill();
+ Meteor.flush();
+
+ // test that 'click *' fires on bubble
+ event_buf.length = 0;
+ R = ReactiveVar('foo');
+ div = OnscreenDiv(render(function () {
+ return '';
+ }, { events: eventmap('click *'), event_data:event_buf }));
+ R.set('bar');
+ Meteor.flush();
+ // click on input
+ clickElement(div.node().getElementsByTagName('input')[0]);
+ test.equal(
+ event_buf,
+ ['click input', 'click *', 'click *', 'click *', 'click *', 'click *']);
+ event_buf.length = 0;
+ div.kill();
+ Meteor.flush();
+
+ // clicking on a div in a nested chunk (without patching)
+ event_buf.length = 0;
+ R = ReactiveVar('foo');
+ div = OnscreenDiv(render(function () {
+ return R.get() + chunk(function () {
+ return 'ism';
+ }, {events: eventmap("click"), event_data:event_buf});
+ }));
+ test.equal(div.text(), 'fooism');
+ clickElement(div.node().getElementsByTagName('SPAN')[0]);
+ test.equal(event_buf, ['click']);
+ event_buf.length = 0;
+ R.set('bar');
+ Meteor.flush();
+ test.equal(div.text(), 'barism');
+ clickElement(div.node().getElementsByTagName('SPAN')[0]);
+ test.equal(event_buf, ['click']);
+ event_buf.length = 0;
+ div.kill();
+ Meteor.flush();
+
+ // Test that reactive fragments manually inserted inside
+ // a reactive fragment eventually get wired.
+ event_buf.length = 0;
+ div = OnscreenDiv(render(function () {
+ return "";
+ }, { events: eventmap("click span", event_buf) }));
+ Meteor.flush();
+ div.node().firstChild.appendChild(render(function () {
+ return 'hello';
+ }));
+ clickElement(getid("foozy"));
+ // implementation has no way to know we've inserted the fragment
+ test.equal(event_buf, []);
+ event_buf.length = 0;
+ Meteor.flush();
+ clickElement(getid("foozy"));
+ // now should be wired up
+ test.equal(event_buf, ['click span']);
+ event_buf.length = 0;
+ div.kill();
+ Meteor.flush();
+
+ // Event data comes from event.currentTarget, not event.target
+ var data_buf = [];
+ div = OnscreenDiv(render(function () {
+ return ""+chunk(function () {
+ return '- Hello
';
+ }, { event_data: {x:'listuff'} })+"
";
+ }, { event_data: {x:'ulstuff'},
+ events: { 'click ul': function () { data_buf.push(this); }}}));
+ clickElement(getid("funyard"));
+ test.equal(data_buf, [{x:'ulstuff'}]);
+ div.kill();
+ Meteor.flush();
+});
+
+
+})();