diff --git a/packages/templating/package.js b/packages/templating/package.js index a43805b71a..d26089a3da 100644 --- a/packages/templating/package.js +++ b/packages/templating/package.js @@ -12,7 +12,7 @@ Package._transitional_registerBuildPlugin({ name: "compileTemplates", use: ['spacebars'], sources: [ - 'plugin/html2_scanner.js', + 'plugin/html_scanner.js', 'plugin/compile-templates.js' ] }); @@ -45,7 +45,7 @@ Package.on_test(function (api) { 'templating_tests.html' ], 'client'); api.add_files([ - 'plugin/html2_scanner.js', + 'plugin/html_scanner.js', 'scanner_tests.js' ], 'server'); }); diff --git a/packages/templating/plugin/html2_scanner.js b/packages/templating/plugin/html_scanner.js similarity index 100% rename from packages/templating/plugin/html2_scanner.js rename to packages/templating/plugin/html_scanner.js diff --git a/packages/ui/attrs2.js b/packages/ui/attrs.js similarity index 100% rename from packages/ui/attrs2.js rename to packages/ui/attrs.js diff --git a/packages/ui/dombackend2.js b/packages/ui/dombackend.js similarity index 100% rename from packages/ui/dombackend2.js rename to packages/ui/dombackend.js diff --git a/packages/ui/each2.js b/packages/ui/each.js similarity index 100% rename from packages/ui/each2.js rename to packages/ui/each.js diff --git a/packages/ui/package.js b/packages/ui/package.js index 661a121365..990c8a0faa 100644 --- a/packages/ui/package.js +++ b/packages/ui/package.js @@ -18,13 +18,13 @@ Package.on_use(function (api) { api.add_files(['exceptions.js', 'base.js']); - api.add_files(['dombackend2.js', + api.add_files(['dombackend.js', 'domrange.js'], 'client'); - api.add_files(['attrs2.js', - 'render2.js', + api.add_files(['attrs.js', + 'render.js', 'components.js', - 'each2.js', + 'each.js', 'fields.js' ]); @@ -41,9 +41,8 @@ Package.on_test(function (api) { api.add_files([ 'base_tests.js', - 'render_tests.js', 'domrange_tests.js', - 'render2_tests.js', + 'render_tests.js', 'dombackend_tests.js' ], 'client'); }); diff --git a/packages/ui/render2.js b/packages/ui/render.js similarity index 100% rename from packages/ui/render2.js rename to packages/ui/render.js diff --git a/packages/ui/render2_tests.js b/packages/ui/render2_tests.js deleted file mode 100644 index 38c19896da..0000000000 --- a/packages/ui/render2_tests.js +++ /dev/null @@ -1,472 +0,0 @@ -var materialize = UI.materialize; -var toHTML = UI.toHTML; -var toCode = UI.toCode; - -var P = HTML.Tag.P; -var CharRef = HTML.CharRef; -var DIV = HTML.Tag.DIV; -var Comment = HTML.Comment; -var BR = HTML.Tag.BR; -var A = HTML.Tag.A; -var UL = HTML.Tag.UL; -var LI = HTML.Tag.LI; -var SPAN = HTML.Tag.SPAN; -var HR = HTML.Tag.HR; - -Tinytest.add("ui - render2 - basic", function (test) { - var run = function (input, expectedInnerHTML, expectedHTML, expectedCode) { - var div = document.createElement("DIV"); - materialize(input, div); - test.equal(canonicalizeHtml(div.innerHTML), expectedInnerHTML); - test.equal(toHTML(input), expectedHTML); - test.equal(toCode(input), expectedCode); - }; - - run(P('Hello'), - '

Hello

', - '

Hello

', - 'HTML.Tag.P("Hello")'); - - run(null, '', '', 'null'); - run([], '', '', '[]'); - run([null, null], '', '', '[null, null]'); - - // Test crazy character references - - // `𝕫` is "Mathematical double-struck small z" a.k.a. "open-face z" - run(P(CharRef({html: '𝕫', str: '\ud835\udd6b'})), - '

\ud835\udd6b

', - '

𝕫

', - 'HTML.Tag.P(HTML.CharRef({html: "𝕫", str: "\\ud835\\udd6b"}))'); - - run(P({id: CharRef({html: '𝕫', str: '\ud835\udd6b'})}, 'Hello'), - '

Hello

', - '

Hello

', - 'HTML.Tag.P({id: HTML.CharRef({html: "𝕫", str: "\\ud835\\udd6b"})}, "Hello")'); - - run(P({id: [CharRef({html: '𝕫', str: '\ud835\udd6b'}), '!']}, 'Hello'), - '

Hello

', - '

Hello

', - 'HTML.Tag.P({id: [HTML.CharRef({html: "𝕫", str: "\\ud835\\udd6b"}), "!"]}, "Hello")'); - - // Test comments - - run(DIV(Comment('Test')), - '
', // our innerHTML-canonicalization function kills comment contents - '
', - 'HTML.Tag.DIV(HTML.Comment("Test"))'); - - // Test arrays - - run([P('Hello'), P('World')], - '

Hello

World

', - '

Hello

World

', - '[HTML.Tag.P("Hello"), HTML.Tag.P("World")]'); - - // Test slightly more complicated structure - - run(DIV({'class': 'foo'}, UL(LI(P(A({href: '#one'}, 'One'))), - LI(P('Two', BR(), 'Three')))), - '
', - '
', - 'HTML.Tag.DIV({"class": "foo"}, HTML.Tag.UL(HTML.Tag.LI(HTML.Tag.P(HTML.Tag.A({href: "#one"}, "One"))), HTML.Tag.LI(HTML.Tag.P("Two", HTML.Tag.BR(), "Three"))))'); -}); - -Tinytest.add("ui - render2 - closures", function (test) { - - // Reactively change a text node - (function () { - var R = ReactiveVar('Hello'); - var test1 = P(function () { return R.get(); }); - - test.equal(toHTML(test1), '

Hello

'); - - var div = document.createElement("DIV"); - materialize(test1, div); - test.equal(canonicalizeHtml(div.innerHTML), "

Hello

"); - - R.set('World'); - Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), "

World

"); - })(); - - // Reactively change an array of text nodes - (function () { - var R = ReactiveVar(['Hello', ' World']); - var test1 = P(function () { return R.get(); }); - - test.equal(toHTML(test1), '

Hello World

'); - - var div = document.createElement("DIV"); - materialize(test1, div); - test.equal(canonicalizeHtml(div.innerHTML), "

Hello World

"); - - R.set(['Goodbye', ' World']); - Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), "

Goodbye World

"); - })(); - -}); - -Tinytest.add("ui - render2 - closure GC", function (test) { - // test that removing parent element removes listeners and stops autoruns. - (function () { - var R = ReactiveVar('Hello'); - var test1 = P(function () { return R.get(); }); - - var div = document.createElement("DIV"); - materialize(test1, div); - test.equal(canonicalizeHtml(div.innerHTML), "

Hello

"); - - R.set('World'); - Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), "

World

"); - - test.equal(R.numListeners(), 1); - - $(div).remove(); - - test.equal(R.numListeners(), 0); - - R.set('Steve'); - Deps.flush(); - // should not have changed: - test.equal(canonicalizeHtml(div.innerHTML), "

World

"); - })(); - -}); - -Tinytest.add("ui - render2 - reactive attributes", function (test) { - (function () { - var R = ReactiveVar({'class': ['david gre', CharRef({html: 'ë', str: '\u00eb'}), 'nspan'], - id: 'foo'}); - - var spanCode = SPAN({$attrs: function () { return R.get(); }}); - test.equal(typeof spanCode.attrs, 'function'); - - test.equal(toHTML(spanCode), ''); - - test.equal(R.numListeners(), 0); - - var div = document.createElement("DIV"); - materialize(spanCode, div); - test.equal(canonicalizeHtml(div.innerHTML), ''); - - test.equal(R.numListeners(), 1); - - var span = div.firstChild; - test.equal(span.nodeName, 'SPAN'); - span.className += ' blah'; - - R.set({'class': 'david smith', id: 'bar'}); - Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); - test.equal(R.numListeners(), 1); - - R.set({}); - Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); - test.equal(R.numListeners(), 1); - - $(div).remove(); - - test.equal(R.numListeners(), 0); - })(); - - // Test `null`, `undefined`, and `[]` attributes - (function () { - var R = ReactiveVar({id: 'foo', - aaa: null, - bbb: undefined, - ccc: [], - ddd: [null], - eee: [undefined], - fff: [[]], - ggg: ['x', ['y', ['z']]]}); - - var spanCode = SPAN({$attrs: function () { return R.get(); }}); - - test.equal(toHTML(spanCode), ''); - test.equal(toCode(SPAN(R.get())), - 'HTML.Tag.SPAN({id: "foo", ggg: ["x", ["y", ["z"]]]})'); - - var div = document.createElement("DIV"); - materialize(spanCode, div); - var span = div.firstChild; - test.equal(span.nodeName, 'SPAN'); - - test.equal(canonicalizeHtml(div.innerHTML), ''); - R.set({id: 'foo', ggg: [[], [], []]}); - Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); - - R.set({id: 'foo', ggg: null}); - Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); - - R.set({id: 'foo', ggg: ''}); - Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); - - $(span).remove(); - - test.equal(R.numListeners(), 0); - })(); -}); - -Tinytest.add("ui - render2 - components", function (test) { - (function () { - var counter = 1; - var buf = []; - - var myComponent = UI.Component.extend({ - init: function () { - // `this` is the component instance - var number = counter++; - this.number = number; - - if (this.parent) - buf.push('parent of ' + this.number + ' is ' + this.parent.number); - - this.data = function () { - return this.number; - }; - }, - created: function () { - // `this` is the template instance - buf.push('created ' + this.data); - }, - render: function () { - // `this` is the component instance - return [String(this.number), - - (this.number < 3 ? myComponent : HR())]; - }, - rendered: function () { - // `this` is the template instance - var nodeDescr = function (node) { - if (node.nodeType === 8) // comment - return ''; - if (node.nodeType === 3) // text - return node.nodeValue; - - return node.nodeName; - }; - - var start = this.firstNode; - var end = this.lastNode; - // skip marker nodes - while (start !== end && ! nodeDescr(start)) - start = start.nextSibling; - while (end !== start && ! nodeDescr(end)) - end = end.previousSibling; - - - // `this` is the template instance - buf.push('dom-' + this.data + ' is ' + nodeDescr(start) +'..' + - nodeDescr(end)); - }, - destroyed: function () { - // `this` is the template instance - buf.push('destroyed ' + this.data); - } - }); - - var div = document.createElement("DIV"); - - materialize(myComponent, div); - test.equal(buf, ['created 1', - 'parent of 2 is 1', - 'created 2', - 'parent of 3 is 2', - 'created 3', - // (proper order for these has not be thought out:) - 'dom-1 is 1..HR', - 'dom-2 is 2..HR', - 'dom-3 is 3..HR']); - - test.equal(canonicalizeHtml(div.innerHTML), '123
'); - - buf.length = 0; - $(div).remove(); - buf.sort(); - test.equal(buf, ['destroyed 1', 'destroyed 2', 'destroyed 3']); - - // Now use toHTML. Should still get most of the callbacks (not `rendered`). - - buf.length = 0; - counter = 1; - - var html = toHTML(myComponent); - - test.equal(buf, ['created 1', - 'parent of 2 is 1', - 'created 2', - 'parent of 3 is 2', - 'created 3']); - - test.equal(html, '123
'); - })(); -}); - - -Tinytest.add("ui - render2 - emboxValue", function (test) { - var R = ReactiveVar('ALPHA'); - - var numCalcs = [0, 0, 0]; - - var firstLetter = UI.emboxValue(function () { - numCalcs[0]++; - return R.get().charAt(0); - }); - - var secondLetter = UI.emboxValue(function () { - numCalcs[1]++; - return R.get().charAt(1); - }); - - var thirdLetter = UI.emboxValue(function () { - numCalcs[2]++; - return R.get().charAt(2); - }); - - var setSink = function (n, value) { - if (sinks[n] === value) - sinks[n] += '-error'; // duplicate, shouldn't happen! - else - sinks[n] = value; - }; - - - test.equal(R.numListeners(), 0); - test.equal(numCalcs, [0, 0, 0]); - - var comps = []; - var sinks = []; - comps[0] = Deps.autorun(function () { - setSink(0, firstLetter()); - }); - comps[1] = Deps.autorun(function () { - setSink(1, firstLetter()); - }); - - test.equal(R.numListeners(), 1); - test.equal(numCalcs, [1, 0, 0]); - test.equal(sinks, ['A', 'A']); - - R.set('APPLE'); - Deps.flush(); - test.equal(R.numListeners(), 1); - test.equal(numCalcs, [2, 0, 0]); - test.equal(sinks, ['A', 'A']); - - // This non-reactive call to firstLetter piggybacks on the - // existing computation, which already has the value handy. - test.equal(firstLetter(), 'A'); - test.equal(numCalcs, [2, 0, 0]); - - comps[0].stop(); - comps[1].stop(); - Deps.flush(); - test.equal(R.numListeners(), 0); - test.equal(numCalcs, [2, 0, 0]); - test.equal(sinks, ['A', 'A']); - - // *This* non-reactive call to firstLetter, on the other hand, - // happens at a time when the emboxed value has no running - // computation, so it gets calculated directly. - test.equal(firstLetter(), 'A'); - test.equal(numCalcs, [3, 0, 0]); - test.equal(R.numListeners(), 0); - - // Start some new autoruns. - sinks = []; - comps[0] = Deps.autorun(function () { - setSink(0, firstLetter()); - }); - comps[1] = Deps.autorun(function () { - firstLetter(); // extra call shouldn't matter - setSink(1, firstLetter()); - }); - - test.equal(R.numListeners(), 1); - test.equal(numCalcs, [4, 0, 0]); - test.equal(sinks, ['A', 'A']); - - R.set('BANANA'); - Deps.flush(); - test.equal(R.numListeners(), 1); - // it's important that exactly one calculation happened, - // which indicates that the inner computation of the - // emboxValue has been re-run but not torn down and - // re-established. - test.equal(numCalcs, [5, 0, 0]); - test.equal(sinks, ['B', 'B']); - - R.set('CUCUMBER'); - Deps.flush(); - test.equal(R.numListeners(), 1); - test.equal(numCalcs, [6, 0, 0]); - test.equal(sinks, ['C', 'C']); - - comps[2] = Deps.autorun(function () { - setSink(2, secondLetter()); - }); - - test.equal(R.numListeners(), 2); - test.equal(numCalcs, [6, 1, 0]); - test.equal(sinks, ['C', 'C', 'U']); - - R.set('DOILY'); - Deps.flush(); - test.equal(R.numListeners(), 2); - test.equal(numCalcs, [7, 2, 0]); - test.equal(sinks, ['D', 'D', 'O']); - - comps[3] = Deps.autorun(function () { - setSink(3, firstLetter() + secondLetter() + thirdLetter()); - }); - - test.equal(R.numListeners(), 3); - test.equal(numCalcs, [7, 2, 1]); - test.equal(sinks, ['D', 'D', 'O', 'DOI']); - - - R.set('ENVY'); - Deps.flush(); - test.equal(R.numListeners(), 3); - test.equal(numCalcs, [8, 3, 2]); - test.equal(sinks, ['E', 'E', 'N', 'ENV']); - - R.set('EMPTY'); - Deps.flush(); - test.equal(R.numListeners(), 3); - test.equal(numCalcs, [9, 4, 3]); - test.equal(sinks, ['E', 'E', 'M', 'EMP']); - - comps[0].stop(); - Deps.flush(); - // comps[3] still listens to first, second, and third, which - // listen to R. - test.equal(R.numListeners(), 3); - - comps[1].stop(); - Deps.flush(); - test.equal(R.numListeners(), 3); - - comps[2].stop(); - Deps.flush(); - test.equal(R.numListeners(), 3); - - comps[3].stop(); - test.equal(firstLetter() + secondLetter() + thirdLetter(), 'EMP'); - Deps.flush(); - // BAM, all listeners gone! - test.equal(R.numListeners(), 0); - - ////// Test non-function case - - test.equal(UI.emboxValue(3)(), 3); - test.equal(UI.emboxValue(null)(), null); - test.equal(UI.emboxValue({x:1})(), {x:1}); -}); \ No newline at end of file diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index 0a56d11cd2..38c19896da 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -1,302 +1,472 @@ +var materialize = UI.materialize; +var toHTML = UI.toHTML; +var toCode = UI.toCode; -/* -TODO - UPDATE THESE TESTS +var P = HTML.Tag.P; +var CharRef = HTML.CharRef; +var DIV = HTML.Tag.DIV; +var Comment = HTML.Comment; +var BR = HTML.Tag.BR; +var A = HTML.Tag.A; +var UL = HTML.Tag.UL; +var LI = HTML.Tag.LI; +var SPAN = HTML.Tag.SPAN; +var HR = HTML.Tag.HR; -Tinytest.add("ui - render", function (test) { +Tinytest.add("ui - render2 - basic", function (test) { + var run = function (input, expectedInnerHTML, expectedHTML, expectedCode) { + var div = document.createElement("DIV"); + materialize(input, div); + test.equal(canonicalizeHtml(div.innerHTML), expectedInnerHTML); + test.equal(toHTML(input), expectedHTML); + test.equal(toCode(input), expectedCode); + }; + + run(P('Hello'), + '

Hello

', + '

Hello

', + 'HTML.Tag.P("Hello")'); + + run(null, '', '', 'null'); + run([], '', '', '[]'); + run([null, null], '', '', '[null, null]'); + + // Test crazy character references + + // `𝕫` is "Mathematical double-struck small z" a.k.a. "open-face z" + run(P(CharRef({html: '𝕫', str: '\ud835\udd6b'})), + '

\ud835\udd6b

', + '

𝕫

', + 'HTML.Tag.P(HTML.CharRef({html: "𝕫", str: "\\ud835\\udd6b"}))'); + + run(P({id: CharRef({html: '𝕫', str: '\ud835\udd6b'})}, 'Hello'), + '

Hello

', + '

Hello

', + 'HTML.Tag.P({id: HTML.CharRef({html: "𝕫", str: "\\ud835\\udd6b"})}, "Hello")'); + + run(P({id: [CharRef({html: '𝕫', str: '\ud835\udd6b'}), '!']}, 'Hello'), + '

Hello

', + '

Hello

', + 'HTML.Tag.P({id: [HTML.CharRef({html: "𝕫", str: "\\ud835\\udd6b"}), "!"]}, "Hello")'); + + // Test comments + + run(DIV(Comment('Test')), + '
', // our innerHTML-canonicalization function kills comment contents + '
', + 'HTML.Tag.DIV(HTML.Comment("Test"))'); + + // Test arrays + + run([P('Hello'), P('World')], + '

Hello

World

', + '

Hello

World

', + '[HTML.Tag.P("Hello"), HTML.Tag.P("World")]'); + + // Test slightly more complicated structure + + run(DIV({'class': 'foo'}, UL(LI(P(A({href: '#one'}, 'One'))), + LI(P('Two', BR(), 'Three')))), + '
', + '
', + 'HTML.Tag.DIV({"class": "foo"}, HTML.Tag.UL(HTML.Tag.LI(HTML.Tag.P(HTML.Tag.A({href: "#one"}, "One"))), HTML.Tag.LI(HTML.Tag.P("Two", HTML.Tag.BR(), "Three"))))'); +}); + +Tinytest.add("ui - render2 - closures", function (test) { + + // Reactively change a text node (function () { - var c = UI.Component.extend({ - render: function (buf) { - buf.write("asdf"); - } - }); + var R = ReactiveVar('Hello'); + var test1 = P(function () { return R.get(); }); - c.build(); - test.equal($(c._offscreen).html(), "asdf"); - c.destroy(); + test.equal(toHTML(test1), '

Hello

'); + + var div = document.createElement("DIV"); + materialize(test1, div); + test.equal(canonicalizeHtml(div.innerHTML), "

Hello

"); + + R.set('World'); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), "

World

"); })(); + // Reactively change an array of text nodes (function () { - var c = UI.Component.extend({ - render: function (buf) { - buf.write("
asdf
"); - } - }); + var R = ReactiveVar(['Hello', ' World']); + var test1 = P(function () { return R.get(); }); - c.build(); - test.equal($(c._offscreen).html(), "
asdf
"); - c.destroy(); - })(); + test.equal(toHTML(test1), '

Hello World

'); - (function () { - var R = ReactiveVar("blam"); - var c = UI.Component.extend({ - render: function (buf) { - buf.write( - "foo", - UI.Text.withData(function () { return R.get(); }), - "bar"); - } - }); + var div = document.createElement("DIV"); + materialize(test1, div); + test.equal(canonicalizeHtml(div.innerHTML), "

Hello World

"); - c.build(); - test.equal($(c._offscreen).html(), "fooblambar"); - R.set("ki"); + R.set(['Goodbye', ' World']); Deps.flush(); - test.equal($(c._offscreen).html(), "fookibar"); - c.destroy(); - test.equal(R.numListeners(), 0); - })(); - - - - (function () { - var R = ReactiveVar("
"); - var c = UI.Component.extend({ - render: function (buf) { - buf.write( - "foo", - UI.HTML.withData(function () { return R.get(); }), - "bar"); - } - }); - - c.build(); - test.equal($(c._offscreen).html(), "foo
bar"); - R.set("
hi
"); - Deps.flush(); - test.equal($(c._offscreen).html(), "foo
hi
bar"); - c.destroy(); - test.equal(R.numListeners(), 0); - })(); - - - (function () { - var c = UI.Component.extend({ - render: function (buf) { - // this isn't idiomatic but serves to test `child:` - buf.write({child: UI.Text, props: {data: "hello"}}); - } - }); - - c.build(); - test.equal($(c._offscreen).html(), "hello"); - c.destroy(); - })(); - - - (function () { - var R = ReactiveVar(1); - var c = UI.Component.extend({ - init: function () { - this.a = this.add(UI.Text.withData("hello")); - }, - render: function (buf) { - // use existing, inited (added) component! - buf.write(this.a); - // create dependency to test re-render behavior - buf.write(String(R.get())); - } - }); - - c.build(); - test.equal($(c._offscreen).html(), "hello1"); - test.equal(_.keys(c.children), [c.a.guid]); - R.set(2); - Deps.flush(); - test.equal($(c._offscreen).html(), "hello2"); - test.equal(_.keys(c.children), [c.a.guid]); - c.destroy(); - })(); - - (function () { - var R = ReactiveVar(1); - var which = ReactiveVar("H"); - var c = UI.Component.extend({ - init: function () { - this.a = this.add(UI.Text.withData("hello")); - this.b = this.add(UI.Text.withData("world")); - }, - render: function (buf) { - var self = this; - // use existing, inited (added) component. - // also, choose which one to use reactively! - buf.write({child: function () { - return which.get() === "H" ? self.a : self.b; - }}); - // create dependency to test re-render behavior - buf.write(String(R.get())); - } - }); - - c.build(); - test.equal($(c._offscreen).html(), "hello1"); - test.equal(_.keys(c.children), [c.a.guid, - c.b.guid]); - test.isTrue(c.a.isAttached); - test.isFalse(c.b.isAttached); - R.set(2); - Deps.flush(); - test.equal($(c._offscreen).html(), "hello2"); - test.equal(_.keys(c.children), [c.a.guid, - c.b.guid]); - test.isTrue(c.a.isAttached); - test.isFalse(c.b.isAttached); - which.set("W"); - Deps.flush(); - test.equal($(c._offscreen).html(), "world2"); - test.equal(_.keys(c.children), [c.a.guid, c.b.guid]); - test.isFalse(c.a.isAttached); - test.isTrue(c.b.isAttached); - c.destroy(); - test.equal(R.numListeners(), 0); - test.equal(which.numListeners(), 0); - })(); - - - (function () { - var R = ReactiveVar(1); - var which = ReactiveVar("H"); - var name = ReactiveVar("David"); - - // two factory (uninited) Components - var Hello = UI.Component.extend({ - render: function (buf) { - buf.write("hello", this.get()); - } - }); - var World = UI.Component.extend({ - render: function (buf) { - buf.write("world", this.get()); - } - }); - - var c = UI.Component.extend({ - render: function (buf) { - var self = this; - // choose which one to use reactively - buf.write({child: function () { - return which.get() === "H" ? Hello : World; - }, props: { - data: function () { return name.get(); } - }}); - // create dependency to test re-render behavior - buf.write(String(R.get())); - } - }); - - c.build(); - test.equal($(c._offscreen).html(), "helloDavid1"); - test.equal(_.keys(c.children).length, 1); - R.set(2); - Deps.flush(); - test.equal($(c._offscreen).html(), "helloDavid2"); - test.equal(_.keys(c.children).length, 1); - which.set("W"); - Deps.flush(); - test.equal($(c._offscreen).html(), "worldDavid2"); - test.equal(_.keys(c.children).length, 1); - for (var theWorld in c.children) {} - name.set("Wayne"); - Deps.flush(); - test.equal($(c._offscreen).html(), "worldWayne2"); - test.equal(_.keys(c.children), [theWorld]); - c.destroy(); - test.equal(R.numListeners(), 0); - test.equal(which.numListeners(), 0); - test.equal(name.numListeners(), 0); - })(); - - (function () { - var which = ReactiveVar("H"); - - // two factory (uninited) Components - var Hello = UI.Text.withData("hello"); - var World = UI.Text.withData("world"); - - var c = UI.Component.extend({ - render: function (buf) { - var self = this; - // choose which one to use reactively - buf.write(function () { - return which.get() === "H" ? Hello : World; - }); - } - }); - - c.build(); - test.equal($(c._offscreen).html(), "hello"); - which.set("W"); - Deps.flush(); - test.equal($(c._offscreen).html(), "world"); - test.equal(_.keys(c.children).length, 1); - which.set("H"); - Deps.flush(); - test.equal($(c._offscreen).html(), "hello"); - test.equal(_.keys(c.children).length, 1); - c.destroy(); - test.equal(which.numListeners(), 0); + test.equal(canonicalizeHtml(div.innerHTML), "

Goodbye World

"); })(); }); -Tinytest.add("ui - if/unless", function (test) { - var hello = UI.Text.withData("hello"); - var world = UI.Text.withData("world"); - var R = ReactiveVar('true'); - var renderedCounts = [0, 0, 0]; - var c = UI.Component.extend({ - rendered: function () { renderedCounts[0]++; }, - render: function (buf) { - buf.write( - UI.If.extend({ - data: function () { - return R.get().charAt(0) === 't'; - }, - content: hello, - elseContent: world, - rendered: function () { - renderedCounts[1]++; - } - }), - UI.Unless.extend({ - data: function () { - return R.get().charAt(0) === 't'; - }, - content: hello, - elseContent: world, - rendered: function () { - renderedCounts[2]++; - } - })); - } +Tinytest.add("ui - render2 - closure GC", function (test) { + // test that removing parent element removes listeners and stops autoruns. + (function () { + var R = ReactiveVar('Hello'); + var test1 = P(function () { return R.get(); }); + + var div = document.createElement("DIV"); + materialize(test1, div); + test.equal(canonicalizeHtml(div.innerHTML), "

Hello

"); + + R.set('World'); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), "

World

"); + + test.equal(R.numListeners(), 1); + + $(div).remove(); + + test.equal(R.numListeners(), 0); + + R.set('Steve'); + Deps.flush(); + // should not have changed: + test.equal(canonicalizeHtml(div.innerHTML), "

World

"); + })(); + +}); + +Tinytest.add("ui - render2 - reactive attributes", function (test) { + (function () { + var R = ReactiveVar({'class': ['david gre', CharRef({html: 'ë', str: '\u00eb'}), 'nspan'], + id: 'foo'}); + + var spanCode = SPAN({$attrs: function () { return R.get(); }}); + test.equal(typeof spanCode.attrs, 'function'); + + test.equal(toHTML(spanCode), ''); + + test.equal(R.numListeners(), 0); + + var div = document.createElement("DIV"); + materialize(spanCode, div); + test.equal(canonicalizeHtml(div.innerHTML), ''); + + test.equal(R.numListeners(), 1); + + var span = div.firstChild; + test.equal(span.nodeName, 'SPAN'); + span.className += ' blah'; + + R.set({'class': 'david smith', id: 'bar'}); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(R.numListeners(), 1); + + R.set({}); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(R.numListeners(), 1); + + $(div).remove(); + + test.equal(R.numListeners(), 0); + })(); + + // Test `null`, `undefined`, and `[]` attributes + (function () { + var R = ReactiveVar({id: 'foo', + aaa: null, + bbb: undefined, + ccc: [], + ddd: [null], + eee: [undefined], + fff: [[]], + ggg: ['x', ['y', ['z']]]}); + + var spanCode = SPAN({$attrs: function () { return R.get(); }}); + + test.equal(toHTML(spanCode), ''); + test.equal(toCode(SPAN(R.get())), + 'HTML.Tag.SPAN({id: "foo", ggg: ["x", ["y", ["z"]]]})'); + + var div = document.createElement("DIV"); + materialize(spanCode, div); + var span = div.firstChild; + test.equal(span.nodeName, 'SPAN'); + + test.equal(canonicalizeHtml(div.innerHTML), ''); + R.set({id: 'foo', ggg: [[], [], []]}); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), ''); + + R.set({id: 'foo', ggg: null}); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), ''); + + R.set({id: 'foo', ggg: ''}); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), ''); + + $(span).remove(); + + test.equal(R.numListeners(), 0); + })(); +}); + +Tinytest.add("ui - render2 - components", function (test) { + (function () { + var counter = 1; + var buf = []; + + var myComponent = UI.Component.extend({ + init: function () { + // `this` is the component instance + var number = counter++; + this.number = number; + + if (this.parent) + buf.push('parent of ' + this.number + ' is ' + this.parent.number); + + this.data = function () { + return this.number; + }; + }, + created: function () { + // `this` is the template instance + buf.push('created ' + this.data); + }, + render: function () { + // `this` is the component instance + return [String(this.number), + + (this.number < 3 ? myComponent : HR())]; + }, + rendered: function () { + // `this` is the template instance + var nodeDescr = function (node) { + if (node.nodeType === 8) // comment + return ''; + if (node.nodeType === 3) // text + return node.nodeValue; + + return node.nodeName; + }; + + var start = this.firstNode; + var end = this.lastNode; + // skip marker nodes + while (start !== end && ! nodeDescr(start)) + start = start.nextSibling; + while (end !== start && ! nodeDescr(end)) + end = end.previousSibling; + + + // `this` is the template instance + buf.push('dom-' + this.data + ' is ' + nodeDescr(start) +'..' + + nodeDescr(end)); + }, + destroyed: function () { + // `this` is the template instance + buf.push('destroyed ' + this.data); + } + }); + + var div = document.createElement("DIV"); + + materialize(myComponent, div); + test.equal(buf, ['created 1', + 'parent of 2 is 1', + 'created 2', + 'parent of 3 is 2', + 'created 3', + // (proper order for these has not be thought out:) + 'dom-1 is 1..HR', + 'dom-2 is 2..HR', + 'dom-3 is 3..HR']); + + test.equal(canonicalizeHtml(div.innerHTML), '123
'); + + buf.length = 0; + $(div).remove(); + buf.sort(); + test.equal(buf, ['destroyed 1', 'destroyed 2', 'destroyed 3']); + + // Now use toHTML. Should still get most of the callbacks (not `rendered`). + + buf.length = 0; + counter = 1; + + var html = toHTML(myComponent); + + test.equal(buf, ['created 1', + 'parent of 2 is 1', + 'created 2', + 'parent of 3 is 2', + 'created 3']); + + test.equal(html, '123
'); + })(); +}); + + +Tinytest.add("ui - render2 - emboxValue", function (test) { + var R = ReactiveVar('ALPHA'); + + var numCalcs = [0, 0, 0]; + + var firstLetter = UI.emboxValue(function () { + numCalcs[0]++; + return R.get().charAt(0); }); - c.build(); - test.equal($(c._offscreen).html(), "helloworld"); - test.equal(renderedCounts, [1,1,1]); - R.set('false'); - Deps.flush(); - test.equal($(c._offscreen).html(), "worldhello"); - test.equal(renderedCounts, [1,2,2]); - R.set('true'); - Deps.flush(); - test.equal($(c._offscreen).html(), "helloworld"); - test.equal(renderedCounts, [1,3,3]); - R.set('torrid'); - Deps.flush(); - test.equal($(c._offscreen).html(), "helloworld"); - test.equal(renderedCounts, [1,3,3]); - R.set('flagrant'); - Deps.flush(); - test.equal($(c._offscreen).html(), "worldhello"); - test.equal(renderedCounts, [1,4,4]); - R.set('fromage'); - Deps.flush(); - test.equal($(c._offscreen).html(), "worldhello"); - test.equal(renderedCounts, [1,4,4]); + var secondLetter = UI.emboxValue(function () { + numCalcs[1]++; + return R.get().charAt(1); + }); + + var thirdLetter = UI.emboxValue(function () { + numCalcs[2]++; + return R.get().charAt(2); + }); + + var setSink = function (n, value) { + if (sinks[n] === value) + sinks[n] += '-error'; // duplicate, shouldn't happen! + else + sinks[n] = value; + }; + - c.destroy(); test.equal(R.numListeners(), 0); -}); + test.equal(numCalcs, [0, 0, 0]); -*/ \ No newline at end of file + var comps = []; + var sinks = []; + comps[0] = Deps.autorun(function () { + setSink(0, firstLetter()); + }); + comps[1] = Deps.autorun(function () { + setSink(1, firstLetter()); + }); + + test.equal(R.numListeners(), 1); + test.equal(numCalcs, [1, 0, 0]); + test.equal(sinks, ['A', 'A']); + + R.set('APPLE'); + Deps.flush(); + test.equal(R.numListeners(), 1); + test.equal(numCalcs, [2, 0, 0]); + test.equal(sinks, ['A', 'A']); + + // This non-reactive call to firstLetter piggybacks on the + // existing computation, which already has the value handy. + test.equal(firstLetter(), 'A'); + test.equal(numCalcs, [2, 0, 0]); + + comps[0].stop(); + comps[1].stop(); + Deps.flush(); + test.equal(R.numListeners(), 0); + test.equal(numCalcs, [2, 0, 0]); + test.equal(sinks, ['A', 'A']); + + // *This* non-reactive call to firstLetter, on the other hand, + // happens at a time when the emboxed value has no running + // computation, so it gets calculated directly. + test.equal(firstLetter(), 'A'); + test.equal(numCalcs, [3, 0, 0]); + test.equal(R.numListeners(), 0); + + // Start some new autoruns. + sinks = []; + comps[0] = Deps.autorun(function () { + setSink(0, firstLetter()); + }); + comps[1] = Deps.autorun(function () { + firstLetter(); // extra call shouldn't matter + setSink(1, firstLetter()); + }); + + test.equal(R.numListeners(), 1); + test.equal(numCalcs, [4, 0, 0]); + test.equal(sinks, ['A', 'A']); + + R.set('BANANA'); + Deps.flush(); + test.equal(R.numListeners(), 1); + // it's important that exactly one calculation happened, + // which indicates that the inner computation of the + // emboxValue has been re-run but not torn down and + // re-established. + test.equal(numCalcs, [5, 0, 0]); + test.equal(sinks, ['B', 'B']); + + R.set('CUCUMBER'); + Deps.flush(); + test.equal(R.numListeners(), 1); + test.equal(numCalcs, [6, 0, 0]); + test.equal(sinks, ['C', 'C']); + + comps[2] = Deps.autorun(function () { + setSink(2, secondLetter()); + }); + + test.equal(R.numListeners(), 2); + test.equal(numCalcs, [6, 1, 0]); + test.equal(sinks, ['C', 'C', 'U']); + + R.set('DOILY'); + Deps.flush(); + test.equal(R.numListeners(), 2); + test.equal(numCalcs, [7, 2, 0]); + test.equal(sinks, ['D', 'D', 'O']); + + comps[3] = Deps.autorun(function () { + setSink(3, firstLetter() + secondLetter() + thirdLetter()); + }); + + test.equal(R.numListeners(), 3); + test.equal(numCalcs, [7, 2, 1]); + test.equal(sinks, ['D', 'D', 'O', 'DOI']); + + + R.set('ENVY'); + Deps.flush(); + test.equal(R.numListeners(), 3); + test.equal(numCalcs, [8, 3, 2]); + test.equal(sinks, ['E', 'E', 'N', 'ENV']); + + R.set('EMPTY'); + Deps.flush(); + test.equal(R.numListeners(), 3); + test.equal(numCalcs, [9, 4, 3]); + test.equal(sinks, ['E', 'E', 'M', 'EMP']); + + comps[0].stop(); + Deps.flush(); + // comps[3] still listens to first, second, and third, which + // listen to R. + test.equal(R.numListeners(), 3); + + comps[1].stop(); + Deps.flush(); + test.equal(R.numListeners(), 3); + + comps[2].stop(); + Deps.flush(); + test.equal(R.numListeners(), 3); + + comps[3].stop(); + test.equal(firstLetter() + secondLetter() + thirdLetter(), 'EMP'); + Deps.flush(); + // BAM, all listeners gone! + test.equal(R.numListeners(), 0); + + ////// Test non-function case + + test.equal(UI.emboxValue(3)(), 3); + test.equal(UI.emboxValue(null)(), null); + test.equal(UI.emboxValue({x:1})(), {x:1}); +}); \ No newline at end of file