can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
1,896 lines (1,557 loc) • 109 kB
JavaScript
/* jshint asi:true,multistr:true*/
/*global Mustache*/
steal("can/model", "can/view/mustache", "can/test", "can/view/mustache/spec/specs", "steal-qunit", function () {
QUnit.module("can/view/mustache, rendering", {
setup: function () {
can.view.ext = '.mustache';
this.animals = ['sloth', 'bear', 'monkey']
if (!this.animals.each) {
this.animals.each = function (func) {
for (var i = 0; i < this.length; i++) {
func(this[i])
}
}
}
this.squareBrackets = "<ul>{{#animals}}" +
"<li>{{.}}</li>" +
"{{/animals}}</ul>";
this.squareBracketsNoThis = "<ul>{{#animals}}" +
"<li>{{.}}</li>" +
"{{/animals}}</ul>";
this.angleBracketsNoThis = "<ul>{{#animals}}" +
"<li>{{.}}</li>" +
"{{/animals}}</ul>";
}
});
// Override expected spec result for whitespace only issues
var override = {
comments: {
'Standalone Without Newline': '!'
},
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 () {
// can uses " to 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, ']');
}
// register the partials in the spec
if (t.partials) {
for (var name in t.partials) {
can.view.registerView(name, t.partials[name], ".mustache");
}
}
// register lambdas
if (t.data.lambda && t.data.lambda.js) {
t.data.lambda = eval('(' + t.data.lambda.js + ')');
}
deepEqual(new can.Mustache({
text: t.template
})
.render(t.data), expected);
});
});
});
var getAttr = function (el, attrName) {
return attrName === "class" ?
el.className :
el.getAttribute(attrName);
}
test("basics", function () {
var template = can.view.mustache("<ul>{{#items}}<li>{{helper foo}}</li>{{/items}}</ul>");
template()
ok(true, "just to force the issue")
})
test("Model hookup", function () {
// Single item hookup
var template = '<p id="foo" {{ data "name " }}>data rocks</p>';
var obsvr = new can.Map({
name: 'Austin'
});
var frag = new can.Mustache({
text: template
})
.render(obsvr);
can.append(can.$('#qunit-fixture'), can.view.frag(frag));
deepEqual(can.data(can.$('#foo'), 'name '), obsvr, 'data hooks worked and fetched');
// Multi-item hookup
var listTemplate = '<ul id="list">{{#list}}<li class="moo" id="li-{{name}}" {{data "obsvr"}}>{{name}}</li>{{/#list}}</ul>';
var obsvrList = new can.List([obsvr]);
var listFrag = new can.Mustache({
text: listTemplate
})
.render({
list: obsvrList
});
can.append(can.$('#qunit-fixture'), can.view.frag(listFrag));
deepEqual(can.data(can.$('#li-Austin'), 'obsvr'), obsvr, 'data hooks for list worked and fetched');
// Mulit-item update with hookup
var obsvr2 = new can.Map({
name: 'Justin'
});
obsvrList.push(obsvr2);
deepEqual(can.data(can.$('#li-Justin'), 'obsvr'), obsvr2, 'data hooks for list push worked and fetched');
// Delete last item added
obsvrList.pop();
deepEqual(can.$('.moo')
.length, 1, 'new item popped off and deleted from ui');
});
/*test("Variable partials", function(){
var template = "{{#items}}<span>{{>partial}}</span>{{/items}}";
var data = { items: [{}], partial: "test_template.mustache" }
var frag = new can.Mustache({ text: template }).render(data);
can.append( can.$('#qunit-fixture'), can.view.frag(frag));
});*/
/*
// FIX THIS
test('Helpers sections not returning values', function(){
Mustache.registerHelper('filter', function(attr,options){
return true;
});
var template = "<div id='sectionshelper'>{{#filter}}moo{{/filter}}</div>";
var frag = new can.Mustache({ text: template }).render({ });;
can.append( can.$('#qunit-fixture'), can.view.frag(frag));
deepEqual(can.$('#sectionshelper')[0].innerHTML, "moo", 'helper section worked');
});
// FIX THIS
test('Helpers with obvservables in them', function(){
Mustache.registerHelper('filter', function(attr,options){
return options.fn(attr === "poo");
});
var template = "<div id='sectionshelper'>{{#filter 'moo'}}moo{{/filter}}</div>";
var obsvr = new can.Map({ filter: 'moo' });
var frag = new can.Mustache({ text: template }).render({ filter: obsvr });;
can.append( can.$('#qunit-fixture'), can.view.frag(frag));
deepEqual(can.$('#sectionshelper')[0].innerHTML, "", 'helper section showed none');
obsvr.attr('filter', 'poo')
deepEqual(can.$('#sectionshelper')[0].innerHTML, "poo", 'helper section worked');
});
*/
test('Tokens returning 0 where they should diplay the number', function () {
var template = "<div id='zero'>{{completed}}</div>";
var frag = new can.Mustache({
text: template
})
.render({
completed: 0
});
can.append(can.$('#qunit-fixture'), can.view.frag(frag));
deepEqual(can.$('#zero')[0].innerHTML, "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 = new can.Mustache({
text: template
})
.render({
todos: todos
});
can.append(can.$('#qunit-fixture'), can.view.frag(frag));
deepEqual(can.$('#completed')[0].innerHTML, "hidden", 'hidden shown');
// now update the named attribute
obsvr.attr('named', true);
deepEqual(can.$('#completed')[0].innerHTML, "", 'hidden gone');
can.remove(can.$('#qunit-fixture>*'));
});
test("Mustache 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 = new can.Mustache({
text: template
});
can.append(can.$('#qunit-fixture'), can.view.frag(tpl.render(teacher)));
deepEqual(can.$('#binder1')[0].innerHTML, "<strong>Mrs Peters</strong>");
deepEqual(can.$('#binder2')[0].getElementsByTagName('strong')[0].innerHTML, "Mrs Peters");
teacher.attr('name', '<i>Mr Scott</i>');
deepEqual(can.$('#binder1')[0].innerHTML, "<i>Mr Scott</i>");
deepEqual(can.$('#binder2')[0].getElementsByTagName('i')[0].innerHTML, "Mr Scott")
can.remove(can.$('#qunit-fixture>*'));
});
test("Mustache 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(new can.Mustache({
text: t.template
})
.render(t.data), expected);
});
test("Mustache 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(new can.Mustache({
text: t.template
})
.render(t.data), expected);
});
test("Handlebars helpers", function () {
can.Mustache.registerHelper('hello', function (options) {
return 'Should not hit this';
});
can.Mustache.registerHelper('there', function (options) {
return 'there';
});
// Test for #1109
can.Mustache.registerHelper('zero', function (options) {
return 0;
});
can.Mustache.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}}!\n{{bark name 'Austin and Andy' 3 obj=name action='growled and snarled' where=2 loud=true}} Then there were {{zero}} barks :(",
expected: "Hello there!\nThe 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(new can.Mustache({ text: t.template }).render(t.data), expected);
});
test("Handlebars advanced helpers (from docs)", function () {
Mustache.registerHelper('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'
}
};
deepEqual(new can.Mustache({
text: t.template
})
.render(t.data), t.expected);
deepEqual(new can.Mustache({
text: t.template
})
.render({}), 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(new can.Mustache({
text: t.template
})
.render(t.data), expected);
});
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(new can.Mustache({
text: t.template1
})
.render({}), t.expected);
deepEqual(new can.Mustache({
text: t.template2
})
.render({}), t.expected);
});
test("No arguments passed to helper", function () {
can.view.mustache("noargs", "{{noargHelper}}");
can.Mustache.registerHelper("noargHelper", function () {
return "foo"
})
var div1 = document.createElement('div');
var div2 = document.createElement('div');
div1.appendChild(can.view("noargs", {}));
div2.appendChild(can.view("noargs", new can.Map()));
deepEqual(div1.innerHTML, "foo");
deepEqual(div2.innerHTML, "foo");
});
test("No arguments passed to helper with list", function () {
can.view.mustache("noargs", "{{#items}}{{noargHelper}}{{/items}}");
var div = document.createElement('div');
div.appendChild(can.view("noargs", {
items: new can.List([{
name: "Brian"
}])
}, {
noargHelper: function () {
return "foo"
}
}));
deepEqual(div.innerHTML, "foo");
});
test("String literals passed to helper should work (#1143)", 1, function() {
can.Mustache.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.view.mustache('testStringArgs', '{{concatStrings "==" "word"}}');
var div = document.createElement('div');
div.appendChild(can.view('testStringArgs', {}));
equal(div.innerHTML, '==word');
});
test("Partials and observes", function () {
var template;
var div = document.createElement('div');
template = can.view.mustache("table", "<table><thead><tr>{{#data}}{{{>" +
can.test.path('view/mustache/test/partial.mustache') + "}}}{{/data}}</tr></thead></table>")
var dom = can.view("table", {
data: new can.Map({
list: ["hi", "there"]
})
});
div.appendChild(dom);
var ths = div.getElementsByTagName('th');
equal(ths.length, 2, 'Got two table headings');
equal(ths[0].innerHTML, 'hi', 'First column heading correct');
equal(ths[1].innerHTML, '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], ".mustache")
}
deepEqual(new can.Mustache({
text: t.template
})
.render(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], ".mustache")
}
deepEqual(new can.Mustache({
text: t.template
})
.render(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(new can.Mustache({
text: t.template
})
.render(t.data), expected);
t.data.missing = null;
expected = t.expected.replace(/"/g, '"')
.replace(/\r\n/g, '\n');
deepEqual(new can.Mustache({
text: t.template
})
.render(t.data), expected);
});
test("Handlebars helper: is/else (with 'eq' alias)", function() {
var t = {
template: '{{#eq "10" "10" ducks getDucks}}10 ducks{{else}}Not 10 ducks{{/eq}}',
expected: "10 ducks",
data: {
ducks: '10',
getDucks: function() {
return '10'
}
},
liveData: new can.Map({
ducks: '10',
getDucks: function() {
return '10'
}
})
};
var div = document.createElement('div');
div.appendChild(can.view.mustache(t.template)(t.data));
deepEqual(div.innerHTML, t.expected);
div = document.createElement('div');
div.appendChild(can.view.mustache(t.template)(t.liveData));
deepEqual(div.innerHTML, t.expected);
t.data.ducks = 5;
div = document.createElement('div');
div.appendChild(can.view.mustache(t.template)(t.data));
deepEqual(div.innerHTML, '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(new can.Mustache({
text: t.template
})
.render(t.data), expected);
// #1019 #unless does not live bind
var div = document.createElement('div');
div.appendChild(can.view.mustache(t.template)(t.liveData));
deepEqual(div.innerHTML, expected, '#unless condition false');
t.liveData.attr('missing', true);
deepEqual(div.innerHTML, '', '#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(new can.Mustache({
text: t.template
})
.render(t.data), expected);
var div = document.createElement('div');
div.appendChild(can.view.mustache(t.template)(t.data2));
deepEqual(div.innerHTML, 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(new can.Mustache({
text: t.template
})
.render(t.data), expected);
});
test("render with left bracket", function () {
var compiled = new can.Mustache({
text: this.squareBrackets,
type: '['
})
.render({
animals: this.animals
})
equal(compiled, "<ul><li>sloth</li><li>bear</li><li>monkey</li></ul>", "renders with bracket")
});
test("render with with", function () {
var compiled = new can.Mustache({
text: this.squareBracketsNoThis,
type: '['
})
.render({
animals: this.animals
});
equal(compiled, "<ul><li>sloth</li><li>bear</li><li>monkey</li></ul>", "renders bracket with no this")
});
test("default carrot", function () {
var compiled = new can.Mustache({
text: this.angleBracketsNoThis
})
.render({
animals: this.animals
});
equal(compiled, "<ul><li>sloth</li><li>bear</li><li>monkey</li></ul>")
})
test("render with double angle", function () {
var text = "{{& replace_me }}{{{ replace_me_too }}}" +
"<ul>{{#animals}}" +
"<li>{{.}}</li>" +
"{{/animals}}</ul>";
var compiled = new can.Mustache({
text: text
})
.render({
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 = new can.Mustache({
text: text
})
.render({
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",
result = new can.Mustache({
text: text
})
.render({});
equal(result, text)
})
test("multi line elements", function () {
var text = "<img\n class=\"{{myClass}}\" />",
result = new can.Mustache({
text: text
})
.render({
myClass: 'a'
});
ok(result.indexOf("<img\n class=\"a\"") !== -1, "Multi-line elements render correctly.");
// clear hookups b/c we are using .render;
can.view.hookups = {};
})
test("escapedContent", function () {
var text = "<span>{{ tags }}</span><label>&</label><strong>{{ number }}</strong><input value='{{ quotes }}'/>";
var compiled = new can.Mustache({
text: text
})
.render({
tags: "foo < bar < car > zar > poo",
quotes: "I use 'quote' fingers & &ersands \"a lot\"",
number: 123
});
var div = document.createElement('div');
div.innerHTML = compiled;
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(div.getElementsByTagName('label')[0].innerHTML, "&");
// clear hookups b/c we are using .render;
can.view.hookups = {};
})
test("unescapedContent", function () {
var text = "<span>{{{ tags }}}</span><div>{{{ tags }}}</div><input value='{{{ quotes }}}'/>";
var compiled = new can.Mustache({
text: text
})
.render({
tags: "<strong>foo</strong><strong>bar</strong>",
quotes: 'I use \'quote\' fingers "a lot"'
});
var div = document.createElement('div');
div.innerHTML = compiled;
equal(div.getElementsByTagName('span')[0].firstChild.nodeType, 1);
equal(div.getElementsByTagName('div')[0].innerHTML.toLowerCase(), "<strong>foo</strong><strong>bar</strong>");
equal(div.getElementsByTagName('span')[0].innerHTML.toLowerCase(), "<strong>foo</strong><strong>bar</strong>");
equal(div.getElementsByTagName('input')[0].value, "I use 'quote' fingers \"a lot\"", "escaped no matter what");
// clear hookups b/c we are using .render;
can.view.hookups = {};
});
/*
not really applicable...but could update to work oince complete
test("returning blocks", function(){
var somethingHelper = function(cb){
return cb([1,2,3,4])
}
var res = can.view.
render("//can/view/mustache/test_template.mustache",{
something: somethingHelper,
items: ['a','b']
});
// make sure expected values are in res
ok(/\s4\s/.test(res), "first block called" );
equal(res.match(/ItemsLength4/g).length, 4, "innerBlock and each")
}); */
test("easy hookup", function () {
var div = document.createElement('div');
div.appendChild(can.view(can.test.path("view/mustache/test/easyhookup.mustache"), {
text: "yes"
}))
ok(div.getElementsByTagName('div')[0].className.indexOf("yes") !== -1, "has yes")
});
test('multiple function hookups in a tag', function () {
var text = "<span {{(el)-> can.data(can.$(el),'foo','bar')}}" +
" {{(el)-> can.data(can.$(el),'baz','qux')}}>lorem ipsum</span>",
compiled = new can.Mustache({
text: text
})
.render(),
div = document.createElement('div');
div.appendChild(can.view.frag(compiled));
var span = div.getElementsByTagName('span')[0];
equal(can.data(can.$(span), 'foo'), 'bar', "first hookup");
equal(can.data(can.$(span), 'baz'), 'qux', "second hookup");
// clear hookups b/c we are using .render;
can.view.hookups = {};
})
/*
needs andy's helper logic
test("helpers", function() {
can.Mustache.Helpers.prototype.simpleHelper = function()
{
return 'Simple';
}
can.Mustache.Helpers.prototype.elementHelper = function()
{
return function(el) {
el.innerHTML = 'Simple';
}
}
var text = "<div>{{ simpleHelper() }}</div>";
var compiled = new can.Mustache({text: text}).render() ;
equal(compiled, "<div>Simple</div>");
text = "<div id=\"hookup\" {{ elementHelper() }}></div>";
compiled = new can.Mustache({text: text}).render() ;
can.append( can.$('#qunit-fixture'), can.view.frag(compiled));
equal(can.$('#hookup')[0].innerHTML, "Simple");
}); */
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 compiled = new can.Mustache({
text: text
})
.render({
task: task
});
var div = document.createElement('div');
div.appendChild(can.view.frag(compiled))
equal(div.getElementsByTagName('div')[0].innerHTML, "dishes", "html correctly dishes")
equal(div.getElementsByTagName('div')[0].className, "", "class empty")
task.attr('name', 'lawn')
equal(div.getElementsByTagName('div')[0].innerHTML, "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");
// clear hookups b/c we are using .render;
can.view.hookups = {};
});
test("event binding / triggering on options", function () {
var addEventListener = function (el, name, fn) {
if (el.addEventListener) {
el.addEventListener(name, fn, false);
} else {
el['on' + name] = fn;
}
};
var frag = can.buildFragment("<select><option>a</option></select>", [document]);
var qta = document.getElementById('qunit-fixture');
qta.innerHTML = "";
qta.appendChild(frag);
/*qta.addEventListener("foo", function(){
ok(false, "event handler called")
},false)*/
// destroyed events should not bubble
addEventListener(qta.getElementsByTagName("option")[0], "foo", function (ev) {
ok(true, "option called");
if (ev.stopPropagation) {
ev.stopPropagation();
}
//ev.cancelBubble = true;
});
addEventListener(qta.getElementsByTagName("select")[0], "foo", function () {
ok(true, "select called")
});
var ev;
if (document.createEvent) {
ev = document.createEvent("HTMLEvents");
} else {
ev = document.createEventObject("HTMLEvents");
}
if (ev.initEvent) {
ev.initEvent("foo", true, true);
} else {
ev.eventType = "foo";
}
if (qta.getElementsByTagName("option")[0].dispatchEvent) {
qta.getElementsByTagName("option")[0].dispatchEvent(ev);
} else {
qta.getElementsByTagName("option")[0].onfoo(ev);
}
can.trigger(qta, "foo")
stop();
setTimeout(function () {
start();
ok(true);
}, 100)
})
test("select live binding", function () {
var text = "<select>{{ #todos }}<option>{{ name }}</option>{{ /todos }}</select>";
var Todos, compiled, div;
Todos = new can.List([{
id: 1,
name: 'Dishes'
}]);
compiled = new can.Mustache({
text: text
})
.render({
todos: Todos
});
div = document.createElement('div');
div.appendChild(can.view.frag(compiled))
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');
// clear hookups b/c we are using .render;
can.view.hookups = {};
});
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 compiled = new can.Mustache({
text: text
})
.render({
obs: obs
})
var div = document.createElement('div');
div.appendChild(can.view.frag(compiled));
var innerDiv = div.childNodes[0];
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');
// clear hookups b/c we are using .render;
can.view.hookups = {};
});
test('adding and removing multiple html content within a single element', function () {
var text, obs, compiled;
text = '<div>{{ obs.a }}{{ obs.b }}{{ obs.c }}</div>';
obs = new can.Map({
a: 'a',
b: 'b',
c: 'c'
});
compiled = new can.Mustache({
text: text
})
.render({
obs: obs
});
var div = document.createElement('div');
div.appendChild(can.view.frag(compiled));
equal(div.childNodes[0].innerHTML, 'abc', 'initial render');
obs.attr({
a: '',
b: '',
c: ''
});
equal(div.childNodes[0].innerHTML, '', 'updated values');
obs.attr({
c: 'c'
});
equal(div.childNodes[0].innerHTML, 'c', 'updated values');
// clear hookups b/c we are using .render;
can.view.hookups = {};
});
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'
}),
compiled = new can.Mustache({
text: text
})
.render({
obs: obs
}),
div = document.createElement('div');
div.appendChild(can.view.frag(compiled));
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(span.innerHTML, '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(span.innerHTML, '', 'text node value is empty');
obs.attr('message', 'Warp drive, Mr. Sulu');
equal(span.innerHTML, 'Warp drive, Mr. Sulu', 'text node updated');
obs.removeAttr('show');
equal(div.innerHTML, '', '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(span.innerHTML, 'Warp drive, Mr. Sulu', 'value in block statement updated innerHTML');
// clear hookups b/c we are using .render;
can.view.hookups = {};
});
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 = new can.Mustache({
text: text
})
.render({
obs: obs
});
var div = document.createElement('div');
div.appendChild(can.view.frag(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');
// clear hookups b/c we are using .render;
can.view.hookups = {};
});
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 = new can.Mustache({
text: text
})
.render({
obs: obs
});
var div = document.createElement('div');
div.appendChild(can.view.frag(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');
// clear hookups b/c we are using .render;
can.view.hookups = {};
});
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 = new can.Mustache({
text: text
})
.render({
obs: obs
});
var div = document.createElement('div');
div.appendChild(can.view.frag(compiled));
equal(div.getElementsByTagName('div')[0].innerHTML, 'foo', 'Element as expected');
// clear hookups b/c we are using .render;
can.view.hookups = {};
})
test("hookup and live binding", function () {
var text = "<div class='{{ task.completed }}' {{ (el)-> can.data(can.$(el),'task',task) }}>" +
"{{ task.name }}" +
"</div>",
task = new can.Map({
completed: false,
className: 'someTask',
name: 'My Name'
}),
compiled = new can.Mustache({
text: text
})
.render({
task: task
}),
div = document.createElement('div');
div.appendChild(can.view.frag(compiled))
var child = div.getElementsByTagName('div')[0];
ok(child.className.indexOf("false") > -1, "is incomplete")
ok( !! can.data(can.$(child), 'task'), "has data")
equal(child.innerHTML, "My Name", "has name")
task.attr({
completed: true,
name: 'New Name'
});
ok(child.className.indexOf("true") !== -1, "is complete")
equal(child.innerHTML, "New Name", "has new name");
// clear hookups b/c we are using .render;
can.view.hookups = {};
})
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 = new can.Mustache({
text: text
})
.render({
obs: obs
});
var ul = document.createElement('ul');
ul.appendChild(can.view.frag(compiled));
equal(ul.getElementsByTagName('li')[0].innerHTML, 'No items', 'initial observable state');
obs.attr('items', [{
name: 'foo'
}]);
equal(ul.getElementsByTagName('li')[0].innerHTML, '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 = new can.Mustache({
text: text
})
.render({
completed: completed
});
var div = document.createElement('div');
div.appendChild(can.view.frag(compiled));
var child = div.getElementsByTagName('div')[0];
equal(child.innerHTML, "2", "at first there are 2 true bindings");
var item = new can.Map({
complete: true,
id: "THIS ONE"
})
l.push(item);
equal(child.innerHTML, "3", "now there are 3 complete");
item.attr('complete', false);
equal(child.innerHTML, "2", "now there are 2 complete");
l.pop();
item.attr('complete', true);
equal(child.innerHTML, "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 = new can.Mustache({
text: text
})
.render({
completed: completed
});
var div = document.createElement('div');
div.appendChild(can.view.frag(compiled));
var child = div.getElementsByTagName('div')[0];
equal(child.innerHTML, "2", "at first there are 2 true bindings");
var item = new can.Map({
complete: true
})
l.push(item);
equal(child.innerHTML, "3", "now there are 3 complete");
item.attr('complete', false);
equal(child.innerHTML, "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 = new can.Mustache({
text: text
})
.render({
completed: completed
});
var div = document.createElement('div');
div.appendChild(can.view.frag(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 = new can.Mustache({
text: text
})
.render({
completed: completed
});
var div = document.createElement('div');
div.appendChild(can.view.frag(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 = new can.Mustache({
text: text
})
.render({
obs: obs
});
var div = document.createElement('div');
div.appendChild(can.view.frag(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 = new can.Mustache({
text: text
})
.render({
obs: obs
});
var div = document.createElement('div');
div.appendChild(can.view.frag(compiled));
div = div.getElementsByTagName('div')[0];
equal(div.innerHTML, "Justin")
obs.attr('name.first', "Brian")
equal(div.innerHTML, "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 = new can.Mustache({
text: text
})
.render({
obs: obs
});
var div = document.createElement('div');
var html = can.view.frag(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 = document.createElement('div');
var template = can.view.mustache('<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 () {
can.view.mustache("list-test", "<ul>{{#data.items}}<li>{{name}}</li>{{/data.items}}</ul>");
var data = new can.Map({
items: [{
name: "Brian"
}, {
name: "Fara"
}]
});
var div = document.createElement('div');
div.appendChild(can.view("list-test", {
data: data
}));
data.items.push(new can.Map({
name: "Scott"
}))
ok(/Brian/.test(div.innerHTML), "added first name")
ok(/Fara/.test(div.innerHTML), "added 2nd name")
ok(/Scott/.test(div.innerHTML), "added name after push")
});
test("trailing text", function () {
can.view.mustache("count", "There are {{ length }} todos")
var div = document.createElement('div');
div.appendChild(can.view("count", new can.List([{}, {}])));
ok(/There are 2 todos/.test(div.innerHTML), "got all text")
})
test("recursive views", function () {
var data = new can.List([{
label: 'branch1',
children: [{
id: 2,
label: 'branch2'
}]
}])
var div = document.createElement('div');
div.appendChild(can.view(can.test.path('view/mustache/test/recursive.mustache'), {
items: data
}));
ok(/class="?leaf"?/.test(div.innerHTML), "we have a leaf")
})
test("live binding textarea", function () {
can.view.mustache("textarea-test", "<textarea>Before{{ obs.middle }}After</textarea>");
var obs = new can.Map({
middle: "yes"
}),
div = document.createElement('div');
div.appendChild(can.view("textarea-test", {
obs: obs
}))
var textarea = div.firstChild
equal(textarea.value, "BeforeyesAfter");
obs.attr("middle", "Middle")
equal(textarea.value, "BeforeMiddleAfter")
})
test("reading a property from a parent object when the current context is an observe", function () {
can.view.mustache("parent-object", "{{#foos}}<span>{{bar}}</span>{{/foos}}")
var data = {
foos: new can.List([{
name: "hi"
}, {
name: 'bye'
}]),
bar: "Hello World"
}
var div = document.createElement('div');
var res = can.view("parent-object", data);
div.appendChild(res);
var spans = div.getElementsByTagName('span');
equal(spans.length, 2, 'Got two <span> elements');
equal(spans[0].innerHTML, 'Hello World', 'First span Hello World');
equal(spans[1].innerHTML, 'Hello World', 'Second span Hello World');
})
test("helper parameters don't convert functions", function () {
can.Mustache.registerHelper('helperWithFn', function (fn) {
ok(can.isFunction(fn), 'Parameter is a function');
equal(fn(), 'Hit me!', 'Got the expected function');
});
var renderer = can.view.mustache('{{helperWithFn test}}');
renderer({
test: function () {
return 'Hit me!';
}
});
})
test("computes as helper parameters don't get converted", function () {
can.Mustache.registerHelper('computeTest', function (no) {
equal(no(), 5, 'Got computed calue');
ok(no.isComputed, 'no is still a compute')
});
var renderer = can.view.mustache('{{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}}"
};
can.view.mustache("count", "There are {{ length }} todos")
var div = document.createElement('div');
div.appendChild(can.view("count", new can.List([{}, {}])));
ok(/There are 2 todos/.test(div.innerHTML), "got all text")
var renderer, result, data, actual, span;
for (result in staches) {
renderer = can.view.mustache("compute_" + result, staches[result]);
data = ["e", "a", "c", "h"];
div = document.createElement("div");
actual = can.view("compute_" + result, {
test: can.compute(data)
});
div.appendChild(actual);
span = div.getElementsByTagName("span")[0];
if (span && span.firstChild) {
div.replaceChild(span.firstChild, span);
}
actual = div.innerHTML;
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.view.mustache("compute_" + result, inv_staches[result]);
data = null;
div = document.createElement("div");
actual = can.view("compute_" + result, {
test: can.compute(data)
});
div.appendChild(actual);
actual = div.innerHTML;
equal(actual, result, "can.compute resolved for helper " + result);
}
});
test("Rendering models in tables produces different results than an equivalent observe (#202)", 2, function () {
var renderer = can.view.mustache('<table>{{#stuff}}<tbody>{{#rows}}<tr></tr>{{/rows}}</tbody>{{/stuff}}</table>');
var div = document.createElement('div');
var dom = renderer({
stuff: new can.Map({
rows: [{
name: 'first'
}]
})
});
div.appendChild(dom);
var elements = div.getElementsByTagName('tbody');
equal(elements.length, 1, 'Only one <tbody> rendered');
div = document.createElement('div');
dom = renderer({
stuff: new can.Model({
rows: [{
name: 'first'
}]
})
});
div.appendChild(dom);
elements = div.getElementsByTagName('tbody');
equal(elements.length, 1, 'Only one <tbody> rendered');
})
//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 = new can.Mustache({
text: text
})
.render({
people: people
});
can.append(can.$('#qunit-fixture'), can.view.frag(compiled));
equal(can.$('#qunit-fixture table 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.view.mustache('<ul>{{#todos}}<li>{{.}}</li>{{/todos}}</ul><div>{{message}}</div>');
var div = document.createElement('div');
var data = new can.Map({
todos: ['Line #1', 'Line #2', 'Line #3'],
message: 'Hello',
count: 2
});
div.appendChild(renderer(data));
equal(div.getElementsByTagName('li')[1].innerHTML, 'Line #2', 'Check initial array');
equal(div.getElementsByTagName('div')[0].innerHTML, 'Hello', 'Check initial message');
data.attr('todos.1', 'Line #2 changed');
data.attr('message', 'Hello again');
equal(div.getElementsByTagName('li')[1].innerHTML, 'Line #2 changed', 'Check updated array');
equal(div.getElementsByTagName('div')[0].innerHTML, 'Hello again', 'Check updated message');
})
test("Observe list returned from the function", function () {
var renderer = can.view.mustache('<ul>{{#todos}}<li>{{.}}</li>{{/todos}}</ul>');
var div = document.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(div.getElementsByTagName('li')[0].innerHTML, 'Todo #1', 'Pushing to the list works');
});
// https://github.com/canjs/canjs/issues/228
test("Contexts within helpers not always resolved correctly", function () {
can.Mustache.registerHelper("bad_context", function (context, options) {
return "<span>" + this.text + "</span> should not be " + options.fn(context);
});
var renderer = can.view.mustache('{{#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 = document.createElement('div');
div.appendChild(renderer(data));
equal(div.getElementsByTagName('span')[0].innerHTML, "foo", 'Incorrect context passed to helper');
equal(div.getElementsByTagName('span')[1].innerHTML, "bar", 'Incorrect text in helper inner template');
equal(div.getElementsByTagName('span')[2].innerHTML, "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}}', ".mustache")
var renderer = can.view.mustache('{{#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 = document.createElement('div');
div.appendChild(renderer(data));
equal(div.getElementsByTagName('span')[0].innerHTML, "foo", 'Incorrect context passed to helper');
equal(div.getElementsByTagName('span')[1].innerHTML, "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 () {
can.Mustache.registerHelper("to_upper", function (fn, options) {
if (!fn.fn) {
return typeof fn === "function" ? fn()
.toString()
.toUpperCase() : fn.toString()
.toUpperCase();
} else {
//fn is options
return can.trim(fn.fn(this))
.toString()
.toUpperCase();
}
});
var renderer = can.view.mustache('"{{next_level.text}}" uppercased should be "<span>{{to_upper next_level.text}}</span>"<br/>"{{next_level.text}}" uppercased with a workaround