can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
1,871 lines (1,478 loc) • 134 kB
JavaScript
/* 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 ".
// can uses \n for new lines, mustache expects \r\n.
var expected = (override[spec] && override[spec][t.name]) || t.expected.replace(/"/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,"<").replace(/\>/g,">")
}
// 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), "<strong>Mrs Peters</strong>");
deepEqual(innerHTML(frag.lastChild.firstChild), "Mrs Peters");
teacher.attr('name', '<i>Mr Scott</i>');
deepEqual(innerHTML(frag.firstChild), "<i>Mr Scott</i>");
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(/"/g, '"')
.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(/"/g, '"')
.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(/"/g, '"')
.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(/"/g, '"')
.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(/"/g, '"')
.replace(/\r\n/g, '\n');
deepEqual(getText(t.template,t.data), expected);
t.data.missing = null;
expected = t.expected.replace(/"/g, '"')
.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(/"/g, '"').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(/"/g, '"')
.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(/"/g, '"')
.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(/"/g, '"')
.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>&</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 & &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 & &ersands \"a lot\"", "attributes are always safe, and strings are kept as-is without additional escaping");
equal( innerHTML(div.getElementsByTagName('label')[0]), "&", "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