Ashley Grant's Blog

Getting My Hands Dirty with Aurelia's Binding Engine, Part Two

Introduction

Ashley Grant

Ashley Grant


Aurelia Custom Attributes tutorial

Getting My Hands Dirty with Aurelia's Binding Engine, Part Two

Posted by Ashley Grant on .
Featured

Aurelia Custom Attributes tutorial

Getting My Hands Dirty with Aurelia's Binding Engine, Part Two

Posted by Ashley Grant on .

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.

  1. There is a need for an auth custom attribute that can enable or disable an HTML element (via the disabled attribute).
  2. There may or may not be an existing binding on the disabled attribute of the HTML element.
  3. If there is no existing disabled binding, then the element will be disabled or enabled based on the value bound to the auth attribute.
  4. If a binding does exist, then the auth custom attribute will override the disabled binding when the auth attribute's bound value is false. When the auth custom attribute's bound value is true, then the existing disabled 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!

Ashley Grant

Ashley Grant

View Comments...