can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
1,773 lines (1,353 loc) • 65.9 kB
JavaScript
steal("can/view/bindings", "can/map", "can/test", "can/component", "can/view/mustache", "can/view/stache", "steal-qunit", function () {
QUnit.module('can/view/bindings', {
setup: function () {
document.getElementById("qunit-fixture").innerHTML = "";
}
});
test("attributeNameInfo", function(){
// MUSTACHE BEHAVIOR
var info = can.bindings.getBindingInfo({name: "foo", value: "bar"},{foo: "@"},"legacy");
deepEqual(info,{
parent: "attribute",
child: "viewModel",
parentToChild: true,
childToParent: true,
childName: "foo",
parentName: "foo",
bindingAttributeName: "foo"
}, "legacy with @");
info = can.bindings.getBindingInfo({name: "foo-ed", value: "bar"},{},"legacy");
deepEqual(info, {
parent: "scope",
child: "viewModel",
parentToChild: true,
childToParent: true,
childName: "fooEd",
parentName: "bar",
bindingAttributeName: "foo-ed"
},"legacy");
// ORIGINAL STACHE BEHAVIOR
info = can.bindings.getBindingInfo({name: "foo-ed", value: "bar"});
deepEqual(info, {
parent: "attribute",
child: "viewModel",
parentToChild: true,
childToParent: true,
childName: "fooEd",
parentName: "foo-ed",
bindingAttributeName: "foo-ed"
}, "OG stache attr binding");
info = can.bindings.getBindingInfo({name: "foo-ed", value: "{bar}"});
deepEqual(info, {
parent: "scope",
child: "viewModel",
parentToChild: true,
childToParent: true,
childName: "fooEd",
parentName: "bar",
bindingAttributeName: "foo-ed"
}, "OG stache vm binding");
// NEW BINDINGS
// element based
info = can.bindings.getBindingInfo({name: "{$foo-ed}", value: "bar"});
deepEqual(info, {
parent: "scope",
child: "attribute",
childToParent: false,
parentToChild: true,
parentName: "bar",
childName: "foo-ed",
bindingAttributeName: "{$foo-ed}",
initializeValues: true
}, "new el binding");
info = can.bindings.getBindingInfo({name: "{($foo-ed)}", value: "bar"});
deepEqual(info, {
parent: "scope",
child: "attribute",
childToParent: true,
parentToChild: true,
parentName: "bar",
childName: "foo-ed",
bindingAttributeName: "{($foo-ed)}",
initializeValues: true
}, "new el binding");
info = can.bindings.getBindingInfo({name: "{^$foo-ed}", value: "bar"});
deepEqual(info, {
parent: "scope",
child: "attribute",
childToParent: true,
parentToChild: false,
parentName: "bar",
childName: "foo-ed",
bindingAttributeName: "{^$foo-ed}",
initializeValues: true
}, "new el binding");
// vm based
info = can.bindings.getBindingInfo({name: "{foo-ed}", value: "bar"});
deepEqual(info, {
parent: "scope",
child: "viewModel",
parentToChild: true,
childToParent: false,
childName: "fooEd",
parentName: "bar",
bindingAttributeName: "{foo-ed}",
initializeValues: true
}, "new vm binding");
info = can.bindings.getBindingInfo({name: "{(foo-ed)}", value: "bar"});
deepEqual(info, {
parent: "scope",
child: "viewModel",
parentToChild: true,
childToParent: true,
childName: "fooEd",
parentName: "bar",
bindingAttributeName: "{(foo-ed)}",
initializeValues: true
}, "new el binding");
info = can.bindings.getBindingInfo({name: "{^foo-ed}", value: "bar"});
deepEqual(info, {
parent: "scope",
child: "viewModel",
parentToChild: false,
childToParent: true,
childName: "fooEd",
parentName: "bar",
bindingAttributeName: "{^foo-ed}",
initializeValues: true
}, "new el binding");
});
var foodTypes = new can.List([{
title: "Fruits",
content: "oranges, apples"
}, {
title: "Breads",
content: "pasta, cereal"
}, {
title: "Sweets",
content: "ice cream, candy"
}]);
if(typeof document.getElementsByClassName === 'function') {
test("can-event handlers", function () {
//expect(12);
var ta = document.getElementById("qunit-fixture");
var template = can.view.stache("<div>" +
"{{#each foodTypes}}" +
"<p can-click='doSomething'>{{content}}</p>" +
"{{/each}}" +
"</div>");
function doSomething(foodType, el, ev) {
ok(true, "doSomething called");
equal(el[0].nodeName.toLowerCase(), "p", "this is the element");
equal(ev.type, "click", "1st argument is the event");
equal(foodType, foodTypes[0], "2nd argument is the 1st foodType");
}
var frag = template({
foodTypes: foodTypes,
doSomething: doSomething
});
ta.appendChild(frag);
var p0 = ta.getElementsByTagName("p")[0];
can.trigger(p0, "click");
});
test("can-event special keys", function(){
var scope = new can.Map({
test: "testval"
});
var ta = document.getElementById("qunit-fixture");
can.Component.extend({
tag: "can-event-args-tester",
scope: scope
});
var template = can.view.mustache("<div>" +
"{{#each foodTypes}}" +
"<can-event-args-tester class='with-args' can-click='{withArgs @event @element @viewModel @viewModel.test . title content=content}'/>" +
"{{/each}}" +
"</div>");
function withArgs(ev1, el1, compScope, testVal, context, title, hash) {
ok(true, "withArgs called");
equal(el1[0].nodeName.toLowerCase(), "can-event-args-tester", "@element is the event's DOM element");
equal(ev1.type, "click", "@event is the click event");
equal(scope, compScope, "Component scope accessible through @viewModel");
equal(testVal, scope.attr("test"), "Attributes accessible");
equal(context.title, foodTypes[0].title, "Context passed in");
equal(title, foodTypes[0].title, "Title passed in");
equal(hash.content, foodTypes[0].content, "Args with = passed in as a hash");
}
var frag = template({
foodTypes: foodTypes,
withArgs: withArgs
});
ta.innerHTML = "";
ta.appendChild(frag);
var p0 = ta.getElementsByClassName("with-args")[0];
can.trigger(p0, "click");
});
test("(event) handlers", function () {
//expect(12);
var ta = document.getElementById("qunit-fixture");
var template = can.view.stache("<div>" +
"{{#each foodTypes}}" +
"<p ($click)='doSomething'>{{content}}</p>" +
"{{/each}}" +
"</div>");
var foodTypes = new can.List([{
title: "Fruits",
content: "oranges, apples"
}, {
title: "Breads",
content: "pasta, cereal"
}, {
title: "Sweets",
content: "ice cream, candy"
}]);
function doSomething(foodType, el, ev) {
ok(true, "doSomething called");
equal(el[0].nodeName.toLowerCase(), "p", "this is the element");
equal(ev.type, "click", "1st argument is the event");
equal(foodType, foodTypes[0], "2nd argument is the 1st foodType");
}
var frag = template({
foodTypes: foodTypes,
doSomething: doSomething
});
ta.appendChild(frag);
var p0 = ta.getElementsByTagName("p")[0];
can.trigger(p0, "click");
var scope = new can.Map({
test: "testval"
});
can.Component.extend({
tag: "fancy-event-args-tester",
scope: scope
});
template = can.view.mustache("<div>" +
"{{#each foodTypes}}" +
"<fancy-event-args-tester class='with-args' (click)='withArgs @event @element @viewModel @viewModel.test . title content=content'/>" +
"{{/each}}" +
"</div>");
function withArgs(ev1, el1, compScope, testVal, context, title, hash) {
ok(true, "withArgs called");
equal(el1[0].nodeName.toLowerCase(), "fancy-event-args-tester", "@element is the event's DOM element");
equal(ev1.type, "click", "@event is the click event");
equal(scope, compScope, "Component scope accessible through @viewModel");
equal(testVal, scope.attr("test"), "Attributes accessible");
equal(context.title, foodTypes[0].title, "Context passed in");
equal(title, foodTypes[0].title, "Title passed in");
equal(hash.content, foodTypes[0].content, "Args with = passed in as a hash");
}
frag = template({
foodTypes: foodTypes,
withArgs: withArgs
});
ta.innerHTML = "";
ta.appendChild(frag);
p0 = ta.getElementsByClassName("with-args")[0];
can.trigger(p0, "click");
});
}
if (window.jQuery) {
test("can-event passes extra args to handler", function () {
expect(3);
var template = can.view.mustache("<p can-myevent='handleMyEvent'>{{content}}</p>");
var frag = template({
handleMyEvent: function(context, el, event, arg1, arg2) {
ok(true, "handleMyEvent called");
equal(arg1, "myarg1", "3rd argument is the extra event args");
equal(arg2, "myarg2", "4rd argument is the extra event args");
}
});
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var p0 = ta.getElementsByTagName("p")[0];
can.trigger(p0, "myevent", ["myarg1", "myarg2"]);
});
}
test("can-value input text", function () {
var template = can.view.stache("<input can-value='age'/>");
var map = new can.Map();
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var input = ta.getElementsByTagName("input")[0];
equal(input.value, "", "input value set correctly if key does not exist in map");
map.attr("age", "30");
equal(input.value, "30", "input value set correctly");
map.attr("age", "31");
equal(input.value, "31", "input value update correctly");
input.value = "32";
can.trigger(input, "change");
equal(map.attr("age"), "32", "updated from input");
});
test("can-value with spaces (#1477)", function () {
var template = can.view.stache("<input can-value='{ age }'/>");
var map = new can.Map();
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var input = ta.getElementsByTagName("input")[0];
equal(input.value, "", "input value set correctly if key does not exist in map");
map.attr("age", "30");
equal(input.value, "30", "input value set correctly");
map.attr("age", "31");
equal(input.value, "31", "input value update correctly");
input.value = "32";
can.trigger(input, "change");
equal(map.attr("age"), "32", "updated from input");
});
test("can-value input radio", function () {
var template = can.view.stache(
"<input type='radio' can-value='color' value='red'/> Red<br/>" +
"<input type='radio' can-value='color' value='green'/> Green<br/>");
var map = new can.Map({
color: "red"
});
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var inputs = ta.getElementsByTagName("input");
ok(inputs[0].checked, "first input checked");
ok(!inputs[1].checked, "second input not checked");
map.attr("color", "green");
ok(!inputs[0].checked, "first notinput checked");
ok(inputs[1].checked, "second input checked");
inputs[0].checked = true;
inputs[1].checked = false;
can.trigger(inputs[0], "change");
equal(map.attr("color"), "red", "updated from input");
});
test("can-enter", function () {
var template = can.view.stache("<input can-enter='update'/>");
var called = 0;
var frag = template({
update: function () {
called++;
ok(called, 1, "update called once");
}
});
var input = frag.childNodes[0];
can.trigger(input, {
type: "keyup",
keyCode: 38
});
can.trigger(input, {
type: "keyup",
keyCode: 13
});
});
test("two bindings on one element call back the correct method", function () {
expect(2);
var template = can.stache("<input can-mousemove='first' can-click='second'/>");
var callingFirst = false,
callingSecond = false;
var frag = template({
first: function () {
ok(callingFirst, "called first");
},
second: function () {
ok(callingSecond, "called second");
}
});
var input = frag.childNodes[0];
callingFirst = true;
can.trigger(input, {
type: "mousemove"
});
callingFirst = false;
callingSecond = true;
can.trigger(input, {
type: "click"
});
});
asyncTest("can-value select remove from DOM", function () {
expect(1);
var template = can.view.stache(
"<select can-value='color'>" +
"<option value='red'>Red</option>" +
"<option value='green'>Green</option>" +
"</select>"),
frag = template(),
ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
can.remove(can.$("select", ta));
setTimeout(function () {
start();
ok(true, 'Nothing should break if we just add and then remove the select');
}, 10);
});
test("checkboxes with can-value bind properly (#628)", function () {
var data = new can.Map({
completed: true
}),
frag = can.view.stache('<input type="checkbox" can-value="completed"/>')(data);
can.append(can.$("#qunit-fixture"), frag);
var input = can.$("#qunit-fixture")[0].getElementsByTagName('input')[0];
equal(input.checked, data.attr('completed'), 'checkbox value bound (via attr check)');
data.attr('completed', false);
equal(input.checked, data.attr('completed'), 'checkbox value bound (via attr uncheck)');
input.checked = true;
can.trigger(input, 'change');
equal(input.checked, true, 'checkbox value bound (via check)');
equal(data.attr('completed'), true, 'checkbox value bound (via check)');
input.checked = false;
can.trigger(input, 'change');
equal(input.checked, false, 'checkbox value bound (via uncheck)');
equal(data.attr('completed'), false, 'checkbox value bound (via uncheck)');
});
// TODO: next
test("checkboxes with can-true-value bind properly", function () {
var data = new can.Map({
sex: "male"
}),
frag = can.view.stache('<input type="checkbox" can-value="sex" can-true-value="male" can-false-value="female"/>')(data);
can.append(can.$("#qunit-fixture"), frag);
var input = can.$("#qunit-fixture")[0].getElementsByTagName('input')[0];
equal(input.checked, true, 'checkbox value bound (via attr check)');
data.attr('sex', 'female');
equal(input.checked, false, 'checkbox value unbound (via attr uncheck)');
input.checked = true;
can.trigger(input, 'change');
equal(input.checked, true, 'checkbox value bound (via check)');
equal(data.attr('sex'), 'male', 'checkbox value bound (via check)');
input.checked = false;
can.trigger(input, 'change');
equal(input.checked, false, 'checkbox value bound (via uncheck)');
equal(data.attr('sex'), 'female', 'checkbox value bound (via uncheck)');
});
test("can-value select single", function () {
var template = can.view.stache(
"<select can-value='color'>" +
"<option value='red'>Red</option>" +
"<option value='green'>Green</option>" +
"</select>");
var map = new can.Map({
color: "red"
});
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var inputs = ta.getElementsByTagName("select");
equal(inputs[0].value, 'red', "default value set");
map.attr("color", "green");
equal(inputs[0].value, 'green', "alternate value set");
can.each(document.getElementsByTagName('option'), function (opt) {
if (opt.value === 'red') {
opt.selected = 'selected';
}
});
equal(map.attr("color"), "green", "not yet updated from input");
can.trigger(inputs[0], "change");
equal(map.attr("color"), "red", "updated from input");
can.each(document.getElementsByTagName('option'), function (opt) {
if (opt.value === 'green') {
opt.selected = 'selected';
}
});
equal(map.attr("color"), "red", "not yet updated from input");
can.trigger(inputs[0], "change");
equal(map.attr("color"), "green", "updated from input");
});
test("can-value select multiple with values seperated by a ;", function () {
var template = can.view.stache(
"<select can-value='color' multiple>" +
"<option value='red'>Red</option>" +
"<option value='green'>Green</option>" +
"<option value='ultraviolet'>Ultraviolet</option>" +
"</select>");
var map = new can.Map({
color: "red"
});
stop();
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var inputs = ta.getElementsByTagName("select"),
options = inputs[0].getElementsByTagName('option');
// Wait for Multiselect.set() to be called.
setTimeout(function() {
equal(inputs[0].value, 'red', "default value set");
map.attr("color", "green");
equal(inputs[0].value, 'green', "alternate value set");
options[0].selected = true;
equal(map.attr("color"), "green", "not yet updated from input");
can.trigger(inputs[0], "change");
equal(map.attr("color"), "red;green", "updated from input");
map.removeAttr("color");
equal(inputs[0].value, '', "attribute removed from map");
options[1].selected = true;
can.trigger(inputs[0], "change");
equal(map.attr("color"), "green", "updated from input");
map.attr("color", "red;green");
ok(options[0].selected, 'red option selected from map');
ok(options[1].selected, 'green option selected from map');
ok(!options[2].selected, 'ultraviolet option NOT selected from map');
can.remove(can.$(inputs));
start();
}, 1);
});
test("can-value select multiple with values cross bound to an array", function () {
var template = can.view.stache(
"<select can-value='colors' multiple>" +
"<option value='red'>Red</option>" +
"<option value='green'>Green</option>" +
"<option value='ultraviolet'>Ultraviolet</option>" +
"</select>");
var map = new can.Map({});
stop();
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var select = ta.getElementsByTagName("select")[0],
options = select.getElementsByTagName('option');
// Wait for Multiselect.set() to be called.
setTimeout(function(){
// Test updating the DOM changes observable values
options[0].selected = true;
can.trigger(select, "change");
deepEqual(map.attr("colors")
.attr(), ["red"], "A can.List property is set even if none existed");
options[1].selected = true;
can.trigger(select, "change");
deepEqual(map.attr("colors")
.attr(), ["red", "green"], "Adds items to the list");
options[0].selected = false;
can.trigger(select, "change");
deepEqual(map.attr("colors")
.attr(), ["green"], "Removes items from the list");
// Test changing observable values changes the DOM
map.attr("colors")
.push("ultraviolet");
options[0].selected = false;
options[1].selected = true;
options[2].selected = true;
can.remove(can.$(select));
start();
}, 1);
});
test("can-value multiple select with a can.List", function () {
var template = can.view.stache(
"<select can-value='colors' multiple>" +
"<option value='red'>Red</option>" +
"<option value='green'>Green</option>" +
"<option value='ultraviolet'>Ultraviolet</option>" +
"</select>");
var list = new can.List();
stop();
var frag = template({
colors: list
});
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var select = ta.getElementsByTagName("select")[0],
options = select.getElementsByTagName('option');
// Wait for Multiselect.set() to be called.
setTimeout(function(){
// Test updating the DOM changes observable values
options[0].selected = true;
can.trigger(select, "change");
deepEqual(list.attr(), ["red"], "A can.List property is set even if none existed");
options[1].selected = true;
can.trigger(select, "change");
deepEqual(list.attr(), ["red", "green"], "Adds items to the list");
options[0].selected = false;
can.trigger(select, "change");
deepEqual(list.attr(), ["green"], "Removes items from the list");
// Test changing observable values changes the DOM
list.push("ultraviolet");
options[0].selected = false;
options[1].selected = true;
options[2].selected = true;
can.remove(can.$(select));
start();
}, 1);
});
test("can-value contenteditable", function () {
var template = can.view.stache("<div id='cdiv' contenteditable can-value='age'></div>");
var map = new can.Map();
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var div = document.getElementById("cdiv");
equal(div.innerHTML, "", "contenteditable set correctly if key does not exist in map");
map.attr("age", "30");
equal(div.innerHTML, "30", "contenteditable set correctly");
map.attr("age", "31");
equal(div.innerHTML, "31", "contenteditable update correctly");
div.innerHTML = "32";
can.trigger(div, "blur");
equal(map.attr("age"), "32", "updated from contenteditable");
});
test("can-event handlers work with {} (#905)", function () {
expect(4);
var template = can.stache("<div>" +
"{{#each foodTypes}}" +
"<p can-click='{doSomething}'>{{content}}</p>" +
"{{/each}}" +
"</div>");
var foodTypes = new can.List([{
title: "Fruits",
content: "oranges, apples"
}, {
title: "Breads",
content: "pasta, cereal"
}, {
title: "Sweets",
content: "ice cream, candy"
}]);
var doSomething = function (foodType, el, ev) {
ok(true, "doSomething called");
equal(el[0].nodeName.toLowerCase(), "p", "this is the element");
equal(ev.type, "click", "1st argument is the event");
equal(foodType, foodTypes[0], "2nd argument is the 1st foodType");
};
var frag = template({
foodTypes: foodTypes,
doSomething: doSomething
});
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var p0 = ta.getElementsByTagName("p")[0];
can.trigger(p0, "click");
});
test("can-value works with {} (#905)", function () {
var template = can.stache("<input can-value='{age}'/>");
var map = new can.Map();
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var input = ta.getElementsByTagName("input")[0];
equal(input.value, "", "input value set correctly if key does not exist in map");
map.attr("age", "30");
equal(input.value, "30", "input value set correctly");
map.attr("age", "31");
equal(input.value, "31", "input value update correctly");
input.value = "32";
can.trigger(input, "change");
equal(map.attr("age"), "32", "updated from input");
});
test("can-value select with null or undefined value (#813)", function () {
var template = can.view.stache(
"<select id='null-select' can-value='color-1'>" +
"<option value=''>Choose</option>" +
"<option value='red'>Red</option>" +
"<option value='green'>Green</option>" +
"</select>" +
"<select id='undefined-select' can-value='color-2'>" +
"<option value=''>Choose</option>" +
"<option value='red'>Red</option>" +
"<option value='green'>Green</option>" +
"</select>");
var map = new can.Map({
'color-1': null,
'color-2': undefined
});
stop();
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var nullInput = document.getElementById("null-select");
var nullInputOptions = nullInput.getElementsByTagName('option');
var undefinedInput = document.getElementById("undefined-select");
var undefinedInputOptions = undefinedInput.getElementsByTagName('option');
// wait for set to be called which will change the selects
setTimeout(function(){
ok(nullInputOptions[0].selected, "default (null) value set");
ok(undefinedInputOptions[0].selected, "default (undefined) value set");
start();
}, 1);
});
test('radio type conversion (#811)', function(){
var data = new can.Map({
id: 1
}),
frag = can.view.stache('<input type="radio" can-value="id" value="1"/>')(data);
can.append(can.$('#qunit-fixture'), frag);
var input = can.$('#qunit-fixture')[0].getElementsByTagName('input')[0];
ok(input.checked, 'checkbox value bound');
});
test("template with view binding breaks in stache, not in mustache (#966)", function(){
var templateString = '<a href="javascript://" can-click="select">'+
'{{#if thing}}\n<div />{{/if}}'+
'<span>{{name}}</span>'+
'</a>';
//var mustacheRenderer = can.mustache(templateString);
var stacheRenderer = can.stache(templateString);
var obj = new can.Map({thing: 'stuff'});
stacheRenderer(obj);
ok(true, 'stache worked without errors');
});
test("can-event throws an error when inside #if block (#1182)", function(){
var flag = can.compute(false),
clickHandlerCount = 0;
var frag = can.view.mustache("<div {{#if flag}}can-click='foo'{{/if}}>Click</div>")({
flag: flag,
foo: function () {
clickHandlerCount++;
}
});
var trig = function(){
var div = can.$('#qunit-fixture')[0].getElementsByTagName('div')[0];
can.trigger(div, {
type: "click"
});
};
can.append(can.$('#qunit-fixture'), frag);
trig();
equal(clickHandlerCount, 0, "click handler not called");
});
test("can-EVENT removed in live bindings doesn't unbind (#1112)", function(){
var flag = can.compute(true),
clickHandlerCount = 0;
var frag = can.view.mustache("<div {{#if flag}}can-click='foo'{{/if}}>Click</div>")({
flag: flag,
foo: function () {
clickHandlerCount++;
}
});
var trig = function(){
var div = can.$('#qunit-fixture')[0].getElementsByTagName('div')[0];
can.trigger(div, {
type: "click"
});
};
can.append(can.$('#qunit-fixture'), frag);
trig();
flag(false);
trig();
flag(true);
trig();
equal(clickHandlerCount, 2, "click handler called twice");
});
test("can-value compute rejects new value (#887)", function() {
var template = can.view.mustache("<input can-value='age'/>");
// Compute only accepts numbers
var compute = can.compute(30, function(newVal, oldVal) {
if(isNaN(+newVal)) {
return oldVal;
} else {
return +newVal;
}
});
var frag = template({
age: compute
});
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var input = ta.getElementsByTagName("input")[0];
// Set to non-number
input.value = "30f";
can.trigger(input, "change");
equal(compute(), 30, "Still the old value");
equal(input.value, "30", "Text input has also not changed");
});
test("can-value select multiple applies initial value, when options rendered from array (#1414)", function () {
var template = can.view.mustache(
"<select can-value='colors' multiple>" +
"{{#each allColors}}<option value='{{value}}'>{{label}}</option>{{/each}}" +
"</select>");
var map = new can.Map({
colors: ["red", "green"],
allColors: [
{ value: "red", label: "Red"},
{ value: "green", label: "Green"},
{ value: "blue", label: "Blue"}
]
});
stop();
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var select = ta.getElementsByTagName("select")[0],
options = select.getElementsByTagName("option");
// Wait for Multiselect.set() to be called.
setTimeout(function(){
ok(options[0].selected, "red should be set initially");
ok(options[1].selected, "green should be set initially");
ok(!options[2].selected, "blue should not be set initially");
start();
}, 1);
});
test('can-value with truthy and falsy values binds to checkbox (#1478)', function() {
var data = new can.Map({
completed: 1
}),
frag = can.view.stache('<input type="checkbox" can-value="completed"/>')(data);
can.append(can.$("#qunit-fixture"), frag);
var input = can.$("#qunit-fixture")[0].getElementsByTagName('input')[0];
equal(input.checked, true, 'checkbox value bound (via attr check)');
data.attr('completed', 0);
equal(input.checked, false, 'checkbox value bound (via attr check)');
});
test("can-EVENT can call intermediate functions before calling the final function (#1474)", function () {
var ta = document.getElementById("qunit-fixture");
var template = can.view.stache("<div id='click-me' can-click='{does.some.thing}'></div>");
var frag = template({
does: function(){
return {
some: function(){
return {
thing: function(context) {
ok(can.isFunction(context.does));
start();
}
};
}
};
}
});
stop();
ta.appendChild(frag);
can.trigger(document.getElementById("click-me"), "click");
});
test("by default can-EVENT calls with values, not computes", function(){
stop();
var ta = document.getElementById("qunit-fixture");
var template = can.stache("<div id='click-me' can-click='{map.method one map.two map.three}'></div>");
var one = can.compute(1);
var three = can.compute(3);
var MyMap = can.Map.extend({
method: function(ONE, two, three){
equal(ONE, 1);
equal(two, 2);
equal(three, 3);
equal(this, map, "this set right");
start();
}
});
var map = new MyMap({"two": 2, "three": three});
var frag = template({one: one, map: map});
ta.appendChild(frag);
can.trigger(document.getElementById("click-me"), "click");
});
test('Conditional can-EVENT bindings are bound/unbound', 2, function () {
var state = new can.Map({
enableClick: true,
clickHandler: function () {
ok(true, '"click" was handled');
}
});
var template = can.stache('<button id="find-me" {{#if enableClick}}can-click="{clickHandler}"{{/if}}></button>');
var frag = template(state);
var sandbox = document.getElementById("qunit-fixture");
sandbox.appendChild(frag);
var btn = document.getElementById('find-me');
can.trigger(btn, 'click');
state.attr('enableClick', false);
stop();
setTimeout(function() {
can.trigger(btn, 'click');
state.attr('enableClick', true);
setTimeout(function() {
can.trigger(btn, 'click');
start();
}, 10);
}, 10);
});
test("<select can-value={value}> with undefined value selects option without value", function () {
var template = can.view.stache("<select can-value='opt'><option>Loading...</option></select>");
var map = new can.Map();
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var select = ta.childNodes[0];
QUnit.equal(select.selectedIndex, 0, 'Got selected index');
});
test("<select can-value> keeps its value as <option>s change with {{#list}} (#1762)", function(){
var template = can.view.stache("<select can-value='{id}'>{{#values}}<option value='{{.}}'>{{.}}</option>{{/values}}</select>");
var values = can.compute( ["1","2","3","4"]);
var id = can.compute("2");
var frag = template({
values: values,
id: id
});
stop();
var select = frag.firstChild;
// the value is set asynchronously
setTimeout(function(){
ok(select.childNodes[1].selected, "value is initially selected");
values(["7","2","5","4"]);
ok(select.childNodes[1].selected, "after changing options, value should still be selected");
start();
},20);
});
test("<select can-value> keeps its value as <option>s change with {{#each}} (#1762)", function(){
var template = can.view.stache("<select can-value='{id}'>{{#each values}}<option value='{{.}}'>{{.}}</option>{{/values}}</select>");
var values = can.compute( ["1","2","3","4"]);
var id = can.compute("2");
var frag = template({
values: values,
id: id
});
stop();
var select = frag.firstChild;
// the value is set asynchronously
setTimeout(function(){
ok(select.childNodes[1].selected, "value is initially selected");
values(["7","2","5","4"]);
ok(select.childNodes[1].selected, "after changing options, value should still be selected");
start();
},20);
});
test("(event) methods on objects are called (#1839)", function(){
var template = can.stache("<div ($click)='setSomething person.message'/>");
var data = {
setSomething: function(message){
equal(message, "Matthew P finds good bugs");
equal(this, data, "setSomething called with correct scope");
},
person: {
name: "Matthew P",
message: function(){
return this.name + " finds good bugs";
}
}
};
var frag = template(data);
can.trigger( frag.firstChild, "click" );
});
test("(event) methods on objects are called with call expressions (#1839)", function(){
var template = can.stache("<div ($click)='setSomething(person.message)'/>");
var data = {
setSomething: function(message){
equal(message, "Matthew P finds good bugs");
equal(this, data, "setSomething called with correct scope");
},
person: {
name: "Matthew P",
message: function(){
return this.name + " finds good bugs";
}
}
};
var frag = template(data);
can.trigger( frag.firstChild, "click" );
});
test("two way - viewModel (#1700)", function(){
can.Component.extend({
tag: "view-model-able"
});
var template = can.stache("<div {(view-model-prop)}='scopeProp'/>");
var attrSetCalled = 0;
var map = new can.Map({scopeProp: "Hello"});
var oldAttr = map.attr;
map.attr = function(attrName, value){
if(typeof attrName === "string" && arguments.length > 1) {
attrSetCalled++;
}
return oldAttr.apply(this, arguments);
};
var frag = template(map);
var viewModel = can.viewModel(frag.firstChild);
equal(attrSetCalled, 0, "set is not called on scope map");
equal( viewModel.attr("viewModelProp"), "Hello", "initial value set" );
viewModel = can.viewModel(frag.firstChild);
var viewModelAttrSetCalled = 1;
viewModel.attr = function(attrName){
if(typeof attrName === "string" && arguments.length > 1) {
viewModelAttrSetCalled++;
}
return oldAttr.apply(this, arguments);
};
viewModel.attr("viewModelProp","HELLO");
equal(map.attr("scopeProp"), "HELLO", "binding from child to parent");
equal(attrSetCalled, 1, "set is called once on scope map");
equal(viewModelAttrSetCalled, 3, "set is called once viewModel");
map.attr("scopeProp","WORLD");
equal( viewModel.attr("viewModelProp"), "WORLD", "binding from parent to child" );
equal(attrSetCalled, 2, "set is called once on scope map");
equal(viewModelAttrSetCalled, 4, "set is called once on viewModel");
});
// new two-way binding
test("two-way - DOM - input text (#1700)", function () {
var template = can.view.stache("<input {($value)}='age'/>");
var map = new can.Map();
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var input = ta.getElementsByTagName("input")[0];
equal(input.value, "", "input value set correctly if key does not exist in map");
map.attr("age", "30");
equal(input.value, "30", "input value set correctly");
map.attr("age", "31");
equal(input.value, "31", "input value update correctly");
input.value = "32";
can.trigger(input, "change");
equal(map.attr("age"), "32", "updated from input");
});
test('two-way - DOM - {($checked)} with truthy and falsy values binds to checkbox (#1700)', function() {
var data = new can.Map({
completed: 1
}),
frag = can.view.stache('<input type="checkbox" {($checked)}="completed"/>')(data);
can.append(can.$("#qunit-fixture"), frag);
var input = can.$("#qunit-fixture")[0].getElementsByTagName('input')[0];
equal(input.checked, true, 'checkbox value bound (via attr check)');
data.attr('completed', 0);
equal(input.checked, false, 'checkbox value bound (via attr check)');
});
test('two-way - reference - {(child)}="*ref" (#1700)', function(){
var data = new can.Map({person: {name: {}}});
can.Component.extend({
tag: 'reference-export',
viewModel: {tag: 'reference-export'}
});
can.Component.extend({
tag: 'ref-import',
viewModel: {tag: 'ref-import'}
});
var template = can.stache("<reference-export {(name)}='*refName'/>"+
"<ref-import {(name)}='*refName'/> {{helperToGetScope}}");
var scope;
var frag = template(data,{
helperToGetScope: function(options){
scope = options.scope;
}
});
var refExport = can.viewModel(frag.firstChild);
var refImport = can.viewModel(frag.firstChild.nextSibling);
refExport.attr("name","v1");
equal( scope.getRefs()._context.attr("*refName"), "v1", "reference scope updated");
equal(refImport.attr("name"),"v1", "updated ref-import");
refImport.attr("name","v2");
equal(refExport.attr("name"),"v2", "updated ref-export");
equal( scope.getRefs()._context.attr("*refName"), "v2", "actually put in refs scope");
});
test('two-way - reference - with <content> tag', function(){
can.Component.extend({
tag: "other-export",
viewModel: {
name: "OTHER-EXPORT"
}
});
can.Component.extend({
tag: "ref-export",
template: can.stache('<other-export {(name)}="*otherExport"/><content>{{*otherExport}}</content>')
});
// this should have otherExport name in the page
var t1 = can.stache("<ref-export></ref-export>");
// this should not have anything in 'one', but something in 'two'
//var t2 = can.stache("<form><other-export *other/><ref-export><b>{{*otherExport.name}}</b><label>{{*other.name}}</label></ref-export></form>");
var f1 = t1();
equal(can.viewModel( f1.firstChild.firstChild ).attr("name"), "OTHER-EXPORT", "viewModel set correctly");
equal(f1.firstChild.lastChild.nodeValue, "OTHER-EXPORT", "content");
/*var f2 = t2();
var one = f2.firstChild.getElementsByTagName('b')[0];
var two = f2.firstChild.getElementsByTagName('label')[0];
equal(one.firstChild.nodeValue, "", "external content, internal export");
equal(two.firstChild.nodeValue, "OTHER-EXPORT", "external content, external export");*/
});
test('two-way - reference shorthand (#1700)', function(){
var data = new can.Map({person: {name: {}}});
can.Component.extend({
tag: 'reference-export',
template: can.stache('<span>{{*referenceExport.name}}</span>'),
viewModel: {}
});
var template = can.stache('{{#person}}{{#name}}'+
"<reference-export *reference-export/>"+
"{{/name}}{{/person}}<span>{{*referenceExport.name}}</span>");
var frag = template(data);
var refExport = can.viewModel(frag.firstChild);
refExport.attr("name","done");
equal( frag.lastChild.firstChild.nodeValue, "done");
equal( frag.firstChild.firstChild.firstChild.nodeValue, "", "not done");
});
test('one-way - parent to child - viewModel', function(){
var template = can.stache("<div {view-model-prop}='scopeProp'/>");
var map = new can.Map({scopeProp: "Venus"});
var frag = template(map);
var viewModel = can.viewModel(frag.firstChild);
equal( viewModel.attr("viewModelProp"), "Venus", "initial value set" );
viewModel.attr("viewModelProp","Earth");
equal(map.attr("scopeProp"), "Venus", "no binding from child to parent");
map.attr("scopeProp","Mars");
equal( viewModel.attr("viewModelProp"), "Mars", "binding from parent to child" );
});
test('one-way - child to parent - viewModel', function(){
can.Component.extend({
tag: "view-model-able",
viewModel: {
viewModelProp: "Mercury"
}
});
var template = can.stache("<view-model-able {^view-model-prop}='scopeProp'/>");
var map = new can.Map({scopeProp: "Venus"});
var frag = template(map);
var viewModel = can.viewModel(frag.firstChild);
equal( viewModel.attr("viewModelProp"), "Mercury", "initial value kept" );
equal( map.attr("scopeProp"), "Mercury", "initial value set on parent" );
viewModel.attr("viewModelProp","Earth");
equal(map.attr("scopeProp"), "Earth", "binding from child to parent");
map.attr("scopeProp","Mars");
equal( viewModel.attr("viewModelProp"), "Earth", "no binding from parent to child" );
});
test('one way - child to parent - importing viewModel {^.}="test"', function() {
can.Component.extend({
tag: 'import-scope',
template: can.stache('Hello {{name}}'),
viewModel: {
name: 'David',
age: 7
}
});
can.Component.extend({
tag: 'import-parent',
template: can.stache('<import-scope {^.}="test"></import-scope>' +
'<div>Imported: {{test.name}} {{test.age}}</div>')
});
var template = can.stache('<import-parent></import-parent>');
var frag = template({});
equal(frag.childNodes[0].childNodes[1].innerHTML,
'Imported: David 7',
'{.} component scope imported into variable');
});
test('one way - child to parent - importing viewModel {^prop}="test"', function() {
can.Component.extend({
tag: 'import-prop-scope',
template: can.stache('Hello {{name}}'),
viewModel: {
name: 'David',
age: 7
}
});
can.Component.extend({
tag: 'import-prop-parent',
template: can.stache('<import-prop-scope {^name}="test"></import-prop-scope>' +
'<div>Imported: {{test}}</div>')
});
var template = can.stache('<import-prop-parent></import-prop-parent>');
var frag = template({});
equal(frag.childNodes[0].childNodes[1].innerHTML,
'Imported: David', '{name} component scope imported into variable');
});
test('one way - child to parent - importing viewModel {^hypenated-prop}="test"', function(){
can.Component.extend({
tag: 'import-prop-scope',
template: can.stache('Hello {{userName}}'),
viewModel: {
userName: 'David',
age: 7,
updateName: function(){
this.attr('userName', 'Justin');
}
}
});
can.Component.extend({
tag: 'import-prop-parent',
template: can.stache('<import-prop-scope {^user-name}="test" {^.}="childComponent"></import-prop-scope>' +
'<div>Imported: {{test}}</div>')
});
var template = can.stache('<import-prop-parent></import-prop-parent>');
var frag = template({});
var importPropParent = frag.firstChild;
var importPropScope = importPropParent.getElementsByTagName("import-prop-scope")[0];
can.viewModel(importPropScope).updateName();
var importPropParentViewModel = can.viewModel(importPropParent);
equal(importPropParentViewModel.attr("test"), "Justin", "got hypenated prop");
equal(importPropParentViewModel.attr("childComponent"), can.viewModel(importPropScope), "got view model");
});
test("one-way - child to parent - parent that does not leak scope, but has no template", function(){
can.Component.extend({
tag: "outer-noleak",
viewModel: {
isOuter: true
},
leakScope: false
});
can.Component.extend({
tag: "my-child",
viewModel : {
isChild: true
},
leakScope: false
});
var template = can.stache("<outer-noleak><my-child {^.}='myChild'/></outer-noleak>");
var frag = template();
var vm = can.viewModel(frag.firstChild);
ok(vm.attr("myChild") instanceof can.Map, "got instance");
});
test("viewModel binding (event)", function(){
can.Component.extend({
tag: "viewmodel-binding",
viewModel: {
makeMyEvent: function(){
this.dispatch("myevent");
}
}
});
var frag = can.stache("<viewmodel-binding (myevent)='doSomething()'/>")({
doSomething: function(){
ok(true, "called!");
}
});
can.viewModel(frag.firstChild).makeMyEvent();
});
test("checkboxes with {($checked)} bind properly", function () {
var data = new can.Map({
completed: true
}),
frag = can.view.stache('<input type="checkbox" {($checked)}="completed"/>')(data);
can.append(can.$("#qunit-fixture"), frag);
var input = can.$("#qunit-fixture")[0].getElementsByTagName('input')[0];
equal(input.checked, data.attr('completed'), 'checkbox value bound (via attr check)');
data.attr('completed', false);
equal(input.checked, data.attr('completed'), 'checkbox value bound (via attr uncheck)');
input.checked = true;
can.trigger(input, 'change');
equal(input.checked, true, 'checkbox value bound (via check)');
equal(data.attr('completed'), true, 'checkbox value bound (via check)');
input.checked = false;
can.trigger(input, 'change');
equal(input.checked, false, 'checkbox value bound (via uncheck)');
equal(data.attr('completed'), false, 'checkbox value bound (via uncheck)');
});
test("two-way element empty value (1996)", function(){
var template = can.stache("<input can-value='age'/>");
var map = new can.Map();
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var input = ta.getElementsByTagName("input")[0];
equal(input.value, "", "input value set correctly if key does not exist in map");
map.attr("age", "30");
equal(input.value, "30", "input value set correctly");
map.attr("age", "31");
equal(input.value, "31", "input value update correctly");
input.value = "";
can.trigger(input, "change");
equal(map.attr("age"), "", "updated from input");
});
test("two-way text input form reset (#2249)", function(){
var template = can.stache(
"<form id='test-form'>" +
"<input value='' can-value='name'/>" +
"<input value='Biff' can-value='name' />" +
"<input />" +
"<input type='checkbox' can-value='unchecked' checked/>" +
"<input type='checkbox' can-value='checked' checked='false'/>" +
"<input type='radio' can-value='radio' value='one' checked>" +
"<input type='radio' can-value='radio' value='two'>" +
"</form>");
var map = new can.Map({
name: "Marty McFly",
unchecked: false,
checked: true,
radio: "two"
});
var frag = template(map);
var ta = document.getElementById("qunit-fixture");
ta.appendChild(frag);
var els = ta.getElementsByTagName("input");
var withBinding = els[0],
withDefaultValue = els[1],
noBinding = els[2],
unchecked = els[3],
checked = els[4],
radioOne = els[5],
radioTwo = els[6];
// set a value different from initial value in tag
noBinding.value = "Lorraine Baines McFly";
equal(withBinding.value, "Marty McFly", "initial value with binding displayed");
equal(withDefaultValue.value, "Marty McFly", "initial value with binding displayed when default value given");
equal(noBinding.value, "Lorraine Baines McFly", "initial value without binding displayed");
ok(!unchecked.checked && checked.checked, "checkboxes set with can-value are correctly set");
ok(!radioOne.checked && radioTwo.checked, "radio buttons set with can-value is correctly set");
var form = ta.getElementsByTagName("form")[0];
form.reset();
equal(withBinding.value, "", "value with binding removed on form reset");
equal(withDefaultValue.value, "Biff", "if default value is given in value tag, reset to that");
equal(noBinding.value, "", "value without binding removed on form reset");
ok(unchecked.checked, "checkbox with default value of checked is reset");
ok(checked.checked, "checkbox with explicit default value of unchecked is reset");
ok(radioOne.checked && radioTwo.checked, "radio buttons are reset");
});
test("exporting methods (#2051)", function(){
expect(2);
can.Component.extend({
tag : 'foo-bar',
viewModel : {
method : function() {
ok(true, "foo called");
return 5;
}
}
});
var template = can.stache("<foo-bar {^@method}='@*refKey'></foo-bar>{{*refKey()}}");
var frag = template({});
equal( frag.lastChild.nodeValue, "5");
});
test("renders dynamic custom attributes (#1800)", function () {
var template = can.view.stache("<ul>{{#actions}}<li can-click='{{.}}'>{{.}}</li>{{/actions}}</ul>");
var map = new can.Map({
actions: ["action1", "action2"],
action1: function(){
equal(calling, 0,"action1");
},
action2: function(){
equal(calling, 1,"action2");
}
});
var frag = template(map),
lis = frag.firstChild.getElementsByTagName("li");
var calling = 0;
can.trigger(lis[0], "click");
calling = 1;
can.trigger(lis[1], "click");
});
//!steal-remove-start
if (can.dev) {
test("warning on a mismatched quote (#1995)", function () {
expect(4);
var oldlog = can.dev.warn,
message = 'can/view/bindings/bindings.js: mismatched binding syntax - (foo}';
can.dev.warn = function (text) {
equal(text, message, 'Got expected message logged.');
};
can.stache("<div (foo}='bar'/>")();
message = 'can/view/bindings/bindings.js: mismatched binding syntax - {foo)';
can.stache("<div {foo)='bar'/>")();
message = 'can/view/bindings/bindings.js: mismatched binding syntax - {(foo})';
can.stache("<div {(foo})='bar'/>")();
message = 'can/view/bindings/bindings.js: mismatched binding syntax - ({foo})';
can.stache("<div ({foo})='bar'/>")();
can.dev.warn = oldlog;
});
}
//!steal-remove-end
test("One way binding from a select's value to a parent compute updates the parent with the select's initial value (#2027)", function(){
var template = can.stache("<select {^$value}='value'><option value='One'>One</option></select>");
var map = new can.Map();
var frag = template(map);
var select = frag.childNodes.item(0);
setTimeout(function(){
equal(select.selectedIndex, 0, "selectedIndex is 0 because no value exists on the map");
equal(map.attr("value"), "One", "The map's value property is set to the select's value");
start();
},1);
stop();
});
test("two way binding from a select's value to null has no selection (#2027)", function(){
var template = can.stache("<select {($value)}='key'><option value='One'>One</option></select>");
var map = new can.Map({key: null});
var frag = template(map);
var select = frag.childNodes.item(0);
setTimeout(function(){
equal(select.selectedIndex, -1, "selectedIndex is 0 because no value exists on the map");
equal(map.attr("key"), null, "The map's value property is set to the select's value");
start();
},1);
stop();
});
test('two-way bound values that do not match a select option set selectedIndex to -1 (#2027)', function() {
var renderer = can.view.stache('<select {($value)}="key"><option value="foo">foo</option><option value="bar">bar</option></select>');
var map = new can.Map({ });
var frag = renderer(map);
equal(frag.firstChild.selectedIndex, 0, 'undefined <- {($first value)}: selectedIndex = 0');
map.attr('key', 'notfoo');
equal(frag.firstChild.selectedIndex, -1, 'notfoo: selectedIndex = -1');
map.attr('key', 'foo');
strictEqual(frag.firstChild.selectedIndex, 0, 'foo: selectedIndex = 0');
map.attr('key', 'notbar');
equal(frag.firstChild.selectedIndex, -1, 'notbar: selectedIndex = -1');
map.attr('key', 'bar');
stri