UNPKG

can-stache

Version:

Live binding handlebars templates

1,825 lines (1,419 loc) 191 kB
/* jshint asi:true,multistr:true,indent:false,latedef:nofunc*/ var stache = require('../can-stache'); var core = require('../src/mustache_core'); var clone = require('steal-clone'); var canSymbol = require("can-symbol"); var canReflect = require("can-reflect"); var QUnit = require('steal-qunit'); var queues = require('can-queues'); var DefineList = require("can-define/list/list"); var DefineMap = require('can-define/map/map'); var Observation = require('can-observation'); var SimpleMap = require('can-simple-map'); var SimpleObservable = require("can-simple-observable"); var encoder = require('can-attribute-encoder'); var viewCallbacks = require('can-view-callbacks'); var Scope = require('can-view-scope'); var parser = require('can-view-parser'); var makeDocument = require('can-vdom/make-document/make-document'); var globals = require('can-globals'); var getChildNodes = require('can-child-nodes'); var domData = require('can-dom-data'); var domMutateNode = require('can-dom-mutate/node'); var DOCUMENT = require('can-globals/document/document'); var canDev = require('can-log/dev/dev'); var string = require('can-string'); var joinURIs = require('can-join-uris'); var getBaseURL = require('can-globals/base-url/base-url'); var testHelpers = require('can-test-helpers'); var canLog = require('can-log'); var debug = require('../helpers/-debugger'); var helpersCore = require('can-stache/helpers/core'); var makeStacheTestHelpers = require("../test/helpers"); var browserDoc = DOCUMENT(); makeTest('can-stache dom', browserDoc); makeTest('can-stache vdom', makeDocument()); // HELPERS function overwriteGlobalHelper(name, fn, method) { var origHelper = helpersCore.getHelper(name); var newHelper = function() { return fn.apply(this, arguments); }; newHelper.requiresOptionsArgument = origHelper.requiresOptionsArgument; helpersCore[method || 'registerHelper'](name, newHelper); return origHelper; } function makeTest(name, doc, mutation) { var stacheTestHelpers = makeStacheTestHelpers(doc); var isNormalDOM = doc === window.document; var innerHTML = function(node){ return "innerHTML" in node ? stacheTestHelpers.cloneAndClean(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 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( stache(template)(data, options) ); return cleanHTMLTextForIE( innerHTML(stacheTestHelpers.cloneAndClean(div)) ); }, getAttr = function (el, attrName) { return attrName === "class" ? el.className : el.getAttribute(attrName); }, cleanHTMLTextForIE = function(html){ // jshint ignore:line return html.replace(/ stache_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, { beforeEach: function(assert) { if(doc === window.document) { DOCUMENT(null); globals.deleteKeyValue('MutationObserver'); } else { oldDoc = window.document; DOCUMENT(doc); globals.setKeyValue('MutationObserver', null); } this.fixture = doc.createElement("div"); doc.body.appendChild(this.fixture); this.animals = ['sloth', 'bear', 'monkey']; // reset stache helpers so that any helpers registered in // the previous test do not conflict with scope properties helpersCore.__resetHelpers(); }, afterEach: function(assert) { doc.body.removeChild(this.fixture); var done = assert.async(); setTimeout(function(){ DOCUMENT(window.document); globals.deleteKeyValue('MutationObserver'); done(); },1) } }); QUnit.test("html to html", function(assert) { var stashed = stache("<h1 class='foo'><span>Hello World!</span></h1>"); var frag = stashed(); assert.equal( innerHTML(frag.childNodes.item(0)).toLowerCase(), "<span>hello world!</span>","got back the right text"); }); QUnit.test("basic replacement", function(assert) { var stashed = stache("<h1 class='foo'><span>Hello {{message}}!</span></h1>"); var frag = stashed({ message: "World" }); assert.equal( innerHTML(stacheTestHelpers.cloneAndClean(frag).firstChild).toLowerCase(), "<span>hello world!</span>","got back the right text"); }); QUnit.test("a section helper", function(assert) { stache.registerHelper("helper", function(options){ return options.fn({message: "World"}); }); var stashed = stache("<h1 class='foo'>{{#helper()}}<span>Hello {{message}}!</span>{{/helper}}</h1>"); var frag = stashed({}); assert.equal(stacheTestHelpers.cloneAndClean(frag).firstChild.firstChild.nodeName.toLowerCase(), "span", "got a span"); assert.equal(innerHTML(stacheTestHelpers.cloneAndClean(frag).firstChild.firstChild), "Hello World!","got back the right text"); }); QUnit.test('helpers used as section should have helperOptionArg.isSection set', function (assert) { var done = assert.async(); stache.registerHelper('genericTestHelper', function (options) { assert.equal(options.isSection, true, 'isSection should be true'); done(); }); var template = '<div>{{#genericTestHelper()}}<span>Test</span>{{/genericTestHelper}}</div>'; var viewModel = {}; stache(template)(viewModel); }); QUnit.test('helpers used inline should have helperOptionArg.isSection unset', function (assert) { var done = assert.async(); stache.registerHelper('genericTestHelper2', function (options) { assert.equal(options.isSection, false, 'isSection should be false'); done(); }); var template = '<div>{{genericTestHelper2()}}</div>'; var viewModel = {}; stache(template)(viewModel); }); testHelpers.dev.devOnlyTest("helpers warn on overwrite (canjs/can-stache-converters#24)", function (assert) { stache.registerHelper('foobar', function() {}); // have to do this after the first registration b/c if the dom and vdom tests run, "foobar" // will already have been registered. var teardown = testHelpers.dev.willWarn(/already been registered/, function(message, matched) { if(matched) { assert.ok(true, "received warning"); } }); stache.registerHelper('foobar', function() {}); assert.equal(teardown(), 1, "Exactly one warning called"); }); QUnit.test("attributes sections", function(assert) { var template = stache("<div {{attributes}}/>"); var frag = template({ attributes: "foo='bar'" }); assert.equal(stacheTestHelpers.cloneAndClean(frag).firstChild.getAttribute('foo'), "bar", "{{attributes}} set"); template = stache("<div {{#if truthy}}foo='{{baz}}'{{/if}}/>"); frag = template({ truthy: true, baz: "bar" }); assert.equal(stacheTestHelpers.cloneAndClean(frag).firstChild.getAttribute('foo'), "bar", "foo='{{baz}}' set"); frag = template({ truthy: false, baz: "bar" }); assert.equal(stacheTestHelpers.cloneAndClean(frag).firstChild.getAttribute('foo'), null, "attribute not set if not truthy"); }); QUnit.test("boxes example", function(assert) { var boxes = [], Box = DefineMap.extend({ count: {value: 0}, content: {value: 0}, top: {value: 0}, left: {value: 0}, color: {value: 0}, tick: function () { var count = this.count + 1; this.assign({ //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 = stache( "<div class='box-view'>"+ "<div class='box' style='top: {{top}}px; left: {{left}}px;'>"+ "</div>"+ "</div>"); var frag = stashed(boxes[0]); //equal(frag.children.length, 2, "there are 2 childNodes"); assert.ok(/top: 0px/.test( stacheTestHelpers.cloneAndClean(frag).firstChild.firstChild.getAttribute("style") ), "0px"); boxes[0].tick(); assert.ok(! /top: 0px/.test( stacheTestHelpers.cloneAndClean(frag).firstChild.firstChild.getAttribute("style")) , "!0px"); }); QUnit.test('Tokens returning 0 where they should display the number', function(assert) { var template = "<div id='zero'>{{completed}}</div>"; var frag = stache( template )({ completed: 0 }); assert.equal( stacheTestHelpers.cloneAndClean(frag).firstChild.firstChild.nodeValue, "0", 'zero shown' ); }); QUnit.test('Inverted section function returning numbers', function(assert) { var template = "<div id='completed'>{{^todos.completed()}}hidden{{/todos.completed}}</div>"; var obsvr = new SimpleMap({ named: false }); var todos = { completed: function () { return obsvr.get('named'); } }; // check hidden there var frag = stache( template ) ({ todos: todos }); assert.deepEqual(stacheTestHelpers.cloneAndClean(frag).firstChild.firstChild.nodeValue, "hidden", 'hidden shown'); // now update the named attribute obsvr.set('named', true); var hiddenTextNode = stacheTestHelpers.cloneAndClean(frag).firstChild.firstChild; assert.notOk(hiddenTextNode && hiddenTextNode.nodeValue, 'hidden gone'); }); QUnit.test("live-binding with escaping", function(assert) { var template = "<span id='binder1'>{{ name }}</span><span id='binder2'>{{{name}}}</span>"; var teacher = new SimpleMap({ name: "<strong>Mrs Peters</strong>" }); var tpl = stache( template ); var frag = tpl(teacher); assert.deepEqual(innerHTML(stacheTestHelpers.cloneAndClean(frag).firstChild), "&lt;strong&gt;Mrs Peters&lt;/strong&gt;"); assert.deepEqual(innerHTML(stacheTestHelpers.cloneAndClean(frag).lastChild.firstChild), "Mrs Peters"); teacher.set('name', '<i>Mr Scott</i>'); assert.deepEqual(innerHTML(stacheTestHelpers.cloneAndClean(frag).firstChild), "&lt;i&gt;Mr Scott&lt;/i&gt;"); assert.deepEqual(innerHTML(stacheTestHelpers.cloneAndClean(frag).lastChild.firstChild), "Mr Scott"); }); QUnit.test("truthy", function(assert) { var t = { template: "{{#name}}Do something, {{this}}!{{/name}}", expected: "Do something, Andy!", data: { name: 'Andy' } }; var expected = t.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); assert.deepEqual( getText( t.template , t.data), expected); }); QUnit.test("falsey", function(assert) { 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'); assert.deepEqual(getText( t.template, t.data), expected); }); QUnit.test("Handlebars helpers", function(assert) { stache.registerHelper('hello', function (options) { return 'Should not hit this'; }); stache.registerHelper('there', function (options) { return 'there'; }); // Test for #1985 stache.registerHelper('zero', function (options) { return 0; }); 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'); assert.deepEqual( getText(t.template, t.data) , expected); }); QUnit.test("Handlebars advanced helpers (from docs)", function(assert) { stache.addHelper('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 = stache(t.template); var frag = template(t.data); var div = doc.createElement("div"); div.appendChild(frag); assert.equal(innerHTML( stacheTestHelpers.cloneAndClean(div) ), t.expected); assert.equal(getText(t.template, {}), t.expected2); }); QUnit.test("Passing functions as data, then executing them", function(assert) { 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'); assert.deepEqual( getText(t.template, t.data), expected); }); QUnit.test("No arguments passed to helper", function(assert) { var template = stache("{{noargHelper()}}"); stache.registerHelper("noargHelper", function () { return "foo" }); var div1 = doc.createElement('div'); var div2 = doc.createElement('div'); div1.appendChild(template({})); div2.appendChild(template(new SimpleMap())); assert.deepEqual(innerHTML(div1), "foo"); assert.deepEqual(innerHTML(div2), "foo"); }); QUnit.test("String literals passed to helper should work (#1143)", function(assert) { assert.expect(1); 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. var template = stache('{{concatStrings "==" "word"}}'); var div = doc.createElement('div'); div.appendChild(template({})); assert.equal(innerHTML(div), '==word'); }); QUnit.test("No arguments passed to helper with list", function(assert) { var template = stache("{{#items}}{{noargHelper()}}{{/items}}"); var div = doc.createElement('div'); div.appendChild(template({ items: new DefineList([{ name: "Brian" }]) }, { noargHelper: function () { return "foo" } })); assert.deepEqual(innerHTML(stacheTestHelpers.cloneAndClean(div)), "foo"); }); if(isNormalDOM) { QUnit.test("Partials and observes", function(assert) { var template; var div = doc.createElement('div'); template = stache("<table><thead><tr>{{#theData}}{{>list}}{{/theData}}</tr></thead></table>") var dom = template({ theData: new SimpleMap({ list: ["hi", "there"] }) },{ partials: { list: stache("{{#list}}<th>{{.}}</th>{{/list}}") } }); div.appendChild(dom); var ths = div.getElementsByTagName('th'); assert.equal(ths.length, 2, 'Got two table headings'); assert.equal(innerHTML(ths[0]), 'hi', 'First column heading correct'); assert.equal(innerHTML(ths[1]), 'there', 'Second column heading correct'); }); } QUnit.test("Handlebars helper: if/else", function(assert) { 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'); assert.deepEqual(getText(t.template,t.data), expected); t.data.missing = null; expected = t.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); assert.deepEqual(getText(t.template,t.data), expected); }); QUnit.test("Handlebars helper: unless", function(assert) { 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 SimpleMap({ name: 'Andy', // #1202 #unless does not work with computes isCool: new Observation(function isCool() { return t.liveData.get("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(stache(t.template)(t.liveData)); assert.deepEqual( innerHTML(div), expected, '#unless condition false = '+expected); t.liveData.set('missing', true); assert.deepEqual( innerHTML(div), '', '#unless condition true'); }); QUnit.test("Handlebars helper: each", function(assert) { var t = { template: "{{#each names}}{{this}} {{/each}}", expected: "Andy Austin Justin ", data: { names: ['Andy', 'Austin', 'Justin'] }, data2: { names: new DefineList(['Andy', 'Austin', 'Justin']) } }; var expected = t.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); assert.deepEqual( getText(t.template,t.data) , expected); var div = doc.createElement('div'); div.appendChild(stache(t.template)(t.data2)); assert.deepEqual( innerHTML(stacheTestHelpers.cloneAndClean(div)), expected, 'Using Observe.List'); t.data2.names.push('What'); }); QUnit.test("Handlebars helper: with", function(assert) { 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'); assert.deepEqual(getText(t.template,t.data), expected, '#with person'); var v = { template: "{{#with person}}{{name}}{{/with}}", expected: "", data: { person: null, name: "Andy" } }; expected = v.expected.replace(/&quot;/g, '&#34;') .replace(/\r\n/g, '\n'); assert.deepEqual(getText(v.template,v.data), expected, '#with person - person === null'); }); QUnit.test("render with double angle", function(assert) { var text = "{{& replace_me }}{{{ replace_me_too }}}" + "<ul>{{#animals}}" + "<li>{{.}}</li>" + "{{/animals}}</ul>"; var compiled = getText(text,{ animals: this.animals }); assert.equal(compiled, "<ul><li>sloth</li><li>bear</li><li>monkey</li></ul>", "works") }); QUnit.test("comments", function(assert) { var text = "{{! replace_me }}" + "<ul>{{#animals}}" + "<li>{{.}}</li>" + "{{/animals}}</ul>"; var compiled = getText(text,{ animals: this.animals }); assert.equal(compiled, "<ul><li>sloth</li><li>bear</li><li>monkey</li></ul>") }); QUnit.test("multi line", function(assert) { var text = "a \n b \n c"; assert.equal(getTextFromFrag( stache(text)({}) ), text) }); QUnit.test("multi line elements", function(assert) { var text = "<div\n class=\"{{myClass}}\" />", result = stache(text)({myClass: 'a'}); assert.equal(result.firstChild.className, "a", "class name is right"); }); QUnit.test("escapedContent", function(assert) { var text = "<span>{{ tags }}</span><label>&amp;</label><strong>{{ number }}</strong><input value='{{ quotes }}'/>"; var div = doc.createElement('div'); div.appendChild( stache(text)({ tags: "foo < bar < car > zar > poo", quotes: "I use 'quote' fingers & &amp;ersands \"a lot\"", number: 123 }) ); assert.equal(div.getElementsByTagName('span')[0].firstChild.nodeValue, "foo < bar < car > zar > poo"); assert.equal(div.getElementsByTagName('strong')[0].firstChild.nodeValue, 123); assert.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"); assert.equal( innerHTML(div.getElementsByTagName('label')[0]), "&amp;", "text-based html entities work fine"); }); QUnit.test("unescapedContent", function(assert) { var text = "<span>{{{ tags }}}</span><div>{{{ tags }}}</div><input value='{{{ quotes }}}'/>"; var div = doc.createElement('div'); div.appendChild( stache(text)({ tags: "<strong>foo</strong><strong>bar</strong>", quotes: 'I use \'quote\' fingers "a lot"' }) ); assert.equal(div.getElementsByTagName('span')[0].firstChild.nodeType, 1,""); assert.equal( innerHTML(div.getElementsByTagName('div')[0]).toLowerCase(), "<strong>foo</strong><strong>bar</strong>"); assert.equal( innerHTML(div.getElementsByTagName('span')[0]).toLowerCase(), "<strong>foo</strong><strong>bar</strong>"); assert.equal(div.getElementsByTagName('input')[0].value, "I use 'quote' fingers \"a lot\"", "escaped no matter what"); }); QUnit.test("attribute single unescaped, html single unescaped", function(assert) { var text = "<div id='me' class='{{#task.completed}}complete{{/task.completed}}'>{{ task.name }}</div>"; var task = new SimpleMap({ name: 'dishes' }); var div = doc.createElement('div'); div.appendChild(stache(text)({ task: task })); assert.equal( innerHTML(div.getElementsByTagName('div')[0]), "dishes", "html correctly dishes") assert.equal(div.getElementsByTagName('div')[0].className, "", "class empty") task.set('name', 'lawn') assert.equal( innerHTML(div.getElementsByTagName('div')[0]), "lawn", "html correctly lawn") assert.equal(div.getElementsByTagName('div')[0].className, "", "class empty") task.set('completed', true); assert.equal(div.getElementsByTagName('div')[0].className, "complete", "class changed to complete") }); QUnit.test("select live binding", function(assert) { var text = "<select>{{ #todos }}<option>{{ name }}</option>{{ /todos }}</select>"; var todos, div; todos = new DefineList([{ id: 1, name: 'Dishes' }]); div = doc.createElement('div'); div.appendChild( stache(text)({todos: todos}) ); assert.equal(div.getElementsByTagName('option') .length, 1, '1 item in list') todos.push({ id: 2, name: 'Laundry' }); assert.equal(div.getElementsByTagName('option') .length, 2, '2 items in list') todos.splice(0, 2); assert.equal(div.getElementsByTagName('option') .length, 0, '0 items in list') }); QUnit.test('multiple hookups in a single attribute', function(assert) { var text = '<div class=\'{{ obs.foo }}' + '{{ obs.bar }}{{ obs.baz }}{{ obs.nest.what }}\'></div>'; var obs = new SimpleMap({ foo: 'a', bar: 'b', baz: 'c', nest: new SimpleMap({ what: 'd' }) }); var div = doc.createElement('div'); div.appendChild( stache(text)({ obs: obs }) ); var innerDiv = div.firstChild; assert.equal(getAttr(innerDiv, 'class'), "abcd", 'initial render'); obs.set('bar', 'e'); assert.equal(getAttr(innerDiv, 'class'), "aecd", 'initial render'); obs.set('bar', 'f'); assert.equal(getAttr(innerDiv, 'class'), "afcd", 'initial render'); obs.get('nest').set('what', 'g'); assert.equal(getAttr(innerDiv, 'class'), "afcg", 'nested observe'); }); QUnit.test('adding and removing multiple html content within a single element', function(assert) { var text, obs; text = '<div>{{ obs.a }}{{ obs.b }}{{ obs.c }}</div>'; obs = new SimpleMap({ a: 'a', b: 'b', c: 'c' }); var div = doc.createElement('div'); div.appendChild(stache(text)({ obs: obs })); assert.equal( innerHTML(div.firstChild), 'abc', 'initial render'); obs.set({ a: '', b: '', c: '' }); assert.equal(innerHTML(div.firstChild), '', 'updated values'); obs.set({ c: 'c' }); assert.equal( innerHTML(div.firstChild), 'c', 'updated values'); }); QUnit.test('live binding and removeAttr', function(assert) { var text = '{{ #obs.show }}' + '<p {{ ../obs.attributes }} class="{{ ../obs.className }}"><span>{{ ../obs.message }}</span></p>' + '{{ /obs.show }}', obs = new SimpleMap({ show: true, className: 'myMessage', attributes: 'some=\"myText\"', message: 'Live long and prosper' }), div = doc.createElement('div'); div.appendChild(stache(text)({ obs: obs })); var p = div.getElementsByTagName('p')[0], span = p.getElementsByTagName('span')[0]; assert.equal(p.getAttribute("some"), "myText", 'initial render attr'); assert.equal(getAttr(p, "class"), "myMessage", 'initial render class'); assert.equal( innerHTML(span), 'Live long and prosper', 'initial render innerHTML'); obs.set('className', undefined); assert.equal(getAttr(p, "class"), '', 'class is undefined'); obs.set('className', 'newClass'); assert.equal(getAttr(p, "class"), 'newClass', 'class updated'); obs.set('attributes',undefined); assert.equal(p.getAttribute('some'), null, 'attribute is undefined'); obs.set('attributes', 'some="newText"'); // assert.equal(p.getAttribute('some'), 'newText', 'attribute updated'); obs.set('message',undefined); assert.equal(innerHTML(span), '', 'text node value is empty'); obs.set('message', 'Warp drive, Mr. Sulu'); assert.equal(innerHTML(span), 'Warp drive, Mr. Sulu', 'text node updated'); obs.set('show',undefined); assert.equal( innerHTML(div), '', 'value in block statement is undefined'); obs.set('show', true); p = div.getElementsByTagName('p')[0]; span = p.getElementsByTagName('span')[0]; assert.equal(p.getAttribute("some"), "newText", 'value in block statement updated attr'); assert.equal(getAttr(p, "class"), "newClass", 'value in block statement updated class'); assert.equal( innerHTML(span), 'Warp drive, Mr. Sulu', 'value in block statement updated innerHTML'); }); QUnit.test('hookup within a tag', function(assert) { var text = '<div {{ obs.foo }} ' + '{{ obs.baz }}>lorem ipsum</div>', obs = new SimpleMap({ foo: 'class="a"', baz: 'some=\'property\'' }), compiled = stache(text)({obs: obs}) var div = doc.createElement('div'); div.appendChild(compiled); var anchor = div.getElementsByTagName('div')[0]; assert.equal(getAttr(anchor, 'class'), 'a'); assert.equal(anchor.getAttribute('some'), 'property'); obs.set('foo', 'class="b"'); assert.equal(getAttr(anchor, 'class'), 'b'); assert.equal(anchor.getAttribute('some'), 'property'); obs.set('baz', 'some=\'new property\''); assert.equal(getAttr(anchor, 'class'), 'b'); assert.equal(anchor.getAttribute('some'), 'new property'); obs.set('foo', 'class=""'); obs.set('baz', ''); assert.equal(getAttr(anchor, 'class'), "", 'anchor class blank'); assert.equal(anchor.getAttribute('some'), undefined, 'attribute "some" is undefined'); }); QUnit.test('single escaped tag, removeAttr', function(assert) { var text = '<div {{ obs.foo }}>lorem ipsum</div>', obs = new SimpleMap({ foo: 'data-bar="john doe\'s bar"' }), compiled = stache(text)({obs: obs}) var div = doc.createElement('div'); div.appendChild(compiled); var anchor = div.getElementsByTagName('div')[0]; assert.equal(anchor.getAttribute('data-bar'), "john doe's bar"); obs.set('foo',undefined); assert.equal(anchor.getAttribute('data-bar'), null); obs.set('foo', 'data-bar="baz"'); assert.equal(anchor.getAttribute('data-bar'), 'baz'); }); QUnit.test('html comments', function(assert) { var text = '<!-- bind to changes in the todo list --> <div>{{obs.foo}}</div>'; var obs = new SimpleMap({ foo: 'foo' }); var compiled = stache(text)({ obs: obs }); var div = doc.createElement('div'); div.appendChild(compiled); assert.equal( innerHTML(div.getElementsByTagName('div')[0]), 'foo', 'Element as expected'); }); QUnit.test("hookup and live binding", function(assert) { var text = "<div class='{{ task.completed }}' {{ domData 'task' task }}>" + "{{ task.name }}" + "</div>", task = new SimpleMap({ completed: false, className: 'someTask', name: 'My Name' }), compiled = stache(text)({ task: task }), div = doc.createElement('div'); div.appendChild(compiled) var child = div.getElementsByTagName('div')[0]; assert.ok(child.className.indexOf("false") > -1, "is incomplete") assert.ok( !! domData.get(child, 'task'), "has data") assert.equal(innerHTML(child), "My Name", "has name") task.set({ completed: true, name: 'New Name' }); assert.ok(child.className.indexOf("true") !== -1, "is complete") assert.equal(innerHTML(child), "New Name", "has new name") }); QUnit.test('multiple curly braces in a block', function(assert) { var text = '{{^obs.items}}' + '<li>No items</li>' + '{{/obs.items}}' + '{{#obs.items}}' + '<li>{{name}}</li>' + '{{/obs.items}}', obs = new SimpleMap({ items: [] }), compiled = stache(text)({obs: obs}) var ul = doc.createElement('ul'); ul.appendChild(compiled); assert.equal( innerHTML(ul.getElementsByTagName('li')[0]), 'No items', 'initial observable state'); obs.set('items', [{ name: 'foo' }]); assert.equal( innerHTML(ul.getElementsByTagName('li')[0]), 'foo', 'updated observable'); }); QUnit.test("unescape bindings change", function(assert) { var l = new DefineList([{ complete: true }, { complete: false }, { complete: true }]); var completed = function () { l.get('length'); var num = 0; l.forEach(function (item) { if (item.get('complete')) { num++; } }) return num; }; var text = '<div>{{ completed() }}</div>', compiled = stache(text)({ completed: completed }); var div = doc.createElement('div'); div.appendChild(compiled); var child = div.getElementsByTagName('div')[0]; assert.equal( innerHTML(child), "2", "at first there are 2 true bindings"); var item = new SimpleMap({ complete: true, id: "THIS ONE" }); l.push(item); assert.equal(innerHTML(child), "3", "now there are 3 complete"); item.set('complete', false); assert.equal(innerHTML(child), "2", "now there are 2 complete"); l.pop(); item.set('complete', true); assert.equal(innerHTML(child), "2", "there are still 2 complete"); }); QUnit.test("escape bindings change", function(assert) { var l = new DefineList([{ complete: true }, { complete: false }, { complete: true }]); var completed = function () { l.get('length'); var num = 0; l.forEach(function (item) { if (item.get('complete')) { num++; } }) return num; }; var text = '<div>{{{ completed() }}}</div>', compiled = stache(text)({ completed: completed }); var div = doc.createElement('div'); div.appendChild(compiled); var child = div.getElementsByTagName('div')[0]; assert.equal(innerHTML(child), "2", "at first there are 2 true bindings"); var item = new SimpleMap({ complete: true }) l.push(item); assert.equal(innerHTML(child), "3", "now there are 3 complete"); item.set('complete', false); assert.equal(innerHTML(child), "2", "now there are 2 complete"); }); QUnit.test("tag bindings change", function(assert) { var l = new DefineList([{ complete: true }, { complete: false }, { complete: true }]); var completed = function () { l.get('length'); var num = 0; l.forEach(function (item) { if (item.get('complete')) { num++; } }) return "items='" + num + "'"; }; var text = '<div {{{ completed() }}}></div>', compiled = stache(text)({ completed: completed }); var div = doc.createElement('div'); div.appendChild(compiled); var child = div.getElementsByTagName('div')[0]; assert.equal(child.getAttribute("items"), "2", "at first there are 2 true bindings"); var item = new SimpleMap({ complete: true }) l.push(item); assert.equal(child.getAttribute("items"), "3", "now there are 3 complete"); item.set('complete', false); assert.equal(child.getAttribute("items"), "2", "now there are 2 complete"); }) QUnit.test("attribute value bindings change", function(assert) { var l = new DefineList([{ complete: true }, { complete: false }, { complete: true }]); var completed = function () { l.get('length'); var num = 0; l.forEach(function (item) { if (item.get('complete')) { num++; } }); return num; }; var text = '<div items="{{{ completed() }}}"></div>', compiled = stache(text)({ completed: completed }); var div = doc.createElement('div'); div.appendChild(compiled); var child = div.getElementsByTagName('div')[0]; assert.equal(child.getAttribute("items"), "2", "at first there are 2 true bindings"); var item = new SimpleMap({ complete: true }); l.push(item); assert.equal(child.getAttribute("items"), "3", "now there are 3 complete"); item.set('complete', false); assert.equal(child.getAttribute("items"), "2", "now there are 2 complete"); }); QUnit.test("in tag toggling", function(assert) { var text = "<div {{ obs.val }}></div>" var obs = new SimpleMap({ val: 'foo="bar"' }) var compiled = stache(text)({ obs: obs }); var div = doc.createElement('div'); div.appendChild(compiled); obs.set('val', "bar='foo'"); obs.set('val', 'foo="bar"') var d2 = div.getElementsByTagName('div')[0]; // toUpperCase added to normalize cases for IE8 assert.equal(d2.getAttribute("foo"), "bar", "bar set"); assert.equal(d2.getAttribute("bar"), null, "bar set") }); // not sure about this w/ mustache QUnit.test("nested properties", function(assert) { var text = "<div>{{ obs.name.first }}</div>" var obs = new SimpleMap({ name: new SimpleMap({ first: "Justin" }) }) var compiled = stache(text)({ obs: obs }); var div = doc.createElement('div'); div.appendChild(compiled); div = div.getElementsByTagName('div')[0]; assert.equal(innerHTML(div), "Justin") obs.get('name').set('first', "Brian") assert.equal(innerHTML(div), "Brian") }); QUnit.test("tags without chidren or ending with /> do not change the state", function(assert) { var text = "<table><tr><td/>{{{ obs.content }}}</tr></div>" var obs = new SimpleMap({ content: "<td>Justin</td>" }) var compiled = stache(text)({ obs: obs }); var div = doc.createElement('div'); var html = compiled; div.appendChild(html); assert.equal(div.getElementsByTagName('span') .length, 0, "there are no spans"); assert.equal(div.getElementsByTagName('td') .length, 2, "there are 2 td"); }) QUnit.test("nested live bindings", function(assert) { assert.expect(0); var items = new DefineList([{ title: 0, is_done: false, id: 0 }]); var div = doc.createElement('div'); var template = 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].set('is_done', true); }); QUnit.test("list nested in observe live bindings", function(assert) { var template = stache("<ul>{{#data.items}}<li>{{name}}</li>{{/data.items}}</ul>"); var data = new SimpleMap({ items: new DefineList([{ name: "Brian" }, { name: "Fara" }]) }); var div = doc.createElement('div'); div.appendChild(template({ data: data })); data.get("items").push(new SimpleMap({ name: "Scott" })) assert.ok(/Brian/.test(innerHTML(div)), "added first name") assert.ok(/Fara/.test(innerHTML(div)), "added 2nd name") assert.ok(/Scott/.test(innerHTML(div)), "added name after push") }); QUnit.test("trailing text", function(assert) { var template = stache("There are {{ length }} todos") var div = doc.createElement('div'); div.appendChild(template(new DefineList([{}, {}]))); assert.ok(/There are 2 todos/.test(innerHTML(div)), "got all text"); }); if(isNormalDOM) { QUnit.test("recursive views", function(assert) { var template = stache('<div class="template">'+ '{{#items}}'+ '<div class="loop">'+ '{{#item.children}}'+ '<div class="node">'+ '{{>recursive}}'+ '</div>'+ '{{/item.children}}'+ '{{^item.children}}'+ '<div class="leaf">L</div>'+ '{{/item.children}}'+ '</div>'+ '{{/items}}'+ '</div>'); var data = new DefineList([{ label: 'branch1', children: [{ id: 2, label: 'branch2' }] }]); var div = doc.createElement('div'); var frag = template({ items: data },{ partials: { recursive: template } }) div.appendChild(frag); assert.ok(/class="?leaf"?/.test(innerHTML(div)), "we have a leaf") }); } QUnit.test("live binding textarea", function(assert) { var template = stache("<textarea>Before{{ obs.middle }}After</textarea>"); var obs = new SimpleMap({ middle: "yes" }), div = doc.createElement('div'); div.appendChild(template({ obs: obs })); var textarea = div.firstChild; assert.equal(getValue(textarea), "BeforeyesAfter"); obs.set("middle", "Middle"); assert.equal(getValue(textarea), "BeforeMiddleAfter"); }); QUnit.test("helper parameters don't convert functions", function(assert) { stache.registerHelper('helperWithFn', function (fn) { assert.ok(typeof fn === "function", 'Parameter is a function'); assert.equal(fn(), 'Hit me!', 'Got the expected function'); }); var renderer = stache('{{helperWithFn test}}'); renderer({ test: function () { return 'Hit me!'; } }); }) QUnit.test("computes as helper parameters don't get converted", function(assert) { stache.registerHelper('computeTest', function (no) { assert.equal(no(), 5, 'Got computed calue'); assert.ok(no.isComputed, 'no is still a compute') }); var renderer = stache('{{computeTest test}}'); renderer({ test: new SimpleObservable(5) }); }); QUnit.test("computes are supported in default helpers", function(assert) { 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 = stache("There are {{ length }} todos"); var div = doc.createElement('div'); div.appendChild(template(new DefineList([{}, {}]))); assert.ok(/There are 2 todos/.test(innerHTML(div)), "got all text"); var renderer, result, data, actual, span; for (result in staches) { renderer = stache(staches[result]); data = ["e", "a", "c", "h"]; div = doc.createElement("div"); actual = renderer({ test: new DefineList(data) }); div.appendChild(actual); span = div.getElementsByTagName("span")[0]; if (span && span.firstChild) { div.insertBefore(span.firstChild, span); div.removeChild(span); } actual = innerHTML(stacheTestHelpers.cloneAndClean(div)); assert.equal(actual, result, "canCompute 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" //{{#with}} *always* renders non-inverse block }; for (result in inv_staches) { renderer = stache(inv_staches[result]); data = null; div = doc.createElement("div"); actual = renderer({ test: null }); div.appendChild(actual); actual = innerHTML(div); assert.equal(actual, result, "canCompute resolved for helper " + result); } }); //Issue 233 QUnit.test("multiple tbodies in table hookup", function(assert) { var text = "<table>" + "{{#people}}" + "<tbody><tr><td>{{name}}</td></tr></tbody>" + "{{/people}}" + "</table>", people = new DefineList([{ name: "Steve" }, { name: "Doug" }]), compiled = stache(text)({ people: people }); assert.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 QUnit.test("Observe with array attributes", function(assert) { var renderer = stache('<ul>{{#todos}}<li>{{.}}</li>{{/todos}}</ul><div>{{message}}</div>'); var div = doc.createElement('div'); var data = new SimpleMap({ todos: new DefineList( ['Line #1', 'Line #2', 'Line #3'] ), message: 'Hello', count: 2 }); div.appendChild(renderer(data)); assert.equal(innerHTML(div.getElementsByTagName('li')[1]), 'Line #2', 'Check initial array'); assert.equal(innerHTML(div.getElementsByTagName('div')[0]), 'Hello', 'Check initial message'); data.get('todos').set(1, 'Line #2 changed'); data.set('message', 'Hello again'); assert.equal(innerHTML(div.getElementsByTagName('li')[1]), 'Line #2 changed', 'Check updated array'); assert.equal(innerHTML(div.getElementsByTagName('div')[0]), 'Hello again', 'Check updated message'); }); QUnit.test("Observe list returned from the function", function(assert) { var renderer = stache('<ul>{{#todos()}}<li>{{.}}</li>{{/todos}}</ul>'); var div = doc.createElement('div'); var todos = new DefineList(); var data = { todos: function () { return todos; } }; div.appendChild(renderer(data)); todos.push("Todo #1") assert.equal(div.getElementsByTagName('li') .length, 1, 'Todo is successfuly created'); assert.equal(innerHTML(div.getElementsByTagName('li')[0]), 'Todo #1', 'Pushing to the list works'); }); // https://github.com/canjs/canjs/issues/228 QUnit.test("Contexts within helpers not always resolved correctly", function(assert) { stache.registerHelper("bad_context", function (context, options) { return ["<span>"+this.text+"</span> should not be ",options.fn(context)]; }); var renderer = 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)); assert.equal(innerHTML(div.getElementsByTagName('span')[0]), "foo", 'Incorrect context passed to helper'); assert.equal(innerHTML(div.getElementsByTagName('span')[1]), "bar", 'Incorrect text in helper inner template'); assert.equal(innerHTML(div.getElementsByTagName('span')[2]), "In the inner context", 'Incorrect other_text in helper inner template'); }); // https://github.com/canjs/canjs/issues/227 QUnit.test("Contexts are not always passed to partials properly", function(assert) { var inner = stache('{{#if ../other_first_level}}{{../other_first_level}}{{else}}{{second_level}}{{/if}}'); var renderer = 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, {partials: {inner: inner}})); assert.equal(innerHTML(div.getElementsByTagName('span')[0]), "foo", 'Incorrect context passed to helper'); assert.equal(innerHTML(div.getElementsByTagName('span')[1]), "foo", 'Incorrect text in helper inner template'); }); // https://github.com/canjs/canjs/issues/231 QUnit.test("Functions and helpers should be passed the same context", function(assert) { 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; } }; stache.registerHelper("to_upper", function (options) { var frag = options.fn(options.context); textNodes(frag, function(el){ el.nodeValue = el.nodeValue.toUpperCase(); }); return frag; }); var renderer = 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'); div.appendChild(renderer(data)); assert.equal(innerHTML(div.getElementsByTagName('span')[0]), data.next_level.other_text.toUpperCase(), 'correct context passed to helper'); }); // https://github.com/canjs/canjs/issues/153 QUnit.test("Interpolated values when iterating through an Observe.List should still render when not surrounded by a DOM node", function(assert) { var renderer = stache('{{ #todos }}{{ name }}{{ /todos }}'), renderer2 = stache('{{ #todos }}<span>{{ name }}</span>{{ /todos }}'), todos = [{ id: 1, name: 'Dishes' }, { id: 2, name: 'Forks' }], liveData = { todos: new DefineList(todos) }, plainData = { todos: todos }, div = doc.createElement('div'); div.appendChild(renderer2(plainData)); assert.equal(innerHTML(div.getElementsByTagName('span')[0]), "Dishes", 'Array item rendered with DOM container'); assert.equal(innerHTML(div.getElementsByTagName('span')[1]), "Forks", 'Array item rendered with DOM container'); div.innerHTML = ''; div.appendChild(renderer2(liveData)); assert.equal(innerHTML(div.getElementsByTagName('span')[0]), "Dishes", 'List item rendered with DOM container'); assert.equal(innerHTML(div.getElementsByTagName('span')[1]), "Forks", 'List item rendered with DOM container'); div = doc.createElement('div'); div.appendChild(renderer(plainData)); assert.equal(innerHTML(div), "DishesForks", 'Array item rendered without DOM container'); div = doc.createElement('div'); div.appendChild(renderer(liveData)); assert.equal(innerHTML(div), "DishesForks", 'List item rendered without DOM container'); liveData.todos.push({ id: 3, name: 'Knives' }); assert.equal(innerHTML(div), "DishesForksKnives", 'New list item rendered without DOM container'); }); QUnit.test("objects with a 'key' or 'index' property should work in helpers", function(assert) { var renderer = stache('{{ #obj }}{{ show_name(this) }}{{ /obj }}'), div = doc.createElement('div'); div.appendChild(renderer({ obj: { id: 2, name: 'Forks', key: 'bar' } }, { show_name: function (obj) { return obj.name; } })); assert.equal(innerHTML(div), "Forks", 'item name rendered'); div = doc.createElement('div'); div.appendChild(renderer({ obj: { id: 2, name: 'Forks', index: 'bar' } }, { show_name: function (obj) { return obj.name; } })); assert.equal(innerHTML(div), "Forks", 'item name rendered'); }); QUnit.test("2 way binding helpers", function(assert) { 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; stache.registerHelper('myValue', function (value) { return function (el) { val = new Value(el, value); } }); var renderer = stache('<input {{myValue user.name}}/>'); var div = doc.createElement('div'), u = new SimpleMap({ name: "Justin" }); div.appendChild(renderer({ user: u })); var input = div.getElementsByTagName('input')[0]; assert.equal(input.value, "Justin", "Name is set correctly") u.set('name', 'Eli') assert.equal(input.value, "Eli", "Changing observe updates value"); input.value = "Austin"; input.onchange(); assert.equal(u.get('name'), "Austin", "Name changed by input field"); val.teardown(); // name is undefined renderer = stache('<input {{myValue user.name}}/>'); div = doc.createElement('div'); u = new SimpleMap({}); div.appendChild(renderer({ user: u })); input = div.getElementsByTagName('input')[0]; assert.equal(input.value, "", "Name is set correctly") u.set('name', 'Eli') assert.equal(input.value, "Eli", "Changing observe updates value"); input.value = "Austin"; input.onchange(); assert.equal(u.get('name'), "Austin", "Name changed by input field"); val.teardown(); // name is null renderer = stache('<input {{myValue user.name}}/>'); div = doc.createElement('div'); u = new SimpleMap({ name: null }); div.appendChild(renderer({ user: u })); input = div.getElementsByTagName('input')[0]; assert.equal(input.value, "", "Name is set correctly with null") u.set('name', 'Eli') assert.equal(input.value, "Eli", "Changing observe updates value"); input.value = "Austin"; input.onchange(); assert.equal(u.get('name'), "Austin", "Name changed by input field"); val.teardown(); }); QUnit.test("can pass in partials", function(assert) { var hello = stache("<p>Hello {{> name}}</p>"); var fancyName = stache("<span class='fancy'>{{name}}</span>"); var result = hello({ name: "World" }, { partials: { name: fancyName } }); assert.ok(/World/.test(innerHTML(result.firstChild)), "Hello World worked"); }); QUnit.test("can pass in helpers", function(assert) { var helpers = stache("<p>Hello {{cap name}}</p>"); var result = helper