UNPKG

can

Version:

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

1,475 lines (1,463 loc) 54.9 kB
steal("can/model", "can/view/ejs", "can/test", "steal-qunit", function () { QUnit.module('can/view/ejs, rendering', { setup: function () { can.view.ext = '.ejs'; 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><% this.animals.each(function(animal){%>' + '<li><%= animal %></li>' + '<%});%></ul>'; this.squareBracketsNoThis = '<ul><% animals.each(function(animal){%>' + '<li><%= animal %></li>' + '<%});%></ul>'; this.angleBracketsNoThis = '<ul><% animals.each(function(animal){%>' + '<li><%= animal %></li>' + '<%});%></ul>'; } }); var getAttr = function (el, attrName) { return attrName === 'class' ? el.className : el.getAttribute(attrName); }; test('render with left bracket', function () { var compiled = new can.EJS({ 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.EJS({ 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.EJS({ 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 %>' + '<ul><% animals.each(function(animal){%>' + '<li><%= animal %></li>' + '<%});%></ul>'; var compiled = new can.EJS({ text: text }) .render({ animals: this.animals }); equal(compiled, '<% replace_me %><ul><li>sloth</li><li>bear</li><li>monkey</li></ul>', 'works'); }); test('comments', function () { var text = '<%# replace_me %>' + '<ul><% animals.each(function(animal){%>' + '<li><%= animal %></li>' + '<%});%></ul>'; var compiled = new can.EJS({ 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.EJS({ text: text }) .render({}); equal(result, text); }); test('multi line elements', function () { var text = '<img\n class="<%=myClass%>" />', result = new can.EJS({ 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>&amp;</label><strong><%= number %></strong><input value=\'<%= quotes %>\'/>'; var compiled = new can.EJS({ text: text }) .render({ tags: 'foo < bar < car > zar > poo', quotes: 'I use \'quote\' fingers "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 "a lot"'); equal(div.getElementsByTagName('label')[0].innerHTML, '&amp;'); // 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.EJS({ 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].firstChild.nodeValue.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"', 'escapped no matter what'); // clear hookups b/c we are using .render; can.view.hookups = {}; }); test('returning blocks', function () { var somethingHelper = function (cb) { return cb([ 1, 2, 3, 4 ]); }; var res = can.view.render(can.test.path('view/ejs/test/test_template.ejs'), { 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/ejs/test/easyhookup.ejs'), { 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.EJS({ 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'); }); test('helpers', function () { can.EJS.Helpers.prototype.simpleHelper = function () { return 'Simple'; }; can.EJS.Helpers.prototype.elementHelper = function () { return function (el) { el.innerHTML = 'Simple'; }; }; var text = '<div><%= simpleHelper() %></div>'; var compiled = new can.EJS({ text: text }) .render(); equal(compiled, '<div>Simple</div>'); text = '<div id="hookup" <%= elementHelper() %>></div>'; compiled = new can.EJS({ text: text }) .render(); can.append(can.$('#qunit-fixture'), can.view.frag(compiled)); equal(can.$('#hookup')[0].innerHTML, 'Simple'); }); test('list helper', function () { var text = '<% list(todos, function(todo){ %><div><%= todo.name %></div><% }) %>'; var todos = new can.List([{ id: 1, name: 'Dishes' }]), compiled = new can.EJS({ text: text }) .render({ todos: todos }), div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); equal(div.getElementsByTagName('div') .length, 1, '1 item in list'); todos.push({ id: 2, name: 'Laundry' }); equal(div.getElementsByTagName('div') .length, 2, '2 items in list'); todos.splice(0, 2); equal(div.getElementsByTagName('div') .length, 0, '0 items in list'); todos.push({ id: 4, name: 'Pick up sticks' }); equal(div.getElementsByTagName('div') .length, 1, '1 item in list again'); }); test('attribute single unescaped, html single unescaped', function () { var text = '<div id=\'me\' class=\'<%== task.attr(\'completed\') ? \'complete\' : \'\'%>\'><%== task.attr(\'name\') %></div>'; var task = new can.Map({ name: 'dishes' }); var compiled = new can.EJS({ 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'); }); test('event binding / triggering on things other than options', 1, function () { var frag = can.buildFragment('<ul><li>a</li></ul>', [document]); var qta = document.getElementById('qunit-fixture'); qta.innerHTML = ''; qta.appendChild(frag); // destroyed events should not bubble can.bind.call(qta.getElementsByTagName('li')[0], 'foo', function (event) { ok(true, 'li called :)'); }); can.bind.call(qta.getElementsByTagName('ul')[0], 'foo', function (event) { ok(false, 'ul called :('); }); can.trigger(qta.getElementsByTagName('li')[0], 'foo', {}, false); qta.removeChild(qta.firstChild); }); test('select live binding', function () { var text = '<select><% todos.each(function(todo){ %><option><%= todo.name %></option><% }) %></select>', Todos = new can.List([{ id: 1, name: 'Dishes' }]), compiled = new can.EJS({ 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'); }); test('block live binding', function () { var text = '<div><% if( obs.attr(\'sex\') == \'male\' ){ %>' + '<span>Mr.</span>' + '<% } else { %>' + '<label>Ms.</label>' + '<% } %>' + '</div>'; var obs = new can.Map({ sex: 'male' }); var compiled = new can.EJS({ text: text }) .render({ obs: obs }); var div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); // We have to test using nodeName and innerHTML (and not outerHTML) because IE 8 and under treats // user-defined properties on nodes as attributes. equal(div.getElementsByTagName('div')[0].firstChild.nodeName.toUpperCase(), 'SPAN', 'initial span tag'); equal(div.getElementsByTagName('div')[0].firstChild.innerHTML, 'Mr.', 'initial span content'); obs.attr('sex', 'female'); equal(div.getElementsByTagName('div')[0].firstChild.nodeName.toUpperCase(), 'LABEL', 'updated label tag'); equal(div.getElementsByTagName('div')[0].firstChild.innerHTML, 'Ms.', 'updated label content'); }); test('hookups in tables', function () { var text = '<table><tbody><% if( obs.attr(\'sex\') == \'male\' ){ %>' + '<tr><td>Mr.</td></tr>' + '<% } else { %>' + '<tr><td>Ms.</td></tr>' + '<% } %>' + '</tbody></table>'; var obs = new can.Map({ sex: 'male' }); var compiled = new can.EJS({ text: text }) .render({ obs: obs }); var div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); // We have to test using nodeName and innerHTML (and not outerHTML) because IE 8 and under treats // user-defined properties on nodes as attributes. equal(div.getElementsByTagName('tbody')[0].firstChild.firstChild.nodeName, 'TD', 'initial tag'); equal(div.getElementsByTagName('tbody')[0].firstChild.firstChild.innerHTML.replace(/(\r|\n)+/g, ''), 'Mr.', 'initial content'); obs.attr('sex', 'female'); equal(div.getElementsByTagName('tbody')[0].firstChild.firstChild.nodeName, 'TD', 'updated tag'); equal(div.getElementsByTagName('tbody')[0].firstChild.firstChild.innerHTML.replace(/(\r|\n)+/g, ''), 'Ms.', 'updated content'); }); //Issue 233 test('multiple tbodies in table hookup', function () { var text = '<table>' + '<% can.each(people, function(person){ %>' + '<tbody><tr><td><%= person.name %></td></tr></tbody>' + '<% }) %>' + '</table>', people = new can.List([{ name: 'Steve' }, { name: 'Doug' }]), compiled = new can.EJS({ text: text }) .render({ people: people }), div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); equal(div.getElementsByTagName('tbody') .length, 2, 'two tbodies'); }); test('multiple hookups in a single attribute', function () { var text = '<div class=\'<%= obs.attr("foo") %>a<%= obs.attr("bar") %>b<%= obs.attr("baz") %>\'></div>', obs = new can.Map({ foo: '1', bar: '2', baz: '3' }), compiled = new can.EJS({ 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'), '1a2b3', 'initial render'); obs.attr('bar', '4'); equal(getAttr(innerDiv, 'class'), '1a4b3', 'initial render'); obs.attr('bar', '5'); equal(getAttr(innerDiv, 'class'), '1a5b3', 'initial render'); }); test('adding and removing multiple html content within a single element', function () { var text = '<div><%== obs.attr("a") %><%== obs.attr("b") %><%== obs.attr("c") %></div>', obs = new can.Map({ a: 'a', b: 'b', c: 'c' }), compiled = new can.EJS({ text: text }) .render({ obs: obs }); var div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); equal(div.firstChild.nodeName.toUpperCase(), 'DIV', 'initial render node name'); equal(div.firstChild.innerHTML, 'abc', 'initial render text'); obs.attr({ a: '', b: '', c: '' }); equal(div.firstChild.nodeName.toUpperCase(), 'DIV', 'updated render node name'); equal(div.firstChild.innerHTML, '', 'updated render text'); obs.attr({ c: 'c' }); equal(div.firstChild.nodeName.toUpperCase(), 'DIV', 'updated render node name'); equal(div.firstChild.innerHTML, 'c', 'updated render text'); }); test('live binding and removeAttr', function () { var text = '<% if(obs.attr("show")) { %>' + '<p <%== obs.attr("attributes") %> class="<%= obs.attr("className")%>"><span><%= obs.attr("message") %></span></p>' + '<% } %>', obs = new can.Map({ show: true, className: 'myMessage', attributes: 'some="myText"', message: 'Live long and prosper' }), compiled = new can.EJS({ 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'); }); test('hookup within a tag', function () { var text = '<div <%== obs.attr("foo") %> ' + '<%== obs.attr("baz") %>>lorem ipsum</div>', obs = new can.Map({ foo: 'class="a"', baz: 'some=\'property\'' }), compiled = new can.EJS({ 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'); }); test('single escaped tag, removeAttr', function () { var text = '<div <%= obs.attr("foo") %>>lorem ipsum</div>', obs = new can.Map({ foo: 'data-bar="john doe\'s bar"' }), compiled = new can.EJS({ 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'); }); test('html comments', function () { var text = '<!-- bind to changes in the todo list --> <div><%= obs.attr("foo") %></div>', obs = new can.Map({ foo: 'foo' }), compiled = new can.EJS({ 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'); }); test('hookup and live binding', function () { var text = '<div class=\'<%= task.attr(\'completed\') ? \'complete\' : \'\' %>\' <%= (el)-> can.data(can.$(el),\'task\',task) %>>' + '<%== task.attr(\'name\') %>' + '</div>', task = new can.Map({ completed: false, className: 'someTask', name: 'My Name' }), compiled = new can.EJS({ 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('complete') === -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('complete') !== -1, 'is complete'); equal(child.innerHTML, 'New Name', 'has new name'); }); /* test('multiple curly braces in a block', function() { var text = '<% if(!obs.attr("items").length) { %>' + '<li>No items</li>' + '<% } else { each(obs.items, function(item) { %>' + '<li><%= item.attr("name") %></li>' + '<% }) }%>', obs = new can.Map({ items: [] }), compiled = new can.EJS({ text: text }).render({ obs: obs }); var ul = document.createElement('ul'); ul.appendChild(can.view.frag(compiled)); equal(ul.innerHTML, '<li>No items</li>', 'initial observable state'); obs.attr('items', [{ name: 'foo' }]); equal(u.innerHTML, '<li>foo</li>', '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.EJS({ 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.EJS({ 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.EJS({ 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.EJS({ 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.attr(\'val\') %>></div>'; var obs = new can.Map({ val: 'foo="bar"' }); var compiled = new can.EJS({ 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'); }); test('parent is right with bock', function () { var text = '<ul><% if(!obs.attr("items").length) { %>' + '<li>No items</li>' + '<% } else { %> <%== obs.attr("content") %>' + '<% } %></ul>', obs = new can.Map({ content: '<li>Hello</li>', items: [{ name: 'Justin' }] }), compiled = new can.EJS({ text: text }) .render({ obs: obs }); var div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); var ul = div.getElementsByTagName('ul')[0]; var li = div.getElementsByTagName('li')[0]; ok(ul, 'we have a ul'); ok(li, 'we have a li'); }); test('property name only attributes', function () { var text = '<input type=\'checkbox\' <%== obs.attr(\'val\') ? \'checked\' : \'\' %>/>'; var obs = new can.Map({ val: true }); var compiled = new can.EJS({ text: text }) .render({ obs: obs }); var div = document.getElementById('qunit-fixture'); div.appendChild(can.view.frag(compiled)); var input = div.getElementsByTagName('input')[0]; can.trigger(input, 'click'); obs.attr('val', false); ok(!input.checked, 'not checked'); obs.attr('val', true); ok(input.checked, 'checked'); div.removeChild(input); }); test('nested properties', function () { var text = '<div><%= obs.attr(\'name.first\')%></div>'; var obs = new can.Map({ name: { first: 'Justin' } }); var compiled = new can.EJS({ 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></td><%== obs.attr(\'content\') %></tr></div>'; var obs = new can.Map({ content: '<td>Justin</td>' }); var compiled = new can.EJS({ 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'); div.appendChild(can.view(can.test.path('view/ejs/test/nested_live_bindings.ejs'), { items: items })); items.push({ title: 1, is_done: false, id: 1 }); // this will throw an error unless EJS protects against // nested objects items[0].attr('is_done', true); }); // Similar to the nested live bindings test, this makes sure templates with control blocks // will eventually remove themselves if at least one change happens // before things are removed. // It is currently commented out because // /*test("memory safe without parentElement of blocks", function(){ })*/ test('trailing text', function () { can.view.ejs('count', 'There are <%= this.attr(\'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/ejs/test/recursive.ejs'), { items: data })); ok(/class="leaf"|class=leaf/.test(div.innerHTML), 'we have a leaf'); }); test('indirectly recursive views', function () { var unordered = new can.List([{ ol: [{ ul: [{ ol: [ 1, 2, 3 ] }] }] }]); can.view.cache = false; var div = document.createElement('div'); div.appendChild(can.view(can.test.path('view/ejs/test/indirect1.ejs'), { unordered: unordered })); document.getElementById('qunit-fixture') .appendChild(div); var el = can.$('#qunit-fixture ul > li > ol > li > ul > li > ol > li')[0]; ok( !! el && can.trim(el.innerHTML) === '1', 'Uncached indirectly recursive EJS working.'); can.view.cache = true; div.appendChild(can.view(can.test.path('view/ejs/test/indirect1.ejs'), { unordered: unordered })); el = can.$('#qunit-fixture ul + ul > li > ol > li > ul > li > ol > li')[0]; ok( !! el && can.trim(el.innerHTML) === '1', 'Cached indirectly recursive EJS working.'); document.getElementById('qunit-fixture') .removeChild(div); }); test('recursive views of previously stolen files shouldn\'t fail', function () { // Using preload to bypass steal dependency (necessary for "grunt test") can.view.preloadStringRenderer('view_ejs_test_indirect1_ejs', can.EJS({ text: '<ul>' + '<% unordered.each(function(item) { %>' + '<li>' + '<% if(item.ol) { %>' + '<%== can.view.render(can.test.path(\'view/ejs/test/indirect2.ejs\'), { ordered: item.ol }) %>' + '<% } else { %>' + '<%= item.toString() %>' + '<% } %>' + '</li>' + '<% }) %>' + '</ul>' })); can.view.preloadStringRenderer('view_ejs_test_indirect2_ejs', can.EJS({ text: '<ol>' + '<% ordered.each(function(item) { %>' + '<li>' + '<% if(item.ul) { %>' + '<%== can.view.render(can.test.path(\'view/ejs/test/indirect1.ejs\'), { unordered: item.ul }) %>' + '<% } else { %>' + '<%= item.toString() %>' + '<% } %>' + '</li>' + '<% }) %>' + '</ol>' })); var unordered = new can.Map.List([{ ol: [{ ul: [{ ol: [ 1, 2, 3 ] }] }] }]); can.view.cache = false; var div = document.createElement('div'); div.appendChild(can.view(can.test.path('view/ejs/test/indirect1.ejs'), { unordered: unordered })); document.getElementById('qunit-fixture') .appendChild(div); var el = can.$('#qunit-fixture ul > li > ol > li > ul > li > ol > li')[0]; ok( !! el && can.trim(el.innerHTML) === '1', 'Uncached indirectly recursive EJS working.'); can.view.cache = true; div.appendChild(can.view(can.test.path('view/ejs/test/indirect1.ejs'), { unordered: unordered })); el = can.$('#qunit-fixture ul + ul > li > ol > li > ul > li > ol > li')[0]; ok( !! el && can.trim(el.innerHTML) === '1', 'Cached indirectly recursive EJS working.'); document.getElementById('qunit-fixture') .removeChild(div); }); test('live binding select', function () { var text = '<select><% items.each(function(ob) { %>' + '<option value=\'<%= ob.attr(\'id\') %>\'><%= ob.attr(\'title\') %></option>' + '<% }); %></select>', items = new can.List([{ title: 'Make bugs', is_done: true, id: 0 }, { title: 'Find bugs', is_done: false, id: 1 }, { title: 'Fix bugs', is_done: false, id: 2 }]), compiled = new can.EJS({ text: text }) .render({ items: items }), div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); equal(div.getElementsByTagName('option') .length, 3, '3 items in list'); var option = div.getElementsByTagName('option')[0]; equal(option.value, '' + items[0].id, 'value attr set'); equal(option.textContent || option.text, items[0].title, 'content of option'); items.push({ id: 3, name: 'Go to pub' }); equal(div.getElementsByTagName('option') .length, 4, '4 items in list'); }); test('live binding textarea', function () { can.view.ejs('textarea-test', '<textarea>Before<%= obs.attr(\'middle\') %>After</textarea>'); var obs = new can.Map({ middle: 'yes' }), div = document.createElement('div'); var node = can.view('textarea-test', { obs: obs }); div.appendChild(node); var textarea = div.firstChild; equal(textarea.value, 'BeforeyesAfter'); obs.attr('middle', 'Middle'); equal(textarea.value, 'BeforeMiddleAfter'); }); test('reset on a live bound input', function () { var text = '<input type=\'text\' value=\'<%= person.attr(\'name\') %>\'><button type=\'reset\'>Reset</button>', person = new can.Map({ name: 'Bob' }), compiled = new can.EJS({ text: text }) .render({ person: person }), form = document.createElement('form'), input; form.appendChild(can.view.frag(compiled)); input = form.getElementsByTagName('input')[0]; form.reset(); equal(input.value, 'Bob', 'value is correct'); }); test('A non-escaping live magic tag within a control structure and no leaks', function () { var nodeLists = can.view.nodeLists; for (var prop in nodeLists.nodeMap) { delete nodeLists.nodeMap[prop]; } var text = '<div><% items.each(function(ob) { %>' + '<%== ob.attr(\'html\') %>' + '<% }); %></div>', items = new can.List([{ html: '<label>Hello World</label>' }]), compiled = new can.EJS({ text: text }) .render({ items: items }), div = can.$('#qunit-fixture')[0]; div.innerHTML = ''; can.append(can.$('#qunit-fixture'), can.view.frag(compiled)); ok(div.getElementsByTagName('label')[0], 'label exists'); items[0].attr('html', '<p>hi</p>'); equal(div.getElementsByTagName('label') .length, 0, 'label is removed'); equal(div.getElementsByTagName('p') .length, 1, 'label is replaced by p'); items.push({ html: '<p>hola</p>' }); equal(div.getElementsByTagName('p') .length, 2, 'label has 2 paragraphs'); can.remove(can.$(div.firstChild)); deepEqual(nodeLists.nodeMap, {}); }); test('attribute unquoting', function () { var text = '<input type="radio" ' + '<%== facet.single ? \'name="facet-\' + facet.attr("id") + \'"\' : "" %> ' + 'value="<%= facet.single ? "facet-" + facet.attr("id") : "" %>" />', facet = new can.Map({ id: 1, single: true }), compiled = new can.EJS({ text: text }) .render({ facet: facet }), div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); equal(div.children[0].name, 'facet-1'); equal(div.children[0].value, 'facet-1'); }); test('empty element hooks work correctly', function () { var text = '<div <%= function(e){ e.innerHTML = "1 Will show"; } %>></div>' + '<div <%= function(e){ e.innerHTML = "2 Will not show"; } %>></div>' + '3 Will not show'; var compiled = new can.EJS({ text: text }) .render(), div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); equal(div.childNodes.length, 3, 'all three elements present'); }); test('live binding with parent dependent tags but without parent tag present in template', function () { var text = [ '<tbody>', '<% if( person.attr("first") ){ %>', '<tr><td><%= person.first %></td></tr>', '<% }%>', '<% if( person.attr("last") ){ %>', '<tr><td><%= person.last %></td></tr>', '<% } %>', '</tbody>' ]; var person = new can.Map({ first: 'Austin', last: 'McDaniel' }); var compiled = new can.EJS({ text: text.join('\n') }) .render({ person: person }); var table = document.createElement('table'); table.appendChild(can.view.frag(compiled)); equal(table.getElementsByTagName('tr')[0].firstChild.nodeName.toUpperCase(), 'TD'); equal(table.getElementsByTagName('tr')[0].firstChild.innerHTML, 'Austin'); equal(table.getElementsByTagName('tr')[1].firstChild.nodeName.toUpperCase(), 'TD'); equal(table.getElementsByTagName('tr')[1].firstChild.innerHTML, 'McDaniel'); person.removeAttr('first'); equal(table.getElementsByTagName('tr')[0].firstChild.nodeName.toUpperCase(), 'TD'); equal(table.getElementsByTagName('tr')[0].firstChild.innerHTML, 'McDaniel'); person.removeAttr('last'); equal(table.getElementsByTagName('tr') .length, 0); person.attr('first', 'Justin'); equal(table.getElementsByTagName('tr')[0].firstChild.nodeName.toUpperCase(), 'TD'); equal(table.getElementsByTagName('tr')[0].firstChild.innerHTML, 'Justin'); }); test('spaces between attribute name and value', function () { var text = '<input type="text" value = "<%= test %>" />', compiled = new can.EJS({ text: text }) .render({ test: 'testing' }), div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); var input = div.getElementsByTagName('input')[0]; equal(input.value, 'testing'); equal(input.type, 'text'); }); test('live binding with computes', function () { var text = '<span><%= compute() %></span>', compute = can.compute(5), compiled = new can.EJS({ text: text }) .render({ compute: compute }), div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); var span = div.getElementsByTagName('span'); equal(span.length, 1); span = span[0]; equal(span.innerHTML, '5'); compute(6); equal(span.innerHTML, '6'); compute('Justin'); equal(span.innerHTML, 'Justin'); compute(true); equal(span.innerHTML, 'true'); }); test('testing for clean tables', function () { var games = new can.List(); games.push({ name: 'The Legend of Zelda', rating: 10 }); games.push({ name: 'The Adventures of Link', rating: 9 }); games.push({ name: 'Dragon Warrior', rating: 9 }); games.push({ name: 'A Dude Named Daffl', rating: 8.5 }); var res = can.view.render(can.test.path('view/ejs/test/table_test.ejs'), { games: games }), div = document.createElement('div'); div.appendChild(can.view.frag(res)); ok(!/@@!!@@/.test(div.innerHTML), 'no placeholders'); }); test('inserting live-binding partials assume the correct parent tag', function () { can.view.ejs('rowView', '<% can.each(columns, function(col) { %>' + '<th><%= col.attr("name") %></th>' + '<% }) %>'); can.view.ejs('tableView', '<table><tbody><tr>' + '<%== can.view.render("rowView", this) %>' + '</tr></tbody></table>'); var data = { columns: new can.List([{ name: 'Test 1' }, { name: 'Test 2' }]) }; var div = document.createElement('div'); var dom = can.view('tableView', data); div.appendChild(dom); var ths = div.getElementsByTagName('th'); equal(ths.length, 2, 'Got two table headings'); equal(ths[0].innerHTML, 'Test 1', 'First column heading correct'); equal(ths[1].innerHTML, 'Test 2', 'Second column heading correct'); equal(can.view.render('tableView', data) .indexOf('<table><tbody><tr><td data-view-id='), 0, 'Rendered output starts' + 'as expected'); // clear hookups b/c we are using .render; can.view.hookups = {}; }); // 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 () { can.view.ejs('observe-array', '<ul><% can.each(todos, function(todo, i) { %><li><%= todos.attr(""+i) %></li><% }) %></ul><div><%= this.attr("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(can.view('observe-array', 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('hookup this correctly', function () { var obj = { from: 'cows' }; var html = '<span <%== (el) -> can.data(can.$(el), \'foo\', this.from) %>>tea</span>'; var compiled = new can.EJS({ text: html }) .render(obj); var div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); var span = div.getElementsByTagName('span')[0]; equal(can.data(can.$(span), 'foo'), obj.from, 'object matches'); }); //Issue 271 test('live binding with html comment', function () { var text = '<table><tr><th>Todo</th></tr><!-- do not bother with me -->' + '<% todos.each(function(todo){ %><tr><td><%= todo.name %></td></tr><% }) %></table>', Todos = new can.List([{ id: 1, name: 'Dishes' }]), compiled = new can.EJS({ text: text }) .render({ todos: Todos }), div = document.createElement('div'); div.appendChild(can.view.frag(compiled)); equal(div.getElementsByTagName('table')[0].getElementsByTagName('td') .length, 1, '1 item in list'); Todos.push({ id: 2, name: 'Laundry' }); equal(div.getElementsByTagName('table')[0].getElementsByTagName('td') .length, 2, '2 items in list'); Todos.splice(0, 2); equal(div.getElementsByTagName('table')[0].getElementsByTagName('td') .length, 0, '0 items in list'); }); test('HTML comment with element callback', function () { var text = [ '<ul>', '<% todos.each(function(todo) { %>', '<li<%= (el) -> can.data(can.$(el),\'todo\',todo) %>>', '<!-- html comment #1 -->', '<%= todo.name %>', '<!-- html comment #2 -->', '</li>', '<% }) %>', '</ul>' ], Todos = new can.List([{ id: 1, name: 'Dishes' }]), compiled = new can.EJS({ text: text.join('\n') }) .render({ todos: Todos }), div = document.createElement('div'), li, comments; comments = function (el) { var count = 0; for (var i = 0; i < el.childNodes.length; i++) { if (el.childNodes[i].nodeType === 8) { ++count; } } return count; }; div.appendChild(can.view.frag(compiled)); li = div.getElementsByTagName('ul')[0].getElementsByTagName('li'); equal(li.length, 1, '1 item in list'); equal(comments(li[0]), 2, '2 comments in item #1'); Todos.push({ id: 2, name: 'Laundry' }); equal(li.length, 2, '2 items in list'); equal(comments(li[0]), 2, '2 comments in item #1'); equal(comments(li[1]), 2, '2 comments in item #2'); Todos.splice(0, 2); equal(li.length, 0, '0 items in list'); }); // 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 () { can.view.ejs('issue-153-no-dom', '<% can.each(todos, function(todo) { %><span><%= todo.attr("name") %></span><% }) %>'); can.view.ejs('issue-153-dom', '<% can.each(todos, function(todo) { %><%= todo.attr("name") %><% }) %>'); var todos = [ new can.Map({ id: 1, name: 'Dishes' }), new can.Map({ id: 2, name: 'Forks' }) ], data = { todos: new can.List(todos) }, arr = { todos: todos }, div = document.createElement('div'); div.appendChild(can.view('issue-153-no-dom', arr)); equal(div.getElementsByTagName('span')[0].innerHTML, 'Dishes', 'Array item rendered with DOM container'); equal(div.getElementsByTagName('span')[1].innerHTML, 'Forks', 'Array item rendered with DOM container'); div = document.createElement('div'); div.appendChild(can.view('issue-153-no-dom', data)); equal(div.getElementsByTagName('span')[0].innerHTML, 'Dishes', 'List item rendered with DOM container'); equal(div.getElementsByTagName('span')[1].innerHTML, 'Forks', 'List item rendered with DOM container'); div = document.createElement('div'); div.appendChild(can.view('issue-153-dom', arr)); equal(div.innerHTML, 'DishesForks', 'Array item rendered without DOM container'); div = document.createElement('div'); div.appendChild(can.view('issue-153-dom', data)); equal(div.innerHTML, 'DishesForks', 'List item rendered without DOM container'); data.todos.push(new can.Map({ id: 3, name: 'Knives' })); equal(div.innerHTML, 'DishesForksKnives', 'New list item rendered without DOM container'); }); test('correctness of data-view-id and only in tag opening', function () { var text = [ '<textarea><select><% can.each(this.items, function(item) { %>', '<option<%= (el) -> el.data(\'item\', item) %>><%= item.title %></option>', '<% }) %></select></textarea>' ], items = [{ id: 1, title: 'One' }, { id: 2, title: 'Two' }], compiled = new can.EJS({ text: text.join('') }) .render({ items: items }), expected = '^<textarea data-view-id=\'[0-9]+\'><select><option data-view-id=\'[0-9]+\'>One</option>' + '<option data-view-id=\'[0-9]+\'>Two</option></select></textarea>$'; ok(compiled.search(expected) === 0, 'Rendered output is as expected'); // clear hookups b/c we are using .render; can.view.hookups = {}; }); test('return blocks within element tags', function () { var animals = new can.List([ 'sloth', 'bear' ]), template = '<ul>' + '<%==lister(animals, function(animal){%>' + '<li><%=animal %></li>' + '<%})%>' + '</ul>'; var renderer = can.view.ejs(template); var div = document.createElement('div'); var frag = renderer({ lister: function (items, callback) { return function (el) { equal(el.nodeName.toLowerCase(), 'li', 'got the LI it created'); }; }, animals: animals }); div.appendChild(frag); }); test('Each does not redraw items', function () { var animals = new can.List([ 'sloth', 'bear' ]), template = '<div>my<b>favorite</b>animals:' + '<%==each(animals, function(animal){%>' + '<label>Animal=</label> <span><%=animal %></span>' + '<%})%>' + '!</div>'; var renderer = can.view.ejs(template); var div = document.createElement('div'); var frag = renderer({ animals: animals }); div.appendChild(frag); div.getElementsByTagName('label')[0].myexpando = 'EXPANDO-ED'; equal(div.getElementsByTagName('label') .length, 2, 'There are 2 labels'); animals.push('turtle'); equal(div.getElementsByTagName('label')[0].myexpando, 'EXPANDO-ED', 'same expando'); equal(div.getElementsByTagName('span')[2].innerHTML, 'turtle', 'turtle added'); }); test('Each works with no elements', function () { var animals = new can.List([ 'sloth', 'bear' ]), template = '<%==each(animals, function(animal){%>' + '<%=animal %> ' + '<%})%>'; var renderer = can.view.ejs(template); var div = document.createElement('div'); var frag = renderer({ animals: animals }); div.appendChild(frag); animals.push('turtle'); equal(div.innerHTML, 'sloth bear turtle ', 'turtle added'); }); test('Each does not redraw items (normal array)', function () { var animals = [ 'sloth', 'bear', 'turtle' ], template = '<div>my<b>favorite</b>animals:' + '<%each(animals, function(animal){%>' + '<label>Animal=</label> <span><%=animal %></span>' + '<%})%>' + '!</div>'; var renderer = can.view.ejs(template); var div = document.createElement('div'); var frag = renderer({ animals: animals }); div.appendChild(frag); div.getElementsByTagName('label')[0].myexpando = 'EXPANDO-ED'; //animals.push("dog") equal(div.getElementsByTagName('label') .length, 3, 'There are 2 labels'); equal(div.getElementsByTagName('label')[0].myexpando, 'EXPANDO-ED', 'same expando'); equal(div.getElementsByTagName('label')[0].myexpando, 'EXPANDO-ED', 'same expando'); equal(div.getElementsByTagName('span')[2].innerHTML, 'turtle', 'turtle added'); }); test('list works within another branch', function () { var animals = new can.List([]), template = '<div>Animals:' + '<% if( animals.attr(\'length\') ){ %>~' + '<% animals.each(function(animal){%>' + '<span><%=animal %></span>' + '<%})%>' + '<% } else { %>' + 'No animals' + '<% } %>' + '!</div>'; var renderer = can.view.ejs(template); var div = document.createElement('div'); // $("#qunit-fixture").html(div); var frag = renderer({ animals: animals }); div.appendChild(frag); equal(div.getElementsByTagName('div')[0].innerHTML, 'Animals:No animals!'); animals.push('sloth'); equal(div.getElementsByTagName('span') .length, 1, 'There is 1 sloth'); animals.pop(); equal(div.getElementsByTagName('div')[0].innerHTML, 'Animals:No animals!'); }); test('each works within another branch', function () { var animals = new can.List([]), template = '<div>Animals:' + '<% if( animals.attr(\'length\') ){ %>~' + '<%==each(animals, function(animal){%>' + '<span><%=animal %></span>' + '<%})%>' + '<% } else { %>' + 'No animals' + '<% } %>' + '!</div>'; var renderer = can.view.ejs(template); var div = document.createElement('div'); var frag = renderer({ animals: animals }); div.appendChild(frag); equal(div.getElementsByTagName('div')[0].innerHTML, 'Animals:No animals!'); animals.push('sloth'); equal(div.getElementsByTagName('span') .length, 1, 'There is 1 sloth'); animals.pop(); equal(div.getElementsByTagName('div')[0].innerHTML, 'Animals:No animals!'); }); test('JS blocks within EJS tags shouldn\'t require isolation', function () { var isolatedBlocks = can.view.ejs('<% if (true) { %>' + '<% if (true) {%>' + 'hi' + '<% } %>' + '<% } %>'), sharedBlocks = can.view.ejs('<% if (true) { %>' + '<% if (true) { %>' + 'hi' + '<% }' + '} %>'), complexIsolatedBlocks = can.view.ejs('<% if (true) { %><% if (1) { %>' + '<% if ({ dumb: \'literal\' }) { %>' + '<% list(items, function(item) { %>' + '<%== item %>' + '<%== something(function(items){ %><%== items.length %><% }) %>' + '<% }) %>' + '<% } %>' + '<% } %><% } %>'), complexSharedBlocks = can.view.ejs('<% if (true) { if (1) { %>' + '<% if ({ dumb: \'literal\' }) { %>' + '<% list(items, function(item) { %>' + '<%== item %>' + '<%== something(function(items){ %><%== items.length %><% }) %>' + '<% }) %>' + '<% }' + '} } %>'), iteratedSharedBlocks = can.view.ejs('<% for (var i = 0; i < items.length; i++) { %>' + '<% if (this.items) { if (1) { %>' + 'hi' + '<% } } else { %>' + 'nope' + '<% } } %>'), iteratedString = can.view.ejs('<% for(var i = 0; i < items.length; i++) { %>' + '\t<% if(this.mode !== "RESULTS") {' + '\t\tif(items[i] !== "SOME_FAKE_VALUE") { %>' + '\t\t\thi' + '\t\t<% }' + '\t} else { %>' + '\t\tnope' + '\t<% }' + '} %>'), iteratedStringNewLines = can.view.ejs('<% for(var i = 0; i < items.length; i++) { %>' + '\t<% if(this.mode !== "RESULTS") {\n' + '\t\tif(items[i] !== "SOME_FAKE_VALUE") { %>' + '\t\t\thi' + '\t\t<% }\n' + '\t} else { %>' + '\t\tnope' + '\t<% }\n' + '} %>'), data = { items: [ 'one', 'two', 'three' ], mode: 'SOMETHING', something: function (cb) { return cb([ 1, 2, 3, 4