Skip to content

Instantly share code, notes, and snippets.

@manuel-guilbault
Last active November 20, 2015 05:52
Show Gist options
  • Save manuel-guilbault/0ef2923baa8be4ffac6b to your computer and use it in GitHub Desktop.
Save manuel-guilbault/0ef2923baa8be4ffac6b to your computer and use it in GitHub Desktop.
Aurelia-Focus-Article
## Introduction ##
I've been a huge fan of [Durandal](http://durandaljs.com/) and [Knockout JS ](http://knockoutjs.com/) 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 ```focus```binding. I decided to take advantage of the [Custom Attribute](http://aurelia.io/docs.html#custom-attributes) API to develop a ```focus``` custom attribute for Aurelia.
## Requirements ##
The custom attribute I have in mind would be used this way:
``` javascript
export class ViewModel {
hasFocus = false;
}
```
``` html
<input focus.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.
## Getting started ##
First, let's follow the [documentation](http://aurelia.io/docs.html#custom-attributes) and create an empty custom attribute:
``` javascript
import {customAttribute, inject, bindingMode} from 'aurelia-framework';
@customAttribute('focus', bindingMode.twoWay)
@inject(Element)
export class Focus {
constructor(element) {
this.element = element;
}
valueChanged(newValue) {
}
}
```
The first and easiest step is to listen for changes of the ```value``` property, and to react accordingly, by focusing or bluring the target element. This is done inside the ```valueChanged``` method:
``` javascript
valueChanged(newValue) {
if (newValue) {
this.element.focus();
} else {
this.element.blur();
}
}
```
Pretty simple! Now, when our view model's ```hasFocus``` property changes, the input is properly focused or blured.
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 mentionned in the [documentation](http://aurelia.io/docs.html#custom-attributes), this should be done in the ```attached()``` method:
``` javascript
attached() {
this.element.addEventListener('focus', e => { this.value = true; });
this.element.addEventListener('blur', e => { this.value = false; });
}
```
Still pretty straight forward, right? Yet, something's still missing: the event listeners need to be removed when the element is ```detached()``` from the document:
``` javascript
detached() {
this.element.removeEventListener('focus', e => { this.value = true; });
this.element.removeEventListener('blur', e => { this.value = false; });
}
```
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](https://babeljs.io/)), you will see the ```removeEventListener``` calls don't work. This is because the ```attached()``` and ```detached()``` methods get transpiled this way:
``` javascript
function attached() {
var _this2 = this;
this.element.addEventListener('focus', function(e) { _this2.value = true; });
this.element.addEventListener('blur', function(e) { _this2.value = false; });
}
function detached() {
var _this3 = this;
this.element.removeEventListener('focus', function(e) { _this3.value = true; });
this.element.removeEventListener('blur', function(e) { _this3.value = false; });
}
```
As you can see, the removed listeners are are not the same as the added ones, because ```this``` is assigned to different variables. Let's try and make them methods, to see if it works:
``` javascript
onFocus(e) {
this.value = true;
}
onBlur(e) {
this.value = false;
}
attached() {
this.element.addEventListener('focus', this.onFocus);
this.element.addEventListener('blur', this.onBlur);
}
detached() {
this.element.removeEventListener('focus', this.onFocus);
this.element.removeEventListener('blur', this.onBlur);
}
```
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:
``` javascript
constructor(element) {
this.element = element;
this.focusListener = e => {
this.value = true;
};
this.blurListener = e => {
this.value = false;
};
}
attached() {
this.element.addEventListener('focus', this.focusListener);
this.element.addEventListener('blur', this.blurListener);
}
detached() {
this.element.removeEventListener('focus', this.focusListener);
this.element.removeEventListener('blur', this.blurListener);
}
```
That works fine now.
## 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.
``` javascript
import {customAttribute, inject, bindingMode, TaskQueue} from 'aurelia-framework';
@customAttribute('focus', bindingMode.twoWay)
@inject(Element, TaskQueue)
export class Focus {
constructor(element, taskQueue) {
this.element = element;
this.taskQueue = taskQueue;
}
giveFocus() {
this.taskQueue.queueMicroTask(() => {
if (this.value) {
this.element.focus();
}
});
}
valueChanged(newValue) {
if (newValue) {
this.giveFocus();
} else {
this.element.blur();
}
}
}
```
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 using either the ```MutationObserver```, if supported by the browser, or a standard timeout. 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?
``` javascript
constructor(element) {
this.element = element;
this.focusListener = e => {
this.value = true;
};
this.blurListener = e => {
if (document.activeElement !== this.element) {
this.value = false;
}
};
}
```
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.
@EisenbergEffect
Copy link

This looks pretty good. Would you mind writing a 2-3 sentence bio of yourself so that I can use that content to help introduce you to the community as part of the blog post? Are there any more revisions you want to make? If not, we can probably post this tomorrow.

@EisenbergEffect
Copy link

Can I post this?

@EisenbergEffect
Copy link

I need a short bio for you and an approval to post this. I wanted to post it today, but didn't hear back from you. @manuel-guilbault

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment