Skip to content

Commit

Permalink
Add bindingContext option to "export" dependencies. This means that t…
Browse files Browse the repository at this point in the history
…he context won't track the dependencies, but they should be tracked by the calling code, which is responsible for re-creating the context. Use this option for "with" and "template".
  • Loading branch information
mbest committed Nov 19, 2015
1 parent 2161ce0 commit 6dbbbe2
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 49 deletions.
77 changes: 43 additions & 34 deletions src/binding/bindingAttributeSyntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

// The ko.bindingContext constructor is only called directly to create the root context. For child
// contexts, use bindingContext.createChildContext or bindingContext.extend.
ko.bindingContext = function(dataItemOrAccessor, parentContext, dataItemAlias, extendCallback) {
ko.bindingContext = function(dataItemOrAccessor, parentContext, dataItemAlias, extendCallback, options) {

// The binding context object includes static properties for the current, parent, and root view models.
// If a view model is actually stored in an observable, the corresponding binding context object, and
Expand All @@ -43,10 +43,7 @@
ko.utils.extend(self, parentContext);

// Because the above copy overwrites our own properties, we need to reset them.
// During the first execution, "subscribable" isn't set, so don't bother doing the update then.
if (subscribable) {
self._subscribable = subscribable;
}
self._subscribable = subscribable;
} else {
self['$parents'] = [];
self['$root'] = dataItem;
Expand Down Expand Up @@ -76,35 +73,43 @@
var self = this,
isFunc = typeof(dataItemOrAccessor) == "function" && !ko.isObservable(dataItemOrAccessor),
nodes,
subscribable;

if (options && options['exportDependencies']) {
// The "exportDependencies" option means that the calling code will track any dependencies and re-create
// the binding context when they change.
updateContext();
} else {
subscribable = ko.dependentObservable(updateContext, null, { disposeWhen: disposeWhen, disposeWhenNodeIsRemoved: true });

// At this point, the binding context has been initialized, and the "subscribable" computed observable is
// subscribed to any observables that were accessed in the process. If there is nothing to track, the
// computed will be inactive, and we can safely throw it away. If it's active, the computed is stored in
// the context object.
if (subscribable.isActive()) {
self._subscribable = subscribable;

// Always notify because even if the model ($data) hasn't changed, other context properties might have changed
subscribable['equalityComparer'] = null;

// We need to be able to dispose of this computed observable when it's no longer needed. This would be
// easy if we had a single node to watch, but binding contexts can be used by many different nodes, and
// we cannot assume that those nodes have any relation to each other. So instead we track any node that
// the context is attached to, and dispose the computed when all of those nodes have been cleaned.

// Add properties to *subscribable* instead of *self* because any properties added to *self* may be overwritten on updates
nodes = [];
subscribable._addNode = function(node) {
nodes.push(node);
ko.utils.domNodeDisposal.addDisposeCallback(node, function(node) {
ko.utils.arrayRemoveItem(nodes, node);
if (!nodes.length) {
subscribable.dispose();
self._subscribable = subscribable = undefined;
}
});
};
// At this point, the binding context has been initialized, and the "subscribable" computed observable is
// subscribed to any observables that were accessed in the process. If there is nothing to track, the
// computed will be inactive, and we can safely throw it away. If it's active, the computed is stored in
// the context object.
if (subscribable.isActive()) {
self._subscribable = subscribable;

// Always notify because even if the model ($data) hasn't changed, other context properties might have changed
subscribable['equalityComparer'] = null;

// We need to be able to dispose of this computed observable when it's no longer needed. This would be
// easy if we had a single node to watch, but binding contexts can be used by many different nodes, and
// we cannot assume that those nodes have any relation to each other. So instead we track any node that
// the context is attached to, and dispose the computed when all of those nodes have been cleaned.

// Add properties to *subscribable* instead of *self* because any properties added to *self* may be overwritten on updates
nodes = [];
subscribable._addNode = function(node) {
nodes.push(node);
ko.utils.domNodeDisposal.addDisposeCallback(node, function(node) {
ko.utils.arrayRemoveItem(nodes, node);
if (!nodes.length) {
subscribable.dispose();
self._subscribable = subscribable = undefined;
}
});
};
}
}
}

Expand All @@ -113,7 +118,7 @@
// 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, extendCallback) {
ko.bindingContext.prototype['createChildContext'] = function (dataItemOrAccessor, dataItemAlias, extendCallback, options) {
return new ko.bindingContext(dataItemOrAccessor, this, dataItemAlias, function(self, parentContext) {
// Extend the context hierarchy by setting the appropriate pointers
self['$parentContext'] = parentContext;
Expand All @@ -122,7 +127,7 @@
self['$parents'].unshift(self['$parent']);
if (extendCallback)
extendCallback(self);
});
}, options);
};

// Extend the binding context with new custom properties. This doesn't change the context hierarchy.
Expand All @@ -139,6 +144,10 @@
});
};

ko.bindingContext.prototype.createStaticChildContext = function (dataItemOrAccessor, dataItemAlias) {
return this['createChildContext'](dataItemOrAccessor, dataItemAlias, null, { "exportDependencies": true });
};

// Returns the valueAccesor function for a binding value
function makeValueAccessor(value) {
return function() {
Expand Down
7 changes: 4 additions & 3 deletions src/binding/defaultBindings/ifIfnotWith.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) {
var didDisplayOnLastUpdate,
savedNodes;
ko.computed(function() {
var dataValue = ko.utils.unwrapObservable(valueAccessor()),
var rawValue = valueAccessor(),
dataValue = ko.utils.unwrapObservable(rawValue),
shouldDisplay = !isNot !== !dataValue, // equivalent to isNot ? !dataValue : !!dataValue
isFirstRender = !savedNodes,
needsRefresh = isFirstRender || isWith || (shouldDisplay !== didDisplayOnLastUpdate);
Expand All @@ -20,7 +21,7 @@ function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) {
if (!isFirstRender) {
ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(savedNodes));
}
ko.applyBindingsToDescendants(makeContextCallback ? makeContextCallback(bindingContext, valueAccessor) : bindingContext, element);
ko.applyBindingsToDescendants(makeContextCallback ? makeContextCallback(bindingContext, rawValue) : bindingContext, element);
} else {
ko.virtualElements.emptyNode(element);
}
Expand All @@ -40,6 +41,6 @@ makeWithIfBinding('if');
makeWithIfBinding('ifnot', false /* isWith */, true /* isNot */);
makeWithIfBinding('with', true /* isWith */, false /* isNot */,
function(bindingContext, dataValue) {
return bindingContext['createChildContext'](dataValue);
return bindingContext.createStaticChildContext(dataValue);
}
);
16 changes: 4 additions & 12 deletions src/templating/templating.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,9 @@
return ko.dependentObservable( // So the DOM is automatically updated when any dependency changes
function () {
// Ensure we've got a proper binding context to work with
var bindingContext;
if (dataOrBindingContext instanceof ko.bindingContext) {
bindingContext = dataOrBindingContext;
} else {
// Create dependency so that a new context is created when data updates
ko.utils.unwrapObservable(dataOrBindingContext);
bindingContext = new ko.bindingContext(dataOrBindingContext);
}
var bindingContext = (dataOrBindingContext && (dataOrBindingContext instanceof ko.bindingContext))
? dataOrBindingContext
: new ko.bindingContext(dataOrBindingContext, null, null, null, { "exportDependencies": true });

var templateName = resolveTemplateName(template, bindingContext['$data'], bindingContext),
renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);
Expand Down Expand Up @@ -266,9 +261,6 @@
shouldDisplay = ko.utils.unwrapObservable(options['if']);
if (shouldDisplay && 'ifnot' in options)
shouldDisplay = !ko.utils.unwrapObservable(options['ifnot']);

// Create dependency on template view model to re-render when it mutates:
ko.utils.unwrapObservable(options['data']);
}

if ('foreach' in options) {
Expand All @@ -280,7 +272,7 @@
} else {
// Render once for this single data point (or use the viewModel if no data was provided)
var innerBindingContext = ('data' in options) ?
bindingContext['createChildContext'](options['data'], options['as']) : // Given an explitit 'data' value, we create a child binding context for it
bindingContext.createStaticChildContext(options['data'], options['as']) : // Given an explitit 'data' value, we create a child binding context for it
bindingContext; // Given no explicit 'data' value, we retain the same binding context
templateComputed = ko.renderTemplate(templateName || element, innerBindingContext, options, element);
}
Expand Down

0 comments on commit 6dbbbe2

Please sign in to comment.