Last week, we looked at some hardcore Aurelia code written by Jeremy Danyow that helps create data-driven forms. This week, we'll check out how I used what I learned from Jeremy's code to answer a Stack Overflow question from my fellow Aurelia Core Team Member Matt Davis.
The SO question is here. The question comes down to the following.
- There is a need for an
auth
custom attribute that can enable or disable an HTML element (via thedisabled
attribute). - There may or may not be an existing binding on the
disabled
attribute of the HTML element. - If there is no existing
disabled
binding, then the element will be disabled or enabled based on the value bound to theauth
attribute. - If a binding does exist, then the
auth
custom attribute will override thedisabled
binding when theauth
attribute's bound value isfalse
. When theauth
custom attribute's bound value istrue
, then the existingdisabled
binding will be respected.
When I read this question.. well, when I re-read the question after initially not answering it correctly, I quickly came up with some pseudo-code of what this custom attribute would need to look like:
constructor
inject reference to element attribute is attached to
also inject whatever framework classes are needed to implement the attribute
created callback
find the existing `disabled` binding, if it exists and hold on to it
valueChanged callback
if disabled binding exists {
if value === false
create a disabled binding that always is true
else
use the original disabled binding
} else {
if value == false
add the disabled attribute to the element
else
remove the disabled attribute from the element (if it exists)
}
unbind callback
cleanup after myself
Let's walk through what is needed for each of these and get an idea for how I accomplished this pseudo-code.
Find a binding
I need to find the existing binding in the created callback because Aurelia passes me the "owning view" as the first parameter to the created
callback. This view has a bindings
collection with any and all bindings that exist for the view. It's an array, which means I can use the find
function to look for the binding.
this.disabledBinding = owningView.bindings.find( b => b.target === this.el &&
b.targetProperty === 'disabled');
Even if you're not an Aurelia wizard, this code should be fairly self explanatory. In the constructor, I had the element this attribute is attached to injected. This is a basic Aurelia concept, so no need to explain it here (though I end up talking about it a fair amount on Stack Overflow sigh). I use the element to check each binding to see if its target
property is the element the attribute is attached to. If it's the same element, then I check if the binding is for disabled
property. I'm not really doing any Aurelia specific craziness here, I'm just doing a bog-standard search of a JavaScript array.
If I've found a binding, then I need to keep a reference to the original binding expression. I more or less copied this bit of code from Jeremy's code. I just create a new property on the binding and set it equal to the sourceExpression
property. I could just have easily have put stored this on the attribute instance.
Next, I need to create a binding expression that will always evaluate to 'true'. This will be used to replace the sourceExpression
property when the value of the attribute is false
to force the element to be disabled. I do this here so I only need to do it once per attribute instance instead of having to create it every time the attribute value changes to false
. This was something I had no idea how to accomplish, but knew that Aurelia must be able to do. In looking at Jeremy's data-driven form code, I saw it using the Aurelia Parser's parse
method to parse a binding expression before passing it to be "rebased." I figured this was worth a try, so I added Parser
to be injected in to the attribute and tried the following:
this.expression = this.parser.parse('true');
As with many things in Aurelia, I said, "there's no way it's that easy... but it is." I now had a binding expression that would always evaluate to true
, because.. well, the expression was simply true
.
This is where things started to get a bit difficult.
Swap out the binding
Next, I decided to just try and always swap out the binding with my new true
binding expression. There's no point in implementing the rest of the attribute if I can't actually swap out the binding expression at will. I'll admit that I had a bit of trouble accomplishing this. I even messaged Jeremy to ask how he would do it. He told me to "Get the expression's value by calling its evaluate method, then update the view by passing the expression result to the binding's updateTarget method."
Jeremy's the wizard, so I did what he said.
this.disabledBinding.sourceExpression = this.expression;
const result = this.disabledBinding.sourceExpression.evaluate(this.owningView);
this.disabledBinding.updateTarget(result);
This code worked! I was able to force the disabled
binding to always be true
. I figured I was done. I just needed to add in the relatively simple code to add/remove the disabled
attribute if there was no existing disabled
binding found (number 3 above). So I added that code, then quickly added the code to swap the binding in and out if the bound value of the attribute changed. My valueChanged
callback look like this:
valueChanged() {
if(this.disabledBinding ) {
if( this.value === true ) {
this.disabledBinding.sourceExpression = this.disabledBinding.originalSourceExpression;
} else {
this.disabledBinding.sourceExpression = this.expression;
}
const result = this.disabledBinding.sourceExpression.evaluate(this.owningView);
this.disabledBinding.updateTarget(result);
} else {
if( this.value === true ) {
this.el.removeAttribute('disabled');
} else {
this.el.setAttribute('disabled', 'disabled');
}
}
}
I was done! Or so I thought.
A problem arises
I decided to implement some test code in the gist I was building that would allow me to change the value of the custom attribute as well as the original binding expression for the element.
<button click.delegate="authorized = !authorized">Toggle Authorized</button>
<button click.delegate="isDisabled = !isDisabled">Toggle Disabled</button> <br /><br />
Has disabled binding: <input type="text" auth.bind="authorized" disabled.bind="isDisabled" value.bind="value" /> <br /><br />
No disabled binding: <input type="text" auth.bind="authorized" value.bind="value" />
Everything worked great on the element with no disabled
binding. But things weren't so rosy for the element that did have an existing disabled
binding. Things would initially work, but if I set authorized
to true
and isDisabled
to false
, the textbox wasn't always getting enabled.
I needed to dig deeper.
A Trip to the Documentation
I decided to go to the Aurelia docs and look at the various APIs I had to work with. Given that I had a binding expression, I decided to check its API first.
It wasn't very helpful. BindingExpression
only has one method, createBinding
. But then I looked at what this method produces: Binding
. Then I smacked myself on the forehead because I actually have a Binding
(this.disabledBinding
) and I'm changing the sourceExpression
property of the binding. I was also calling the updateTarget
method of the binding. So I checked the API for Binding
.
Well, that seems pretty promising. There's also an unbind
method. What if I were to call unbind
, then immediately call bind
. I needed to pass something called source
to the bind
method, but I can get that from the binding itself before I call unbind
. So I got rid of the evaluate
and updateTarget
code from before, and tried out unbind
then rebind
.
const source = this.disabledBinding.source;
this.disabledBinding.unbind();
this.disabledBinding.bind(source);
And it worked! I could now swap the binding in and out at will. Out of curiosity, I decided to go check the Aurelia source code for bind
. I noticed that it calls the evaluate
method on the sourceExpression
and then calls updateTarget
passing the result of the evaluate
call. This was probably why Jeremy initially told me to do this. He's so knowledgeable of the Aurelia binding engine that he just told me to do what the binding engine does for me by calling bind
. But it turns out that there's a lot more stuff that happens in the bind
method, and a lot of it is necessary for Aurelia to fully "rebind" the disabled binding on the fly. And the cleanup that happens in unbind
is necessary if I want to avoid memory leaks.
So at this point, my valueChanged
callback looked like this:
valueChanged() {
if(this.disabledBinding ) {
if( this.value === true ) {
this.disabledBinding.sourceExpression = this.disabledBinding.originalSourceExpression;
} else {
this.disabledBinding.sourceExpression = this.expression;
}
const source = this.disabledBinding.source;
this.disabledBinding.unbind();
this.disabledBinding.bind(source);
} else {
if( this.value === true ) {
this.el.removeAttribute('disabled');
} else {
this.el.setAttribute('disabled', 'disabled');
}
}
}
This code handled everything.
There was one remaining issue. For some reason (that I still am not sure of), if I only implemented valueChanged
, it would sometimes get called before the created
callback. I solved this by implementing a bind
callback and simply calling valueChanged
from there. When you implement the bind
callback in your component, Aurelia refrains from calling any of your changed
callbacks during initial binding.
Clean up after myself
Implementing this left me with only needing to implement cleanup code in the unbind
callback:
unbind() {
if(this.disabledBinding) {
this.disabledBinding.sourceExpression = this.disabledBinding.originalSourceExpression;
this.disabledBinding.originalSourceExpression = null;
this.rebind();
this.disabledBinding = null;
}
}
I simply reset the sourceExpression
of the disabled binding back to whatever it was originally (if a binding even exists), get rid of the reference I added to the original binding expression, and call rebind
. Chances are calling rebind
won't be necessary in 99.9% of use cases, but it's good to be thorough. I then get rid of my own reference to the binding to allow prevent a possible memory leak when using this attribute of the attribute never releasing the binding.
Here's the attribute's complete source:
import {inject} from 'aurelia-framework';
import {Parser} from 'aurelia-binding';
@inject(Element, Parser)
export class AuthCustomAttribute {
constructor(element, parser) {
this.el = element;
this.parser = parser;
}
created(owningView) {
this.disabledBinding = owningView.bindings.find( b => b.target === this.el && b.targetProperty === 'disabled');
if( this.disabledBinding ) {
this.disabledBinding.originalSourceExpression = this.disabledBinding.sourceExpression;
// this expression will always evaluate to true
this.expression = this.parser.parse('true');
}
}
bind() {
// for some reason if I don't do this, then valueChanged is getting called before created
this.valueChanged();
}
unbind() {
if(this.disabledBinding) {
this.disabledBinding.sourceExpression = this.disabledBinding.originalSourceExpression;
this.disabledBinding.originalSourceExpression = null;
this.rebind();
this.disabledBinding = null;
}
}
valueChanged() {
if(this.disabledBinding ) {
if( this.value === true ) {
this.disabledBinding.sourceExpression = this.disabledBinding.originalSourceExpression;
} else {
this.disabledBinding.sourceExpression = this.expression;
}
this.rebind();
} else {
if( this.value === true ) {
this.el.removeAttribute('disabled');
} else {
this.el.setAttribute('disabled', 'disabled');
}
}
}
rebind() {
const source = this.disabledBinding.source;
this.disabledBinding.unbind();
this.disabledBinding.bind(source);
}
}
Conclusions
So, here's the thing with this code. I often talk at conferences about how "Aurelia gets out of your way and lets you focus on your own code." This is a case where Aurelia and its binding engine definitely didn't get out of the way, and I had to write some Aurelia specific code. But the cool thing is that I was able to use some fairly simple APIs the framework provides to accomplish something that is rather powerful. Just learning that I could do this.parser.parse('true');
was a revelation. It's not a feature I'll use very often (hopefully), but when I need it, it's there.
If I wanted to get really fancy with things, I could probably implement components that can build a view and its binding on the fly from data coming from the server. It wouldn't be pretty code, but it would likely be fairly simple to grok after reading this blog post.
What I did in this blog post is pretty advanced Aurelia code, no doubt, but as far as the JavaScript code itself, it's really simple code. I'm not using any advanced techniques to be "clever." The only ES2015+ features I'm using are classes, decorators, the find
method on the array, and the arrow function in the find
method. All of these are features that we all use every day while building Aurelia applications. So you shouldn't be scared to dig deep in the framework and get your hands a bit dirty!