can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
1,780 lines (1,406 loc) • 44.9 kB
JavaScript
steal("can", "can/map/define", "can/component", "can/view/stache" ,"can/route", "steal-qunit", function () {
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}}" ($click)="paginate.prev()">Prev</a>' +
'<a href="javascript://"' +
'class="next {{#paginate.canNext}}enabled{{/paginate.canNext}}" ($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 and class should work now (#694)", function () {
can.Component.extend({
tag: "stay-classy",
viewModel: {
notid: "foo",
notclass: 5,
notdataviewid: {}
}
});
var data = {
idData: "id-success",
classData: "class-success"
};
var frag = can.stache(
"<stay-classy {(id)}='idData'" +
" {(class)}='classData'></stay-classy>")(data);
var stayClassy = frag.firstChild;
can.append(this.$fixture, frag);
var viewModel = can.viewModel(stayClassy);
equal(viewModel.attr("id"), "id-success");
equal(viewModel.attr("class"), "class-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");
});
test("passing id works now", function(){
can.Component.extend({
tag: 'my-thing',
template: can.stache('hello')
});
var stache = can.stache("<my-thing {(id)}='productId'></my-tagged>");
var frag = stache(new can.Map({productId: 123}));
equal( can.viewModel(frag.firstChild).attr("id"), 123);
});
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',
viewModel: {
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({
tag: "product-swatch",
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("references scopes are available to bindings nested in components (#2029)", function(){
var template = can.stache('<export-er {^value}="*reference" />'+
'<wrap-er><simple-example {key}="*reference"/></wrap-er>');
can.Component.extend({
tag : "wrap-er"
});
can.Component.extend({
tag : "export-er",
events : {
"init" : function() {
var self = this.viewModel;
stop();
setTimeout(function() {
self.attr("value", 100);
var wrapper = frag.lastChild,
simpleExample = wrapper.firstChild,
textNode = simpleExample.firstChild;
equal(textNode.nodeValue, "100", "updated value with reference");
start();
}, 10);
}
}
});
can.Component.extend({
tag : "simple-example",
template : can.stache("{{key}}"),
viewModel : {}
});
var frag = template({});
});
test('two-way binding syntax PRIOR to v2.3 shall NOT let a child property initialize an undefined parent property (#2020)', function(){
var renderer = can.stache('<pa-rent/>');
can.Component.extend({
tag : 'pa-rent',
template: can.stache('<chi-ld child-prop="{parentProp}" />')
});
can.Component.extend({
tag : 'chi-ld',
viewModel: {
childProp: 'bar'
}
});
var frag = renderer({});
var parentVM = can.viewModel(frag.firstChild);
var childVM = can.viewModel(frag.firstChild.firstChild);
equal(parentVM.attr('parentProp'), undefined, 'parentProp is undefined');
equal(childVM.attr('childProp'), 'bar', 'childProp is bar');
parentVM.attr('parentProp', 'foo');
equal(parentVM.attr('parentProp'), 'foo', 'parentProp is foo');
equal(childVM.attr('childProp'), 'foo', 'childProp is foo');
childVM.attr('childProp', 'baz');
equal(parentVM.attr('parentProp'), 'baz', 'parentProp is baz');
equal(childVM.attr('childProp'), 'baz', 'childProp is baz');
});
test('two-way binding syntax INTRODUCED in v2.3 ALLOWS a child property to initialize an undefined parent property', function(){
var renderer = can.stache('<pa-rent/>');
can.Component.extend({
tag : 'pa-rent',
template: can.stache('<chi-ld {(child-prop)}="parentProp" />')
});
can.Component.extend({
tag : 'chi-ld',
viewModel: {
childProp: 'bar'
}
});
var frag = renderer({});
var parentVM = can.viewModel(frag.firstChild);
var childVM = can.viewModel(frag.firstChild.firstChild);
equal(parentVM.attr('parentProp'), 'bar', 'parentProp is bar');
equal(childVM.attr('childProp'), 'bar', 'childProp is bar');
parentVM.attr('parentProp', 'foo');
equal(parentVM.attr('parentProp'), 'foo', 'parentProp is foo');
equal(childVM.attr('childProp'), 'foo', 'childProp is foo');
childVM.attr('childProp', 'baz');
equal(parentVM.attr('parentProp'), 'baz', 'parentProp is baz');
equal(childVM.attr('childProp'), 'baz', 'childProp is baz');
});
test("conditional attributes (#2077)", function(){
can.Component.extend({
tag: 'some-comp',
viewModel: {}
});
var template = can.stache("<some-comp "+
"{{#if preview}}{next}='nextPage'{{/if}} "+
"{swap}='{{swapName}}' "+
"{{#preview}}checked{{/preview}} "+
"></some-comp>");
var map = new can.Map({
preview: true,
nextPage: 2,
swapName: "preview"
});
var frag = template(map);
var vm = can.viewModel(frag.firstChild);
var threads = [
function(){
equal(vm.attr("next"), 2, "has binidng");
equal(vm.attr("swap"), true, "swap - has binding");
equal(vm.attr("checked"), "", "attr - has binding");
map.attr("preview", false);
},
function(){
equal(vm.attr("swap"), false, "swap - updated binidng");
ok(vm.attr("checked") === null, "attr - value set to null");
map.attr("nextPage", 3);
equal(vm.attr("next"), 2, "not updating after binding is torn down");
map.attr("preview", true);
},
function(){
equal(vm.attr("next"), 3, "re-initialized with binding");
equal(vm.attr("swap"), true, "swap - updated binidng");
equal(vm.attr("checked"), "", "attr - has binding set again");
map.attr("swapName", "nextPage");
},
function(){
equal(vm.attr("swap"), 3, "swap - updated binding key");
map.attr("nextPage",4);
equal(vm.attr("swap"), 4, "swap - updated binding");
}
];
stop();
var index = 0;
var next = function(){
if(index < threads.length) {
threads[index]();
index++;
setTimeout(next, 10);
} else {
start();
}
};
setTimeout(next,10);
});
test("<content> (#2151)", function(){
can.Component.extend({
tag : 'list-items',
template : can.stache("<ul>"+
"{{#items}}"+
"{{#if render}}"+
"<li><content /></li>"+
"{{/if}}"+
"{{/items}}"+
"</ul>"),
viewModel : {
define : {
items : {
value : function() {
return new can.List([{
id : 1,
context : 'Item 1',
render : false
}, {
id : 2,
context : 'Item 2',
render : false
}]);
}
}
}
}
});
can.Component.extend({
tag : 'list-item',
template : can.stache("{{item.context}}")
});
var template = can.stache("<list-items><list-item item='{.}'/></list-items>");
var frag = template();
can.batch.start();
can.viewModel(frag.firstChild).attr('items').each(function(item, index) {
item.attr('render', true);
});
can.batch.stop();
var lis = frag.firstChild.getElementsByTagName("li");
ok( innerHTML(lis[0]).indexOf("Item 1") >= 0, "Item 1 written out");
ok( innerHTML(lis[1]).indexOf("Item 2") >= 0, "Item 2 written out");
});
test("out of order rendering (#2323)", function(){
var order = 0;
can.Component.extend({
tag: 'c1',
template: can.stache("c1"),
viewModel: {
inner: 'inner-initial'
},
events: {
"{viewModel} inner": function() {
equal(++order, 1, "got inner first");
}
}
});
can.Component.extend({
tag: 'c2',
template: can.stache("c2: <c1 {inner}='innerLink'></c2>"),
viewModel: {
outer: 'outer-initial',
innerLink: 'innerlink-initial'
},
events: {
"{viewModel} outer": function() {
can.batch.start(function() {
equal(++order, 2, "finished batch later");
});
this.viewModel.attr('innerLink', Math.random());
can.batch.stop();
}
}
});
var myMap = new can.Map({
"foo": 1
});
can.stache('foo: {{foo}} <c2 {outer}="foo"></c2>')(myMap);
myMap.attr('foo', 2);
});
}
makeTest("can/component new bindings dom", document);
});