Skip to content

Commit

Permalink
Make sure observable view models work with templates. Make sure $inde…
Browse files Browse the repository at this point in the history
…x is up-to-date when using observable view models. Try to avoid double curly brace in binding string (messes up jQuery-tmpl).

Remove support for temporary binding context when extending a child context. Instead, use new extendCallback parameter.
  • Loading branch information
mbest committed Jun 18, 2013
1 parent 900c573 commit c910082
Show file tree
Hide file tree
Showing 7 changed files with 38 additions and 17 deletions.
4 changes: 3 additions & 1 deletion spec/bindingDependencyBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,9 @@ describe('Binding dependencies', function() {
it('Should update an extended child context', function() {
ko.bindingHandlers.withProperties = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var childBindingContext = bindingContext.createChildContext(null).extend(valueAccessor);
var childBindingContext = bindingContext.createChildContext(null, null, function(context) {
ko.utils.extend(context, valueAccessor());
});
ko.applyBindingsToDescendants(childBindingContext, element);
return { controlsDescendantBindings: true };
}
Expand Down
2 changes: 1 addition & 1 deletion spec/expressionRewritingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ describe('Expression Rewriting', function() {

it('Should return accessor functions for each value when called with the valueAccessors option', function() {
var rewritten = ko.expressionRewriting.preProcessBindings("a: 1", {valueAccessors:true});
expect(rewritten).toEqual("'a':function(){return 1}");
expect(rewritten).toEqual("'a':function(){return 1 }");
var evaluated = eval("({" + rewritten + "})");
expect(evaluated['a']()).toEqual(1);
});
Expand Down
20 changes: 19 additions & 1 deletion spec/templatingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ describe('Templating', function() {
ko.bindingProvider.instance = originalBindingProvider;
});

Describe('Data binding \'foreach\' option', function() {
describe('Data binding \'foreach\' option', function() {
it('Should render for each item in an array but doesn\'t rerender everything if you push or splice', function () {
var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: "Frank"}]);
ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "<div>The item is [js: personName]</div>" }));
Expand Down Expand Up @@ -675,6 +675,24 @@ describe('Templating', function() {
expect(testNode.childNodes[0]).toContainHtml("<div>template1output, firstitemvalue</div><div>template2output, seconditemvalue</div>");
});

it('Should update all child contexts and bindings when used with a top-level observable view model', function() {
var myVm = ko.observable({items: ['A', 'B', 'C'], itemValues: { 'A': [1, 2, 3], 'B': [4, 5, 6], 'C': [7, 8, 9] }});
var engine = new dummyTemplateEngine({
itemTemplate: "The <span data-bind='text: $index'></span> item <span data-bind='text: $data'></span> has <div data-bind='template: { name: \"valueTemplate\", foreach: $root.itemValues[$data] }'></div> ",
valueTemplate: "<span data-bind='text: $index'></span>.<span data-bind='text: $data'></span>,"
});
engine.createJavaScriptEvaluatorBlock = function (script) { return "[[js:" + script + "]]"; }; // because we're using a binding with brackets
ko.setTemplateEngine(engine);

testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: items }'></div>";

ko.applyBindings(myVm, testNode);
expect(testNode.childNodes[0]).toContainText("The 0 item A has 0.1,1.2,2.3, The 1 item B has 0.4,1.5,2.6, The 2 item C has 0.7,1.8,2.9, ");

myVm({items: ['C', 'B', 'A'], itemValues: { 'A': [10, 20, 30], 'B': [40, 50, 60], 'C': [70, 80, 90] }});
expect(testNode.childNodes[0]).toContainText("The 0 item C has 0.70,1.80,2.90, The 1 item B has 0.40,1.50,2.60, The 2 item A has 0.10,1.20,2.30, ");
});

});

it('Data binding syntax should support \"if\" condition', function() {
Expand Down
20 changes: 10 additions & 10 deletions src/binding/bindingAttributeSyntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,6 @@
self._subscribable = subscribable = undefined;
}
});
// Make sure that the parent context is watching at least one node; otherwise the parent context might
// get disponsed right away.
if (parentContext && parentContext._subscribable && !parentContext._subscribable._nodes.length) {
parentContext._subscribable._addNode(node);
}
};
} else {
self['$dataFn'] = function() { return self['$data']; }
Expand All @@ -103,13 +98,15 @@
// But this does not mean that the $data value of the child context will also get updated. If the child
// 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) {
ko.bindingContext.prototype['createChildContext'] = function (dataItemOrAccessor, dataItemAlias, extendCallback) {
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'];
self['$parents'] = (parentContext['$parents'] || []).slice(0);
self['$parents'].unshift(self['$parent']);
if (extendCallback)
extendCallback(self);
});
};

Expand Down Expand Up @@ -239,10 +236,10 @@
return result;
}

function applyBindingsToNodeInternal(node, bindings, bindingContext, bindingContextMayDifferFromDomParentElement) {
function applyBindingsToNodeInternal(node, sourceBindings, bindingContext, bindingContextMayDifferFromDomParentElement) {
// Prevent multiple applyBindings calls for the same node, except when a binding value is specified
var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
if (!bindings) {
if (!sourceBindings) {
if (alreadyBound) {
throw Error("You cannot apply bindings multiple times to the same element.");
}
Expand All @@ -256,7 +253,10 @@
ko.storedBindingContextForNode(node, bindingContext);

// Use bindings if given, otherwise fall back on asking the bindings provider to give us some bindings
if (!bindings) {
var bindings;
if (sourceBindings && typeof sourceBindings !== 'function') {
bindings = sourceBindings;
} else {
var provider = ko.bindingProvider['instance'],
getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors;

Expand All @@ -265,7 +265,7 @@
// the binding context is updated.
var bindingsUpdater = ko.dependentObservable(
function() {
bindings = getBindings.call(provider, node, bindingContext);
bindings = sourceBindings ? sourceBindings(bindingContext, node) : getBindings.call(provider, node, bindingContext);
// Register a dependency on the binding context
if (bindings && bindingContext._subscribable)
bindingContext._subscribable();
Expand Down
2 changes: 1 addition & 1 deletion src/binding/expressionRewriting.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ ko.expressionRewriting = (function () {

// Values are wrapped in a function so that each value can be accessed independently
if (makeValueAccessors) {
val = 'function(){return ' + val + '}';
val = 'function(){return ' + val + ' }';
}
resultStrings.push("'" + key + "':" + val);
}
Expand Down
2 changes: 1 addition & 1 deletion src/templating/templateRewriting.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ ko.templateRewriting = (function () {
return ko.memoization.memoize(function (domNode, bindingContext) {
var nodeToBind = domNode.nextSibling;
if (nodeToBind && nodeToBind.nodeName.toLowerCase() === nodeName) {
ko.applyBindingAccessorsToNode(nodeToBind, bindings(bindingContext, nodeToBind), bindingContext);
ko.applyBindingAccessorsToNode(nodeToBind, bindings, bindingContext);
}
});
}
Expand Down
5 changes: 3 additions & 2 deletions src/templating/templating.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,9 @@
// This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode
var executeTemplateForArrayItem = function (arrayValue, index) {
// Support selecting template as a function of the data being rendered
arrayItemContext = parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue), options['as']);
arrayItemContext['$index'] = index;
arrayItemContext = parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue), options['as'], function(context) {
context['$index'] = index;
});
var templateName = typeof(template) == 'function' ? template(arrayValue, arrayItemContext) : template;
return executeTemplate(null, "ignoreTargetNode", templateName, arrayItemContext, options);
}
Expand Down

0 comments on commit c910082

Please sign in to comment.