UNPKG

can-stache-bindings

Version:

Default binding syntaxes for can-stache

1,335 lines (987 loc) 41.6 kB
var QUnit = require('steal-qunit'); var testHelpers = require('../helpers'); var stache = require('can-stache'); var stacheBindings = require('can-stache-bindings'); var SimpleMap = require("can-simple-map"); var DefineList = require("can-define/list/list"); var MockComponent = require("../mock-component-simple-map"); var canViewModel = require('can-view-model'); var SimpleObservable = require("can-simple-observable"); var canSymbol = require('can-symbol'); var canReflect = require('can-reflect'); var domMutate = require('can-dom-mutate'); var domMutateNode = require('can-dom-mutate/node'); var domEvents = require('can-dom-events'); var DefineMap = require("can-define/map/map"); stache.addBindings(stacheBindings); testHelpers.makeTests("can-stache-bindings - colon - element", function(name, doc, enableMO, testIfRealDocument){ QUnit.test("<input text> value:bind input text", function(assert) { var template = stache("<input value:bind='age'/>"); var map = new SimpleMap(); var frag = template(map); var ta = this.fixture; ta.appendChild(frag); var input = ta.getElementsByTagName("input")[0]; assert.equal(input.value, "", "input value set correctly if key does not exist in map"); map.set("age", "30"); assert.equal(input.value, "30", "input value set correctly"); map.set("age", "31"); assert.equal(input.value, "31", "input value update correctly"); input.value = "32"; domEvents.dispatch(input, "change"); assert.equal(map.get("age"), "32", "updated from input"); }); QUnit.test('<input text> el:prop:to/:from/:bind work (#280)', function(assert) { var template = stache( "<input el:value:to='scope1' value='1'/>" + "<input el:value:from='scope2' value='2'/>" + "<input el:value:bind='scope3' value='3'/>" ); var scope = new SimpleMap({ scope1: 'scope1', scope2: 'scope2', scope3: 'scope3' }); var frag = template(scope); var ta = this.fixture; ta.appendChild(frag); var inputTo = ta.getElementsByTagName('input')[0]; var inputFrom = ta.getElementsByTagName('input')[1]; var inputBind = ta.getElementsByTagName('input')[2]; // el:value:to assert.equal(scope.attr('scope1'), '1', 'el:value:to - scope value set from attribute'); inputTo.value = '4'; domEvents.dispatch(inputTo, 'change'); assert.equal(scope.attr('scope1'), '4', 'el:value:to - scope updated when attribute changed'); scope.attr('scope1', 'scope4'); assert.equal(inputTo.value, '4', 'el:value:to - attribute not updated when scope changed'); // el:value:from assert.equal(inputFrom.value, 'scope2', 'el:value:from - attribute set from scope'); inputFrom.value = 'scope5'; domEvents.dispatch(inputFrom, 'change'); assert.equal(scope.attr('scope2'), 'scope2', 'el:value:from - scope not updated when attribute changed'); scope.attr('scope2', 'scope6'); assert.equal(inputFrom.value, 'scope6', 'el:value:from - attribute updated when scope changed'); // el:value:bind assert.equal(inputBind.value, 'scope3', 'el:value:bind - attribute set from scope prop (parent -> child wins)'); inputBind.value = 'scope6'; domEvents.dispatch(inputBind, 'change'); assert.equal(scope.attr('scope3'), 'scope6', 'el:value:bind - scope updated when attribute changed'); scope.attr('scope3', 'scope7'); assert.equal(inputBind.value, 'scope7', 'el:value:bind - attribute updated when scope changed'); }); if (System.env !== 'canjs-test') { QUnit.test("<input text> dynamic attribute bindings (#2016)", function(assert){ var done = assert.async(); var template = stache("<input value:bind='{{propName}}'/>"); var map = new SimpleMap({propName: 'first', first: "Justin", last: "Meyer"}); var frag = template(map); var ta = this.fixture; ta.appendChild(frag); var input = ta.getElementsByTagName("input")[0]; testHelpers.afterMutation(function () { assert.equal(input.value, "Justin", "input value set correctly if key does not exist in map"); map.set('propName','last'); testHelpers.afterMutation(function (){ assert.equal(input.value, "Meyer", "input value set correctly if key does not exist in map"); input.value = "Lueke"; domEvents.dispatch(input, "change"); testHelpers.afterMutation(function () { assert.equal(map.get("last"), "Lueke", "updated from input"); done(); }); }); }); }); } QUnit.test("value:bind compute rejects new value (#887)", function(assert) { var template = stache("<input value:bind='age'/>"); // Compute only accepts numbers var compute = new SimpleObservable(30); canReflect.assignSymbols(compute,{ "can.setValue": function(newVal){ if(isNaN(+newVal)) { // do nothing } else { this.set( +newVal ); } } }); var frag = template({ age: compute }); var ta = this.fixture; ta.appendChild(frag); var input = ta.getElementsByTagName("input")[0]; // Set to non-number input.value = "30f"; domEvents.dispatch(input, "change"); assert.equal(compute.get(), 30, "Still the old value"); assert.equal(input.value, "30", "Text input has also not changed"); }); QUnit.test("value:from works with camelCase and kebab-case properties", function(assert) { var template = stache( "<input value:from='theProp'/>" + "<input value:from='the-prop'/>" ); var map = new SimpleMap({}); var frag = template(map); var ta = this.fixture; ta.appendChild(frag); var camelPropInput = ta.getElementsByTagName("input")[0]; var kebabPropInput = ta.getElementsByTagName("input")[1]; assert.equal(camelPropInput.value, "", "input bound to camelCase prop value set correctly if camelCase key does not exist in map"); assert.equal(kebabPropInput.value, "", "input bound to kebab-case prop value set correctly if kebab-case key does not exist in map"); map.attr("theProp", "30"); assert.equal(camelPropInput.value, "30", "input bound to camelCase prop value set correctly when camelCase prop changes"); assert.equal(kebabPropInput.value, "", "input bound to kebab-case prop value not updated when camelCase prop changes"); map.attr("theProp", "31"); assert.equal(camelPropInput.value, "31", "input bound to camelCase prop value updated correctly when camelCase prop changes"); assert.ok(!kebabPropInput.value, "input bound to kebab-case prop value not updated when camelCase prop changes"); camelPropInput.value = "32"; domEvents.dispatch(camelPropInput, "change"); assert.equal(map.attr("theProp"), "31", "camelCase prop NOT updated when input bound to camelCase prop changes"); assert.ok(!map.attr("the-prop"), "kebabCase prop NOT updated when input bound to camelCase prop changes"); map.attr("the-prop", "33"); assert.equal(kebabPropInput.value, "33", "input bound to kebab-case prop value set correctly when kebab-case prop changes"); assert.equal(camelPropInput.value, "32", "input bound to camelCase prop value not updated when kebab-case prop changes"); map.attr("the-prop", "34"); assert.equal(kebabPropInput.value, "34", "input bound to kebab-case prop value updated correctly when kebab-case prop changes"); assert.equal(camelPropInput.value, "32", "input bound to camelCase prop value not updated when kebab-case prop changes"); kebabPropInput.value = "35"; domEvents.dispatch(kebabPropInput, "change"); assert.equal(map.attr("the-prop"), "34", "kebab-case prop NOT updated from input bound to kebab-case prop"); assert.equal(map.attr("theProp"), "31", "camelCase prop NOT updated from input bound to kebab-case prop"); }); QUnit.test("value:to works with camelCase and kebab-case properties", function(assert) { var template = stache( "<input value:to='theProp'/>" + "<input value:to='the-prop'/>" ); var map = new SimpleMap({}); var frag = template(map); var ta = this.fixture; ta.appendChild(frag); var camelPropInput = ta.getElementsByTagName("input")[0]; var kebabPropInput = ta.getElementsByTagName("input")[1]; camelPropInput.value = "32"; domEvents.dispatch(camelPropInput, "change"); assert.equal(map.attr("theProp"), "32", "camelCaseProp updated from input bound to camelCase Prop"); assert.ok(!map.attr("the-prop"), "kebabCaseProp NOT updated from input bound to camelCase Prop"); map.attr("theProp", "30"); assert.equal(camelPropInput.value, "32", "input bound to camelCase Prop value NOT updated when camelCase prop changes"); assert.ok(!kebabPropInput.value, "input bound to kebabCase Prop value NOT updated when camelCase prop changes"); kebabPropInput.value = "33"; domEvents.dispatch(kebabPropInput, "change"); assert.equal(map.attr("the-prop"), "33", "kebabCaseProp updated from input bound to kebabCase Prop"); assert.equal(map.attr("theProp"), "30", "camelCaseProp NOT updated from input bound to camelCase Prop"); map.attr("theProp", "34"); assert.equal(kebabPropInput.value, "33", "input bound to kebabCase Prop value NOT updated when kebabCase prop changes"); assert.equal(camelPropInput.value, "32", "input bound to camelCase Prop value NOT updated when kebabCase prop changes"); }); QUnit.test("value:bind works with camelCase and kebab-case properties", function(assert) { var template = stache( "<input value:bind='theProp'/>" + "<input value:bind='the-prop'/>" ); var map = new SimpleMap({}); var frag = template(map); var ta = this.fixture; ta.appendChild(frag); var camelPropInput = ta.getElementsByTagName("input")[0]; var kebabPropInput = ta.getElementsByTagName("input")[1]; camelPropInput.value = "32"; domEvents.dispatch(camelPropInput, "change"); assert.equal(map.attr("theProp"), "32", "camelCaseProp updated from input bound to camelCase Prop"); assert.ok(!map.attr("the-prop"), "kebabCaseProp NOT updated from input bound to camelCase Prop"); map.attr("theProp", "30"); assert.equal(camelPropInput.value, "30", "input bound to camelCase Prop value updated when camelCase prop changes"); assert.ok(!kebabPropInput.value, "input bound to kebabCase Prop value NOT updated when camelCase prop changes"); kebabPropInput.value = "33"; domEvents.dispatch(kebabPropInput, "change"); assert.equal(map.attr("the-prop"), "33", "kebabCaseProp updated from input bound to kebabCase Prop"); assert.equal(map.attr("theProp"), "30", "camelCaseProp NOT updated from input bound to camelCase Prop"); map.attr("theProp", "34"); assert.equal(kebabPropInput.value, "33", "input bound to kebabCase Prop value NOT updated when kebabCase prop changes"); assert.equal(camelPropInput.value, "34", "input bound to camelCase Prop value updated when kebabCase prop changes"); }); QUnit.test("Bracket expression with dot and no explicit root and value:bind", function(assert) { var template; var div = this.fixture; template = stache('<input value:bind="[\'two.hops\']" >'); var data = new SimpleMap(); // var data = new DefineMap({ // "two.hops": "" // }); var dom = template(data); div.appendChild(dom); var input = div.getElementsByTagName('input')[0]; assert.equal(input.value, "", "input value set correctly if key does not exist in map"); data.set("two.hops", "slide to the left"); assert.equal(input.value, "slide to the left", "input value set correctly"); data.set("two.hops", "slide to the right"); assert.equal(input.value, "slide to the right", "input value update correctly"); input.value = "REVERSE REVERSE"; domEvents.dispatch(input, "change"); assert.equal(data.get("two.hops"), "REVERSE REVERSE", "updated from input"); }); QUnit.test("Bracket expression with colon and no explicit root and value:bind", function(assert) { var template; var div = this.fixture; template = stache('<input value:bind="[\'two:hops\']" >'); var data = new SimpleMap(); // var data = new DefineMap({ // "two.hops": "" // }); var dom = template(data); div.appendChild(dom); var input = div.getElementsByTagName('input')[0]; assert.equal(input.value, "", "input value set correctly if key does not exist in map"); data.set("two:hops", "slide to the left"); assert.equal(input.value, "slide to the left", "input value set correctly"); data.set("two:hops", "slide to the right"); assert.equal(input.value, "slide to the right", "input value update correctly"); input.value = "REVERSE REVERSE"; domEvents.dispatch(input, "change"); assert.equal(data.get("two:hops"), "REVERSE REVERSE", "updated from input"); }); QUnit.test('el:prop:to/:from/:bind work (#280)', function(assert) { var template = stache( "<input el:value:to='scope1' value='1'/>" + "<input el:value:from='scope2' value='2'/>" + "<input el:value:bind='scope3' value='3'/>" ); var scope = new SimpleMap({ scope1: 'scope1', scope2: 'scope2', scope3: 'scope3' }); var frag = template(scope); var ta = this.fixture; ta.appendChild(frag); var inputTo = ta.getElementsByTagName('input')[0]; var inputFrom = ta.getElementsByTagName('input')[1]; var inputBind = ta.getElementsByTagName('input')[2]; // el:value:to assert.equal(scope.attr('scope1'), '1', 'el:value:to - scope value set from attribute'); inputTo.value = '4'; domEvents.dispatch(inputTo, 'change'); assert.equal(scope.attr('scope1'), '4', 'el:value:to - scope updated when attribute changed'); scope.attr('scope1', 'scope4'); assert.equal(inputTo.value, '4', 'el:value:to - attribute not updated when scope changed'); // el:value:from assert.equal(inputFrom.value, 'scope2', 'el:value:from - attribute set from scope'); inputFrom.value = 'scope5'; domEvents.dispatch(inputFrom, 'change'); assert.equal(scope.attr('scope2'), 'scope2', 'el:value:from - scope not updated when attribute changed'); scope.attr('scope2', 'scope6'); assert.equal(inputFrom.value, 'scope6', 'el:value:from - attribute updated when scope changed'); // el:value:bind assert.equal(inputBind.value, 'scope3', 'el:value:bind - attribute set from scope prop (parent -> child wins)'); inputBind.value = 'scope6'; domEvents.dispatch(inputBind, 'change'); assert.equal(scope.attr('scope3'), 'scope6', 'el:value:bind - scope updated when attribute changed'); scope.attr('scope3', 'scope7'); assert.equal(inputBind.value, 'scope7', 'el:value:bind - attribute updated when scope changed'); }); QUnit.test("<input text> two-way - DOM - input text (#1700)", function(assert) { var template = stache("<input value:bind='age'/>"); var map = new SimpleMap(); var frag = template(map); var ta = this.fixture; ta.appendChild(frag); var input = ta.getElementsByTagName("input")[0]; assert.equal(input.value, "", "input value set correctly if key does not exist in map"); map.attr("age", "30"); var done = assert.async(); testHelpers.afterMutation(function () { assert.equal(input.value, "30", "input value set correctly"); map.attr("age", "31"); testHelpers.afterMutation(function () { assert.equal(input.value, "31", "input value update correctly"); input.value = "32"; domEvents.dispatch(input, "change"); testHelpers.afterMutation(function () { done(); assert.equal(map.attr("age"), "32", "updated from input"); }); }); }); }); QUnit.test("errors subproperties of undefined properties (#298)", function(assert) { try { stache("<input value:to='prop.subprop'/>")(); assert.ok(true, "renderer was made without error"); } catch(e) { assert.ok(false, e.message); } }); QUnit.test("updates happen on two-way even when one binding is satisfied", function(assert) { var done = assert.async(); var template = stache('<input value:bind="firstName"/>'); var viewModel = new SimpleMap({ firstName: "jeffrey" }); canReflect.assignSymbols(viewModel,{ "can.setKeyValue": function(key, val) { if(val) { this.set(key, val.toLowerCase()); } } }); var frag = template(viewModel); domMutateNode.appendChild.call(this.fixture, frag); var input = this.fixture.firstChild; assert.equal(input.value, "jeffrey", 'initial value should be "jeffrey"'); input.value = "JEFFREY"; domEvents.dispatch(input, "change"); assert.equal(input.value, "jeffrey", 'updated value should be "jeffrey"'); testHelpers.afterMutation(function () { done(); }); }); QUnit.test("updates happen on changed two-way even when one binding is satisfied", function(assert) { var done = assert.async(); var template = stache('<input value:bind="{{bindValue}}"/>'); var ViewModel = DefineMap.extend({ firstName: { set: function(newValue) { if(newValue) { return newValue.toLowerCase(); } } }, lastName: { set: function(newValue) { if(newValue) { return newValue.toLowerCase(); } } }, bindValue: "string" }); var viewModel = new ViewModel({ firstName: "Jeffrey", lastName: "King", bindValue: "firstName" }); var frag = template(viewModel); domMutateNode.appendChild.call(this.fixture, frag); var input = this.fixture.firstChild; testHelpers.afterMutation(function () { assert.equal(input.value, "jeffrey"); var undo = domMutate.onNodeAttributeChange(input, function() { undo(); assert.equal(input.value, "king", "should be king"); // set the value to "KING" after the current batch setTimeout(function(){ input.value = "KING"; domEvents.dispatch(input, "change"); assert.equal(input.value, "king"); done(); },13); }.bind(this)); viewModel.bindValue = "lastName"; }.bind(this)); }); QUnit.test("value:bind memory leak (#2270)", function(assert) { var template = stache('<div><input value:bind="foo"></div>'); var vm = new SimpleMap({foo: ''}); var frag = template(vm); var ta = this.fixture; domMutateNode.appendChild.call(ta,frag); var done = assert.async(); testHelpers.afterMutation(function (){ domMutateNode.removeChild.call(ta, ta.firstChild); // still 1 binding, should be 0 testHelpers.afterMutation(function (){ var checkCount = 0; var checkLifecycleBindings = function(){ var meta = vm[canSymbol.for("can.meta")]; if( meta.handlers.get([]).length === 0 ) { assert.ok(true, "no bindings"); done(); } else { checkCount++; if (checkCount > 5) { assert.ok(false, "lifecycle bindings still existed after timeout"); return done(); } setTimeout(checkLifecycleBindings, 1000); } }; checkLifecycleBindings(); }); }); }); QUnit.test("converters work (#2299)", function(assert) { stache.registerConverter("numberToString",{ get: function(source){ return source() + ""; }, set: function(newVal, source){ source(newVal === "" ? null : +newVal ); } }); var template = stache('<input value:bind="numberToString(~age)">'); var map = new SimpleMap({age: 25}); var frag = template(map); assert.equal(frag.firstChild.value, "25"); assert.equal(map.get("age"), 25); map.set("age",33); assert.equal(frag.firstChild.value, "33"); assert.equal(map.get("age"), 33); frag.firstChild.value = "1"; domEvents.dispatch(frag.firstChild, "change"); var done = assert.async(); testHelpers.afterMutation(function () { done(); assert.equal(frag.firstChild.value, "1"); assert.equal(map.get("age"), 1); }); }); testIfRealDocument("<input radio> checked:bind should trigger a radiochange event for radio buttons", function(assert) { // NOTE: `testIfRealDocument` is used because the vdom does not simulate document event dispatch var template = stache([ '<input type="radio" name="baz" checked:bind="foo"/><span>{{foo}}</span>', '<input type="radio" name="baz" checked:bind="bar"/><span>{{bar}}</span>' ].join('')); var data = new SimpleMap({ foo: false, bar: false }); var fragment = template(data); domMutateNode.appendChild.call(this.fixture, fragment); var self = this; function child (index) { return self.fixture.childNodes.item(index); } var fooRadio = child(0); var fooText = child(1); var barRadio = child(2); var barText = child(3); function text (node) { while (node && node.nodeType !== 3) { node = node.firstChild; } return node && node.nodeValue; } fooRadio.checked = true; domEvents.dispatch(fooRadio, 'change'); barRadio.checked = true; domEvents.dispatch(barRadio, 'change'); assert.equal(text(fooText), 'false', 'foo text is false'); assert.equal(text(barText), 'true', 'bar text is true'); assert.equal(data.get("foo"), false); assert.equal(data.get("bar"), true); }); QUnit.test('<input radio> change event handler set up when binding on radiochange (#206)', function(assert) { var template = stache('<input type="radio" checked:bind="attending" />'); var map = new SimpleMap({ attending: false }); var frag = template(map); var input = frag.firstChild; input.checked = true; domEvents.dispatch(input, "change"); assert.equal(map.get('attending'), true, "now it is true"); }); QUnit.test('<input checkbox> one-way - DOM - with undefined (#135)', function(assert) { var data = new SimpleMap({ completed: undefined }), frag = stache('<input type="checkbox" el:checked:from="completed"/>')(data); domMutateNode.appendChild.call(this.fixture, frag); var input = this.fixture.getElementsByTagName('input')[0]; assert.equal(input.checked, false, 'checkbox value should be false for undefined'); }); QUnit.test('<input checkbox> two-way - DOM - with truthy and falsy values binds to checkbox (#1700)', function(assert) { var data = new SimpleMap({ completed: 1 }), frag = stache('<input type="checkbox" el:checked:bind="completed"/>')(data); domMutateNode.appendChild.call(this.fixture, frag); var input = this.fixture.getElementsByTagName('input')[0]; assert.equal(input.checked, true, 'checkbox value bound (via attr check)'); data.attr('completed', 0); var done = assert.async(); testHelpers.afterMutation(function () { done(); assert.equal(input.checked, false, 'checkbox value bound (via attr check)'); }); }); QUnit.test("<input checkbox> checkboxes with checked:bind bind properly (#628)", function(assert) { var data = new SimpleMap({ completed: true }), frag = stache('<input type="checkbox" checked:bind="completed"/>')(data); domMutateNode.appendChild.call(this.fixture, frag); var input = this.fixture.getElementsByTagName('input')[0]; assert.equal(input.checked, data.get('completed'), 'checkbox value bound (via attr check)'); data.attr('completed', false); assert.equal(input.checked, data.get('completed'), 'checkbox value bound (via attr uncheck)'); input.checked = true; domEvents.dispatch(input, 'change'); assert.equal(input.checked, true, 'checkbox value bound (via check)'); assert.equal(data.get('completed'), true, 'checkbox value bound (via check)'); input.checked = false; domEvents.dispatch(input, 'change'); assert.equal(input.checked, false, 'checkbox value bound (via uncheck)'); assert.equal(data.get('completed'), false, 'checkbox value bound (via uncheck)'); }); testIfRealDocument("<select> keeps its value as <option>s change with {{#each}} (#1762)", function(assert){ var template = stache("<select value:bind='id'>{{#each values}}<option value='{{this}}'>{{this}}</option>{{/each}}</select>"); var values = new SimpleObservable( ["1","2","3","4"] ); var id = new SimpleObservable("2"); var frag = template({ values: values, id: id }); var done = assert.async(); var select = frag.firstChild; var options = select.getElementsByTagName("option"); // the value is set asynchronously testHelpers.afterMutation(function (){ assert.ok(options[1].selected, "value is initially selected"); values.set(["7","2","5","4"]); testHelpers.afterMutation(function (){ assert.ok(options[1].selected, "after changing options, value should still be selected"); done(); }); }); }); testIfRealDocument("<select> with undefined value selects option without value", function(assert) { var template = stache("<select value:bind='opt'><option>Loading...</option></select>"); var map = new SimpleMap(); var frag = template(map); var ta = this.fixture; ta.appendChild(frag); var select = ta.childNodes.item(0); assert.equal(select.selectedIndex, 0, 'Got selected index'); }); testIfRealDocument('<select> two-way bound values that do not match a select option set selectedIndex to -1 (#2027)', function(assert) { var renderer = stache('<select el:value:bind="key"><option value="foo">foo</option><option value="bar">bar</option></select>'); var map = new SimpleMap({ }); var frag = renderer(map); assert.equal(frag.firstChild.selectedIndex, 0, 'undefined <- {($first value)}: selectedIndex = 0'); map.attr('key', 'notfoo'); var done = assert.async(); testHelpers.afterMutation(function () { assert.equal(frag.firstChild.selectedIndex, -1, 'notfoo: selectedIndex = -1'); map.attr('key', 'foo'); assert.strictEqual(frag.firstChild.selectedIndex, 0, 'foo: selectedIndex = 0'); map.attr('key', 'notbar'); testHelpers.afterMutation(function () { done(); assert.equal(frag.firstChild.selectedIndex, -1, 'notbar: selectedIndex = -1'); map.attr('key', 'bar'); assert.strictEqual(frag.firstChild.selectedIndex, 1, 'bar: selectedIndex = 1'); map.attr('key', 'bar'); assert.strictEqual(frag.firstChild.selectedIndex, 1, 'bar (no change): selectedIndex = 1'); }); }); }); QUnit.test("<select multiple> Multi-select empty string works(#1263)", function(assert) { var data = new SimpleMap({ isMultiple: 1, isSelect: 1, name: "attribute_ 0", options: new DefineList([ {label: 'empty', value: ""}, {label: 'zero', value: 0}, {label: 'one', value: 1}, {label: 'two', value: 2}, {label: 'three', value: 3}, {label: 'four', value: 4} ]), value: new DefineList(["1"]) }); var template = stache("<select {{#if isMultiple}}multiple{{/if}} values:bind='value'> " + "{{#each options}} <option value='{{value}}' >{{label}}</option>{{/each}} </select>"); var frag = template(data); assert.equal(frag.firstChild.getElementsByTagName("option")[0].selected, false, "The first empty value is not selected"); assert.equal(frag.firstChild.getElementsByTagName("option")[2].selected, true, "One is selected"); }); testIfRealDocument("<select multiple> applies initial value, when options rendered from array (#1414)", function(assert) { var template = stache( "<select values:bind='colors' multiple>" + "{{#each allColors}}<option value='{{value}}'>{{label}}</option>{{/each}}" + "</select>"); var map = new SimpleMap({ colors: new DefineList(["red", "green"]), allColors: new DefineList([ { value: "red", label: "Red"}, { value: "green", label: "Green"}, { value: "blue", label: "Blue"} ]) }); var done = assert.async(); var frag = template(map); var ta = this.fixture; ta.appendChild(frag); var select = ta.getElementsByTagName("select")[0], options = select.getElementsByTagName("option"); // Wait for Multiselect.set() to be called. testHelpers.afterMutation(function (){ assert.ok(options[0].selected, "red should be set initially"); assert.ok(options[1].selected, "green should be set initially"); assert.ok(!options[2].selected, "blue should not be set initially"); done(); }); }); QUnit.test("<select> one-way bindings keep value if options are replaced - each (#1762)", function(assert) { var countries = [{code: 'MX', countryName:'MEXICO'}, {code: 'US', countryName:'USA'} ]; var data = new SimpleMap({ countryCode: 'US', countries: new DefineList(countries) }); var template = stache('<select el:value:from="countryCode">'+ '{{#each countries}}'+ '<option value="{{code}}">{{countryName}}</option>'+ '{{/each}}'+ '</select>'); var frag = template(data); var select = frag.firstChild; var done = assert.async(); testHelpers.afterMutation(function (){ data.get("countries").replace([]); testHelpers.afterMutation(function (){ data.attr("countries").replace(countries); assert.equal(data.attr("countryCode"), "US", "country kept as USA"); testHelpers.afterMutation(function (){ assert.ok( select.getElementsByTagName("option")[1].selected, "USA still selected"); }); done(); }); }); }); testIfRealDocument("<select> value:bind select single", function(assert) { var template = stache( "<select value:bind='color'>" + "<option value='red'>Red</option>" + "<option value='green'>Green</option>" + "</select>"); var map = new SimpleMap({ color: "red" }); var frag = template(map); var ta = this.fixture; ta.appendChild(frag); var inputs = ta.getElementsByTagName("select"); assert.equal(inputs[0].value, 'red', "default value set"); map.set("color", "green"); assert.equal(inputs[0].value, 'green', "alternate value set"); canReflect.each(ta.getElementsByTagName('option'), function(opt) { if (opt.value === 'red') { opt.selected = 'selected'; } }); assert.equal(map.get("color"), "green", "not yet updated from input"); domEvents.dispatch(inputs[0], "change"); assert.equal(map.get("color"), "red", "updated from input"); canReflect.each(ta.getElementsByTagName('option'), function(opt) { if (opt.value === 'green') { opt.selected = 'selected'; } }); assert.equal(map.get("color"), "red", "not yet updated from input"); domEvents.dispatch(inputs[0], "change"); assert.equal(map.get("color"), "green", "updated from input"); }); testIfRealDocument("<select> values:bind multiple select with a DefineList", function(assert) { var template = stache( "<select values:bind='colors' multiple>" + "<option value='red'>Red</option>" + "<option value='green'>Green</option>" + "<option value='ultraviolet'>Ultraviolet</option>" + "</select>"); var list = new DefineList(); var done = assert.async(); var frag = template({ colors: list }); var ta = this.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; domEvents.dispatch(select, "change"); assert.deepEqual(list.get(), ["red"], "A DefineList value is set even if none existed"); options[1].selected = true; domEvents.dispatch(select, "change"); assert.deepEqual(list.get(), ["red", "green"], "Adds items to the list"); options[0].selected = false; domEvents.dispatch(select, "change"); assert.deepEqual(list.get(), ["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; ta.removeChild(select); done(); }, 1); }); QUnit.test("<select> one-way bindings keep value if options are replaced (#1762)", function(assert) { var countries = [{code: 'MX', countryName:'MEXICO'}, {code: 'US', countryName:'USA'} ]; var data = new SimpleMap({ countryCode: 'US', countries: new DefineList(countries) }); var template = stache('<select el:value:from="countryCode">'+ '{{#countries}}'+ '<option value="{{code}}">{{countryName}}</option>'+ '{{/countries}}'+ '</select>'); var frag = template(data); var select = frag.firstChild; var done = assert.async(); testHelpers.afterMutation(function (){ data.get("countries").replace([]); testHelpers.afterMutation(function (){ data.get("countries").replace(countries); assert.equal(data.get("countryCode"), "US", "country kept as USA"); testHelpers.afterMutation(function (){ assert.ok( select.getElementsByTagName("option")[1].selected, "USA still selected"); }); done(); }); }); }); testIfRealDocument("<select> two-way bindings update to `undefined` if options are replaced - each (#1762)", function(assert){ var countries = [{code: 'MX', countryName:'MEXICO'}, {code: 'US', countryName:'USA'} ]; var data = new SimpleMap({ countryCode: 'US', countries: new DefineList(countries) }); var template = stache('<select el:value:bind="countryCode">'+ '{{#each countries}}'+ '<option value="{{code}}">{{countryName}}</option>'+ '{{/each}}'+ '</select>'); template(data); var done = assert.async(); testHelpers.afterMutation(function (){ data.attr("countries").replace([]); testHelpers.afterMutation(function (){ assert.equal(data.get("countryCode"), undefined, "countryCode set to undefined"); done(); }); }); }); testIfRealDocument('<select> - previously non-existing select value gets selected from a list when it is added (#1762)', function(assert) { // this breaks with VDOM can-stache-bindings#258 because of selectedIndex var template = stache('<select el:value:bind="{person}">' + '<option></option>' + '{{#each people}}<option value="{{.}}">{{.}}</option>{{/each}}' + '</select>' + '<input type="text" size="5" el:value:bind="person">' ); var people = new DefineList([ "Justin", "Zed", "Tom", "Paula" ]); var vm = new SimpleMap({ person: 'Brian', people: people }); var done = assert.async(); vm.on('person', function(ev, newVal, oldVal) { assert.ok(false, 'person attribute should not change'); }); var frag = template(vm); assert.equal(vm.attr('person'), 'Brian', 'Person is still set'); testHelpers.afterMutation(function () { people.push('Brian'); testHelpers.afterMutation(function() { var options = frag.firstChild.getElementsByTagName("option"); assert.ok(options[options.length - 1].selected, 'New child should be selected'); done(); }); }); }); QUnit.test("<select> select bindings respond to changes immediately or during insert using bind (#2134)", function(assert) { var countries = [{code: 'MX', countryName:'MEXICO'}, {code: 'US', countryName:'USA'}, {code: 'IND', countryName:'INDIA'}, {code: 'RUS', countryName:'RUSSIA'} ]; var template = stache('<select value:bind="countryCode">'+ '{{#each countries}}'+ '<option value="{{code}}">{{countryName}}</option>'+ '{{/each}}'+ '</select>'); var data = new SimpleMap({ countryCode: 'US', countries: new DefineList(countries) }); var frag = template(data); data.set('countryCode', 'IND'); var done = assert.async(); testHelpers.afterMutation(function (){ done(); assert.equal(frag.firstChild.value, "IND", "got last updated value"); }); }); testIfRealDocument("<select> two way bound select empty string null or undefined value (#2027)", function(assert) { var template = stache( "<select id='null-select' value:bind='color-1'>" + "<option value=''>Choose</option>" + "<option value='red'>Red</option>" + "<option value='green'>Green</option>" + "</select>" + "<select id='undefined-select' value:bind='color-2'>" + "<option value=''>Choose</option>" + "<option value='red'>Red</option>" + "<option value='green'>Green</option>" + "</select>"+ "<select id='string-select' value:bind='color-3'>" + "<option value=''>Choose</option>" + "<option value='red'>Red</option>" + "<option value='green'>Green</option>" + "</select>"); var map = new SimpleMap({ 'color-1': null, 'color-2': undefined, 'color-3': "" }); var done = assert.async(); var frag = template(map); domMutateNode.appendChild.call(this.fixture, frag); var nullInput = doc.getElementById("null-select"); var nullInputOptions = nullInput.getElementsByTagName('option'); var undefinedInput = doc.getElementById("undefined-select"); var undefinedInputOptions = undefinedInput.getElementsByTagName('option'); var stringInput = doc.getElementById("string-select"); var stringInputOptions = stringInput.getElementsByTagName('option'); // wait for set to be called which will change the selects testHelpers.afterMutation(function (){ assert.ok(!nullInputOptions[0].selected, "default (null) value set"); // the first item is selected because "" is the value. assert.ok(undefinedInputOptions[0].selected, "default (undefined) value set"); assert.ok(stringInputOptions[0].selected, "default ('') value set"); done(); }); }); testIfRealDocument("<select> two way binding from a select's value to null has no selection (#2027)", function(assert){ var template = stache("<select value:bind='key'><option value='One'>One</option></select>"); var map = new SimpleMap({key: null}); var frag = template(map); var select = frag.childNodes.item(0); testHelpers.afterMutation(function (){ assert.equal(select.selectedIndex, -1, "selectedIndex is 0 because no value exists on the map"); assert.equal(map.get("key"), null, "The map's value property is set to the select's value"); done(); }); var done = assert.async(); }); testIfRealDocument("<select> One way binding from a select's value to a parent compute updates the parent with the select's initial value (#2027)", function(assert){ var template = stache("<select value:to='value'><option value='One'>One</option></select>"); var map = new SimpleMap(); var frag = template(map); var select = frag.childNodes.item(0); testHelpers.afterMutation(function (){ assert.equal(select.selectedIndex, 0, "selectedIndex is 0 because no value exists on the map"); assert.equal(map.attr("value"), "One", "The map's value property is set to the select's value"); done(); }); var done = assert.async(); }); testIfRealDocument("Bi-directional binding among sibling components, new syntax (#325)", function(assert) { var groupCollapsed = console.groupCollapsed; if(groupCollapsed) { console.groupCollapsed = null; //no op } var demoContext = new DefineMap({ person: '' }); var SourceComponentVM = DefineMap.extend("SourceComponentVM",{ defaultPerson: { value: 'John' }, person: { set: function(val) { return val || this.defaultPerson; } } }); var ClearComponentVM = DefineMap.extend("ClearComponentVM",{ person: 'string', clearPerson: function() { this.set('person', ''); } }); MockComponent.extend({ tag: "source-component", viewModel: SourceComponentVM, template: stache('<span>{{person}}</span><input type="text" value:bind="./person" />') }); MockComponent.extend({ tag: "clear-button", viewModel: ClearComponentVM, template: stache('<input type="button" value="Clear" on:click="./clearPerson()" /><span>{{./person}}</span>') }); var demoRenderer = stache( '<span>{{./person}}</span>' + '<source-component person:bind="./person" />' + '<clear-button person:bind="./person" />' ); var frag = demoRenderer(demoContext); var sourceComponentVM = canViewModel(frag.childNodes[1]); var clearButtonVM = canViewModel(frag.childNodes[2]); assert.equal(frag.childNodes[0].childNodes[0].nodeValue, '', "demoContext person is empty"); assert.equal(frag.childNodes[1].childNodes[0].childNodes[0].nodeValue, 'John', "source-component person is default"); assert.equal(frag.childNodes[2].childNodes[1].childNodes[0].nodeValue, '', "clear-button person is empty"); sourceComponentVM.person = 'Bob'; assert.equal(frag.childNodes[0].childNodes[0].nodeValue, 'Bob', "demoContext person set correctly"); assert.equal(frag.childNodes[1].childNodes[0].childNodes[0].nodeValue, 'Bob', "source-component person set correctly"); assert.equal(frag.childNodes[2].childNodes[1].childNodes[0].nodeValue, 'Bob', "clear-button person set correctly"); clearButtonVM.clearPerson(); // Note that 'John' will not be set on the parent or clear button because parent was already set // to an empty string and the bindingSemaphore will not allow another change to the parent // (giving the parent priority) to prevent cyclic dependencies. assert.equal(frag.childNodes[0].childNodes[0].nodeValue, '', "demoContext person set correctly"); assert.equal(frag.childNodes[1].childNodes[0].childNodes[0].nodeValue, 'John', "source-component person set correctly"); assert.equal(frag.childNodes[2].childNodes[1].childNodes[0].nodeValue, '', "clear-button person set correctly"); if(groupCollapsed) { console.groupCollapsed = groupCollapsed; } }); testIfRealDocument("Bracket Expression with :to bindings", function(assert) { var demoContext = new DefineMap({ person: { name: 'Matt' } }); var SourceComponentVM = DefineMap.extend("SourceComponentVM", { name: { default: 'Kevin' } }); MockComponent.extend({ tag: "source-component", viewModel: SourceComponentVM, template: stache('<span>{{name}}</span>') }); var demoRenderer = stache( '<source-component name:to="person[\'name\']" />' ); demoRenderer(demoContext); assert.equal(demoContext.person.name, 'Kevin', "source-component has correct name set"); }); QUnit.test('this:to works', function(assert) { var template = stache('<input this:to="this.input" />'); var map = new SimpleMap({ input: null }); var frag = template(map); var input = frag.firstChild; assert.equal(input, map.get("input"), "set the input"); }); });