Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

value binding on select doesn't respond to null when not using options binding #493

Merged
merged 2 commits into from Mar 17, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
323 changes: 182 additions & 141 deletions spec/defaultBindings/valueBehaviors.js
Expand Up @@ -198,147 +198,6 @@ describe('Binding: Value', function() {
expect(myobservable()).toEqual("some user-entered value");
});

it('For select boxes, should update selectedIndex when the model changes (options specified before value)', function() {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='options:[\"A\", \"B\"], value:myObservable'></select>";
ko.applyBindings({ myObservable: observable }, testNode);
expect(testNode.childNodes[0].selectedIndex).toEqual(1);
expect(observable()).toEqual('B');

observable('A');
expect(testNode.childNodes[0].selectedIndex).toEqual(0);
expect(observable()).toEqual('A');
});

it('For select boxes, should update selectedIndex when the model changes (value specified before options)', function() {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='value:myObservable, options:[\"A\", \"B\"]'></select>";
ko.applyBindings({ myObservable: observable }, testNode);
expect(testNode.childNodes[0].selectedIndex).toEqual(1);
expect(observable()).toEqual('B');

observable('A');
expect(testNode.childNodes[0].selectedIndex).toEqual(0);
expect(observable()).toEqual('A');
});

it('For select boxes, should display the caption when the model value changes to undefined', function() {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='options:[\"A\", \"B\"], optionsCaption:\"Select...\", value:myObservable'></select>";
ko.applyBindings({ myObservable: observable }, testNode);
expect(testNode.childNodes[0].selectedIndex).toEqual(2);
observable(undefined);
expect(testNode.childNodes[0].selectedIndex).toEqual(0);
});

it('For select boxes, should update the model value when the UI is changed (setting it to undefined when the caption is selected)', function () {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='options:[\"A\", \"B\"], optionsCaption:\"Select...\", value:myObservable'></select>";
ko.applyBindings({ myObservable: observable }, testNode);
var dropdown = testNode.childNodes[0];

dropdown.selectedIndex = 1;
ko.utils.triggerEvent(dropdown, "change");
expect(observable()).toEqual("A");

dropdown.selectedIndex = 0;
ko.utils.triggerEvent(dropdown, "change");
expect(observable()).toEqual(undefined);
});

it('For select boxes, should be able to associate option values with arbitrary objects (not just strings)', function() {
var x = {}, y = {};
var selectedValue = ko.observable(y);
testNode.innerHTML = "<select data-bind='options: myOptions, value: selectedValue'></select>";
var dropdown = testNode.childNodes[0];
ko.applyBindings({ myOptions: [x, y], selectedValue: selectedValue }, testNode);

// Check the UI displays the entry corresponding to the chosen value
expect(dropdown.selectedIndex).toEqual(1);

// Check that when we change the model value, the UI is updated
selectedValue(x);
expect(dropdown.selectedIndex).toEqual(0);

// Check that when we change the UI, this changes the model value
dropdown.selectedIndex = 1;
ko.utils.triggerEvent(dropdown, "change");
expect(selectedValue()).toEqual(y);
});

it('For select boxes, should automatically initialize the model property to match the first option value if no option value matches the current model property value', function() {
// The rationale here is that we always want the model value to match the option that appears to be selected in the UI
// * If there is *any* option value that equals the model value, we'd initalise the select box such that *that* option is the selected one
// * If there is *no* option value that equals the model value (often because the model value is undefined), we should set the model
// value to match an arbitrary option value to avoid inconsistency between the visible UI and the model
var observable = new ko.observable(); // Undefined by default

// Should work with options specified before value
testNode.innerHTML = "<select data-bind='options:[\"A\", \"B\"], value:myObservable'></select>";
ko.applyBindings({ myObservable: observable }, testNode);
expect(observable()).toEqual("A");

// ... and with value specified before options
ko.utils.domData.clear(testNode);
testNode.innerHTML = "<select data-bind='value:myObservable, options:[\"A\", \"B\"]'></select>";
observable(undefined);
expect(observable()).toEqual(undefined);
ko.applyBindings({ myObservable: observable }, testNode);
expect(observable()).toEqual("A");
});

it('For nonempty select boxes, should reject model values that don\'t match any option value, resetting the model value to whatever is visibly selected in the UI', function() {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='options:[\"A\", \"B\", \"C\"], value:myObservable'></select>";
ko.applyBindings({ myObservable: observable }, testNode);
expect(testNode.childNodes[0].selectedIndex).toEqual(1);

observable('D'); // This change should be rejected, as there's no corresponding option in the UI
expect(observable()).not.toEqual('D');
});

it('For select boxes, option values can be numerical, and are not implicitly converted to strings', function() {
var observable = new ko.observable(30);
testNode.innerHTML = "<select data-bind='options:[10,20,30,40], value:myObservable'></select>";
ko.applyBindings({ myObservable: observable }, testNode);

// First check that numerical model values will match a dropdown option
expect(testNode.childNodes[0].selectedIndex).toEqual(2); // 3rd element, zero-indexed

// Then check that dropdown options map back to numerical model values
testNode.childNodes[0].selectedIndex = 1;
ko.utils.triggerEvent(testNode.childNodes[0], "change");
expect(typeof observable()).toEqual("number");
expect(observable()).toEqual(20);
});

it('For select boxes with values attributes, should always use value (and not text)', function() {
var observable = new ko.observable('A');
testNode.innerHTML = "<select data-bind='value:myObservable'><option value=''>A</option><option value='A'>B</option></select>";
ko.applyBindings({ myObservable: observable }, testNode);
var dropdown = testNode.childNodes[0];
expect(dropdown.selectedIndex).toEqual(1);

dropdown.selectedIndex = 0;
ko.utils.triggerEvent(dropdown, "change");
expect(observable()).toEqual("");
});

it('For select boxes with text values but no value property, should use text value', function() {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='value:myObservable'><option>A</option><option>B</option><option>C</option></select>";
ko.applyBindings({ myObservable: observable }, testNode);
var dropdown = testNode.childNodes[0];
expect(dropdown.selectedIndex).toEqual(1);

dropdown.selectedIndex = 0;
ko.utils.triggerEvent(dropdown, "change");
expect(observable()).toEqual("A");

observable('C');
expect(dropdown.selectedIndex).toEqual(2);
});

it('On IE < 10, should handle autofill selection by treating "propertychange" followed by "blur" as a change event', function() {
// This spec describes the awkward choreography of events needed to detect changes to text boxes on IE < 10,
// because it doesn't fire regular "change" events when the user selects an autofill entry. It isn't applicable
Expand Down Expand Up @@ -379,4 +238,186 @@ describe('Binding: Value', function() {
expect(numUpdates).toEqual(3);
}
});

describe('For select boxes', function() {
it('Should update selectedIndex when the model changes (options specified before value)', function() {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='options:[\"A\", \"B\"], value:myObservable'></select>";
ko.applyBindings({ myObservable: observable }, testNode);
expect(testNode.childNodes[0].selectedIndex).toEqual(1);
expect(observable()).toEqual('B');

observable('A');
expect(testNode.childNodes[0].selectedIndex).toEqual(0);
expect(observable()).toEqual('A');
});

it('Should update selectedIndex when the model changes (value specified before options)', function() {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='value:myObservable, options:[\"A\", \"B\"]'></select>";
ko.applyBindings({ myObservable: observable }, testNode);
expect(testNode.childNodes[0].selectedIndex).toEqual(1);
expect(observable()).toEqual('B');

observable('A');
expect(testNode.childNodes[0].selectedIndex).toEqual(0);
expect(observable()).toEqual('A');
});

it('Should display the caption when the model value changes to undefined, null, or \"\" when using \'options\' binding', function() {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='options:[\"A\", \"B\"], optionsCaption:\"Select...\", value:myObservable'></select>";
ko.applyBindings({ myObservable: observable }, testNode);

// Caption is selected when observable changed to undefined
expect(testNode.childNodes[0].selectedIndex).toEqual(2);
observable(undefined);
expect(testNode.childNodes[0].selectedIndex).toEqual(0);

// Caption is selected when observable changed to null
observable("B");
expect(testNode.childNodes[0].selectedIndex).toEqual(2);
observable(null);
expect(testNode.childNodes[0].selectedIndex).toEqual(0);

// Caption is selected when observable changed to ""
observable("B");
expect(testNode.childNodes[0].selectedIndex).toEqual(2);
observable("");
expect(testNode.childNodes[0].selectedIndex).toEqual(0);

});

it('Should display the caption when the model value changes to undefined, null, or \"\" when options specified directly', function() {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='value:myObservable'><option value=''>Select...</option><option>A</option><option>B</option></select>";
ko.applyBindings({ myObservable: observable }, testNode);

// Caption is selected when observable changed to undefined
expect(testNode.childNodes[0].selectedIndex).toEqual(2);
observable(undefined);
expect(testNode.childNodes[0].selectedIndex).toEqual(0);

// Caption is selected when observable changed to null
observable("B");
expect(testNode.childNodes[0].selectedIndex).toEqual(2);
observable(null);
expect(testNode.childNodes[0].selectedIndex).toEqual(0);

// Caption is selected when observable changed to ""
observable("B");
expect(testNode.childNodes[0].selectedIndex).toEqual(2);
observable("");
expect(testNode.childNodes[0].selectedIndex).toEqual(0);

});

it('Should update the model value when the UI is changed (setting it to undefined when the caption is selected)', function () {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='options:[\"A\", \"B\"], optionsCaption:\"Select...\", value:myObservable'></select>";
ko.applyBindings({ myObservable: observable }, testNode);
var dropdown = testNode.childNodes[0];

dropdown.selectedIndex = 1;
ko.utils.triggerEvent(dropdown, "change");
expect(observable()).toEqual("A");

dropdown.selectedIndex = 0;
ko.utils.triggerEvent(dropdown, "change");
expect(observable()).toEqual(undefined);
});

it('Should be able to associate option values with arbitrary objects (not just strings)', function() {
var x = {}, y = {};
var selectedValue = ko.observable(y);
testNode.innerHTML = "<select data-bind='options: myOptions, value: selectedValue'></select>";
var dropdown = testNode.childNodes[0];
ko.applyBindings({ myOptions: [x, y], selectedValue: selectedValue }, testNode);

// Check the UI displays the entry corresponding to the chosen value
expect(dropdown.selectedIndex).toEqual(1);

// Check that when we change the model value, the UI is updated
selectedValue(x);
expect(dropdown.selectedIndex).toEqual(0);

// Check that when we change the UI, this changes the model value
dropdown.selectedIndex = 1;
ko.utils.triggerEvent(dropdown, "change");
expect(selectedValue()).toEqual(y);
});

it('Should automatically initialize the model property to match the first option value if no option value matches the current model property value', function() {
// The rationale here is that we always want the model value to match the option that appears to be selected in the UI
// * If there is *any* option value that equals the model value, we'd initalise the select box such that *that* option is the selected one
// * If there is *no* option value that equals the model value (often because the model value is undefined), we should set the model
// value to match an arbitrary option value to avoid inconsistency between the visible UI and the model
var observable = new ko.observable(); // Undefined by default

// Should work with options specified before value
testNode.innerHTML = "<select data-bind='options:[\"A\", \"B\"], value:myObservable'></select>";
ko.applyBindings({ myObservable: observable }, testNode);
expect(observable()).toEqual("A");

// ... and with value specified before options
ko.utils.domData.clear(testNode);
testNode.innerHTML = "<select data-bind='value:myObservable, options:[\"A\", \"B\"]'></select>";
observable(undefined);
expect(observable()).toEqual(undefined);
ko.applyBindings({ myObservable: observable }, testNode);
expect(observable()).toEqual("A");
});

it('When non-empty, should reject model values that don\'t match any option value, resetting the model value to whatever is visibly selected in the UI', function() {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='options:[\"A\", \"B\", \"C\"], value:myObservable'></select>";
ko.applyBindings({ myObservable: observable }, testNode);
expect(testNode.childNodes[0].selectedIndex).toEqual(1);

observable('D'); // This change should be rejected, as there's no corresponding option in the UI
expect(observable()).not.toEqual('D');
});

it('Should support numerical option values, which are not implicitly converted to strings', function() {
var observable = new ko.observable(30);
testNode.innerHTML = "<select data-bind='options:[10,20,30,40], value:myObservable'></select>";
ko.applyBindings({ myObservable: observable }, testNode);

// First check that numerical model values will match a dropdown option
expect(testNode.childNodes[0].selectedIndex).toEqual(2); // 3rd element, zero-indexed

// Then check that dropdown options map back to numerical model values
testNode.childNodes[0].selectedIndex = 1;
ko.utils.triggerEvent(testNode.childNodes[0], "change");
expect(typeof observable()).toEqual("number");
expect(observable()).toEqual(20);
});

it('Should always use value (and not text) when options have value attributes', function() {
var observable = new ko.observable('A');
testNode.innerHTML = "<select data-bind='value:myObservable'><option value=''>A</option><option value='A'>B</option></select>";
ko.applyBindings({ myObservable: observable }, testNode);
var dropdown = testNode.childNodes[0];
expect(dropdown.selectedIndex).toEqual(1);

dropdown.selectedIndex = 0;
ko.utils.triggerEvent(dropdown, "change");
expect(observable()).toEqual("");
});

it('Should use text value when options have text values but no value attribute', function() {
var observable = new ko.observable('B');
testNode.innerHTML = "<select data-bind='value:myObservable'><option>A</option><option>B</option><option>C</option></select>";
ko.applyBindings({ myObservable: observable }, testNode);
var dropdown = testNode.childNodes[0];
expect(dropdown.selectedIndex).toEqual(1);

dropdown.selectedIndex = 0;
ko.utils.triggerEvent(dropdown, "change");
expect(observable()).toEqual("A");

observable('C');
expect(dropdown.selectedIndex).toEqual(2);
});
});
});
8 changes: 8 additions & 0 deletions src/binding/selectExtensions.js
Expand Up @@ -42,12 +42,20 @@
}
break;
case 'select':
if (value === "")
value = undefined;
if (value === null || value === undefined)
element.selectedIndex = -1;
for (var i = element.options.length - 1; i >= 0; i--) {
if (ko.selectExtensions.readValue(element.options[i]) == value) {
element.selectedIndex = i;
break;
}
}
// for drop-down select, ensure first is selected
if (!(element.size > 1) && element.selectedIndex === -1) {
element.selectedIndex = 0;
}
break;
default:
if ((value === null) || (value === undefined))
Expand Down