Skip to content

Commit

Permalink
Add noChildContextWithAs option to not create a child binding context…
Browse files Browse the repository at this point in the history
… when using the `as` option with `with` or `foreach`.
  • Loading branch information
mbest committed Nov 25, 2017
1 parent 34b4123 commit 89b056e
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 15 deletions.
62 changes: 58 additions & 4 deletions spec/defaultBindings/foreachBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ describe('Binding: Foreach', function() {

it('Should be able to give an alias to $data using \"as\", and use it within a nested loop', function() {
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, as: \"item\" }'>"
+ "<span data-bind='foreach: sub'>"
+ "<span data-bind='foreach: item.sub'>"
+ "<span data-bind='text: item.name+\":\"+$data'></span>,"
+ "</span>"
+ "</div>";
Expand All @@ -551,7 +551,7 @@ describe('Binding: Foreach', function() {

it('Should be able to set up multiple nested levels of aliases using \"as\"', function() {
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, as: \"item\" }'>"
+ "<span data-bind='foreach: { data: sub, as: \"subvalue\" }'>"
+ "<span data-bind='foreach: { data: item.sub, as: \"subvalue\" }'>"
+ "<span data-bind='text: item.name+\":\"+subvalue'></span>,"
+ "</span>"
+ "</div>";
Expand Down Expand Up @@ -600,7 +600,7 @@ describe('Binding: Foreach', function() {
}
});

it('Should provide access to observable array items through $rawData', function() {
it('Should provide access to observable items through $rawData', function() {
testNode.innerHTML = "<div data-bind='foreach: someItems'><input data-bind='value: $rawData'/></div>";
var x = ko.observable('first'), y = ko.observable('second'), someItems = ko.observableArray([ x, y ]);
ko.applyBindings({ someItems: someItems }, testNode);
Expand All @@ -620,7 +620,7 @@ describe('Binding: Foreach', function() {
expect(testNode.childNodes[0]).toHaveValues(['third']);
});

it('Should not re-render the nodes when a observable array item changes', function() {
it('Should not re-render the nodes when an observable item changes', function() {
testNode.innerHTML = "<div data-bind='foreach: someItems'><span data-bind='text: $data'></span></div>";
var x = ko.observable('first'), someItems = [ x ];
ko.applyBindings({ someItems: someItems }, testNode);
Expand Down Expand Up @@ -677,4 +677,58 @@ describe('Binding: Foreach', function() {
expect(testNode).toContainText('--Mercury++--Venus++--Earth++--Mars++--Jupiter++--Saturn++');

});

describe('With \"noChildContextWithAs\" and \"as\"', function () {
beforeEach(function() {
this.restoreAfter(ko.options, 'noChildContextWithAs');
ko.options.noChildContextWithAs = true;
});

it('Should not create a child context', function () {
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, as: \"item\" }'><span data-bind='text: item'></span></div>";
var someItems = ['alpha', 'beta'];
ko.applyBindings({ someItems: someItems }, testNode);

expect(testNode.childNodes[0].childNodes[0]).toContainText('alpha');
expect(testNode.childNodes[0].childNodes[1]).toContainText('beta');

expect(ko.dataFor(testNode.childNodes[0].childNodes[0])).toEqual(ko.dataFor(testNode));
expect(ko.dataFor(testNode.childNodes[0].childNodes[1])).toEqual(ko.dataFor(testNode));
});

it('Should provide access to observable items', function() {
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, as: \"item\" }'><input data-bind='value: item'/></div>";
var x = ko.observable('first'), y = ko.observable('second'), someItems = ko.observableArray([ x, y ]);
ko.applyBindings({ someItems: someItems }, testNode);
expect(testNode.childNodes[0]).toHaveValues(['first', 'second']);

expect(ko.dataFor(testNode.childNodes[0].childNodes[0])).toEqual(ko.dataFor(testNode));
expect(ko.dataFor(testNode.childNodes[0].childNodes[1])).toEqual(ko.dataFor(testNode));

// Should update observable when input is changed
testNode.childNodes[0].childNodes[0].value = 'third';
ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "change");
expect(x()).toEqual('third');

// Should update the input when the observable changes
y('fourth');
expect(testNode.childNodes[0]).toHaveValues(['third', 'fourth']);

// Should update the inputs when the array changes
someItems([x]);
expect(testNode.childNodes[0]).toHaveValues(['third']);
});

it('Should not re-render the nodes when an observable item changes', function() {
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, as: \"item\" }'><span data-bind='text: item'></span></div>";
var x = ko.observable('first'), someItems = [ x ];
ko.applyBindings({ someItems: someItems }, testNode);
expect(testNode.childNodes[0]).toContainText('first');

var saveNode = testNode.childNodes[0].childNodes[0];
x('second');
expect(testNode.childNodes[0]).toContainText('second');
expect(testNode.childNodes[0].childNodes[0]).toEqual(saveNode);
});
});
});
68 changes: 66 additions & 2 deletions spec/defaultBindings/withBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ describe('Binding: With', function() {
expect(ko.contextFor(firstSpan).$parents[1].name).toEqual("top");
});

it('Should be able to access all parent binding when using `as:`', function() {
it('Should be able to access all parent bindings when using \"as\"', function() {
testNode.innerHTML = "<div data-bind='with: topItem'>" +
"<div data-bind='with: middleItem, as: \"middle\"'>" +
"<div data-bind='with: bottomItem'>" +
Expand Down Expand Up @@ -275,4 +275,68 @@ describe('Binding: With', function() {
expect(callbacks).toEqual(2);
expect(testNode.childNodes[0]).toContainText('new child');
});
});

describe('With \"noChildContextWithAs\" and \"as\"', function () {
beforeEach(function() {
this.restoreAfter(ko.options, 'noChildContextWithAs');
ko.options.noChildContextWithAs = true;
});

it('Should not create a child context', function () {
testNode.innerHTML = "<div data-bind='with: someItem, as: \"item\"'><span data-bind='text: item.childProp'></span></div>";
var someItem = { childProp: 'Hello' };
ko.applyBindings({ someItem: someItem }, testNode);

expect(testNode.childNodes[0].childNodes[0]).toContainText('Hello');
expect(ko.dataFor(testNode.childNodes[0].childNodes[0])).toEqual(ko.dataFor(testNode));
});

it('Should provide access to observable value', function() {
testNode.innerHTML = "<div data-bind='with: someItem, as: \"item\"'><input data-bind='value: item'/></div>";
var someItem = ko.observable('Hello');
ko.applyBindings({ someItem: someItem }, testNode);
expect(testNode.childNodes[0].childNodes[0].value).toEqual('Hello');

expect(ko.dataFor(testNode.childNodes[0].childNodes[0])).toEqual(ko.dataFor(testNode));

// Should update observable when input is changed
testNode.childNodes[0].childNodes[0].value = 'Goodbye';
ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "change");
expect(someItem()).toEqual('Goodbye');

// Should update the input when the observable changes
someItem('Hello again');
expect(testNode.childNodes[0].childNodes[0].value).toEqual('Hello again');
});

it('Should not re-render the nodes when an observable value changes', function() {
testNode.innerHTML = "<div data-bind='with: someItem, as: \"item\"'><span data-bind='text: item'></span></div>";
var someItem = ko.observable('first');
ko.applyBindings({ someItem: someItem }, testNode);
expect(testNode.childNodes[0]).toContainText('first');

var saveNode = testNode.childNodes[0].childNodes[0];
someItem('second');
expect(testNode.childNodes[0]).toContainText('second');
expect(testNode.childNodes[0].childNodes[0]).toEqual(saveNode);
});

it('Should remove nodes with an observable value become falsy', function() {
var someItem = ko.observable(undefined);
testNode.innerHTML = "<div data-bind='with: someItem, as: \"item\"'><span data-bind='text: item().occasionallyExistentChildProp'></span></div>";
ko.applyBindings({ someItem: someItem }, testNode);

// First it's not there
expect(testNode.childNodes[0].childNodes.length).toEqual(0);

// Then it's there
someItem({ occasionallyExistentChildProp: 'Child prop value' });
expect(testNode.childNodes[0].childNodes.length).toEqual(1);
expect(testNode.childNodes[0].childNodes[0]).toContainText("Child prop value");

// Then it's gone again
someItem(null);
expect(testNode.childNodes[0].childNodes.length).toEqual(0);
});
});
});
11 changes: 10 additions & 1 deletion src/binding/bindingAttributeSyntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,16 @@
// view model also depends on the parent view model, you must provide a function that returns the correct
// view model on each update.
ko.bindingContext.prototype['createChildContext'] = function (dataItemOrAccessor, dataItemAlias, extendCallback, options) {
return new ko.bindingContext(dataItemOrAccessor, this, dataItemAlias, function(self, parentContext) {
if (dataItemAlias && ko.options['noChildContextWithAs']) {
var isFunc = typeof(dataItemOrAccessor) == "function" && !ko.isObservable(dataItemOrAccessor);
return new ko.bindingContext(inheritParentVm, this, null, function (self) {
if (extendCallback)
extendCallback(self);
self[dataItemAlias] = isFunc ? dataItemOrAccessor() : dataItemOrAccessor;
});
}

return new ko.bindingContext(dataItemOrAccessor, this, dataItemAlias, function (self, parentContext) {
// Extend the context hierarchy by setting the appropriate pointers
self['$parentContext'] = parentContext;
self['$parent'] = parentContext['$data'];
Expand Down
16 changes: 9 additions & 7 deletions src/binding/defaultBindings/ifIfnotWith.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
function makeWithIfBinding(bindingKey, isWith, isNot) {
ko.bindingHandlers[bindingKey] = {
'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var savedNodes;
var ifCondition = !isWith && ko.computed(function() {
return !isNot !== !ko.utils.unwrapObservable(valueAccessor());
}, null, { disposeWhenNodeIsRemoved: element });
var savedNodes,
asOption = allBindings.get('as'),
wrapCondition = !isWith || (asOption && ko.options['noChildContextWithAs']),
ifCondition = wrapCondition && ko.computed(function() {
return !isNot !== !ko.utils.unwrapObservable(valueAccessor());
}, null, { disposeWhenNodeIsRemoved: element });

ko.computed(function() {
var rawWithValue = isWith && ko.utils.unwrapObservable(valueAccessor()),
shouldDisplay = isWith ? !!rawWithValue : ifCondition(),
var rawWithValue = !wrapCondition && ko.utils.unwrapObservable(valueAccessor()),
shouldDisplay = wrapCondition ? ifCondition() : !!rawWithValue,
isFirstRender = !savedNodes;

// Save a copy of the inner nodes on the initial update, but only if we have dependencies.
Expand All @@ -23,7 +25,7 @@ function makeWithIfBinding(bindingKey, isWith, isNot) {
}
ko.applyBindingsToDescendants(
isWith ?
bindingContext['createChildContext'](typeof rawWithValue == "function" ? rawWithValue : valueAccessor, allBindings.get('as')) :
bindingContext['createChildContext'](typeof rawWithValue == "function" ? rawWithValue : valueAccessor, asOption) :
ifCondition.isActive() ?
bindingContext['extend'](function() { ifCondition(); return null; }) :
bindingContext,
Expand Down
3 changes: 2 additions & 1 deletion src/options.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// For any options that may affect various areas of Knockout and aren't directly associated with data binding.
ko.options = {
'deferUpdates': false,
'useOnlyNativeEvents': false
'useOnlyNativeEvents': false,
'noChildContextWithAs': false
};

//ko.exportSymbol('options', ko.options); // 'options' isn't minified

0 comments on commit 89b056e

Please sign in to comment.