Skip to content

Commit

Permalink
Merge pull request knockout#1206 from knockout/1206-fix-rawData
Browse files Browse the repository at this point in the history
For observable view models, $rawData should be the observable
  • Loading branch information
SteveSanderson committed Feb 10, 2014
2 parents 706251e + f5832fd commit 613c3d1
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 13 deletions.
85 changes: 78 additions & 7 deletions spec/bindingDependencyBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,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 @@ -332,24 +371,24 @@ describe('Binding dependencies', function() {

it('Should update all extended contexts (including values copied from the parent)', function() {
ko.bindingHandlers.withProperties = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var innerBindingContext = bindingContext.extend(valueAccessor);
ko.applyBindingsToDescendants(innerBindingContext, element);
return { controlsDescendantBindings : true };
}
};

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 All @@ -358,9 +397,41 @@ describe('Binding dependencies', function() {
expect(vm.getSubscriptionsCount()).toEqual(0);
});

it('Should maintain correct $rawData in extended context when parent is bound to a function that returns an observable view model', function() {
ko.bindingHandlers.extended = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
ko.applyBindingsToDescendants(bindingContext.extend(valueAccessor), element);
return { controlsDescendantBindings : true };
}
};

var vm1 = ko.observable('vm1'),
vm2 = ko.observable('vm2'),
whichVm = ko.observable(vm1);
testNode.innerHTML = "<div data-bind='extended: {}'><div data-bind='text: $data'></div></div>";
ko.applyBindings(function() { return whichVm(); }, testNode);
expect(testNode).toContainText('vm1');

var parentContext = ko.contextFor(testNode),
childContext = ko.contextFor(testNode.childNodes[0].childNodes[0]);

expect(parentContext.$data).toEqual('vm1');
expect(parentContext.$rawData).toBe(vm1);

expect(childContext).not.toBe(parentContext);
expect(childContext.$data).toEqual('vm1');
expect(childContext.$rawData).toBe(vm1);

// Updating view model updates bindings and context
whichVm(vm2);
expect(testNode).toContainText('vm2');
expect(childContext.$data).toEqual('vm2');
expect(childContext.$rawData).toBe(vm2);
});

it('Should update an extended child context', function() {
ko.bindingHandlers.withProperties = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var childBindingContext = bindingContext.createChildContext(null, null, function(context) {
ko.utils.extend(context, valueAccessor());
});
Expand Down
18 changes: 12 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,12 @@
// 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) {
// If the parent context references an observable view model, "_subscribable" will always be the
// latest view model object. If not, "_subscribable" isn't set, and we can use the static "$data" value.
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 613c3d1

Please sign in to comment.