Building Aurelia's Focus Attribute

Posted by AureliaEffect on June 5, 2015

This week we are really excited to have our first community member highlight post!
Manuel Guilbault
is a Canadian-born, Paris-based developer, where he works as a consultant in the financial sector. Passionate about software craftsmanship, agility and lean principles, he loves to learn and debate about how we do things, why we do them this way and how they can be improved. This week we've invited Manuel to guest blog with us and share the work he did implementing a new custom attribute that's shipping in the next Aurelia update.

Take it away Manuel....

Introduction

I've been a huge fan of
Durandal
and
Knockout JS
for many years now, and I've been closely following Aurelia since I first heard about it. After playing with it for a while, I noticed that one of the features I used with Knockout was missing from Aurelia: a focusbinding. I decided to take advantage of the
Custom Attribute
API to develop a focus custom attribute for Aurelia.

Requirements

The custom attribute I have in mind would be used this way:

exportclassViewModel{
hasFocus =false;}

<inputfocus.bind="hasFocus"/>

The requirements are as follows:

When hasFocus is set to true, the input gets focus;

When hasFocus is set to false, the input loses focus;

When input gains focus following a user action, hasFocus is set to true;

When input loses focus following a user action, hasFocus is set to false.

So, what I want is basically a two-way binding between the bound property and the focus state of the input. You might think that this kind of two-way binding will trigger an infinite loop, but rest assured: Aurelia's binding module will take care of that.

Next, the attribute's value property needs to be updated when the input receives or loses focus after a user action. To do this, we need to register event listeners on the element. As mentioned in the
documentation
, this should be done in the attached() method:

Now if you go and test what we have so far, and you are running this on a setup that doesn't fully support ECMAScript 6 and uses a transpiler (like
Babel
), you will see the removeEventListener calls don't work. This is because the attached() and detached() methods get transpiled this way:

It still doesn't work: when the onFocus and onBlur listeners are called by the browser, this doesn't contain the Focus instance but the element that fired the event. That's actually a common mistake; I should have known better. Let's solve this issue by creating instance functions that capture this in their scope:

Fine-tuning

We now have a Focus custom attribute that answers to all of our initial requirements. But if you play a little bit with it, you will see that there are still some edge cases that are not yet covered.

Interaction With Other Attributes

By definition, a custom attribute is used to decorate an element, so it has to work along fine with other custom attributes that can be on the same element.

What if the view model property bound to our focus attribute is also bound to the show attribute? This will be problematic, because depending on the order of evaluation when the bound property turns to true, the target element may not be visible yet when our attribute tries to give it focus. We can solve this problem by using another part of Aurelia's API: the TaskQueue class.

In the above snippet, I first added injection of the TaskQueue instance into the Focus constructor. I also added a giveFocus() method, which will enqueue a microtask responsible for giving focus to the element. This will actually delay the focus() call by pushing it to the end of the binding queue. This will ensure that all queued events, including the bound property's value change, are processed before the focus is given.

Handling Window Change

You may have noticed that our Focus custom attribute does not react correctly when the element is focused and you change browser tabs. How can we fix that?

In the above code snippet, I changed the blurListener function, so that, when a blur event is triggered, the value is set to false only if the element is not the document's active element. This scenario occurs typically when you change tabs in the browser (or change window in the OS). By preventing setting value to false, we prevent valueChanged(false) from being called, which would call element.blur() and would make the document's active element to become the body, and therefore cause the element to have lost focus when you go back to the browser tab.

Summary

As you can see, it is pretty easy to create new features for Aurelia. We were able to quickly come up with a new focus attribute, thanks to Aurelia's modular and extensible design.