Skip to content

Commit

Permalink
Treat functions and observables differently in ko.bindingContext. If …
Browse files Browse the repository at this point in the history
…a function is provided, the return value of the function becomes $rawData, and if an observable, will be unwrapped.
  • Loading branch information
mbest committed Feb 1, 2014
1 parent 65d6906 commit ea06bdd
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 11 deletions.
49 changes: 44 additions & 5 deletions spec/bindingDependencyBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,45 @@ describe('Binding dependencies', function() {
expect(vm.getSubscriptionsCount()).toEqual(0);
});

it('Should provide access to the view model\'s observable through $rawData', function() {
var vm = ko.observable('text');
testNode.innerHTML = "<div data-bind='text:$data'></div>";
ko.applyBindings(vm, testNode);
expect(testNode).toContainText("text");

var context = ko.contextFor(testNode);
expect(context.$data).toEqual('text');
expect(context.$rawData).toBe(vm);
});

it('Should set $rawData to the observable returned from a function', function() {
var vm = ko.observable('text');
testNode.innerHTML = "<div data-bind='text:$data'></div>";
ko.applyBindings(function() { return vm; }, testNode);
expect(testNode).toContainText("text");

var context = ko.contextFor(testNode);
expect(context.$data).toEqual('text');
expect(context.$rawData).toBe(vm);
});

it('Should set $rawData to the view model if a function unwraps the observable view model', function() {
var vm = ko.observable('text');
testNode.innerHTML = "<div data-bind='text:$data'></div>";
ko.applyBindings(function() { return vm(); }, testNode);
expect(testNode).toContainText("text");

var context = ko.contextFor(testNode);
expect(context.$data).toEqual('text');
expect(context.$rawData).toBe('text');

// Updating view model updates bindings and context
vm('new text');
expect(testNode).toContainText("new text");
expect(context.$data).toEqual('new text');
expect(context.$rawData).toBe('new text');
});

it('Should dispose view model subscription on next update when bound node is removed outside of KO', function() {
var vm = ko.observable('text');
testNode.innerHTML = "<div data-bind='text:$data'></div>";
Expand Down Expand Up @@ -310,17 +349,17 @@ describe('Binding dependencies', function() {
}
};

testNode.innerHTML = "<div data-bind='withProperties: obj1'><span data-bind='text:prop1'></span><span data-bind='text:prop2'></span></div>";
var vm = ko.observable({obj1: {prop1: "First "}, prop2: "view model"});
testNode.innerHTML = "<div data-bind='withProperties: obj1'><span data-bind='text:prop1'></span><span data-bind='text:prop2'></span><span data-bind='text:$rawData().prop3'></span></div>";
var vm = ko.observable({obj1: {prop1: "First "}, prop2: "view ", prop3: "model"});
ko.applyBindings(vm, testNode);
expect(testNode).toContainText("First view model");

// ch ange view model to new object
vm({obj1: {prop1: "Second view "}, prop2: "model"});
// change view model to new object
vm({obj1: {prop1: "Second view "}, prop2: "model", prop3: ""});
expect(testNode).toContainText("Second view model");

// change it again
vm({obj1: {prop1: "Third view model"}, prop2: ""});
vm({obj1: {prop1: ""}, prop2: "", prop3: "Third view model"});
expect(testNode).toContainText("Third view model");

// clear the element and the view-model (shouldn't be any errors) and the subscription should be cleared
Expand Down
16 changes: 10 additions & 6 deletions src/binding/bindingAttributeSyntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@
// any child contexts, must be updated when the view model is changed.
function updateContext() {
// Most of the time, the context will directly get a view model object, but if a function is given,
// we call the function to retrieve the view model. If the function accesses any obsevables (or is
// itself an observable), the dependency is tracked, and those observables can later cause the binding
// we call the function to retrieve the view model. If the function accesses any obsevables or returns
// an observable, the dependency is tracked, and those observables can later cause the binding
// context to be updated.
var dataItem = isFunc ? dataItemOrAccessor() : dataItemOrAccessor;
var dataItemOrObservable = isFunc ? dataItemOrAccessor() : dataItemOrAccessor,
dataItem = ko.utils.unwrapObservable(dataItemOrObservable);

if (parentContext) {
// When a "parent" context is given, register a dependency on the parent context. Thus whenever the
Expand All @@ -52,7 +53,7 @@
// See https://github.com/SteveSanderson/knockout/issues/490
self['ko'] = ko;
}
self['$rawData'] = dataItemOrAccessor;
self['$rawData'] = dataItemOrObservable;
self['$data'] = dataItem;
if (dataItemAlias)
self[dataItemAlias] = dataItem;
Expand All @@ -70,7 +71,7 @@
}

var self = this,
isFunc = typeof(dataItemOrAccessor) == "function",
isFunc = typeof(dataItemOrAccessor) == "function" && !ko.isObservable(dataItemOrAccessor),
nodes,
subscribable = ko.dependentObservable(updateContext, null, { disposeWhen: disposeWhen, disposeWhenNodeIsRemoved: true });

Expand Down Expand Up @@ -125,7 +126,10 @@
// Similarly to "child" contexts, provide a function here to make sure that the correct values are set
// when an observable view model is updated.
ko.bindingContext.prototype['extend'] = function(properties) {
return new ko.bindingContext(this['$rawData'], this, null, function(self) {
return new ko.bindingContext(this._subscribable || this['$data'], this, null, function(self, parentContext) {
// This "child" context doesn't directly track a parent observable view model,
// so we need to manually set the $rawData value to match the parent.
self['$rawData'] = parentContext['$rawData'];
ko.utils.extend(self, typeof(properties) == "function" ? properties() : properties);
});
};
Expand Down

0 comments on commit ea06bdd

Please sign in to comment.