Skip to content

Commit

Permalink
Add comments about the new observable-view-model implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
mbest committed Jun 17, 2013
1 parent 2273c72 commit 5f72341
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 5 deletions.
58 changes: 53 additions & 5 deletions src/binding/bindingAttributeSyntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,38 @@
return ko.bindingHandlers[bindingKey];
};

// 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) {

// 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
// 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
// context to be updated.
var dataItem = isFunc ? dataItemOrAccessor() : dataItemOrAccessor;

if (parentContext) {
// Register a dependency on the parent context
// When a "parent" context is given, register a dependency on the parent context. Thus whenever the
// parent context is updated, this context will also be updated.
if (parentContext._subscribable)
parentContext._subscribable();
// Inherit $root and any custom properties

// Copy $root and any custom properties from the parent context
ko.utils.extend(self, parentContext);
// Update our properties

// 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['$dataFn'] = self._subscribable = subscribable;
}
} else {
self['$parents'] = [];
self['$root'] = dataItem;

// Export 'ko' in the binding context so it will be available in bindings and templates
// even if 'ko' isn't exported as a global, such as when using an AMD loader.
// See https://github.com/SteveSanderson/knockout/issues/490
Expand All @@ -30,8 +46,13 @@
self['$data'] = dataItem;
if (dataItemAlias)
self[dataItemAlias] = dataItem;

// The extendCallback function is provided when creating a child context or extending a context.
// It handles the specific actions needed to finish setting up the binding context. Actions in this
// function could also add dependencies to this binding context.
if (extendCallback)
extendCallback(self, parentContext, dataItem);

return self['$data'];
}
function disposeWhen() {
Expand All @@ -43,10 +64,20 @@
nodes = [],
subscribable = ko.dependentObservable(updateContext, null, { disposeWhen: disposeWhen });

// 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['$dataFn'] = self._subscribable = subscribable;

// 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
subscribable._nodes = nodes;
subscribable._addNode = function(node) {
nodes.push(node);
ko.utils.domNodeDisposal.addDisposeCallback(node, function(node) {
Expand All @@ -56,24 +87,35 @@
self._subscribable = subscribable = undefined;
}
});
// Make sure that our parent context is watching at least one node
// 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);
}
};
subscribable._nodes = nodes;
} else {
self['$dataFn'] = function() { return self['$data']; }
}
}

// Extend the binding context hierarchy with a new view model object. If the parent context is watching
// any obsevables, the new child context will automatically get a dependency on the parent context.
// 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) {
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']);
});
};

// Extend the binding context with new custom properties. This doesn't change the context hierarchy.
// 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['$dataFn'], this, null, function(self) {
ko.utils.extend(self, typeof(properties) == "function" ? properties() : properties);
Expand Down Expand Up @@ -218,6 +260,9 @@
var provider = ko.bindingProvider['instance'],
getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors;

// When an obsevable view model is used, the binding context will expose an observable $dataFn value. A binding
// provider that supports observable view models should use bindingContext.$dataFn to access the view model, thus
// triggering a dependency here.
var bindingsUpdater = ko.dependentObservable(
function() {
return (bindings = getBindings.call(provider, node, bindingContext));
Expand All @@ -231,6 +276,9 @@

var bindingHandlerThatControlsDescendantBindings;
if (bindings) {
// Return the value accessor for a given binding. When bindings are static (won't be updated because of a binding
// context update), just return the value accessor from the binding. Otherwise, return a function that always gets
// the latest binding value and registers a dependency on the binding updater.
var getValueAccessor = bindingsUpdater
? function(bindingKey) {
return function() {
Expand Down
2 changes: 2 additions & 0 deletions src/binding/bindingProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
// Build the source for a function that evaluates "expression"
// For each scope variable, add an extra level of "with" nesting
// Example result: with(sc1) { with(sc0) { return (expression) } }
// This binding evaluator function uses "$dataFn()" so that the bindings have a dependency
// on a binding context that's updatable.
var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString, options),
functionBody = "with($context){with($dataFn()||{}){return{" + rewrittenBindings + "}}}";
return new Function("$context", "$element", functionBody);
Expand Down

0 comments on commit 5f72341

Please sign in to comment.