UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

1,871 lines (1,478 loc) 134 kB
/* jshint asi:true,multistr:true,indent:false,latedef:nofunc*/ steal("can/util/vdom/document", "can/util/vdom/build_fragment","can/view/stache", "can/view", "can/test","can/view/mustache/spec/specs","steal-qunit", "can/view/stache/expression_test.js","can/view/stache/mustache_helpers.js", function(){ var browserDoc = window.document; var simpleDocument = can.simpleDocument; makeTest('can/view/stache dom', browserDoc); if(window.jQuery && window.steal) { makeTest('can/view/stache vdom', simpleDocument); } // Add tests that shouldn't run in VDOM here. if(window.steal) { module("can/view/stache alternate window"); QUnit.asyncTest("routeUrl and routeCurrent helper", function(){ makeIframe( can.test.path("view/stache/test/route-url-current.html?"+Math.random()) ); }); QUnit.asyncTest("system/stache plugin accepts nodelists", function(){ makeIframe( can.test.path("view/stache/test/system-nodelist.html?"+Math.random()) ); }); } function makeIframe(src){ var iframe = document.createElement('iframe'); window.removeMyself = function(){ delete window.removeMyself; document.body.removeChild(iframe); start(); }; document.body.appendChild(iframe); iframe.src = src; } // HELPERS function makeTest(name, doc) { var isNormalDOM = doc === window.document; var innerHTML = function(node){ return "innerHTML" in node ? node.innerHTML : undefined; }; var getValue = function(node){ // textareas are cross bound to their internal innerHTML if(node.nodeName.toLowerCase() === "textarea") { return innerHTML(node); } else { return node.value; } }; var getChildNodes = function(node){ var childNodes = node.childNodes; if("length" in childNodes) { return childNodes; } else { var cur = node.firstChild; var nodes = []; while(cur) { nodes.push(cur); cur = cur.nextSibling; } return nodes; } }; var empty = function(node){ var last = node.lastChild; while(last) { node.removeChild(last); last = node.lastChild; } }; var getText = function(template, data, options){ var div = doc.createElement("div"); div.appendChild( can.stache(template)(data) ); return cleanHTMLTextForIE( innerHTML(div) ); }, getAttr = function (el, attrName) { return attrName === "class" ? el.className : el.getAttribute(attrName); }, cleanHTMLTextForIE = function(html){ return html.replace(/ ejs_0\.\d+="[^"]+"/g,"").replace(/<(\/?[-A-Za-z0-9_]+)/g, function(whole, tagName){ return "<"+tagName.toLowerCase(); }).replace(/\r?\n/g,""); }, getTextFromFrag = function(node){ var txt = ""; node = node.firstChild; while(node) { if(node.nodeType === 3) { txt += node.nodeValue; } else { txt += getTextFromFrag(node); } node = node.nextSibling; } return txt; }; var oldDoc; QUnit.module(name ,{ setup: function(){ if(doc === window.document) { can.document = undefined; } else { oldDoc = can.document; can.document = doc; } if(doc !== window.document) { this.fixture = doc.createElement("div"); doc.body.appendChild(this.fixture); this.$fixture = can.$(this.fixture); } else { this.fixture = can.$('#qunit-fixture')[0]; this.$fixture = can.$('#qunit-fixture'); } can.view.ext = '.stache'; this.animals = ['sloth', 'bear', 'monkey']; }, teardown: function(){ if(doc !== window.document) { can.document = oldDoc; doc.body.removeChild(this.fixture); } } }); test("html to html", function(){ var stashed = can.stache("<h1 class='foo'><span>Hello World!</span></h1>"); var frag = stashed(); equal( innerHTML(frag.childNodes.item(0)).toLowerCase(), "<span>hello world!</span>","got back the right text"); }); test("basic replacement", function(){ var stashed = can.stache("<h1 class='foo'><span>Hello {{message}}!</span></h1>"); var frag = stashed({ message: "World" }); equal( innerHTML(frag.firstChild).toLowerCase(), "<span>hello world!</span>","got back the right text"); }); test("a section helper", function(){ can.stache.registerHelper("helper", function(options){ return options.fn({message: "World"}); }); var stashed = can.stache("<h1 class='foo'>{{#helper}}<span>Hello {{message}}!</span>{{/helper}}</h1>"); var frag = stashed({}); equal(frag.firstChild.firstChild.nodeName.toLowerCase(), "span", "got a span"); equal(innerHTML(frag.firstChild.firstChild), "Hello World!","got back the right text"); }); /*test("attribute sections", function(){ var stashed = can.stache("<h1 style='top: {{top}}px; left: {{left}}px; background: rgb(0,0,{{color}});'>Hi</h1>"); var frag = stashed({ top: 1, left: 2, color: 3 }); equal(frag.firstChild.style.top, "1px", "top works"); equal(frag.firstChild.style.left, "2px", "left works"); equal(frag.firstChild.style.backgroundColor.replace(/\s/g,""), "rgb(0,0,3)", "color works"); });*/ test("attributes sections", function(){ var template = can.stache("<div {{attributes}}/>"); var frag = template({ attributes: "foo='bar'" }); equal(frag.firstChild.getAttribute('foo'), "bar", "set attribute"); template = can.stache("<div {{#truthy}}foo='{{baz}}'{{/truthy}}/>"); frag = template({ truthy: true, baz: "bar" }); equal(frag.firstChild.getAttribute('foo'), "bar", "set attribute"); frag = template({ truthy: false, baz: "bar" }); equal(frag.firstChild.getAttribute('foo'), null, "attribute not set if not truthy"); }); test("boxes example", function(){ var boxes = [], Box = can.Map.extend({ count: 0, content: 0, top: 0, left: 0, color: 0, tick: function () { var count = this.attr("count") + 1; this.attr({ count: count, left: Math.cos(count / 10) * 10, top: Math.sin(count / 10) * 10, color: count % 255, content: count }); } }); for (var i = 0; i < 1; i++) { boxes.push(new Box({ number: i })); } var stashed = can.stache("{{#each boxes}}"+ "<div class='box-view'>"+ "<div class='box' id='box-{{number}}' style='top: {{top}}px; left: {{left}}px; background: rgb(0,0,{{color}});'>"+ "{{content}}"+ "</div>"+ "</div>"+ "{{/each}}"); var frag = stashed({ boxes: boxes }); //equal(frag.children.length, 2, "there are 2 childNodes"); ok(/top: 0px/.test( frag.firstChild.firstChild.getAttribute("style") ), "0px"); boxes[0].tick(); ok(! /top: 0px/.test( frag.firstChild.firstChild.getAttribute("style")) , "!0px"); }); var override = { comments: { 'Standalone Without Newline': '!', // \r\n isn't possible within some browsers 'Standalone Line Endings': "|\n|" }, interpolation: { // Stashe does not needs to escape .nodeValues of text nodes 'HTML Escaping' : "These characters should be HTML escaped: & \" < >\n", 'Triple Mustache' : "These characters should not be HTML escaped: & \" < >\n", 'Ampersand' : "These characters should not be HTML escaped: & \" < >\n" }, inverted: { 'Standalone Line Endings': '|\n\n|', 'Standalone Without Newline': '^\n/' }, partials: { 'Standalone Line Endings': '|\n>\n|', 'Standalone Without Newline': '>\n >\n>', 'Standalone Without Previous Line': ' >\n>\n>', 'Standalone Indentation': '\\\n |\n<\n->\n|\n\n/\n' }, sections: { 'Standalone Line Endings': '|\n\n|', 'Standalone Without Newline': '#\n/' } }; // Add mustache specs to the test can.each(window.MUSTACHE_SPECS, function(specData){ var spec = specData.name; can.each(specData.data.tests, function (t) { test('specs/' + spec + ' - ' + t.name + ': ' + t.desc, function () { // stache does not escape double quotes, mustache expects &quot;. // can uses \n for new lines, mustache expects \r\n. var expected = (override[spec] && override[spec][t.name]) || t.expected.replace(/&quot;/g, '"'); //.replace(/\r\n/g, '\n'); // Mustache's "Recursion" spec generates invalid HTML if (spec === 'partials' && t.name === 'Recursion') { t.partials.node = t.partials.node.replace(/</g, '[') .replace(/\}>/g, '}]'); expected = expected.replace(/</g, '[') .replace(/>/g, ']'); } else if(spec === 'partials'){ //expected = expected.replace(/\</g,"&lt;").replace(/\>/g,"&gt;") } // register the partials in the spec if (t.partials) { for (var name in t.partials) { can.view.registerView(name, t.partials[name]) } } // register lambdas if (t.data.lambda && t.data.lambda.js) { t.data.lambda = eval('(' + t.data.lambda.js + ')'); } var res = can.stache(t.template)(t.data); deepEqual(getTextFromFrag(res), expected); }); }); }); test('Tokens returning 0 where they should display the number', function () { var template = "<div id='zero'>{{completed}}</div>"; var frag = can.stache( template )({ completed: 0 }); equal( frag.firstChild.firstChild.nodeValue, "0", 'zero shown' ); }); test('Inverted section function returning numbers', function () { var template = "<div id='completed'>{{^todos.completed}}hidden{{/todos.completed}}</div>"; var obsvr = new can.Map({ named: false }); var todos = { completed: function () { return obsvr.attr('named'); } }; // check hidden there var frag = can.stache( template ) ({ todos: todos }); deepEqual(frag.firstChild.firstChild.nodeValue, "hidden", 'hidden shown'); // now update the named attribute obsvr.attr('named', true); deepEqual(frag.firstChild.firstChild.nodeValue, "", 'hidden gone'); }); test("live-binding with escaping", function () { var template = "<span id='binder1'>{{ name }}</span><span id='binder2'>{{{name}}}</span>"; var teacher = new can.Map({ name: "<strong>Mrs Peters</strong>" }); var tpl = can.stache( template ); var frag = tpl(teacher); deepEqual(innerHTML(frag.firstChild), "&lt;strong&gt;Mrs Peters&lt;/strong&gt;"); deepEqual(innerHTML(frag.lastChild.firstChild), "Mrs Peters"); teacher.attr('name', '<i>Mr Scott</i>'); deepEqual(innerHTML(frag.firstChild), "&lt;i&gt;Mr Scott&lt;/i&gt;"); deepEqual(innerHTML(frag.lastChild.firstChild), "Mr Scott"); }); test("truthy", function () { var t = { template: "{{#name}}Do something, {{name}}!{{/name}}", expected: "Do something, Andy!", data: { name: 'Andy' } }; var expected = t.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); deepEqual( getText( t.template , t.data), expected); }); test("falsey", function () { var t = { template: "{{^cannot}}Don't do it, {{name}}!{{/cannot}}", expected: "Don't do it, Andy!", data: { name: 'Andy' } }; var expected = t.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); deepEqual(getText( t.template, t.data), expected); }); test("Handlebars helpers", function () { can.stache.registerHelper('hello', function (options) { return 'Should not hit this'; }); can.stache.registerHelper('there', function (options) { return 'there'; }); // Test for #1985 can.stache.registerHelper('zero', function (options) { return 0; }); can.stache.registerHelper('bark', function (obj, str, number, options) { var hash = options.hash || {}; return 'The ' + obj + ' barked at ' + str + ' ' + number + ' times, ' + 'then the ' + hash.obj + ' ' + hash.action + ' ' + hash.where + ' times' + (hash.loud === true ? ' loudly' : '') + '.'; }); var t = { template: "{{hello}} {{there}}! {{bark name 'Austin and Andy' 3 obj=name action='growled and snarled' where=2 loud=true}} Then there were {{zero}} barks :(", expected: "Hello there! The dog barked at Austin and Andy 3 times, then the dog growled and snarled 2 times loudly. Then there were 0 barks :(", data: { name: 'dog', hello: 'Hello' } }; var expected = t.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); deepEqual( getText(t.template, t.data) , expected); }); test("Handlebars advanced helpers (from docs)", function () { can.stache.registerSimpleHelper('exercise', function (group, action, num, options) { if (group && group.length > 0 && action && num > 0) { return options.fn({ group: group, action: action, where: options.hash.where, when: options.hash.when, num: num }); } else { return options.inverse(this); } }); var t = { template: "{{#exercise pets 'walked' 3 where='around the block' when=time}}" + "Along with the {{#group}}{{.}}, {{/group}}" + "we {{action}} {{where}} {{num}} times {{when}}." + "{{else}}" + "We were lazy today." + "{{/exercise}}", expected: "Along with the cat, dog, parrot, we walked around the block 3 times this morning.", expected2: "We were lazy today.", data: { pets: ['cat', 'dog', 'parrot'], time: 'this morning' } }; var template = can.stache(t.template); var frag = template(t.data); var div = doc.createElement("div"); div.appendChild(frag); equal(innerHTML( div ), t.expected); equal(getText(t.template, {}), t.expected2); }); test("Passing functions as data, then executing them", function () { var t = { template: "{{#nested}}{{welcome name}}{{/nested}}", expected: "Welcome Andy!", data: { name: 'Andy', nested: { welcome: function (name) { return 'Welcome ' + name + '!'; } } } }; var expected = t.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); deepEqual( getText(t.template, t.data), expected); }); if(doc === window.document) { test("Absolute partials", function () { var test_template = can.test.path('view/mustache/test/test_template.mustache'); var t = { template1: "{{> " + test_template + "}}", template2: "{{> " + test_template + "}}", expected: "Partials Rock" }; deepEqual(getText(t.template1, {}), t.expected); deepEqual(getText(t.template2, {}), t.expected); }); } test("No arguments passed to helper", function () { var template = can.stache("{{noargHelper}}"); can.stache.registerHelper("noargHelper", function () { return "foo" }); var div1 = doc.createElement('div'); var div2 = doc.createElement('div'); div1.appendChild(template( {})); div2.appendChild(template( new can.Map())); deepEqual(innerHTML(div1), "foo"); deepEqual(innerHTML(div2), "foo"); }); test("String literals passed to helper should work (#1143)", 1, function() { can.stache.registerHelper("concatStrings", function(arg1, arg2) { return arg1 + arg2; }); // Test with '=' because the regexp to find arguments uses that char // to delimit a keyword-arg name from its value. can.stache('testStringArgs', '{{concatStrings "==" "word"}}'); var div = doc.createElement('div'); div.appendChild(can.view('testStringArgs', {})); equal(innerHTML(div), '==word'); }); test("No arguments passed to helper with list", function () { var template = can.stache("{{#items}}{{noargHelper}}{{/items}}"); var div = doc.createElement('div'); div.appendChild(template({ items: new can.List([{ name: "Brian" }]) }, { noargHelper: function () { return "foo" } })); deepEqual(innerHTML(div), "foo"); }); if(isNormalDOM) { test("Partials and observes", function () { var template; var div = doc.createElement('div'); template = can.stache("<table><thead><tr>{{#data}}{{>" + can.test.path('view/stache/test/partial.stache') + "}}{{/data}}</tr></thead></table>") var dom = template({ data: new can.Map({ list: ["hi", "there"] }) }); div.appendChild(dom); var ths = div.getElementsByTagName('th'); equal(ths.length, 2, 'Got two table headings'); equal(innerHTML(ths[0]), 'hi', 'First column heading correct'); equal(innerHTML(ths[1]), 'there', 'Second column heading correct'); }); } test("Deeply nested partials", function () { var t = { template: "{{#nest1}}{{#nest2}}{{>partial}}{{/nest2}}{{/nest1}}", expected: "Hello!", partials: { partial: '{{#nest3}}{{name}}{{/nest3}}' }, data: { nest1: { nest2: { nest3: { name: 'Hello!' } } } } }; for (var name in t.partials) { can.view.registerView(name, t.partials[name]) } deepEqual(getText(t.template,t.data), t.expected); }); test("Partials correctly set context", function () { var t = { template: "{{#users}}{{>partial}}{{/users}}", expected: "foo - bar", partials: { partial: '{{ name }} - {{ company }}' }, data: { users: [{ name: 'foo' }], company: 'bar' } }; for (var name in t.partials) { can.view.registerView(name, t.partials[name]) } deepEqual( getText(t.template,t.data), t.expected); }); test("Handlebars helper: if/else", function () { var expected; var t = { template: "{{#if name}}{{name}}{{/if}}{{#if missing}} is missing!{{/if}}", expected: "Andy", data: { name: 'Andy', missing: undefined } }; expected = t.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); deepEqual(getText(t.template,t.data), expected); t.data.missing = null; expected = t.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); deepEqual(getText(t.template,t.data), expected); }); test("Handlebars helper: is/else (with 'eq' alias)", function() { var expected; var t = { template: '{{#eq ducks tenDucks "10"}}10 ducks{{else}}Not 10 ducks{{/eq}}', expected: "10 ducks", data: { ducks: '10', tenDucks: function() { return '10' } }, liveData: new can.Map({ ducks: '10', tenDucks: function() { return '10' } }) }; expected = t.expected.replace(/&quot;/g, '&#34;').replace(/\r\n/g, '\n'); deepEqual(getText(t.template, t.data), expected); deepEqual(getText(t.template, t.liveData), expected); t.data.ducks = 5; deepEqual(getText(t.template, t.data), 'Not 10 ducks'); }); test("Handlebars helper: unless", function () { var t = { template: "{{#unless missing}}Andy is missing!{{/unless}}" + "{{#unless isCool}} But he wasn't cool anyways.{{/unless}}", expected: "Andy is missing! But he wasn't cool anyways.", data: { name: 'Andy' }, liveData: new can.Map({ name: 'Andy', // #1202 #unless does not work with computes isCool: can.compute(function () { return t.liveData.attr("missing"); }) }) }; var expected = t.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); deepEqual(getText(t.template, t.data), expected); // #1019 #unless does not live bind var div = doc.createElement('div'); div.appendChild(can.stache(t.template)(t.liveData)); deepEqual( innerHTML(div), expected, '#unless condition false'); t.liveData.attr('missing', true); deepEqual( innerHTML(div), '', '#unless condition true'); }); test("Handlebars helper: each", function () { var t = { template: "{{#each names}}{{this}} {{/each}}", expected: "Andy Austin Justin ", data: { names: ['Andy', 'Austin', 'Justin'] }, data2: { names: new can.List(['Andy', 'Austin', 'Justin']) } }; var expected = t.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); deepEqual( getText(t.template,t.data) , expected); var div = doc.createElement('div'); div.appendChild(can.stache(t.template)(t.data2)); deepEqual( innerHTML(div), expected, 'Using Observe.List'); t.data2.names.push('What'); }); test("Handlebars helper: with", function () { var t = { template: "{{#with person}}{{name}}{{/with}}", expected: "Andy", data: { person: { name: 'Andy' } } }; var expected = t.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); deepEqual(getText(t.template,t.data), expected); }); test("render with double angle", function () { var text = "{{& replace_me }}{{{ replace_me_too }}}" + "<ul>{{#animals}}" + "<li>{{.}}</li>" + "{{/animals}}</ul>"; var compiled = getText(text,{ animals: this.animals }); equal(compiled, "<ul><li>sloth</li><li>bear</li><li>monkey</li></ul>", "works") }); test("comments", function () { var text = "{{! replace_me }}" + "<ul>{{#animals}}" + "<li>{{.}}</li>" + "{{/animals}}</ul>"; var compiled = getText(text,{ animals: this.animals }); equal(compiled, "<ul><li>sloth</li><li>bear</li><li>monkey</li></ul>") }); test("multi line", function () { var text = "a \n b \n c"; equal(getTextFromFrag( can.stache(text)({}) ), text) }); test("multi line elements", function () { var text = "<div\n class=\"{{myClass}}\" />", result = can.stache(text)({myClass: 'a'}); equal(result.firstChild.className, "a", "class name is right"); }); test("escapedContent", function () { var text = "<span>{{ tags }}</span><label>&amp;</label><strong>{{ number }}</strong><input value='{{ quotes }}'/>"; var div = doc.createElement('div'); div.appendChild( can.stache(text)({ tags: "foo < bar < car > zar > poo", quotes: "I use 'quote' fingers & &amp;ersands \"a lot\"", number: 123 }) ); equal(div.getElementsByTagName('span')[0].firstChild.nodeValue, "foo < bar < car > zar > poo"); equal(div.getElementsByTagName('strong')[0].firstChild.nodeValue, 123); equal(div.getElementsByTagName('input')[0].value, "I use 'quote' fingers & &amp;ersands \"a lot\"", "attributes are always safe, and strings are kept as-is without additional escaping"); equal( innerHTML(div.getElementsByTagName('label')[0]), "&amp;", "text-based html entities work fine"); }); test("unescapedContent", function () { var text = "<span>{{{ tags }}}</span><div>{{{ tags }}}</div><input value='{{{ quotes }}}'/>"; var div = doc.createElement('div'); div.appendChild( can.stache(text)({ tags: "<strong>foo</strong><strong>bar</strong>", quotes: 'I use \'quote\' fingers "a lot"' }) ); equal(div.getElementsByTagName('span')[0].firstChild.nodeType, 1,""); equal( innerHTML(div.getElementsByTagName('div')[0]).toLowerCase(), "<strong>foo</strong><strong>bar</strong>"); equal( innerHTML(div.getElementsByTagName('span')[0]).toLowerCase(), "<strong>foo</strong><strong>bar</strong>"); equal(div.getElementsByTagName('input')[0].value, "I use 'quote' fingers \"a lot\"", "escaped no matter what"); }); test("attribute single unescaped, html single unescaped", function () { var text = "<div id='me' class='{{#task.completed}}complete{{/task.completed}}'>{{ task.name }}</div>"; var task = new can.Map({ name: 'dishes' }); var div = doc.createElement('div'); div.appendChild(can.stache(text)({ task: task })); equal( innerHTML(div.getElementsByTagName('div')[0]), "dishes", "html correctly dishes") equal(div.getElementsByTagName('div')[0].className, "", "class empty") task.attr('name', 'lawn') equal( innerHTML(div.getElementsByTagName('div')[0]), "lawn", "html correctly lawn") equal(div.getElementsByTagName('div')[0].className, "", "class empty") task.attr('completed', true); equal(div.getElementsByTagName('div')[0].className, "complete", "class changed to complete") }); test("select live binding", function () { var text = "<select>{{ #todos }}<option>{{ name }}</option>{{ /todos }}</select>"; var todos, div; todos = new can.List([{ id: 1, name: 'Dishes' }]); div = doc.createElement('div'); div.appendChild( can.stache(text)({todos: todos}) ); equal(div.getElementsByTagName('option') .length, 1, '1 item in list') todos.push({ id: 2, name: 'Laundry' }); equal(div.getElementsByTagName('option') .length, 2, '2 items in list') todos.splice(0, 2); equal(div.getElementsByTagName('option') .length, 0, '0 items in list') }); test('multiple hookups in a single attribute', function () { var text = '<div class=\'{{ obs.foo }}' + '{{ obs.bar }}{{ obs.baz }}{{ obs.nest.what }}\'></div>'; var obs = new can.Map({ foo: 'a', bar: 'b', baz: 'c', nest: new can.Map({ what: 'd' }) }); var div = doc.createElement('div'); div.appendChild( can.stache(text)({ obs: obs }) ); var innerDiv = div.firstChild; equal(getAttr(innerDiv, 'class'), "abcd", 'initial render'); obs.attr('bar', 'e'); equal(getAttr(innerDiv, 'class'), "aecd", 'initial render'); obs.attr('bar', 'f'); equal(getAttr(innerDiv, 'class'), "afcd", 'initial render'); obs.nest.attr('what', 'g'); equal(getAttr(innerDiv, 'class'), "afcg", 'nested observe'); }); test('adding and removing multiple html content within a single element', function () { var text, obs; text = '<div>{{ obs.a }}{{ obs.b }}{{ obs.c }}</div>'; obs = new can.Map({ a: 'a', b: 'b', c: 'c' }); var div = doc.createElement('div'); div.appendChild(can.stache(text)({ obs: obs })); equal( innerHTML(div.firstChild), 'abc', 'initial render'); obs.attr({ a: '', b: '', c: '' }); equal(innerHTML(div.firstChild), '', 'updated values'); obs.attr({ c: 'c' }); equal( innerHTML(div.firstChild), 'c', 'updated values'); }); test('live binding and removeAttr', function () { var text = '{{ #obs.show }}' + '<p {{ obs.attributes }} class="{{ obs.className }}"><span>{{ obs.message }}</span></p>' + '{{ /obs.show }}', obs = new can.Map({ show: true, className: 'myMessage', attributes: 'some=\"myText\"', message: 'Live long and prosper' }), div = doc.createElement('div'); div.appendChild(can.stache(text)({ obs: obs })); var p = div.getElementsByTagName('p')[0], span = p.getElementsByTagName('span')[0]; equal(p.getAttribute("some"), "myText", 'initial render attr'); equal(getAttr(p, "class"), "myMessage", 'initial render class'); equal( innerHTML(span), 'Live long and prosper', 'initial render innerHTML'); obs.removeAttr('className'); equal(getAttr(p, "class"), '', 'class is undefined'); obs.attr('className', 'newClass'); equal(getAttr(p, "class"), 'newClass', 'class updated'); obs.removeAttr('attributes'); equal(p.getAttribute('some'), null, 'attribute is undefined'); obs.attr('attributes', 'some="newText"'); // equal(p.getAttribute('some'), 'newText', 'attribute updated'); obs.removeAttr('message'); equal(innerHTML(span), '', 'text node value is empty'); obs.attr('message', 'Warp drive, Mr. Sulu'); equal(innerHTML(span), 'Warp drive, Mr. Sulu', 'text node updated'); obs.removeAttr('show'); equal( innerHTML(div), '', 'value in block statement is undefined'); obs.attr('show', true); p = div.getElementsByTagName('p')[0]; span = p.getElementsByTagName('span')[0]; equal(p.getAttribute("some"), "newText", 'value in block statement updated attr'); equal(getAttr(p, "class"), "newClass", 'value in block statement updated class'); equal( innerHTML(span), 'Warp drive, Mr. Sulu', 'value in block statement updated innerHTML'); }); test('hookup within a tag', function () { var text = '<div {{ obs.foo }} ' + '{{ obs.baz }}>lorem ipsum</div>', obs = new can.Map({ foo: 'class="a"', baz: 'some=\'property\'' }), compiled = can.stache(text)({obs: obs}) var div = doc.createElement('div'); div.appendChild(compiled); var anchor = div.getElementsByTagName('div')[0]; equal(getAttr(anchor, 'class'), 'a'); equal(anchor.getAttribute('some'), 'property'); obs.attr('foo', 'class="b"'); equal(getAttr(anchor, 'class'), 'b'); equal(anchor.getAttribute('some'), 'property'); obs.attr('baz', 'some=\'new property\''); equal(getAttr(anchor, 'class'), 'b'); equal(anchor.getAttribute('some'), 'new property'); obs.attr('foo', 'class=""'); obs.attr('baz', ''); equal(getAttr(anchor, 'class'), "", 'anchor class blank'); equal(anchor.getAttribute('some'), undefined, 'attribute "some" is undefined'); }); test('single escaped tag, removeAttr', function () { var text = '<div {{ obs.foo }}>lorem ipsum</div>', obs = new can.Map({ foo: 'data-bar="john doe\'s bar"' }), compiled = can.stache(text)({obs: obs}) var div = doc.createElement('div'); div.appendChild(compiled); var anchor = div.getElementsByTagName('div')[0]; equal(anchor.getAttribute('data-bar'), "john doe's bar"); obs.removeAttr('foo'); equal(anchor.getAttribute('data-bar'), null); obs.attr('foo', 'data-bar="baz"'); equal(anchor.getAttribute('data-bar'), 'baz'); }); test('html comments', function () { var text = '<!-- bind to changes in the todo list --> <div>{{obs.foo}}</div>'; var obs = new can.Map({ foo: 'foo' }); var compiled = can.stache(text)({ obs: obs }); var div = doc.createElement('div'); div.appendChild(compiled); equal( innerHTML(div.getElementsByTagName('div')[0]), 'foo', 'Element as expected'); }); test("hookup and live binding", function () { var text = "<div class='{{ task.completed }}' {{ data 'task' task }}>" + "{{ task.name }}" + "</div>", task = new can.Map({ completed: false, className: 'someTask', name: 'My Name' }), compiled = can.stache(text)({ task: task }), div = doc.createElement('div'); div.appendChild(compiled) var child = div.getElementsByTagName('div')[0]; ok(child.className.indexOf("false") > -1, "is incomplete") ok( !! can.data(can.$(child), 'task'), "has data") equal(innerHTML(child), "My Name", "has name") task.attr({ completed: true, name: 'New Name' }); ok(child.className.indexOf("true") !== -1, "is complete") equal(innerHTML(child), "New Name", "has new name") }); test('multiple curly braces in a block', function () { var text = '{{^obs.items}}' + '<li>No items</li>' + '{{/obs.items}}' + '{{#obs.items}}' + '<li>{{name}}</li>' + '{{/obs.items}}', obs = new can.Map({ items: [] }), compiled = can.stache(text)({obs: obs}) var ul = doc.createElement('ul'); ul.appendChild(compiled); equal( innerHTML(ul.getElementsByTagName('li')[0]), 'No items', 'initial observable state'); obs.attr('items', [{ name: 'foo' }]); equal( innerHTML(ul.getElementsByTagName('li')[0]), 'foo', 'updated observable'); }); test("unescape bindings change", function () { var l = new can.List([{ complete: true }, { complete: false }, { complete: true }]); var completed = function () { l.attr('length'); var num = 0; l.each(function (item) { if (item.attr('complete')) { num++; } }) return num; }; var text = '<div>{{ completed }}</div>', compiled = can.stache(text)({ completed: completed }); var div = doc.createElement('div'); div.appendChild(compiled); var child = div.getElementsByTagName('div')[0]; equal( innerHTML(child), "2", "at first there are 2 true bindings"); var item = new can.Map({ complete: true, id: "THIS ONE" }); l.push(item); equal(innerHTML(child), "3", "now there are 3 complete"); item.attr('complete', false); equal(innerHTML(child), "2", "now there are 2 complete"); l.pop(); item.attr('complete', true); equal(innerHTML(child), "2", "there are still 2 complete"); }); test("escape bindings change", function () { var l = new can.List([{ complete: true }, { complete: false }, { complete: true }]); var completed = function () { l.attr('length'); var num = 0; l.each(function (item) { if (item.attr('complete')) { num++; } }) return num; }; var text = '<div>{{{ completed }}}</div>', compiled = can.stache(text)({ completed: completed }); var div = doc.createElement('div'); div.appendChild(compiled); var child = div.getElementsByTagName('div')[0]; equal(innerHTML(child), "2", "at first there are 2 true bindings"); var item = new can.Map({ complete: true }) l.push(item); equal(innerHTML(child), "3", "now there are 3 complete"); item.attr('complete', false); equal(innerHTML(child), "2", "now there are 2 complete"); }); test("tag bindings change", function () { var l = new can.List([{ complete: true }, { complete: false }, { complete: true }]); var completed = function () { l.attr('length'); var num = 0; l.each(function (item) { if (item.attr('complete')) { num++; } }) return "items='" + num + "'"; }; var text = '<div {{{ completed }}}></div>', compiled = can.stache(text)({ completed: completed }); var div = doc.createElement('div'); div.appendChild(compiled); var child = div.getElementsByTagName('div')[0]; equal(child.getAttribute("items"), "2", "at first there are 2 true bindings"); var item = new can.Map({ complete: true }) l.push(item); equal(child.getAttribute("items"), "3", "now there are 3 complete"); item.attr('complete', false); equal(child.getAttribute("items"), "2", "now there are 2 complete"); }) test("attribute value bindings change", function () { var l = new can.List([{ complete: true }, { complete: false }, { complete: true }]); var completed = function () { l.attr('length'); var num = 0; l.each(function (item) { if (item.attr('complete')) { num++; } }); return num; }; var text = '<div items="{{{ completed }}}"></div>', compiled = can.stache(text)({ completed: completed }); var div = doc.createElement('div'); div.appendChild(compiled); var child = div.getElementsByTagName('div')[0]; equal(child.getAttribute("items"), "2", "at first there are 2 true bindings"); var item = new can.Map({ complete: true }); l.push(item); equal(child.getAttribute("items"), "3", "now there are 3 complete"); item.attr('complete', false); equal(child.getAttribute("items"), "2", "now there are 2 complete"); }); test("in tag toggling", function () { var text = "<div {{ obs.val }}></div>" var obs = new can.Map({ val: 'foo="bar"' }) var compiled = can.stache(text)({ obs: obs }); var div = doc.createElement('div'); div.appendChild(compiled); obs.attr('val', "bar='foo'"); obs.attr('val', 'foo="bar"') var d2 = div.getElementsByTagName('div')[0]; // toUpperCase added to normalize cases for IE8 equal(d2.getAttribute("foo"), "bar", "bar set"); equal(d2.getAttribute("bar"), null, "bar set") }); // not sure about this w/ mustache test("nested properties", function () { var text = "<div>{{ obs.name.first }}</div>" var obs = new can.Map({ name: { first: "Justin" } }) var compiled = can.stache(text)({ obs: obs }); var div = doc.createElement('div'); div.appendChild(compiled); div = div.getElementsByTagName('div')[0]; equal(innerHTML(div), "Justin") obs.attr('name.first', "Brian") equal(innerHTML(div), "Brian") }); test("tags without chidren or ending with /> do not change the state", function () { var text = "<table><tr><td/>{{{ obs.content }}}</tr></div>" var obs = new can.Map({ content: "<td>Justin</td>" }) var compiled = can.stache(text)({ obs: obs }); var div = doc.createElement('div'); var html = compiled; div.appendChild(html); equal(div.getElementsByTagName('span') .length, 0, "there are no spans"); equal(div.getElementsByTagName('td') .length, 2, "there are 2 td"); }) test("nested live bindings", function () { expect(0); var items = new can.List([{ title: 0, is_done: false, id: 0 }]); var div = doc.createElement('div'); var template = can.stache('<form>{{#items}}{{^is_done}}<div id="{{title}}"></div>{{/is_done}}{{/items}}</form>') div.appendChild(template({ items: items })); items.push({ title: 1, is_done: false, id: 1 }); // this will throw an error unless Mustache protects against // nested objects items[0].attr('is_done', true); }); test("list nested in observe live bindings", function () { var template = can.stache("<ul>{{#data.items}}<li>{{name}}</li>{{/data.items}}</ul>"); var data = new can.Map({ items: [{ name: "Brian" }, { name: "Fara" }] }); var div = doc.createElement('div'); div.appendChild(template({ data: data })); data.items.push(new can.Map({ name: "Scott" })) ok(/Brian/.test(innerHTML(div)), "added first name") ok(/Fara/.test(innerHTML(div)), "added 2nd name") ok(/Scott/.test(innerHTML(div)), "added name after push") }); test("trailing text", function () { var template = can.stache("There are {{ length }} todos") var div = doc.createElement('div'); div.appendChild(template(new can.List([{}, {}]))); ok(/There are 2 todos/.test(innerHTML(div)), "got all text"); }); if(isNormalDOM) { test("recursive views", function () { var data = new can.List([{ label: 'branch1', children: [{ id: 2, label: 'branch2' }] }]); var div = doc.createElement('div'); div.appendChild(can.view(can.test.path('view/stache/test/recursive.stache'), { items: data })); ok(/class="?leaf"?/.test(innerHTML(div)), "we have a leaf") }); } test("live binding textarea", function () { var template = can.stache("<textarea>Before{{ obs.middle }}After</textarea>"); var obs = new can.Map({ middle: "yes" }), div = doc.createElement('div'); div.appendChild(template({ obs: obs })); var textarea = div.firstChild; equal(getValue(textarea), "BeforeyesAfter"); obs.attr("middle", "Middle"); equal(getValue(textarea), "BeforeMiddleAfter"); }); test("reading a property from a parent object when the current context is an observe", function () { var template = can.stache("{{#foos}}<span>{{bar}}</span>{{/foos}}") var data = { foos: new can.List([{ name: "hi" }, { name: 'bye' }]), bar: "Hello World" }; var div = doc.createElement('div'); var res = template(data); div.appendChild(res); var spans = div.getElementsByTagName('span'); equal(spans.length, 2, 'Got two <span> elements'); equal(innerHTML(spans[0]), 'Hello World', 'First span Hello World'); equal(innerHTML(spans[1]), 'Hello World', 'Second span Hello World'); }); test("helper parameters don't convert functions", function () { can.stache.registerHelper('helperWithFn', function (fn) { ok(can.isFunction(fn), 'Parameter is a function'); equal(fn(), 'Hit me!', 'Got the expected function'); }); var renderer = can.stache('{{helperWithFn test}}'); renderer({ test: function () { return 'Hit me!'; } }); }) test("computes as helper parameters don't get converted", function () { can.stache.registerHelper('computeTest', function (no) { equal(no(), 5, 'Got computed calue'); ok(no.isComputed, 'no is still a compute') }); var renderer = can.stache('{{computeTest test}}'); renderer({ test: can.compute(5) }); }); test("computes are supported in default helpers", function () { var staches = { "if": "{{#if test}}if{{else}}else{{/if}}", "not_if": "not_{{^if test}}not{{/if}}if", "each": "{{#each test}}{{.}}{{/each}}", "with": "wit{{#with test}}<span>{{3}}</span>{{/with}}" }; var template = can.stache("There are {{ length }} todos"); var div = doc.createElement('div'); div.appendChild(template(new can.List([{}, {}]))); ok(/There are 2 todos/.test(innerHTML(div)), "got all text"); var renderer, result, data, actual, span; for (result in staches) { renderer = can.stache(staches[result]); data = ["e", "a", "c", "h"]; div = doc.createElement("div"); actual = renderer({ test: can.compute(data) }); div.appendChild(actual); span = div.getElementsByTagName("span")[0]; if (span && span.firstChild) { div.insertBefore(span.firstChild, span); div.removeChild(span); } actual = innerHTML(div); equal(actual, result, "can.compute resolved for helper " + result); } var inv_staches = { "else": "{{#if test}}if{{else}}else{{/if}}", "not_not_if": "not_{{^if test}}not_{{/if}}if", "not_each": "not_{{#each test}}_{{/each}}each", "not_with": "not{{#with test}}_{{/with}}_with" }; for (result in inv_staches) { renderer = can.stache(inv_staches[result]); data = null; div = doc.createElement("div"); actual = renderer({ test: can.compute(data) }); div.appendChild(actual); actual = innerHTML(div); equal(actual, result, "can.compute resolved for helper " + result); } }); //Issue 233 test("multiple tbodies in table hookup", function () { var text = "<table>" + "{{#people}}" + "<tbody><tr><td>{{name}}</td></tr></tbody>" + "{{/people}}" + "</table>", people = new can.List([{ name: "Steve" }, { name: "Doug" }]), compiled = can.stache(text)({ people: people }); equal( compiled.firstChild.getElementsByTagName("tbody").length, 2, "two tbodies"); }); // http://forum.javascriptmvc.com/topic/live-binding-on-mustache-template-does-not-seem-to-be-working-with-nested-properties test("Observe with array attributes", function () { var renderer = can.stache('<ul>{{#todos}}<li>{{.}}</li>{{/todos}}</ul><div>{{message}}</div>'); var div = doc.createElement('div'); var data = new can.Map({ todos: ['Line #1', 'Line #2', 'Line #3'], message: 'Hello', count: 2 }); div.appendChild(renderer(data)); equal(innerHTML(div.getElementsByTagName('li')[1]), 'Line #2', 'Check initial array'); equal(innerHTML(div.getElementsByTagName('div')[0]), 'Hello', 'Check initial message'); data.attr('todos.1', 'Line #2 changed'); data.attr('message', 'Hello again'); equal(innerHTML(div.getElementsByTagName('li')[1]), 'Line #2 changed', 'Check updated array'); equal(innerHTML(div.getElementsByTagName('div')[0]), 'Hello again', 'Check updated message'); }); test("Observe list returned from the function", function () { var renderer = can.stache('<ul>{{#todos}}<li>{{.}}</li>{{/todos}}</ul>'); var div = doc.createElement('div'); var todos = new can.List(); var data = { todos: function () { return todos; } }; div.appendChild(renderer(data)); todos.push("Todo #1") equal(div.getElementsByTagName('li') .length, 1, 'Todo is successfuly created'); equal(innerHTML(div.getElementsByTagName('li')[0]), 'Todo #1', 'Pushing to the list works'); }); // https://github.com/canjs/canjs/issues/228 test("Contexts within helpers not always resolved correctly", function () { can.stache.registerHelper("bad_context", function (context, options) { return ["<span>"+this.text+"</span> should not be ",options.fn(context)]; }); var renderer = can.stache('{{#bad_context next_level}}<span>{{text}}</span><br/><span>{{other_text}}</span>{{/bad_context}}'), data = { next_level: { text: "bar", other_text: "In the inner context" }, text: "foo" }, div = doc.createElement('div'); div.appendChild(renderer(data)); equal(innerHTML(div.getElementsByTagName('span')[0]), "foo", 'Incorrect context passed to helper'); equal(innerHTML(div.getElementsByTagName('span')[1]), "bar", 'Incorrect text in helper inner template'); equal(innerHTML(div.getElementsByTagName('span')[2]), "In the inner context", 'Incorrect other_text in helper inner template'); }); // https://github.com/canjs/canjs/issues/227 test("Contexts are not always passed to partials properly", function () { can.view.registerView('inner', '{{#if other_first_level}}{{other_first_level}}{{else}}{{second_level}}{{/if}}') var renderer = can.stache('{{#first_level}}<span>{{> inner}}</span> should equal <span>{{other_first_level}}</span>{{/first_level}}'), data = { first_level: { second_level: "bar" }, other_first_level: "foo" }, div = doc.createElement('div'); div.appendChild(renderer(data)); equal(innerHTML(div.getElementsByTagName('span')[0]), "foo", 'Incorrect context passed to helper'); equal(innerHTML(div.getElementsByTagName('span')[1]), "foo", 'Incorrect text in helper inner template'); }); // https://github.com/canjs/canjs/issues/231 test("Functions and helpers should be passed the same context", function () { var textNodes = function(el, cb) { var cur = el.firstChild; while(cur){ if(cur.nodeType === 3) { cb(cur) } else if(el.nodeType === 1) { textNodes(cur, cb) } cur = cur.nextSibling; } }; can.stache.registerHelper("to_upper", function (fn, options) { if (!fn.fn) { return typeof fn === "function" ? fn() .toString() .toUpperCase() : fn.toString() .toUpperCase(); } else { //fn is options, we need to go through that document and lower case all text nodes var frag = fn.fn(this); textNodes(frag, function(el){ el.nodeValue = el.nodeValue.toUpperCase(); }); return frag; } }); var renderer = can.stache(' "<span>{{#to_upper}}{{next_level.text}}{{/to_upper}}</span>"'), data = { next_level: { text: function () { return this.other_text; }, other_text: "In the inner context" } }, div = doc.createElement('div'); window.other_text = 'Window context'; div.appendChild(renderer(data)); equal(innerHTML(div.getElementsByTagName('span')[0]), data.next_level.other_text.toUpperCase(), 'correct context passed to helper'); }); // https://github.com/canjs/canjs/issues/153 test("Interpolated values when iterating through an Observe.List should still render when not surrounded by a DOM node", function () { var renderer = can.stache('{{ #todos }}{{ name }}{{ /todos }}'), renderer2 = can.stache('{{ #todos }}<span>{{ name }}</span>{{ /todos }}'), todos = [{ id: 1, name: 'Dishes' }, { id: 2, name: 'Forks' }], liveData = { todos: new can.List(todos) }, plainData = { todos: todos }, div = doc.createElement('div'); div.appendChild(renderer2(plainData)); equal(innerHTML(div.getElementsByTagName('span')[0]), "Dishes", 'Array item rendered with DOM container'); equal(innerHTML(div.getElementsByTagName('span')[1]), "Forks", 'Array item rendered with DOM container'); div.innerHTML = ''; div.appendChild(renderer2(liveData)); equal(innerHTML(div.getElementsByTagName('span')[0]), "Dishes", 'List item rendered with DOM container'); equal(innerHTML(div.getElementsByTagName('span')[1]), "Forks", 'List item rendered with DOM container'); div = doc.createElement('div'); div.appendChild(renderer(plainData)); equal(innerHTML(div), "DishesForks", 'Array item rendered without DOM container'); div = doc.createElement('div'); div.appendChild(renderer(liveData)); equal(innerHTML(div), "DishesForks", 'List item rendered without DOM container'); liveData.todos.push({ id: 3, name: 'Knives' }); equal(innerHTML(div), "DishesForksKnives", 'New list item rendered without DOM container'); }); test("objects with a 'key' or 'index' property should work in helpers", function () { var renderer = can.stache('{{ #obj }}{{ show_name }}{{ /obj }}'), div = doc.createElement('div'); div.appendChild(renderer({ obj: { id: 2, name: 'Forks', key: 'bar' } }, { show_name: function () { return this.name; } })); equal(innerHTML(div), "Forks", 'item name rendered'); div = doc.createElement('div'); div.appendChild(renderer({ obj: { id: 2, name: 'Forks', index: 'bar' } }, { show_name: function () { return this.name; } })); equal(innerHTML(div), "Forks", 'item name rendered'); }); test("2 way binding helpers", function () { var Value = function (el, value) { this.updateElement = function (ev, newVal) { el.value = newVal || ""; }; value.bind("change", this.updateElement); el.onchange = function () { value(el.value) } this.teardown = function () { value.unbind("change", this.updateElement); el.onchange = null; } el.value = value() || ""; } var val; can.stache.registerHelper('myValue', function (value) { return function (el) { val = new Value(el, value); } }); var renderer = can.stache('<input {{myValue user.name}}/>'); var div = doc.createElement('div'), u = new can.Map({ name: "Justin" }); div.appendChild(renderer({ user: u })); var input = div.getElementsByTagName('input')[0]; equal(input.value, "Justin", "Name is set correctly") u.attr('name', 'Eli') equal(input.value, "Eli", "Changing observe updates