can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
1,925 lines (1,529 loc) • 50.3 kB
JavaScript
steal("can/util/vdom/document", "can/util/vdom/build_fragment","can", "can/map/define", "can/component", "can/view/stache" ,"can/route", "steal-qunit", "can/component/component_bindings_test.js",function () {
var simpleDocument = can.simpleDocument;
var innerHTML = function(node){
if("innerHTML" in node) {
return node.innerHTML;
}
};
function makeTest(name, doc) {
var oldDoc;
QUnit.module(name, {
setup: function () {
oldDoc = can.document;
can.document = doc;
if(doc) {
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");
}
},
teardown: function(){
can.document = oldDoc;
if(doc) {
doc.body.removeChild(this.fixture);
}
}
});
var Paginate = can.Map.extend({
count: Infinity,
offset: 0,
limit: 100,
// Prevent negative counts
setCount: function (newCount, success, error) {
return newCount < 0 ? 0 : newCount;
},
// Prevent negative offsets
setOffset: function (newOffset) {
return newOffset < 0 ?
0 :
Math.min(newOffset, !isNaN(this.count - 1) ?
this.count - 1 :
Infinity);
},
// move next
next: function () {
this.attr('offset', this.offset + this.limit);
},
prev: function () {
this.attr('offset', this.offset - this.limit);
},
canNext: function () {
return this.attr('offset') < this.attr('count') -
this.attr('limit');
},
canPrev: function () {
return this.attr('offset') > 0;
},
page: function (newVal) {
if (newVal === undefined) {
return Math.floor(this.attr('offset') / this.attr('limit')) + 1;
} else {
this.attr('offset', (parseInt(newVal) - 1) * this.attr('limit'));
}
},
pageCount: function () {
return this.attr('count') ?
Math.ceil(this.attr('count') / this.attr('limit')) : null;
}
});
test("basic tabs", function () {
// new Tabs() ..
can.Component.extend({
tag: "tabs",
template: can.stache("<ul>" +
"{{#panels}}" +
"<li {{#isActive}}class='active'{{/isActive}} can-click='makeActive'>{{title}}</li>" +
"{{/panels}}" +
"</ul>" +
"<content></content>"),
viewModel: {
panels: [],
addPanel: function (panel) {
if (this.attr("panels")
.length === 0) {
this.makeActive(panel);
}
this.attr("panels")
.push(panel);
},
removePanel: function (panel) {
var panels = this.attr("panels");
can.batch.start();
panels.splice(panels.indexOf(panel), 1);
if (panel === this.attr("active")) {
if (panels.length) {
this.makeActive(panels[0]);
} else {
this.removeAttr("active");
}
}
can.batch.stop();
},
makeActive: function (panel) {
this.attr("active", panel);
this.attr("panels")
.each(function (panel) {
panel.attr("active", false);
});
panel.attr("active", true);
},
// this is viewModel, not mustache
// consider removing viewModel as arg
isActive: function (panel) {
return this.attr('active') === panel;
}
}
});
can.Component.extend({
// make sure <content/> works
template: can.stache("{{#if active}}<content></content>{{/if}}"),
tag: "panel",
viewModel: {
active: false,
title: "@"
},
events: {
" inserted": function () {
can.viewModel(this.element[0].parentNode)
.addPanel(this.viewModel);
},
" removed": function () {
if (!can.viewModel(this.element[0].parentNode)) {
console.log("bruke");
}
can.viewModel(this.element[0].parentNode)
.removePanel(this.viewModel);
}
}
});
var template = can.stache("<tabs>{{#each foodTypes}}<panel title='{{title}}'>{{content}}</panel>{{/each}}</tabs>");
var foodTypes = new can.List([{
title: "Fruits",
content: "oranges, apples"
}, {
title: "Breads",
content: "pasta, cereal"
}, {
title: "Sweets",
content: "ice cream, candy"
}]);
var frag = template({
foodTypes: foodTypes
});
can.append(this.$fixture, frag);
var testArea = this.fixture,
lis = testArea.getElementsByTagName("li");
equal(lis.length, 3, "three lis added");
foodTypes.each(function (type, i) {
equal(innerHTML(lis[i]), type.attr("title"), "li " + i + " has the right content");
});
foodTypes.push({
title: "Vegies",
content: "carrots, kale"
});
lis = testArea.getElementsByTagName("li");
//lis = testArea.getElementsByTagName("li");
equal(lis.length, 4, "li added");
foodTypes.each(function (type, i) {
equal( innerHTML(lis[i]), type.attr("title"), "li " + i + " has the right content");
});
equal(testArea.getElementsByTagName("panel")
.length, 4, "panel added");
foodTypes.shift();
lis = testArea.getElementsByTagName("li");
equal(lis.length, 3, "removed li after shifting a foodType");
foodTypes.each(function (type, i) {
equal( innerHTML(lis[i]), type.attr("title"), "li " + i + " has the right content");
});
// test changing the active element
var panels = testArea.getElementsByTagName("panel");
equal(lis[0].className, "active", "the first element is active");
equal(innerHTML( panels[0] ), "pasta, cereal", "the first content is shown");
equal(innerHTML( panels[1] ), "", "the second content is removed");
can.trigger(lis[1], "click");
lis = testArea.getElementsByTagName("li");
equal(lis[1].className, "active", "the second element is active");
equal(lis[0].className, "", "the first element is not active");
equal( innerHTML( panels[0]), "", "the second content is removed");
equal( innerHTML( panels[1]), "ice cream, candy", "the second content is shown");
});
test("lexical scoping", function() {
can.Component.extend({
tag: "hello-world",
leakScope: false,
template: can.stache("{{greeting}} <content>World</content>{{exclamation}}"),
viewModel: {
greeting: "Hello"
}
});
var template = can.stache("<hello-world>{{greeting}}</hello-world>");
var frag = template({
greeting: "World",
exclamation: "!"
});
var hello = frag.firstChild;
equal(can.trim( innerHTML(hello) ), "Hello World");
can.Component.extend({
tag: "hello-world-no-template",
leakScope: false,
viewModel: {greeting: "Hello"}
});
template = can.stache("<hello-world-no-template>{{greeting}}</hello-world-no-template>");
frag = template({
greeting: "World",
exclamation: "!"
});
hello = frag.firstChild;
equal(can.trim( innerHTML(hello) ), "Hello",
"If no template is provided to can.Component, treat <content> bindings as dynamic.");
});
test("dynamic scoping", function() {
can.Component.extend({
tag: "hello-world",
leakScope: true,
template: can.stache("{{greeting}} <content>World</content>{{exclamation}}"),
viewModel: {greeting: "Hello"}
});
var template = can.stache("<hello-world>{{greeting}}</hello-world>");
var frag = template({
greeting: "World",
exclamation: "!"
});
var hello = frag.firstChild;
equal( can.trim( innerHTML(hello) ) , "Hello Hello!");
});
test("treecombo", function () {
can.Component.extend({
tag: "treecombo",
template: can.stache("<ul class='breadcrumb'>" +
"<li can-click='emptyBreadcrumb'>{{title}}</li>" +
"{{#each breadcrumb}}" +
"<li can-click='updateBreadcrumb'>{{title}}</li>" +
"{{/each}}" +
"</ul>" +
"<ul class='options'>" +
"<content>" +
"{{#selectableItems}}" +
"<li {{#isSelected}}class='active'{{/isSelected}} can-click='toggle'>" +
"<input type='checkbox' {{#isSelected}}checked{{/isSelected}}/>" +
"{{title}}" +
"{{#if children.length}}" +
"<button class='showChildren' can-click='showChildren'>+</button>" +
"{{/if}}" +
"</li>" +
"{{/selectableItems}}" +
"</content>" +
"</ul>"),
viewModel: {
items: [],
breadcrumb: [],
selected: [],
selectableItems: function () {
var breadcrumb = this.attr("breadcrumb");
// if there's an item in the breadcrumb
if (breadcrumb.attr('length')) {
// return the last item's children
return breadcrumb.attr("" + (breadcrumb.length - 1) + '.children');
} else {
// return the top list of items
return this.attr('items');
}
},
showChildren: function (item, el, ev) {
ev.stopPropagation();
this.attr('breadcrumb')
.push(item);
},
emptyBreadcrumb: function () {
this.attr("breadcrumb")
.attr([], true);
},
updateBreadcrumb: function (item) {
var breadcrumb = this.attr("breadcrumb"),
index = breadcrumb.indexOf(item);
breadcrumb.splice(index + 1, breadcrumb.length - index - 1);
},
toggle: function (item) {
var selected = this.attr('selected'),
index = selected.indexOf(item);
if (index === -1) {
selected.push(item);
} else {
selected.splice(index, 1);
}
}
},
helpers: {
isSelected: function (options) {
if (this.attr("selected")
.indexOf(options.context) > -1) {
return options.fn();
} else {
return options.inverse();
}
}
}
});
var template = can.stache("<treecombo items='{locations}' title='Locations'></treecombo>");
var base = new can.Map({});
var frag = template(base);
var root = doc.createElement("div");
root.appendChild(frag);
var items = [{
id: 1,
title: "Midwest",
children: [{
id: 5,
title: "Illinois",
children: [{
id: 23423,
title: "Chicago"
}, {
id: 4563,
title: "Springfield"
}, {
id: 4564,
title: "Naperville"
}]
}, {
id: 6,
title: "Wisconsin",
children: [{
id: 232423,
title: "Milwaulkee"
}, {
id: 45463,
title: "Green Bay"
}, {
id: 45464,
title: "Madison"
}]
}]
}, {
id: 2,
title: "East Coast",
children: [{
id: 25,
title: "New York",
children: [{
id: 3413,
title: "New York"
}, {
id: 4613,
title: "Rochester"
}, {
id: 4516,
title: "Syracuse"
}]
}, {
id: 6,
title: "Pennsylvania",
children: [{
id: 2362423,
title: "Philadelphia"
}, {
id: 454663,
title: "Harrisburg"
}, {
id: 454664,
title: "Scranton"
}]
}]
}];
stop();
setTimeout(function () {
base.attr('locations', items);
var itemsList = base.attr('locations');
// check that the DOM is right
var treecombo = root.firstChild,
breadcrumb = treecombo.firstChild,
breadcrumbLIs = function(){
return breadcrumb.getElementsByTagName('li');
},
options = treecombo.lastChild,
optionsLis = function(){
return options.getElementsByTagName('li');
};
equal(breadcrumbLIs().length, 1, "Only the default title is shown");
equal( innerHTML( breadcrumbLIs()[0] ) , "Locations", "The correct title from the attribute is shown");
equal( itemsList.length, optionsLis().length, "first level items are displayed");
// Test toggling selected, first by clicking
can.trigger(optionsLis()[0], "click");
equal(optionsLis()[0].className, "active", "toggling something not selected adds active");
ok(optionsLis()[0].getElementsByTagName('input')[0].checked, "toggling something not selected checks checkbox");
equal(can.viewModel(treecombo, "selected")
.length, 1, "there is one selected item");
equal(can.viewModel(treecombo, "selected.0"), itemsList.attr("0"), "the midwest is in selected");
// adjust the state and everything should update
can.viewModel(treecombo, "selected")
.pop();
equal(optionsLis()[0].className, "", "toggling something not selected adds active");
// Test going in a location
can.trigger(optionsLis()[0].getElementsByTagName('button')[0], "click");
equal(breadcrumbLIs().length, 2, "Only the default title is shown");
equal(innerHTML(breadcrumbLIs()[1]), "Midwest", "The breadcrumb has an item in it");
ok(/Illinois/.test( innerHTML(optionsLis()[0])), "A child of the top breadcrumb is displayed");
// Test going in a location without children
can.trigger(optionsLis()[0].getElementsByTagName('button')[0], "click");
ok(/Chicago/.test( innerHTML(optionsLis()[0] ) ), "A child of the top breadcrumb is displayed");
ok(!optionsLis()[0].getElementsByTagName('button')
.length, "no show children button");
// Test poping off breadcrumb
can.trigger(breadcrumbLIs()[1], "click");
equal(innerHTML(breadcrumbLIs()[1]), "Midwest", "The breadcrumb has an item in it");
ok(/Illinois/.test( innerHTML( optionsLis()[0])), "A child of the top breadcrumb is displayed");
// Test removing everything
can.trigger(breadcrumbLIs()[0], "click");
equal(breadcrumbLIs().length, 1, "Only the default title is shown");
equal( innerHTML(breadcrumbLIs()[0]), "Locations", "The correct title from the attribute is shown");
start();
}, 100);
});
test("deferred grid", function () {
// This test simulates a grid that reads a `deferreddata` property for
// items and displays them.
// If `deferreddata` is a deferred, it waits for those items to resolve.
// The grid also has a `waiting` property that is true while the deferred is being resolved.
can.Component.extend({
tag: "grid",
viewModel: {
items: [],
waiting: true
},
template: can.stache("<table><tbody><content></content></tbody></table>"),
events: {
init: function () {
this.update();
},
"{viewModel} deferreddata": "update",
update: function () {
var deferred = this.viewModel.attr('deferreddata'),
viewModel = this.viewModel;
if (can.isPromise(deferred)) {
this.viewModel.attr("waiting", true);
deferred.then(function (items) {
viewModel.attr('items')
.attr(items, true);
});
} else {
viewModel.attr('items')
.attr(deferred, true);
}
},
"{items} change": function () {
this.viewModel.attr("waiting", false);
}
}
});
// The context object has a `set` property and a
// deferredData property that reads from it and returns a new deferred.
var SimulatedScope = can.Map.extend({
set: 0,
deferredData: function () {
var deferred = new can.Deferred();
var set = this.attr('set');
if (set === 0) {
setTimeout(function () {
deferred.resolve([{
first: "Justin",
last: "Meyer"
}]);
}, 100);
} else if (set === 1) {
setTimeout(function () {
deferred.resolve([{
first: "Brian",
last: "Moschel"
}]);
}, 100);
}
return deferred;
}
});
var viewModel = new SimulatedScope();
var template = can.stache("<grid deferreddata='{viewModel.deferredData}'>" +
"{{#each items}}" +
"<tr>" +
"<td width='40%'>{{first}}</td>" +
"<td width='70%'>{{last}}</td>" +
"</tr>" +
"{{/each}}" +
"</grid>");
can.append(this.$fixture, template({
viewModel: viewModel
}));
var gridScope = can.viewModel(this.fixture.firstChild);
equal(gridScope.attr("waiting"), true, "The grid is initially waiting on the deferreddata to resolve");
stop();
var self = this;
var waitingHandler = function() {
gridScope.unbind('waiting', waitingHandler);
setTimeout(function () {
var tds = self.fixture.getElementsByTagName("td");
equal(tds.length, 2, "there are 2 tds");
gridScope.bind("waiting", function (ev, newVal) {
if (newVal === false) {
setTimeout(function () {
equal(innerHTML(tds[0]), "Brian", "td changed to brian");
start();
}, 10);
}
});
// update set to change the deferred.
viewModel.attr("set", 1);
}, 10);
};
gridScope.bind('waiting', waitingHandler);
});
test("nextprev", function () {
can.Component.extend({
tag: "next-prev",
template: can.stache(
'<a href="javascript://"' +
'class="prev {{#paginate.canPrev}}enabled{{/paginate.canPrev}}" can-click="{paginate.prev}">Prev</a>' +
'<a href="javascript://"' +
'class="next {{#paginate.canNext}}enabled{{/paginate.canNext}}" can-click="{paginate.next}">Next</a>')
});
var paginator = new Paginate({
limit: 20,
offset: 0,
count: 100
});
var template = can.stache("<next-prev paginate='{paginator}'></next-prev>");
var frag = template({
paginator: paginator
});
var nextPrev = frag.firstChild;
var prev = nextPrev.firstChild,
next = nextPrev.lastChild;
ok(!/enabled/.test( prev.className ), "prev is not enabled");
ok(/enabled/.test( next.className ), "next is enabled");
can.trigger(next, "click");
ok(/enabled/.test( prev.className ), "prev is enabled");
});
test("page-count", function () {
can.Component.extend({
tag: "page-count",
template: can.stache('Page <span>{{page}}</span>.')
});
var paginator = new Paginate({
limit: 20,
offset: 0,
count: 100
});
var template = can.stache("<page-count page='{paginator.page}'></page-count>");
var frag = template( new can.Map({
paginator: paginator
}) );
var span = frag.firstChild.getElementsByTagName("span")[0];
equal(span.firstChild.nodeValue, "1");
paginator.next();
equal(span.firstChild.nodeValue, "2");
paginator.next();
equal(span.firstChild.nodeValue, "3");
});
test("hello-world and whitespace around custom elements", function () {
can.Component.extend({
tag: "hello-world",
template: can.stache("{{#if visible}}{{message}}{{else}}Click me{{/if}}"),
viewModel: {
visible: false,
message: "Hello There!"
},
events: {
click: function () {
this.viewModel.attr("visible", true);
}
}
});
var template = can.stache(" <hello-world></hello-world> ");
var frag = template({});
var helloWorld = frag.childNodes.item(1);
can.trigger(can.$(helloWorld), "click");
equal( innerHTML(helloWorld) , "Hello There!");
});
test("self closing content tags", function () {
can.Component.extend({
"tag": "my-greeting",
template: can.stache("<h1><content/></h1>"),
viewModel: {
title: "can.Component"
}
});
var template = can.stache("<my-greeting><span>{{site}} - {{title}}</span></my-greeting>");
var frag = template({
site: "CanJS"
});
equal(frag.firstChild.getElementsByTagName("span")
.length, 1, "there is an h1");
});
test("can.viewModel utility", function() {
can.Component({
tag: "my-taggy-tag",
template: can.stache("<h1>hello</h1>"),
viewModel: {
foo: "bar"
}
});
var frag = can.stache("<my-taggy-tag id='x'></my-taggy-tag>")();
var el = can.$(frag.firstChild);
equal(can.viewModel(el), can.data(el, "viewModel"), "one argument grabs the viewModel object");
equal(can.viewModel(el, "foo"), "bar", "two arguments fetches a value");
can.viewModel(el, "foo", "baz");
equal(can.viewModel(el, "foo"), "baz", "Three arguments sets the value");
if (window.$ && $.fn) {
el = $(frag.firstChild);
equal(el.viewModel(), can.data(el, "viewModel"), "jQuery helper grabs the viewModel object");
equal(el.viewModel("foo"), "baz", "jQuery helper with one argument fetches a property");
equal(el.viewModel("foo", "bar").get(0), el.get(0), "jQuery helper returns the element");
equal(el.viewModel("foo"), "bar", "jQuery helper with two arguments sets the property");
}
});
test("can.viewModel backwards compatible with can.scope", function() {
equal(can.viewModel, can.scope, "can helper");
if (window.$ && $.fn) {
equal($.scope, $.viewModel, "jQuery helper");
}
});
test("can.viewModel creates one if it doesn't exist", function(){
var frag = can.stache("<div id='me'></div>")();
var el = can.$(frag.firstChild);
var viewModel = can.viewModel(el);
ok(!!viewModel, "viewModel created where it didn't exist.");
equal(viewModel, can.data(el, "viewModel"), "viewModel is in the data.");
});
test('setting passed variables - two way binding', function () {
can.Component.extend({
tag: "my-toggler",
template: can.stache("{{#if visible}}<content/>{{/if}}"),
viewModel: {
visible: true,
show: function () {
this.attr('visible', true);
},
hide: function () {
this.attr("visible", false);
}
}
});
can.Component.extend({
tag: "my-app",
viewModel: {
visible: true,
show: function () {
this.attr('visible', true);
}
}
});
var template = can.stache("<my-app>" +
'{{^visible}}<button can-click="show">show</button>{{/visible}}' +
'<my-toggler visible="{visible}">' +
'content' +
'<button can-click="hide">hide</button>' +
'</my-toggler>' +
'</my-app>');
var frag = template({});
var myApp = frag.firstChild,
buttons = myApp.getElementsByTagName("button");
equal( buttons.length, 1, "there is one button");
equal( innerHTML(buttons[0]) , "hide", "the button's text is hide");
can.trigger(buttons[0], "click");
buttons = myApp.getElementsByTagName("button");
equal(buttons.length, 1, "there is one button");
equal(innerHTML(buttons[0]), "show", "the button's text is show");
can.trigger(buttons[0], "click");
buttons = myApp.getElementsByTagName("button");
equal(buttons.length, 1, "there is one button");
equal(innerHTML(buttons[0]), "hide", "the button's text is hide");
});
test("helpers reference the correct instance (#515)", function () {
expect(2);
can.Component({
tag: 'my-text',
template: can.stache('<p>{{valueHelper}}</p>'),
helpers: {
valueHelper: function () {
return this.attr('value');
}
}
});
var template = can.stache('<my-text value="value1"></my-text><my-text value="value2"></my-text>');
var frag = template({});
equal(frag.firstChild.firstChild.firstChild.nodeValue, 'value1');
equal(frag.lastChild.firstChild.firstChild.nodeValue, 'value2');
});
test('access hypenated attributes via camelCase or hypenated', function () {
can.Component({
tag: 'hyphen',
viewModel: {
},
template: can.stache('<p>{{valueHelper}}</p>'),
helpers: {
valueHelper: function () {
return this.attr('camelCase');
}
}
});
var template = can.stache('<hyphen camel-case="value1"></hyphen>');
var frag = template({});
equal(frag.firstChild.firstChild.firstChild.nodeValue, 'value1');
});
test("a map as viewModel", function () {
var me = new can.Map({
name: "Justin"
});
can.Component.extend({
tag: 'my-viewmodel',
template: can.stache("{{name}}}"),
viewModel: me
});
var template = can.stache('<my-viewmodel></my-viewmodel>');
equal(template().firstChild.firstChild.nodeValue, "Justin");
});
test("content in a list", function () {
var template = can.stache('<my-list>{{name}}</my-list>');
can.Component.extend({
tag: "my-list",
template: can.stache("{{#each items}}<li><content/></li>{{/each}}"),
viewModel: {
items: new can.List([{
name: "one"
}, {
name: "two"
}])
}
});
var lis = template()
.firstChild.getElementsByTagName("li");
equal(innerHTML(lis[0]), "one", "first li has correct content");
equal(innerHTML(lis[1]), "two", "second li has correct content");
});
test("don't update computes unnecessarily", function () {
var sourceAge = 30,
timesComputeIsCalled = 0;
var age = can.compute(function (newVal) {
timesComputeIsCalled++;
if (timesComputeIsCalled === 1) {
ok(true, "reading initial value to set as years");
} else if (timesComputeIsCalled === 2) {
equal(newVal, 31, "updating value to 31");
} else if (timesComputeIsCalled === 3) {
ok(true, "called back another time after set to get the value");
} else {
ok(false, "You've called the callback " + timesComputeIsCalled + " times");
}
if (arguments.length) {
sourceAge = newVal;
} else {
return sourceAge;
}
});
can.Component.extend({
tag: "age-er"
});
var template = can.stache("<age-er years='{age}'></age-er>");
template({
age: age
});
age(31);
});
test("component does not respect can.compute passed via attributes (#540)", function () {
var data = {
compute: can.compute(30)
};
can.Component.extend({
tag: "my-component",
template: can.stache("<span>{{blocks}}</span>")
});
var template = can.stache("<my-component blocks='{compute}'></my-component>");
var frag = template(data);
equal( innerHTML(frag.firstChild.firstChild), "30");
});
test("defined view models (#563)", function () {
var HelloWorldModel = can.Map.extend({
visible: true,
toggle: function () {
this.attr("visible", !this.attr("visible"));
}
});
can.Component.extend({
tag: "my-helloworld",
template: can.stache("<h1>{{#if visible}}visible{{else}}invisible{{/if}}</h1>"),
viewModel: HelloWorldModel
});
var template = can.stache("<my-helloworld></my-helloworld>");
var frag = template({});
equal( innerHTML(frag.firstChild.firstChild), "visible");
});
test("viewModel not rebound correctly (#550)", function () {
var nameChanges = 0;
can.Component.extend({
tag: "viewmodel-rebinder",
events: {
"{name} change": function () {
nameChanges++;
}
}
});
var template = can.stache("<viewmodel-rebinder></viewmodel-rebinder>");
var frag = template();
var viewModel = can.viewModel(can.$(frag.firstChild));
var n1 = can.compute(),
n2 = can.compute();
viewModel.attr("name", n1);
n1("updated");
viewModel.attr("name", n2);
n2("updated");
equal(nameChanges, 2);
});
test("content extension stack overflow error", function () {
can.Component({
tag: 'outer-tag',
template: can.stache('<inner-tag>inner-tag CONTENT <content/></inner-tag>')
});
can.Component({
tag: 'inner-tag',
template: can.stache('inner-tag TEMPLATE <content/>')
});
// currently causes Maximum call stack size exceeded
var template = can.stache("<outer-tag>outer-tag CONTENT</outer-tag>");
// RESULT = <outer-tag><inner-tag>inner-tag TEMPLATE inner-tag CONTENT outer-tag CONTENT</inner-tag></outer-tag>
var frag = template();
equal( innerHTML(frag.firstChild.firstChild), 'inner-tag TEMPLATE inner-tag CONTENT outer-tag CONTENT');
});
test("inserted event fires twice if component inside live binding block", function () {
var inited = 0,
inserted = 0;
can.Component({
tag: 'child-tag',
viewModel: {
init: function () {
inited++;
}
},
events: {
' inserted': function () {
inserted++;
}
}
});
can.Component({
tag: 'parent-tag',
template: can.stache('{{#shown}}<child-tag></child-tag>{{/shown}}'),
viewModel: {
shown: false
},
events: {
' inserted': function () {
this.viewModel.attr('shown', true);
}
}
});
var frag = can.stache("<parent-tag></parent-tag>")({});
can.append(this.$fixture, frag);
equal(inited, 1);
equal(inserted, 1);
});
test("@ keeps properties live now", function () {
can.Component.extend({
tag: "attr-fun",
template: can.stache("<h1>{{fullName}}</h1>"),
viewModel: {
fullName: function () {
return this.attr("firstName") + " " + this.attr("lastName");
}
}
});
var frag = can.stache("<attr-fun first-name='Justin' last-name='Meyer'></attr-fun>")();
var attrFun = frag.firstChild;
this.fixture.appendChild(attrFun);
equal( innerHTML(attrFun.firstChild), "Justin Meyer");
can.attr.set(attrFun, "first-name", "Brian");
stop();
setTimeout(function () {
equal(attrFun.firstChild.firstChild.nodeValue, "Brian Meyer");
start();
}, 100);
});
test("id, class, and dataViewId should be ignored (#694)", function () {
can.Component.extend({
tag: "stay-classy",
viewModel: {
notid: "foo",
notclass: 5,
notdataviewid: {}
}
});
var data = {
idFromData: "id-success",
classFromData: "class-success",
dviFromData: "dvi-success"
};
var frag = can.stache(
"<stay-classy id='an-id' notid='{idFromData}'" +
" class='a-class' notclass='{classFromData}'" +
" notdataviewid='{dviFromData}'></stay-classy>")(data);
var stayClassy = frag.firstChild;
can.append(this.$fixture, frag);
var viewModel = can.viewModel(stayClassy);
equal(viewModel.attr("id"), undefined);
equal(viewModel.attr("notid"), "id-success");
equal(viewModel.attr("class"), undefined);
equal(viewModel.attr("notclass"), "class-success");
equal(viewModel.attr("dataViewId"), undefined);
equal(viewModel.attr("notdataviewid"), "dvi-success");
});
test("Component can-click method should be not called while component's init", function () {
var called = false;
can.Component.extend({
tag: "child-tag"
});
can.Component.extend({
tag: "parent-tag",
template: can.stache('<child-tag can-click="method"></child-tag>'),
viewModel: {
method: function () {
called = true;
}
}
});
can.stache('<parent-tag></parent-tag>')();
equal(called, false);
});
test('Same component tag nested', function () {
can.Component({
'tag': 'my-tag',
template: can.stache('<p><content/></p>')
});
//simplest case
var template = can.stache('<div><my-tag>Outter<my-tag>Inner</my-tag></my-tag></div>');
//complex case
var template2 = can.stache('<div><my-tag>3<my-tag>2<my-tag>1<my-tag>0</my-tag></my-tag></my-tag></my-tag></div>');
//edge case for new logic (same custom tag at same depth as one previously encountered)
var template3 = can.stache('<div><my-tag>First</my-tag><my-tag>Second</my-tag></div>');
equal( template({}).firstChild.getElementsByTagName('p').length, 2, 'proper number of p tags');
equal( template2({}).firstChild.getElementsByTagName('p').length, 4, 'proper number of p tags');
equal( template3({}).firstChild.getElementsByTagName('p').length, 2, 'proper number of p tags');
});
test("Component events bind to window", function(){
window.tempMap = new can.Map();
can.Component.extend({
tag: "window-events",
events: {
"{tempMap} prop": function(){
ok(true, "called templated event");
}
}
});
var template = can.stache('<window-events></window-events>');
template();
window.tempMap.attr("prop","value");
// IE 6-8 throws an error when deleting globals created via assignment:
// http://perfectionkills.com/understanding-delete/#ie_bugs
window.tempMap = undefined;
try{
delete window.tempMap;
} catch(e) {}
});
test("can.Construct are passed normally", function(){
var Constructed = can.Construct.extend({foo:"bar"},{});
can.Component.extend({
tag: "con-struct",
template: can.stache("{{con.foo}}")
});
var stached = can.stache("<con-struct con='{Constructed}'></con-struct>");
var res = stached({
Constructed: Constructed
});
equal(innerHTML(res.firstChild), "bar");
});
//!steal-remove-start
if (can.dev) {
test("passing unsupported attributes gives a warning", function(){
var oldlog = can.dev.warn;
can.dev.warn = function (text) {
ok(text, "got a message");
can.dev.warn = oldlog;
};
can.Component.extend({
tag: 'my-thing',
template: can.stache('hello')
});
var stache = can.stache("<my-thing id='{productId}'></my-tagged>");
stache(new can.Map({productId: 123}));
});
}
//!steal-remove-end
test("stache conditionally nested components calls inserted once (#967)", function(){
expect(2);
can.Component.extend({
tag: "can-parent-stache",
viewModel: {
shown: true
},
template: can.stache("{{#if shown}}<can-child></can-child>{{/if}}")
});
can.Component.extend({
tag: "can-parent-mustache",
viewModel: {
shown: true
},
template: can.stache("{{#if shown}}<can-child></can-child>{{/if}}")
});
can.Component.extend({
tag: "can-child",
events: {
inserted: function(){
this.viewModel.attr('bar', 'foo');
ok(true, "called inserted once");
}
}
});
var template = can.stache("<can-parent-stache></can-parent-stache>");
can.append(this.$fixture, template());
var template2 = can.stache("<can-parent-mustache></can-parent-mustache>");
can.append(this.$fixture, template2());
});
test("hyphen-less tag names", function () {
var template = can.stache('<span></span><foobar></foobar>');
can.Component.extend({
tag: "foobar",
template: can.stache("<div>{{name}}</div>"),
viewModel: {
name: "Brian"
}
});
equal(template().lastChild.firstChild.firstChild.nodeValue, "Brian");
});
test('nested component within an #if is not live bound(#1025)', function() {
can.Component.extend({
tag: 'parent-component',
template: can.stache('{{#if shown}}<child-component></child-component>{{/if}}'),
viewModel: {
shown: false
}
});
can.Component.extend({
tag: 'child-component',
template: can.stache('Hello world.')
});
var template = can.stache('<parent-component></parent-component>');
var frag = template({});
equal( innerHTML(frag.firstChild), '', 'child component is not inserted');
can.viewModel(frag.firstChild).attr('shown', true);
equal( innerHTML(frag.firstChild.firstChild), 'Hello world.', 'child component is inserted');
can.viewModel(frag.firstChild).attr('shown', false);
equal( innerHTML(frag.firstChild), '', 'child component is removed');
});
test('component does not update viewModel on id, class, and data-view-id attribute changes (#1079)', function(){
can.Component.extend({
tag:'x-app'
});
var frag=can.stache('<x-app></x-app>')({});
var el = frag.firstChild;
var viewModel = can.viewModel(el);
// element must be inserted, otherwise attributes event will not be fired
can.append(this.$fixture,frag);
// update the class
can.addClass(can.$(el),"foo");
stop();
setTimeout(function(){
equal(viewModel.attr('class'),undefined, "the viewModel is not updated when the class attribute changes");
start();
},20);
});
test('viewModel objects with Constructor functions as properties do not get converted (#1261)', 1, function(){
stop();
var Test = can.Map.extend({
test: 'Yeah'
});
can.Component.extend({
tag:'my-app',
viewModel: {
MyConstruct: Test
},
events: {
'{MyConstruct} something': function() {
ok(true, 'Event got triggered');
start();
}
}
});
var frag = can.stache('<my-app></my-app>')();
// element must be inserted, otherwise attributes event will not be fired
can.append(this.$fixture,frag);
can.trigger(Test, 'something');
});
test('removing bound viewModel properties on destroy #1415', function(){
var state = new can.Map({
product: {
id: 1,
name: "Tom"
}
});
can.Component.extend({
tag: 'destroyable-component',
events: {
destroy: function(){
this.viewModel.attr('product', null);
}
}
});
var frag = can.stache('<destroyable-component product="{product}"></destroyable-component>')(state);
// element must be inserted, otherwise attributes event will not be fired
can.append(this.$fixture,frag);
can.remove( can.$(this.fixture.firstChild) );
ok(state.attr('product') == null, 'product was removed');
});
test('changing viewModel property rebinds {viewModel.<...>} events (#1529)', 2, function(){
can.Component.extend({
tag: 'rebind-viewmodel',
events: {
inserted: function(){
this.viewModel.attr('item', {});
},
'{scope.item} change': function() {
ok(true, 'Change event on scope');
},
'{viewModel.item} change': function() {
ok(true, 'Change event on viewModel');
}
}
});
var frag = can.stache('<rebind-viewmodel></rebind-viewmodel>')();
var rebind = frag.firstChild;
can.append(this.$fixture, rebind);
can.viewModel(can.$(rebind)).attr('item.name', 'CDN');
});
test('Component two way binding loop (#1579)', function() {
var changeCount = 0;
can.Component.extend({
tag: 'product-swatch-color'
});
can.Component.extend({
tag: 'product-swatch',
template: can.stache('<product-swatch-color variations="{variations}"></product-swatch-color>'),
viewModel: can.Map.extend({
define: {
variations: {
set: function(variations) {
if(changeCount > 500) {
return;
}
changeCount++;
return new can.List(variations.attr());
}
}
}
})
});
var frag = can.stache('<product-swatch></product-swatch>')(),
productSwatch = frag.firstChild;
can.batch.start();
can.viewModel( can.$(productSwatch) ).attr('variations', new can.List());
can.batch.stop();
ok(changeCount < 500, "more than 500 events");
});
test('DOM trees not releasing when referencing can.Map inside can.Map in template (#1593)', function() {
var baseTemplate = can.stache('{{#if show}}<my-outside></my-outside>{{/if}}'),
show = can.compute(true),
state = new can.Map({
inner: 1
});
var removeCount = 0;
can.Component.extend({
tag: 'my-inside',
events: {
removed: function() {
removeCount++;
}
}
});
can.Component.extend({
tag: 'my-outside',
template: can.stache('{{#if state.inner}}<my-inside></my-inside>{{/if}}')
});
can.append( this.$fixture, baseTemplate({
show: show,
state: state
}) );
show(false);
state.removeAttr('inner');
equal(removeCount, 1, 'internal removed once');
show(true);
state.attr('inner', 2);
state.removeAttr('inner');
equal(removeCount, 2, 'internal removed twice');
});
test("component viewModels are initialized with binding values", function(){
can.Component.extend({
tag: 'init-viewmodel',
viewModel: {
init: function(props){
equal(props.prop, "value", "got initialized with a value");
}
}
});
can.stache("<init-viewmodel {prop}='passedProp'/>")({
passedProp: "value"
});
});
test("wrong context inside <content> tags (#2092)", function(){
can.Component.extend({
tag: 'some-context',
viewModel: {
value: "WRONG",
items: [{value: "A", name: "X"}, {value: "B", name: "Y"}]
},
template: can.stache("{{#each items}}<content><span>{{name}}</span></content>{{/each}}")
});
var templateA = can.stache("<some-context><span>{{value}}</span></some-context>");
var frag = templateA({});
var spans = frag.firstChild.getElementsByTagName("span");
equal(spans[0].firstChild.nodeValue, "A", "context is right");
equal(spans[1].firstChild.nodeValue, "B", "context is right");
var templateB = can.stache("<some-context/>");
frag = templateB({});
spans = frag.firstChild.getElementsByTagName("span");
equal(spans[0].firstChild.nodeValue, "X", "context is right X");
equal(spans[1].firstChild.nodeValue, "Y", "context is right");
});
test("%root property should not be serialized inside prototype of can.Component constructor (#2080)", function () {
var viewModel = can.Map.extend({});
can.Component.extend({
tag: "foo",
viewModel: viewModel,
init: function () {
ok(!this.viewModel.serialize()['%root'], "serialized viewModel contains '%root' property");
}
});
var template = can.stache("<foo/>");
can.append(this.$fixture, template());
});
test("%root property is available in a viewModel", function () {
var viewModel = can.Map.extend({});
can.Component.extend({
tag: "foo",
viewModel: viewModel,
init: function () {
ok(this.viewModel.attr('%root'), "viewModel contains '%root' property");
}
});
var template = can.stache("<foo/>");
can.append(this.$fixture, template());
});
test('Type in a component’s viewModel doesn’t work correctly with lists (#2250)', function() {
var Item = can.Map.extend({
define: {
prop: {
value: true
}
}
});
var Collection = can.List.extend({
Map: Item
}, {});
can.Component.extend({
tag: "test-component",
viewModel: {
define: {
collection: {
Type: Collection
},
vmProp: {
get: function() {
return this.attr('collection.0.prop');
}
}
}
}
});
var frag = can.stache('<test-component collection="{collection}"></test-component>')({
collection: [{}]
});
var vmPropVal = can.viewModel(can.$(frag.firstChild)).attr('vmProp');
ok(vmPropVal, 'value is from defined prop');
});
// PUT NEW TESTS ABOVE THIS LINE
}
makeTest("can/component dom", document);
if(window.jQuery && window.steal) {
makeTest("can/component vdom", simpleDocument);
}
QUnit.module("can/component mustache");
asyncTest('(mu)stache integration', function(){
can.Component.extend({
tag: 'my-tagged',
template: '{{p1}},{{p2.val}},{{p3}},{{p4}}'
});
var stache = can.stache("<my-tagged p1='v1' p2='{v2}' p3='{{v3}}'></my-tagged>");
var mustache = can.mustache("<my-tagged p1='v1' p2='{v2}' p3='{{v3}}'></my-tagged>");
var data = new can.Map({
v1: "value1",
v2: {val: "value2"},
v3: "value3",
value3: "value 3",
VALUE3: "VALUE 3"
});
var stacheFrag = stache(data),
stacheResult = stacheFrag.childNodes[0].innerHTML.split(",");
var mustacheFrag = mustache(data),
mustacheResult = mustacheFrag.childNodes[0].innerHTML.split(",");
equal(stacheResult[0], "v1", "stache uses attribute values");
equal(stacheResult[1], "value2", "stache single {} cross binds value");
equal(stacheResult[2], "value3", "stache {{}} cross binds attribute");
equal(mustacheResult[0], "value1", "mustache looks up attribute values");
equal(mustacheResult[1], "value2", "mustache single {} cross binds value");
equal(mustacheResult[2], "value 3", "mustache {{}} cross binds string value");
data.attr("v1","VALUE1");
data.attr("v2",new can.Map({val: "VALUE 2"}));
data.attr("v3","VALUE3");
can.attr.set( stacheFrag.childNodes[0],"p4","value4");
stacheResult = stacheFrag.childNodes[0].innerHTML.split(",");
mustacheResult = mustacheFrag.childNodes[0].innerHTML.split(",");
equal(stacheResult[0], "v1", "stache uses attribute values so it should not change");
equal(mustacheResult[0], "VALUE1", "mustache looks up attribute values and updates immediately");
equal(stacheResult[1], "VALUE 2", "stache single {} cross binds value and updates immediately");
equal(mustacheResult[1], "VALUE 2", "mustache single {} cross binds value and updates immediately");
equal(stacheResult[2], "value3", "stache {{}} cross binds attribute changes so it wont be updated immediately");
setTimeout(function(){
stacheResult = stacheFrag.childNodes[0].innerHTML.split(",");
mustacheResult = mustacheFrag.childNodes[0].innerHTML.split(",");
equal(stacheResult[2], "VALUE3", "stache {{}} cross binds attribute");
equal(mustacheResult[2], "VALUE 3", "mustache sticks with old value even though property has changed");
equal(stacheResult[3], "value4", "stache sees new attributes");
start();
},20);
});
test("attach events on init", function(){
expect(2);
can.Component.extend({
tag: 'app-foo',
template: can.stache('<div>click me</div>'),
events: {
init: function(){
this.on("div", 'click', 'doSomethingfromInit');
},
inserted: function(){
this.on("div", 'click', 'doSomethingfromInserted');
},
doSomethingfromInserted: function(){
ok(true, "bound in inserted");
},
doSomethingfromInit: function(){
ok(true, "bound in init");
}
}
});
can.append( can.$("#qunit-fixture"), can.stache("<app-foo></app-foo>")({}));
can.trigger(can.$('#qunit-fixture div'), 'click');
});
test("attach events on init", function(){
expect(2);
can.Component.extend({
tag: 'app-foo',
template: can.stache('<div>click me</div>'),
events: {
init: function(){
this.on("div", 'click', 'doSomethingfromInit');
},
inserted: function(){
this.on("div", 'click', 'doSomethingfromInserted');
},
doSomethingfromInserted: function(){
ok(true, "bound in inserted");
},
doSomethingfromInit: function(){
ok(true, "bound in init");
}
}
});
can.append( can.$("#qunit-fixture"), can.stache("<app-foo></app-foo>")({}));
can.trigger(can.$('#qunit-fixture div'), 'click');
});
if(can.isFunction(Object.keys)) {
test('<content> node list cleans up properly as direct child (#1625, #1627)', 2, function() {
var size = Object.keys(can.view.nodeLists.nodeMap).length;
var items = [];
var viewModel = new can.Map({
show: false
});
var toggle = function() {
viewModel.attr('show', !viewModel.attr('show'));
};
for (var i = 0; i < 100; i++) {
items.push({
// Random 5 character String
name: Math.random().toString(36)
.replace(/[^a-z]+/g, '').substr(0, 5)
});
}
can.Component.extend({
tag: 'grandparent-component',
template: can.stache('{{#if show}}<parent-component></parent-component>{{/if}}'),
scope: viewModel
});
can.Component.extend({
tag: 'parent-component',
template: can.stache('{{#items}}<child-component>\n:)\n</child-component>{{/items}}'),
scope: {
items: items
}
});
can.Component.extend({
tag: 'child-component',
template: can.stache('<div>\n<content/>\n</div>')
});
can.append(can.$("#qunit-fixture"), can.stache('<grandparent-component></grandparent-component>')());
toggle();
equal(Object.keys(can.view.nodeLists.nodeMap).length - size, 0,
'No new items added to nodeMap');
toggle();
equal(Object.keys(can.view.nodeLists.nodeMap).length - size, 0,
'No new items added to nodeMap');
can.remove(can.$("#qunit-fixture>*"));
});
asyncTest('<content> node list cleans up properly, directly nested (#1625, #1627)', function() {
can.Component.extend({
tag: 'parent-component',
template: can.stache('{{#items}}<child-component>{{parentAttrInContent}}</child-component>{{/items}}'),
scope: {
items: [{parentAttrInContent: 0},{parentAttrInContent: 1} ]
}
});
can.Component.extend({
tag: 'child-component',
template: can.stache('<div>{{#if bar}}<content/>{{/if}}</div>'),
scope: {
bar: true
}
});
can.append(can.$("#qunit-fixture"), can.stache('<parent-component/>')());
var old = can.unbindAndTeardown;
var count = 0;
can.unbindAndTeardown = function(name) {
if(name === 'parentAttrInContent') {
count++;
}
return old.call(this, arguments);
};
can.remove(can.$("#qunit-fixture>*"));
// Dispatches async
setTimeout(function() {
equal(count, 2, '2 items unbound. Previously, this would unbind 4 times because of switching to fast path');
can.unbindAndTeardown = old;
start();
}, 20);
});
test("components control destroy method is called", function(){
expect(0);
can.Component.extend({
tag: 'comp-control-destroy-test',
template: can.stache('<div>click me</div>'),
events: {
"{document} click" : function () {
ok(true, "click registered");
}
}
});
can.append(can.$("#qunit-fixture"), can.stache("<comp-control-destroy-test></comp-control-destroy-test>")({}));
can.remove(can.$("#qunit-fixture>*"));
can.trigger(can.$(document), 'click');
});
test("<content> tag doesn't leak memory", function(){
can.Component.extend({
tag: 'my-viewer',
template: can.stache('<div><content /></div>')
});
var template = can.stache('{{#show}}<my-viewer>{{value}}</my-viewer>{{/show}}');
var map = new can.Map({show: true, value: "hi"});
can.append(can.$("#qunit-fixture"), template(map));
map.attr("show", false);
map.attr("show", true);
map.attr("show", false);
map.attr("show", true);
map.attr("show", false);
map.attr("show", true);
map.attr("show", false);
stop();
setTimeout(function(){
equal(map._bindings,1, "only one binding");
can.remove(can.$("#qunit-fixture>*"));
start();
},10);
});
// PUT NEW TESTS THAT NEED TO TEST AGAINST MUSTACHE JUST ABOVE THIS LINE
}
test('component simpleHelpers', function() {
can.Component.extend({
tag: 'simple-helper',
template: can.stache('Result: {{add first second}}'),
scope: {
first: 4,
second: 3
},
simpleHelpers: {
add: function(a, b) {
return a + b;
}
}
});
var frag = can.stache('<simple-helper></simple-helper>')();
equal(frag.childNodes[0].innerHTML, 'Result: 7');
});
test("component destroy should teardown event handlers", function () {
var count = 0,
map = new can.Map({value: 1});
can.Component.extend({
tag: "page-element",
viewModel: { map: map },
events: {
'{scope.map} value': function(){
count++;
}
}
});
can.append(can.$("#qu