Permalink
Browse files

Merge branch 'master' into foreach-context-index

Conflicts:
	spec/templatingBehaviors.js
	src/binding/editDetection/arrayToDomNodeChildren.js
	src/templating/templating.js
  • Loading branch information...
2 parents d190e51 + c1d4d08 commit 0535189e9b6c3416bad43b1c512a5c814c23536e Mark Bradley committed Dec 22, 2011
Oops, something went wrong.
Oops, something went wrong.
@@ -75,6 +75,12 @@ describe('Binding attribute syntax', {
ko.applyBindings(null, testNode); // No exception means success
},
+ 'Should tolerate wacky IE conditional comments': function() {
+ // Represents issue https://github.com/SteveSanderson/knockout/issues/186. Would fail on IE9, but work on earlier IE versions.
+ testNode.innerHTML = "<div><!--[if IE]><!-->Hello<!--<![endif]--></div>";
+ ko.applyBindings(null, testNode); // No exception means success
+ },
+
'Should invoke registered handlers\' init() then update() methods passing binding data': function () {
var methodsInvoked = [];
ko.bindingHandlers.test = {
@@ -227,5 +233,55 @@ describe('Binding attribute syntax', {
value_of(ex.message).should_be("The binding 'visible' cannot be used with virtual elements");
}
value_of(didThrow).should_be(true);
+ },
+
+ 'Should not reinvoke init for notifications triggered during first evaluation': function () {
+ var observable = ko.observable('A');
+ var initCalls = 0;
+ ko.bindingHandlers.test = {
+ init: function (element, valueAccessor) {
+ initCalls++;
+
+ var value = valueAccessor();
+
+ // Read the observable (to set up a dependency on it), and then also write to it (to trigger re-eval of bindings)
+ // This logic probably wouldn't be in init but might be indirectly invoked by init
+ value();
+ value('B');
+ }
+ };
+ testNode.innerHTML = "<div data-bind='test: myObservable'></div>";
+
+ ko.applyBindings({ myObservable: observable }, testNode);
+ value_of(initCalls).should_be(1);
+ },
+
+ 'Should not run update before init, even if an associated observable is updated by a different binding before init': function() {
+ // Represents the "theoretical issue" posed by Ryan in comments on https://github.com/SteveSanderson/knockout/pull/193
+
+ var observable = ko.observable('A'), hasInittedSecondBinding = false, hasUpdatedSecondBinding = false;
+ ko.bindingHandlers.test1 = {
+ init: function(element, valueAccessor) {
+ // Read the observable (to set up a dependency on it), and then also write to it (to trigger re-eval of bindings)
+ // This logic probably wouldn't be in init but might be indirectly invoked by init
+ var value = valueAccessor();
+ value();
+ value('B');
+ }
+ }
+ ko.bindingHandlers.test2 = {
+ init: function() {
+ hasInittedSecondBinding = true;
+ },
+ update: function() {
+ if (!hasInittedSecondBinding)
+ throw new Error("Called 'update' before 'init'");
+ hasUpdatedSecondBinding = true;
+ }
+ }
+ testNode.innerHTML = "<div data-bind='test1: myObservable, test2: true'></div>";
+
+ ko.applyBindings({ myObservable: observable }, testNode);
+ value_of(hasUpdatedSecondBinding).should_be(true);
}
});
@@ -266,14 +266,29 @@ describe('Binding: Value', {
value_of(myobservable()).should_be("some user-entered value");
},
- 'For select boxes, should update selectedIndex when the model changes': function() {
+ '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);
value_of(testNode.childNodes[0].selectedIndex).should_be(1);
+ value_of(observable()).should_be('B');
+
observable('A');
value_of(testNode.childNodes[0].selectedIndex).should_be(0);
+ value_of(observable()).should_be('A');
},
+
+ '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);
+ value_of(testNode.childNodes[0].selectedIndex).should_be(1);
+ value_of(observable()).should_be('B');
+
+ observable('A');
+ value_of(testNode.childNodes[0].selectedIndex).should_be(0);
+ value_of(observable()).should_be('A');
+ },
'For select boxes, should display the caption when the model value changes to undefined': function() {
var observable = new ko.observable('B');
@@ -325,12 +340,21 @@ describe('Binding: Value', {
// * 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);
value_of(observable()).should_be("A");
+
+ // ... and with value specified before options
+ testNode.innerHTML = "<select data-bind='value:myObservable, options:[\"A\", \"B\"]'></select>";
+ observable(undefined);
+ value_of(observable()).should_be(undefined);
+ ko.applyBindings({ myObservable: observable }, testNode);
+ value_of(observable()).should_be("A");
},
- 'For 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() {
+ '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);
@@ -688,7 +712,8 @@ describe('Binding: Checked', {
testNode.innerHTML = "<input type='radio' value='this radio button value' data-bind='checked:someProp' />";
ko.applyBindings({ someProp: myobservable }, testNode);
- ko.utils.triggerEvent(testNode.childNodes[0], "click");
+ value_of(myobservable()).should_be("another value");
+ testNode.childNodes[0].click();
value_of(myobservable()).should_be("this radio button value");
},
@@ -1194,6 +1219,29 @@ describe('Binding: Foreach', {
value_of(testNode.childNodes[0]).should_contain_html('<span data-bind="text: childprop">second child</span>');
},
+ 'Should remove all nodes corresponding to a removed array item, even if they were generated via containerless templates': function() {
+ // Represents issue https://github.com/SteveSanderson/knockout/issues/185
+ testNode.innerHTML = "<div data-bind='foreach: someitems'>a<!-- ko if:true -->b<!-- /ko --></div>";
+ var someitems = ko.observableArray([1,2]);
+ ko.applyBindings({ someitems: someitems }, testNode);
+ value_of(testNode).should_contain_html('<div data-bind="foreach: someitems">a<!-- ko if:true -->b<!-- /ko -->a<!-- ko if:true -->b<!-- /ko --></div>');
+
+ // Now remove items, and check the corresponding child nodes vanished
+ someitems.splice(1, 1);
+ value_of(testNode).should_contain_html('<div data-bind="foreach: someitems">a<!-- ko if:true -->b<!-- /ko --></div>');
+ },
+
+ 'Should update all nodes corresponding to a changed array item, even if they were generated via containerless templates': function() {
+ testNode.innerHTML = "<div data-bind='foreach: someitems'><!-- ko if:true --><span data-bind='text: $data'></span><!-- /ko --></div>";
+ var someitems = [ ko.observable('A'), ko.observable('B') ];
+ ko.applyBindings({ someitems: someitems }, testNode);
+ value_of(testNode).should_contain_text('AB');
+
+ // Now update an item
+ someitems[0]('A2');
+ value_of(testNode).should_contain_text('A2B');
+ },
+
'Should be able to supply show "_destroy"ed items via includeDestroyed option': function() {
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, includeDestroyed: true }'><span data-bind='text: childProp'></span></div>";
var someItems = ko.observableArray([
@@ -67,6 +67,19 @@ describe('Dependent Observable', {
value_of(invokedWriteWithArgs[2]).should_be(["third1", "third2"]);
value_of(invokedWriteWithThis).should_be(someOwner);
},
+
+ 'Should use the second arg (evaluatorFunctionTarget) for "this" when calling read/write if no options.owner was given': function() {
+ var expectedThis = {}, actualReadThis, actualWriteThis;
+ var instance = new ko.dependentObservable({
+ read: function() { actualReadThis = this },
+ write: function() { actualWriteThis = this }
+ }, expectedThis);
+
+ instance("force invocation of write");
+
+ value_of(actualReadThis).should_be(expectedThis);
+ value_of(actualWriteThis).should_be(expectedThis);
+ },
'Should be able to pass evaluator function using "options" parameter called "read"': function() {
var instance = new ko.dependentObservable({
@@ -683,5 +683,14 @@ describe('Templating', {
testNode.innerHTML = "Start <!-- ko template: { data: someData } -->Childprop: [js: childProp]<!-- /ko --> End";
ko.applyBindings({ someData: { childProp: 'abc' } }, testNode);
value_of(testNode).should_contain_html("start <!-- ko template: { data: somedata } --><div>childprop: abc</div><!-- /ko -->end");
- }
+ },
+
+ 'Should be able to use anonymous templates that contain first-child comment nodes': function() {
+ // This represents issue https://github.com/SteveSanderson/knockout/issues/188
+ // (IE < 9 strips out leading comment nodes when you use .innerHTML)
+ ko.setTemplateEngine(new dummyTemplateEngine({}));
+ testNode.innerHTML = "start <div data-bind='foreach: [1,2]'><span><!-- leading comment -->hello</span></div>";
+ ko.applyBindings(null, testNode);
+ value_of(testNode).should_contain_html('start <div data-bind="foreach: [1,2]"><span><!-- leading comment -->hello</span><span><!-- leading comment -->hello</span></div>');
+ }
})
@@ -13,10 +13,8 @@
this['$root'] = dataItem;
}
}
- ko.bindingContext.prototype = {
- createChildContext: function (dataItem) {
- return new ko.bindingContext(dataItem, this);
- }
+ ko.bindingContext.prototype['createChildContext'] = function (dataItem) {
+ return new ko.bindingContext(dataItem, this);
};
function validateThatBindingIsAllowedForVirtualElements(bindingName) {
@@ -55,7 +53,8 @@
}
function applyBindingsToNodeInternal (node, bindings, viewModelOrBindingContext, isRootNodeForBindingContext) {
- var isFirstEvaluation = true;
+ // Need to be sure that inits are only run once, and updates never run until all the inits have been run
+ var initPhase = 0; // 0 = before all inits, 1 = during inits, 2 = after all inits
// Pre-process any anonymous template bounded by comment nodes
ko.virtualElements.extractAnonymousTemplateIfVirtualElement(node);
@@ -94,7 +93,8 @@
if (parsedBindings) {
// First run all the inits, so bindings can register for notification on changes
- if (isFirstEvaluation) {
+ if (initPhase === 0) {
+ initPhase = 1;
for (var bindingKey in parsedBindings) {
var binding = ko.bindingHandlers[bindingKey];
if (binding && node.nodeType === 8)
@@ -111,15 +111,18 @@
bindingHandlerThatControlsDescendantBindings = bindingKey;
}
}
- }
+ }
+ initPhase = 2;
}
// ... then run all the updates, which might trigger changes even on the first evaluation
- for (var bindingKey in parsedBindings) {
- var binding = ko.bindingHandlers[bindingKey];
- if (binding && typeof binding["update"] == "function") {
- var handlerUpdateFn = binding["update"];
- handlerUpdateFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContextInstance);
+ if (initPhase === 2) {
+ for (var bindingKey in parsedBindings) {
+ var binding = ko.bindingHandlers[bindingKey];
+ if (binding && typeof binding["update"] == "function") {
+ var handlerUpdateFn = binding["update"];
+ handlerUpdateFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContextInstance);
+ }
}
}
}
@@ -128,7 +131,6 @@
{ 'disposeWhenNodeIsRemoved' : node }
);
- isFirstEvaluation = false;
return {
shouldBindDescendants: bindingHandlerThatControlsDescendantBindings === undefined
};
@@ -148,6 +150,11 @@
return applyBindingsToNodeInternal(node, bindings, viewModel, true);
};
+ ko.applyBindingsToDescendants = function(viewModel, rootNode) {
+ if (rootNode.nodeType === 1)
+ applyBindingsToDescendantsInternal(viewModel, rootNode);
+ };
+
ko.applyBindings = function (viewModel, rootNode) {
if (rootNode && (rootNode.nodeType !== 1) && (rootNode.nodeType !== 8))
throw new Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");
@@ -176,6 +183,7 @@
ko.exportSymbol('ko.bindingHandlers', ko.bindingHandlers);
ko.exportSymbol('ko.applyBindings', ko.applyBindings);
+ ko.exportSymbol('ko.applyBindingsToDescendants', ko.applyBindingsToDescendants);
ko.exportSymbol('ko.applyBindingsToNode', ko.applyBindingsToNode);
ko.exportSymbol('ko.contextFor', ko.contextFor);
ko.exportSymbol('ko.dataFor', ko.dataFor);
@@ -100,6 +100,19 @@ ko.bindingHandlers['disable'] = {
}
};
+function ensureDropdownSelectionIsConsistentWithModelValue(element, modelValue, preferModelValue) {
+ if (preferModelValue) {
+ if (modelValue !== ko.selectExtensions.readValue(element))
+ ko.selectExtensions.writeValue(element, modelValue);
+ }
+
+ // No matter which direction we're syncing in, we want the end result to be equality between dropdown value and model value.
+ // If they aren't equal, either we prefer the dropdown value, or the model value couldn't be represented, so either way,
+ // change the model value to match the dropdown.
+ if (modelValue !== ko.selectExtensions.readValue(element))
+ ko.utils.triggerEvent(element, "change");
+};
+
ko.bindingHandlers['value'] = {
'init': function (element, valueAccessor, allBindingsAccessor) {
// Always catch "change" event; possibly other events too if asked
@@ -161,13 +174,10 @@ ko.bindingHandlers['value'] = {
setTimeout(applyValueAction, 0);
}
- // For SELECT nodes, you're not allowed to have a model value that disagrees with the UI selection, so if there is a
- // difference, treat it as a change that should be written back to the model
- if (element.tagName == "SELECT") {
- elementValue = ko.selectExtensions.readValue(element);
- if(elementValue !== newValue)
- ko.utils.triggerEvent(element, "change");
- }
+ // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change,
+ // because you're not allowed to have a model value that disagrees with a visible UI selection.
+ if ((element.tagName == "SELECT") && (element.length > 0))
+ ensureDropdownSelectionIsConsistentWithModelValue(element, newValue, /* preferModelValue */ false);
}
};
@@ -176,6 +186,7 @@ ko.bindingHandlers['options'] = {
if (element.tagName != "SELECT")
throw new Error("options binding applies only to SELECT elements");
+ var selectWasPreviouslyEmpty = element.length == 0;
var previousSelectedValues = ko.utils.arrayMap(ko.utils.arrayFilter(element.childNodes, function (node) {
return node.tagName && node.tagName == "OPTION" && node.selected;
}), function (node) {
@@ -185,7 +196,13 @@ ko.bindingHandlers['options'] = {
var value = ko.utils.unwrapObservable(valueAccessor());
var selectedValue = element.value;
- ko.utils.emptyDomNode(element);
+
+ // Remove all existing <option>s.
+ // Need to use .remove() rather than .removeChild() for <option>s otherwise IE behaves oddly (https://github.com/SteveSanderson/knockout/issues/134)
+ while (element.length > 0) {
+ ko.cleanNode(element.options[0]);
+ element.remove(0);
+ }
if (value) {
var allBindings = allBindingsAccessor();
@@ -236,6 +253,13 @@ ko.bindingHandlers['options'] = {
if (previousScrollTop)
element.scrollTop = previousScrollTop;
+
+ if (selectWasPreviouslyEmpty && ('value' in allBindings)) {
+ // Ensure consistency between model value and selected option.
+ // If the dropdown is being populated for the first time here (or was otherwise previously empty),
+ // the dropdown selection state is meaningless, so we preserve the model value.
+ ensureDropdownSelectionIsConsistentWithModelValue(element, ko.utils.unwrapObservable(allBindings['value']), /* preferModelValue */ true);
+ }
}
}
};
@@ -330,8 +354,10 @@ ko.bindingHandlers['uniqueName'] = {
if (valueAccessor()) {
element.name = "ko_unique_" + (++ko.bindingHandlers['uniqueName'].currentIndex);
- // Workaround IE 6 issue - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/
- if (ko.utils.isIe6)
+ // Workaround IE 6/7 issue
+ // - https://github.com/SteveSanderson/knockout/issues/197
+ // - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/
+ if (ko.utils.isIe6 || ko.utils.isIe7)
element.mergeAttributes(document.createElement("<input name='" + element.name + "'/>"), false);
}
}
@@ -516,3 +542,4 @@ ko.bindingHandlers['foreach'] = {
};
ko.jsonExpressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
ko.virtualElements.allowedBindings['foreach'] = true;
+ko.exportSymbol('ko.allowedVirtualElementBindings', ko.virtualElements.allowedBindings);
Oops, something went wrong.

0 comments on commit 0535189

Please sign in to comment.