End to End
This article explains how the binding system works. There is a lot to cover here, and to start we're actually going to look at a component outside of the binding system so you can gain a full understanding of the process. A good place to start is in the templating module, with a component called the
. The ViewCompiler's job is to compile views into a
ViewFactory which will be used to instantiate instances of your templates, called Views. Some view factories will be used to instantiate only one instance of a particular template. The ViewFactory that resulted from compiling your
app.html template typically falls into this category. Other view factories for templates used with
repeat.for may be used to instantiate tens, hundreds or even thousands of View instances.
A ViewFactory is comprised, among other things, of a template (a document fragment), a set of HTML behavior instructions (instructions for creating custom elements or custom attributes that appear in the template), binding expressions (factories for creating bindings that appear in the template), and dependencies (things you've
When Aurelia loads one of your HTML templates the markup is parsed by the browser into a document fragment. The document fragment is the browser's object representation of your HTML. It is a tree structure and all the node names and attribute names have been lower-cased by the browser's HTML parser (except for SVG elements). The ViewCompiler traverses each DOM node in the tree and checks whether the node's name matches the name of a custom element. For now we won't go into the details of what happens when a custom element is discovered because it doesn't really matter to the binding system whether an element is custom or built-in. In addition to checking for custom elements, the ViewCompiler asks the BindingLanguage implementation to examine each node's attributes and content. The
standard BindingLanguage implementation
that ships with Aurelia looks for attributes whose name ends with
.trigger or starts with
ref, etc. These attribute postfixes and prefixes are known as "binding commands". The standard BindingLanguage also checks the element's content for string interpolation expressions. When a binding command or string interpolation is discovered, the attribute's value (text) or interpolation's content (also text) is sent to the binding system's
BindingExpression which is a factory for creating binding instances.
Once the ViewCompiler has completely traversed the document fragment it returns the
ViewFactory instance, which will be cached until it is needed. Aurelia compiles templates on an as-needed basis so it is likely to be used soon after it is compiled. Orchestrating all the work described above and the steps to follow is the
CompositionEngine in conjunction with a
Controller, which are parts of the templating module. The CompositionEngine is a high level component that creates controllers and executes composition instructions. Composition instructions are the result of application bootstrapping, routing and the
<compose> element. They tell Aurelia to compose a view with a view-model. The Controller is a little bit lower level. It "owns" a
View and its corresponding view-model, and takes us through the
unbind composition lifecycle events that occur when composing a view and view-model, injecting the view into the DOM and later removing it when it is no longer needed. Let's go through these steps, one-by-one, focusing on what happens with respect to data-binding.
create method will be called to create a
@inject(Element). Next, the ViewCompiler will execute all its instructions: creating custom element instances, custom attribute instances, and binding instances.
Creating a binding instance is accomplished by invoking the
createBinding method on each BindingExpression, passing in the specific DOM element, custom element or custom attribute that is relevant to the BindingExpression. The
createBinding method uses the ObserverLocator to locate the appropriate property observer for the combination of DOM element and property. Property observers expose a
setValue interface. Each property observer has a specific observation strategy tailored for certain types of objects or elements and their various properties. For example, if you were binding an input element's value attribute there is an observer implementation that will subscribe to the input's
input DOM events. With the property observer in hand
createBinding will instantiate a Binding instance, passing the property observer to the Binding's constructor. This observer is known as the Binding's
targetObserver. A few other items are passed to the Binding's constructor: the DOM element, known as the binding's
target, the attribute name, the binding mode (
one-time) and last but not least, the BindingExpression's AST, which is known as the binding's
Once the ViewFactory's
create method has finished executing all the instructions and creating all the bindings it will instantiate the
View, whose constructor will receive the DOM element, bindings, controllers, and a few other items. With the View created the Controller can execute the view's
bind method, passing in the binding context and override context. The binding context and override context tuple is known as the
scope- more on that in a minute...
bind method loops through all of its binding instances and calls their
bind method, passing in the binding context and override context. This is where the AST in the binding's
evaluate method and passing in the
sourceExpression it assigns this value to the view by calling the
setValue method. Next the binding will check its binding mode. If it is
one-time, there is nothing left to do. If it is
two-way the binding will use the AST's
connect method to subscribe to changes in the view-model. Each node in the AST knows which view-model properties to observe and will use the ObserverLocator to create property observers to subscribe to property change events. Finally, if the binding mode is
two-way the binding will call the
targetObserver's subscribe method. Since
to-view all require observers, property interceptors need to be added during binding to detect changes. By default, the intercepted property will be added to the view model or view model property with an
undefined value if not already defined on the object (e.g.
value.bind="foo" will create the
foo property on the view model if it does not exist and
value.bind="foo.bar" will create the
bar property on the
foo object if the
bar property is not already defined on
At this point the view and view-model are data-bound. When changes occur in the model, the property observers created when the AST was
connected will fire change events, triggering the binding to update the target by calling
targetObserver.setValue. When changes occur in the view the property observer known as the
targetObserver will trigger the binding to update the source by calling
sourceExpression.assign(scope, value). All that remains is for the Controller to
attach the view to the DOM. Later, when the view is no longer needed it will be
detached from the DOM and
unbind will be invoked, unbinding all the views, which will unsubscribe all the property observers.
Abstract Syntax Tree
The abstract syntax tree is a key part of the binding system, we've discussed how it is used but it is easier to understand if you can visualize it. Below is a demo where you can enter any binding expression and see the AST resulting from parsing the expression. Click on the buttons to view some example expressions we put together or enter your own.
Binding Context / Scope
The "scope" in aurelia is made up of two objects: the
bindingContext (almost always a view-model instance) and the
overrideContext which can be thought of as an "overlay" of the bindingContext. Properties on the overrideContext "override" corresponding properties on the bindingContext. It is actually rare for there to be a property on the overrideContext that is "hiding" a property on the bindingContext beneath. Most of the time the overrideContext is storing extra contextual properties such as
$even in the case of the repeat,
$event when event bindings are firing, etc. The other purpose of the overrideContext is to enable scope traversal. The overrideContext also has a reference to the parent overrideContext and to its corresponding bindingContext which enables the binding system to traverse the scope as-needed when it evaluates a binding expression. If you've been using Aurelia for a while you might remember needing to use
$parent to access the outer scope. It is not needed anymore because the binding system knows how to traverse the scope (read: traverse the bindingContext/overrideContext hierarchy) automatically.
When are bindingContexts created? Not often. The repeat is the only thing that creates them on the fly. The rest of the time the bindingContext is the view-model instance you'd expect it to be. On the other hand, overrideContexts are created on an as-needed basis, typically when it is time to compose a view and view-model.