Last year, I was preparing for a talk on extending Aurelia. The talk would look at how Value Converters, Custom Attributes, Custom Elements, and Binding Behaviors can be used to extend Aurelia's binding engine. I was well versed in building the first three, as I expect most experienced Aurelia users would be. The only problem is I didn't know a damned thing about building Binding Behaviors. Sure, I knew how to use debounce
or updateTrigger
and all of the other binding behaviors that ship with Aurelia, but I had no clue what it took to build one.
Luckily, I knew just the person to ask about how to build them, and hopefully get a sweet demo of a custom binding behavior: Jeremy Danyow. Jeremy knows more about Aurelia's binding engine than pretty much anyone else (including Rob Eisenberg). So I messaged him and he came back to me with a binding behavior demo that pretty much blew my mind. It allowed the user to create a data driven form. The demo itself is runnable here.
Here's a screenshot of the demo running:
The data driving the application is shown to the right, while the form itself is on the left. The view-model for this page consists solely of the two objects displayed on the left side of the screenshot.
Let's look at the view code that for this page, and you'll see why it blew me away.
<template>
<require from="./dynamic-expression-binding-behavior"></require>
<require from="./debug"></require>
<form role="form">
<div class="form-group col-sm-7" repeat.for="field of fields">
<label for="${field.expression}">
${field.label}:
</label>
<input type="text" value.bind="model & dynamicExpression:field.expression"
id="${field.expression}" class="form-control" />
</div>
</form>
<debug></debug>
</template>
It simply loads up a dynamicExpression
binding behavior and a debug
custom element. The debug custom element is fairly uninteresting as it simply takes a binding context (page VM in this case) and renders it as JSON in its view. The code for this can be viewed at the link above. The dynamicExpression
binding behavior, though, is really cool. Notice that the form is built by iterating over the fields
array. Each item in this array has a label
property and an expression
property. We use the expression property with the dynamicExpression
binding behavior to change the binding expression for the textbox on the fly.
value.bind="model & dynamicExpression:field.expression"
Note that on every iteration of the repeater, the input textbox is simply bound to the model
object. On its own, this is not very useful, but with this binding behavior, we are able to change the binding expression from, for example, model
to model.name
.
As you might imagine, this could be extremely powerful for building forms over data UIs dynamically. But for me the interesting part is how this demo shows the power of Aurelia binding behaviors, and Aurelia's binding engine. It also showed me how relatively easy it can be to suddenly look like an Aurelia wizard, something I'll explore in part two of this blog post.
But for now, I want to look at the source code for this binding behavior.
DynamicExpressionBindingBehavior
is the binding behavior itself, and is actually relatively simple code. Let's take a look:
import {inject} from 'aurelia-dependency-injection';
import {Parser} from 'aurelia-binding';
import {rebaseExpression} from './expression-rebaser';
@inject(Parser)
export class DynamicExpressionBindingBehavior {
constructor(parser) {
this.parser = parser;
}
bind(binding, source, rawExpression) {
// Parse the expression that was passed as a string argument to
// the binding behavior.
let expression = this.parser.parse(rawExpression);
// Rebase the expression
expression = rebaseExpression(expression, binding.sourceExpression);
// Squirrel away the binding's original expression so we can restore
// the binding to it's initial state later.
binding.originalSourceExpression = binding.sourceExpression;
// Replace the binding's expression.
binding.sourceExpression = expression;
}
unbind(binding, source) {
// Restore the binding to it's initial state.
binding.sourceExpression = binding.originalSourceExpression;
binding.originalSourceExpression = null;
}
}
The class has an instance of the Aurelia's binding engine's Parser
class injected in to it. The bind
function is the callback that Aurelia calls when binding occurs on the binding expression in the template.
I want to mention that, currently, binding behaviors are only run once when a view is initially bound, and so any databound parameters for a binding behavior won't cause the binding behavior to re-run. This is, likely, done for performance reasons.
The bind
function takes three parameters. The first two parameters are passed to the function by Aurelia. When implementing a custom binding behavior, you can add an arbitrary number of custom parameters, which will be passed to your bind
callback in order. This binding behavior only takes one parameter, rawExpression
. This parameter will be equal to the field.expression
property.
The first thing the code does is parse this expression. This gives us an expression that Aurelia can work with. Next this expression is passed to the rebaseExpression
function along with the original binding's source expression, which will always be model
in the view for this demo. Conceptually, what the rebaseExpression
function does is convert value.bind="model & dynamicExpression:field.expression"
where field.expression
might equal address.city
to value.bind="model.address.city"
. We'll look at how it does this in a moment.
Now that we have the "rebased" expression, we need to hold on to the original binding expression. So we just place it on a custom property on the original binding. Then we change the binding's expression to our newly rebased expression. Once the function returns, Aurelia will look at the binding expression and now bind the textbox to model.address.city
instead of to model
.
When unbinding occurs, we need to prevent memory leaks and switch back to the original binding expression, so we simply reset sourceExpression
to the originalSourceExpression
property we previously created, and then set originalSourceExpression
to null
to remove our own reference to it.
The binding behavior itself is the simple part of this code, but we'll see in Part 2 of this blog post, that doesn't stop us from using these lessons learned to do powerful things.
The complicated, nitty gritty, "only a few people in the world know Aurelia's binding engine well enough to accomplish this" stuff happens in expression-rebaser.js
. Even though what's happening requires intimate knowledge of Aurelia, the code itself ends up being rather succinct.
import {ExpressionCloner, AccessMember, CallMember} from 'aurelia-binding';
export class ExpressionRebaser extends ExpressionCloner {
constructor(base) {
super();
this.base = base;
}
visitAccessThis(access) {
if (access.ancestor !== 0) {
throw new Error('$parent expressions cannot be rebased.');
}
return this.base;
}
visitAccessScope(access) {
if (access.ancestor !== 0) {
throw new Error('$parent expressions cannot be rebased.');
}
return new AccessMember(this.base, access.name);
}
visitCallScope(call) {
if (call.ancestor !== 0) {
throw new Error('$parent expressions cannot be rebased.');
}
return new CallMember(this.base, call.name, this.cloneExpressionArray(call.args));
}
}
export function rebaseExpression(expression, baseExpression) {
let visitor = new ExpressionRebaser(baseExpression);
return expression.accept(visitor);
}
The ExpressionRebaser
class extends the ExpressionCloner
class built in to Aurelia. This means it only needs to override a couple of methods to implement some really complex stuff. We create an isntance of the class, passing the base expression (the original binding expression). The three other functions are used by Aurelia to actually do the rebasing of the expression. In the case of address.city
to model.address.city
, visitAccessScope
is used. The parsed expression for address.city
is told to "accept" the new "visitor", which is the ExpressionRebaser
instance. This results in the visitAccessScope
function being called, and a new AccessMember
instance is created. The AccessMember
class represents "an expression that accesses a property on an object" (quoting the Aurelia docs). Its constructor takes two parameters, an Expression
object, which will be the original model
expression in this case, and a name
string, which represents the property name. In the case of address.name
, the access.name
property will be address
. Aurelia chains together these simple binding expressions when it parses the binding expression, so the the final binding expression that is returned by expression.accept
will look like this:
You can see how the model.address.city
binding expression gets built up by Aurelia in to the "Abstract Syntax Tree". The nice thing for us building the binding behavior is that we can lean on Aurelia's binding engine and don't need to reinvent the wheel, because I don't know about you, but I wouldn't know an AST from a hole in the ground.
If you're still wanting more information on how the expression rebasing stuff works, I encourage you to read the Aurelia documentation written by Jeremy Danyow here.
In Part One of this blog post, we've looked at a rather complex, though also simple to implement, binding behavior that gives us the ability to build data-driven forms. In Part Two, we'll take some of the stuff learned in this post (especially getting Aurelia to parse binding expressions for us) and put it to work in a custom attribute that can dynamically create or rewrite binding expressions on the element it is attached to.
As my teenage niece would say: "Get excited!"