Scope and context
Understand the scope and binding context.
You might have noticed the words "Scope", "binding context", and "override context" in other places in the documentation or while working with Aurelia in general. Although you can go a long way without even understanding what these are (Aurelia is cool that way), these are some (of many) powerful concepts that are essential when dealing with the lower-level Aurelia 2 API. This section explains what these terms mean.
Here's what you'll learn...
What is Scope?
What is binding context and override context?
How to troubleshoot the rare and weird data binding issues?
How is a context selected?
Background
When we start an Aurelia app, the compilation pipeline JIT compiles the templates (HTML/markup) associated with custom elements. The compilation process demands documentation of its own and is out of this topic's scope. Without going into much detail about that, we can think of the compilation process in terms of the following steps:
Parse the template text,
Create instructions for custom elements, custom attributes, and template controllers (
if
,else
,repeat.for
etc.), andCreate a set of bindings for every instruction.
Most of the bindings also contain expressions.
In the example above, the interpolation binding has the expression firsName
, and the property binding has the expression address.pin
(quite unsurprisingly, things are a bit more involved in actuality, but this abstraction will do for now).
An expression in itself might not be that interesting, but when it is evaluated, it becomes of interest. Enter scope. To evaluate an expression, we need a scope.
Scope and binding context
The expressions themselves do not hold any state or context. This means that the expression firstName
only knows that given an object, it needs to grab the firstName
property of that object. However, the expression, in itself, does not hold that object. The scope is the container that holds the object(s) which can be supplied to the expression when it is evaluated.
These objects are known as contexts. There are typically two types of contexts: binding context and override context. An expression can be evaluated against any of these two kinds of contexts. Even though there are a few subtle differences between these two kinds of contexts (see Override context), in terms of expression evaluation, there is no difference between these two.
JavaScript analogy
One way to think about expression and binding context is in terms of functions and binding those functions with an execution context (Refer: Function.bind).
Let us consider the following example.
If we invoke this function like foo()
, we will get NaN
. However, binding any object to it might return a more meaningful value, depending on the bound object.
Following that analogy, the expressions are like this function, or more precisely, like the expression a ** 2
in the function. Binding contexts are like the objects used to bind the function. That is, given 2 different binding contexts, the same expression can produce different results when evaluated. Scope, as said before, wraps the binding context, almost like the scope in JavaScript. The need to have this wrapper over the binding context is explained in later sections.
How to access the scope and the binding context?
Aurelia pipeline injects a $controller
property to every custom element, custom attribute, and template controller. This property can be used to access the scope and binding context.
Let us consider the following example.
Note that we haven't assigned any value explicitly to the $controller
property, and the Aurelia pipeline assigns that. We can use the $controller.scope
to access the scope and subsequently $controller.scope.bindingContext
can be used to access the binding context.
Note how the bindingContext
in the above example points to this
, that is the current instance of App
(with template controllers, this gets a little more involved; but we will leave that one out for now). However, we refer to the data source as a "context" in evaluating expressions.
The relations explored so far can be expressed as follows.
From here, let us proceed to understand what override context is.
Override context
As the name suggests, it is also a context that overrides the binding context. Aurelia gives higher precedence to the overriding context when the desired property is found there. This means that while binding if a property is found in both binding and override context, the latter will be selected to evaluate the expression.
We continue with the previous example; it renders <div>Hello World!</div>
. However, things might be a bit different if we toy with the overriding context, as shown in the following example.
The assignment to overrideContext.message
the rendered output is now <div>Hello Aurelia!</div>
, instead of <div>Hello World!</div>
. This is because of the existence of the property message
in the overriding context.
As the assignment is made pre-binding phase (created
hook in the example above), the context selection process sees that the required property exists in the overriding context and selects that with higher precedence even though a property with the same name also exists in the binding context.
Now with this information, we also have a new diagram.
Motivation
Now let's address the question 'Why do we need override context at all?'. The reason it exists has to do with the template controllers (mostly). While writing template controllers, many times we want a context object that is not the underlying view-model instance. One such prominent example is the repeat.for
template controller.
As you might know that repeat.for
template controller provides contextual properties such as $index
, $first
, $last
etc. These properties end up being in the override context.
Now imagine if those properties actually end up being in the binding context, which is often the underlying view-model instance. It would have caused a lot of other issues. First, that would have restricted you from having properties with the same name to avoid conflicts.
This, in turn, means that you need to know the template controllers you are using thoroughly to know about such restrictions, which is not a sound idea in itself. And with that, if you define a property with the same name, as used by the template controller, coupled with change observation etc., we could have found ourselves dealing with numerous bugs in the process. Override context helps us to get out of that horrific mess.
Another prominent use caseend for override context is the let
binding. When not specified otherwise, the properties bound via the let
binding ends up in the overriding context.
This can be seen in the example below.
Typically the properties for the let
-bindings are view-only properties. It makes sense to have those properties in the overriding context.
Do you know that you can use to-binding-context
attribute in let
-binding to target the binding context instead of override context? Why don't you try <let foo.bind="42" to-binding-context></let>
and inspect the scope contexts by yourself?
Parent scope
The discussion so far has explained the necessity of context. However, that still does not answer the question, 'If the expressions are evaluated based on the context, why do we even need scope?'. Apart from serving as a logical container for the contexts, a scope also optionally points to the parent scope.
Let us consider the following example to understand that.
The example above App
uses the FooBar
custom element, and both have property named message
, initialized with different values. As expected, the rendered output in this case is Hello Foo-Bar! Hello App!
.
You might have used the $parent
keyword a lot, but for completeness, it should be clarified that the parent scope can be accessed using the $parent
keyword. The example above FooBar#$controller.scope.parentScope.bindingContext
points to the instance of App
where <foo-bar>
is used. In short, every scope instance has a parentScope
property that points to the parent scope when available.
With this information, our diagram changes one last time.
Note that the parentScope
for the scope of the root component is null
.
Host scope
As we are talking about scope, it needs to be noted that the term 'host scope' is used in the context of au-slot
. There is no difference between a "normal" scope and a host scope; it just acts as the special marker to instruct the scope selection process to use the scope of the host element instead of the scope of the parent element.
Moreover, this is a special kind of scope that is valid only in the context of au-slot
. This is already discussed in detail in the au-slot
documentation, and thus not repeated here.
Context and change observation
Now let us discuss change observation. A comprehensive discussion on change observation is a bit out of this documentation's scope. However, for this discussion, it would suffice to say that generally, whenever Aurelia binds an expression to the view, it employs one or more observers.
This is how when the value of the underlying property changes, the change is also propagated to view or other associated components. The focus of this discussion is how some interesting scenarios occur in conjunction with binding/override context and the change observation.
Let's start with a simple example.
The example above updates the message
property of the binding context every 1 second. As Aurelia is also observing the property, the interpolated output is also updated after every 1 second. Note that as the scope.bindingContext
above points to the this
, updating this.message
that way has the same effect.
As the next example, we change the property in both the binding context and the override context.
Although it has been said before that the property in override context takes precedence over binding context, the output from the example above is Hello Binding Context! #i: 1
, Hello Binding Context! #i: 2
, and so on. The reason for this behavior is that the scope.bindingContext.message
is bound to the view instead of scope.overrideContext.message
, as the latter was non-existent during the binding phase (note that the values are being changed in attached
lifecycle hook).
Therefore, the change observation is also applied for the scope.bindingContext.message
as opposed to that of override context. This explains why updating the scope.overrideContext.message
is rather 'futile' in the example above.
However, the result would have been quite different, if the message
property is introduced to override context during the binding
phase (or before that, for that matter).
Note that the example above introduces the message
property in the overriding context during the binding
phase. When the interpolation expression is evaluated in the view, it is that property from the overriding context that ends up being bound. This means that the message
property in the overriding context is also observed.
Thus, quite expectedly, every 1-second output of the above-shown example changes as Hello Override Context! #i: 1
, Hello Override Context! #i: 2
, and so on.
Context selection
So far, we have seen various aspects of scope, binding and override context. One thing we have not addressed so far is how the contexts are selected for expression evaluation or assignment. In this section, we will look into that aspect.
The context selection process can be summed up (simplified) as follows.
IF
$parent
keyword is used once or more than once, THENtraverse up the scope, the required number of parents (that is, for
$parent.$parent.foo
, we will go two steps/scopes up)RETURN override context if the desired property is found there, ELSE RETURN binding context.
ELSE
LOOP till either the desired property is found in the context or the component boundary is hit. Then perform the following.
IF the desired property is found in the overriding context, return the override context.
ELSE RETURN binding context.
The first rule involving $parent
should be self-explanatory. We will focus on the second part.
Let us first see an example to demonstrate the utility of the rule #2.1.
.
As expected, the example produces the following output.
Note that both App
and FooBar
initializes their own message
properties. According to our rule #2.3.
binding context is selected, and the corresponding message
property is bound to the view. However, it is important to note that if the FooBar#message
stays uninitialized, that is the message
property exists neither in binding context nor in override context (of FooBar
's scope), the output would have been as follows.
Although it should be quite as per expectation, the point to be noted here is that the scope traversal never reaches to App
in the process. This is because of the 'component boundary' clause in rule #2.1.
. In case of this example, the expression evaluation starts with the scope of the innermost repeat.for
, and traversed upwards.
When traversal hits the scope of FooBar
, it recognize the scope as a component boundary and stops traversing any further, irrespective of whether the property is found or not. Contextually note that if you want to cross the component boundary, you need to explicitly use $parent
keyword.
The rule #2.2.
is also self-explanatory, as we have seen plenty of examples of overriding context precedence so far. Thus the last bit of this story boils down to the rule #2.3.
. This rule facilitates using an uninitialized property in binding context by default or as a fallback, as can be seen in the example below.
The example shown above produces Hello World!
as output after 2 seconds of the invocation of the attached
hook. This happens because of the fallback to binding context by the rule #2.3.
.
That's it! Congratulations! You have made it till the end. Go have that tea break now! Hope you have enjoyed this documentation as much as you will enjoy that tea. Have fun with Aurelia2!
Last updated