Skip to content

Commit

Permalink
Merge branch 'version-3.0.0-development' into merge-2.3.0-updates
Browse files Browse the repository at this point in the history
Conflicts:
	src/binding/defaultBindings/options.js
  • Loading branch information
mbest committed Jul 5, 2013
2 parents eb0e382 + b82fa8a commit 25b8388
Show file tree
Hide file tree
Showing 30 changed files with 1,942 additions and 778 deletions.
196 changes: 75 additions & 121 deletions spec/bindingAttributeBehaviors.js

Large diffs are not rendered by default.

411 changes: 411 additions & 0 deletions spec/bindingDependencyBehaviors.js

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions spec/bindingPreprocessingBehaviors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
describe('Binding preprocessing', function() {
it('Should allow binding to modify value through "preprocess" method', function() {
delete ko.bindingHandlers.a;
// create binding that has a default value of false
ko.bindingHandlers.b = {
preprocess: function(value) {
return value || "false";
}
};
var rewritten = ko.expressionRewriting.preProcessBindings("a: 1, b");
var parsedRewritten = eval("({" + rewritten + "})");
expect(parsedRewritten.a).toEqual(1);
expect(parsedRewritten.b).toEqual(false);
});

it('Should allow binding to add/replace bindings through "preprocess" method\'s "addBinding" callback', function() {
ko.bindingHandlers.a = {
preprocess: function(value, key, addBinding) {
// the a binding will be copied to a2
addBinding(key+"2", value);
return value;
}
};
ko.bindingHandlers.b = {
preprocess: function(value, key, addBinding) {
// the b binding will be replaced by b2
addBinding(key+"2", value);
}
};
var rewritten = ko.expressionRewriting.preProcessBindings("a: 1, b: 2");
var parsedRewritten = eval("({" + rewritten + "})");

expect(parsedRewritten.a).toEqual(1);
expect(parsedRewritten.a2).toEqual(1);

expect(parsedRewritten.b).toBeUndefined();
expect(parsedRewritten.b2).toEqual(2);
});

it('Should be able to chain "preprocess" calls when one adds a binding for another', function() {
ko.bindingHandlers.a = {
preprocess: function(value, key, addBinding) {
// replace with b
addBinding("b", value);
}
};
ko.bindingHandlers.b = {
preprocess: function(value, key, addBinding) {
// adds 1 to value
return '' + (+value + 1);
}
};
var rewritten = ko.expressionRewriting.preProcessBindings("a: 2");
var parsedRewritten = eval("({" + rewritten + "})");
expect(parsedRewritten.a).toBeUndefined();
expect(parsedRewritten.b).toEqual(3);
});

it('Should be able to get a dynamically created binding handler during preprocessing', function() {
var oldGetHandler = ko.getBindingHandler;
ko.getBindingHandler = function(bindingKey) {
return {
preprocess: function(value) {
return value + '2';
}
};
};
var rewritten = ko.expressionRewriting.preProcessBindings("a: 1");
ko.getBindingHandler = oldGetHandler; // restore original function

var parsedRewritten = eval("({" + rewritten + "})");
expect(parsedRewritten.a).toEqual(12);
});
});
64 changes: 48 additions & 16 deletions spec/defaultBindings/checkedBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,26 @@ describe('Binding: Checked', function() {
expect(testNode.childNodes[0].checked).toEqual(false);
});

it('When the radio button \'value\' attribute is set via attr binding, should set initial checked state correctly (attr before checked)', function() {
var myobservable = new ko.observable("this radio button value");
testNode.innerHTML = "<input type='radio' data-bind='attr:{value:\"this radio button value\"}, checked:someProp' />";
ko.applyBindings({ someProp: myobservable }, testNode);

expect(testNode.childNodes[0].checked).toEqual(true);
myobservable("another value");
expect(testNode.childNodes[0].checked).toEqual(false);
});

it('When the radio button \'value\' attribute is set via attr binding, should set initial checked state correctly (checked before attr)', function() {
var myobservable = new ko.observable("this radio button value");
testNode.innerHTML = "<input type='radio' data-bind='checked:someProp, attr:{value:\"this radio button value\"}' />";
ko.applyBindings({ someProp: myobservable }, testNode);

expect(testNode.childNodes[0].checked).toEqual(true);
myobservable("another value");
expect(testNode.childNodes[0].checked).toEqual(false);
});

it('When a \'checkedValue\' is specified, should use that as the checkbox value in the array', function() {
var model = { myArray: ko.observableArray([1,3]) };
testNode.innerHTML = "<input type='checkbox' data-bind='checked:myArray, checkedValue:1' />"
Expand Down Expand Up @@ -224,17 +244,23 @@ describe('Binding: Checked', function() {
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false);

// Update the value observable; should update that checkbox
// Update the value observable of the checked item; should update the selected values and leave checked values unchanged
object1.id(3);
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false);
expect(model.values).toEqual([3]);

// Represents current behavior, that the array is unchanged and the checkbox is unchecked
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(false);
expect(model.values).toEqual([1]);
// Update the value observable of the unchecked item; should do nothing
object2.id(4);
expect(model.values).toEqual([3]);
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false);

// But the correct behavior might be to keep it checked and update the array
// Implementing this correct behavior will probably require independent bindings (#321) and/or binding ordering
//expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
//expect(model.values).toEqual([3]);
// Update the value observable of the unchecked item to the current model value; should set to checked
object2.id(3);
expect(model.values).toEqual([3]);
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
expect(testNode.childNodes[0].childNodes[1].checked).toEqual(true);
});

it('When a \'checkedValue\' is specified, should use that as the radio button\'s value', function () {
Expand Down Expand Up @@ -273,17 +299,23 @@ describe('Binding: Checked', function() {
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false);

// Update the value observable
// Update the value observable of the checked item; should update the selected value and leave checked values unchanged
object1.id(3);
expect(model.value).toEqual(3);
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false);

// The current behavior is to uncheck the radio button
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(false);
expect(model.value).toEqual(1);
// Update the value observable of the unchecked item; should do nothing
object2.id(4);
expect(model.value).toEqual(3);
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false);

// But the correct behavior might be to keep it checked and update the model "value"
// Implementing this correct behavior will probably require independent bindings (#321) and/or binding ordering
//expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
//expect(model.value).toEqual(3);
// Update the value observable of the unchecked item to the current model value; should set to checked
object2.id(3);
expect(model.value).toEqual(3);
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
expect(testNode.childNodes[0].childNodes[1].checked).toEqual(true);
});

});
43 changes: 43 additions & 0 deletions spec/defaultBindings/foreachBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,49 @@ describe('Binding: Foreach', function() {
expect(testNode.childNodes[0]).toContainText('first childhidden child');
});

it('Should call an afterRender callback, passing all of the rendered nodes, accounting for node preprocessing and virtual element bindings', function() {
// Set up a binding provider that converts text nodes to expressions
var originalBindingProvider = ko.bindingProvider.instance,
preprocessingBindingProvider = function() { };
preprocessingBindingProvider.prototype = originalBindingProvider;
ko.bindingProvider.instance = new preprocessingBindingProvider();
ko.bindingProvider.instance.preprocessNode = function(node) {
if (node.nodeType === 3 && node.data.charAt(0) === "$") {
var newNodes = [
document.createComment('ko text: ' + node.data),
document.createComment('/ko'),
];
for (var i = 0; i < newNodes.length; i++) {
node.parentNode.insertBefore(newNodes[i], node);
}
node.parentNode.removeChild(node);
return newNodes;
}
};

// Now perform a foreach binding, and see that afterRender gets the output from the preprocessor and bindings
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, afterRender: callback }'><span>[</span>$data<span>]</span></div>";
var someItems = ko.observableArray(['Alpha', 'Beta']),
callbackReceivedArrayValues = [];
ko.applyBindings({
someItems: someItems,
callback: function(nodes, arrayValue) {
expect(nodes.length).toBe(5);
expect(nodes[0]).toContainText('['); // <span>[</span>
expect(nodes[1].nodeType).toBe(8); // <!-- ko text: $data -->
expect(nodes[2].nodeType).toBe(3); // text node inserted by text binding
expect(nodes[3].nodeType).toBe(8); // <!-- /ko -->
expect(nodes[4]).toContainText(']'); // <span>]</span>
callbackReceivedArrayValues.push(arrayValue);
}
}, testNode);

expect(testNode.childNodes[0]).toContainText('[Alpha][Beta]');
expect(callbackReceivedArrayValues).toEqual(['Alpha', 'Beta']);

ko.bindingProvider.instance = originalBindingProvider;
});

it('Should call an afterAdd callback function and not cause updates if an observable accessed in the callback is changed', function () {
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, afterAdd: callback }'><span data-bind='text: childprop'></span></div>";
var callbackObservable = ko.observable(1),
Expand Down
34 changes: 34 additions & 0 deletions spec/defaultBindings/textBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,38 @@ describe('Binding: Text', function() {
expect(testNode).toContainText("xxx ");
expect(testNode).toContainHtml("xxx <!--ko text: undefined--><!--/ko-->");
});

it('Should not attempt data binding on the generated text node', function() {
// Since custom binding providers can regard text nodes as bindable, it would be a
// security risk to bind against user-supplied text (XSS).

// First replace the binding provider with one that's hardcoded to replace all text
// content with a special message, via a binding handler that operates on text nodes
var originalBindingProvider = ko.bindingProvider.instance;
ko.bindingProvider.instance = {
nodeHasBindings: function(node, bindingContext) {
return true;
},
getBindingAccessors: function(node, bindingContext) {
if (node.nodeType === 3) {
return {
replaceTextNodeContent: function() { return "should not see this value in the output"; }
};
} else {
return originalBindingProvider.getBindingAccessors(node, bindingContext);
}
}
};
ko.bindingHandlers.replaceTextNodeContent = {
update: function(textNode, valueAccessor) { textNode.data = valueAccessor(); }
};

// Now check that, after applying the "text" binding, the emitted text node does *not*
// get replaced by the special message.
testNode.innerHTML = "<span data-bind='text: sometext'></span>";
ko.applyBindings({ sometext: 'hello' }, testNode);
expect("textContent" in testNode ? testNode.textContent : testNode.innerText).toEqual('hello');

ko.bindingProvider.instance = originalBindingProvider;
});
});
Loading

0 comments on commit 25b8388

Please sign in to comment.