Before Aurelia came onto the scene I had worked extensively with Angular and then shortly thereafter, React.js (sometimes both together). After using React for a while, I became accustomed to passing functions as callbacks to my components. So naturally when I started using Aurelia and creating custom elements, I tried the callback function approach first.
Callbacks are not the answer
While I was successful in creating a bindable property on my initial custom elements and then passing a function, it became apparent this approach is flawed. Not only do you have scope issues to deal with, but I encountered issues with singletons not being singletons and struggling to pass exact references.
In custom elements we can inject a reference to the custom element’s actual DOM element. What this allows us to do is manipulate the custom element itself and that includes firing custom events that can be listened too when used in your application.
This gives us the benefit of having callback like values, except we listen for events and fire a callback function, not expect the custom element to handle that for us internally.
An example
Recently I published a tutorial on creating a custom Select 2 custom styled select in Aurelia using custom elements. In this tutorial I use custom events to listen for changes on the custom styled select.
If you can’t be bothered wading through the tutorial, I am going to show how to trigger custom events on custom elements in Aurelia. This article from here onwards assumes you understand Aurelia enough to know what a custom element looks like and how Javascript native events sort of work.
I am also going to be using code by my tutorial, so ignore anything specific to jQuery or Select2. We are going to be mostly focusing on events themselves, events which have no 3rd party library dependencies.
The code
custom-element.js
This is the ViewModel for our custom element. This code is taken from my above linked tutorial, hence the references to jQuery and Select2.
// Aurelia Framework specific functionality
import {bindable, inject, customElement} from 'aurelia-framework';
// Import JSPM modules we installed earlier
import $ from 'jquery';
import 'select2';
import 'select2/css/select2.css!'
@customElement('select2') // Define the name of our custom element
@inject(Element) // Inject the instance of this element
export class CustomSelect {
@bindable name = null; // The name of our custom select
@bindable selected = false; // The default selected value
@bindable options = {}; // The label/option values
constructor(element) {
this.element = element;
}
// Once the Custom Element has its DOM instantiated and ready for binding
// to happenings within the DOM itself
attached() {
$(this.element).find('select')
.select2()
.on('change', (event) => {
let changeEvent;
if (window.CustomEvent) {
changeEvent = new CustomEvent('change', {
detail: {
value: event.target.value
},
bubbles: true
});
} else {
changeEvent = document.createEvent('CustomEvent');
changeEvent.initCustomEvent('change', true, true, {
detail: {
value: event.target.value
}
});
}
this.element.dispatchEvent(changeEvent);
});
}
}
Now lets zero in on the part we are most interested in, the custom event logic.
attached() {
$(this.element).find('select')
.select2()
.on('change', (event) => {
let changeEvent;
if (window.CustomEvent) {
changeEvent = new CustomEvent('change', {
detail: {
value: event.target.value
},
bubbles: true
});
} else {
changeEvent = document.createEvent('CustomEvent');
changeEvent.initCustomEvent('change', true, true, {
detail: {
value: event.target.value
}
});
}
this.element.dispatchEvent(changeEvent);
});
}
As you can see, we are creating a variable called changeEvent
and then checking what the browser supports. Because CustomEvent is quite new and not supported that well in IE, we check for older support using document.createEvent()
The value of change we can see in the above code is the name of our custom event. For the sake of this example, we are going to keep the event name of “change”. The rest of the logic is pure native events API which you can read up on more here.
The important thing to note here is that setting the property bubbles
to boolean of true will help prevent any issues you might encounter, particularly when using shadowDom in your custom elements.
Listening for events on custom elements
As you can see above our Select2 plugin fires off a jQuery change event, then inside we fire off a Javascript event called “change” with a property of “detail” and a sub-value of “value” which is on the event object itself accessible by: event.detail.value.
On our custom element we can subscribe to this event like this: <select2 change.delegate="myEventCallback($event)"></select2>
That’s it. It is that simple. Replace <select2></select2>
with your custom element tagName and that’s it. Your custom elements can now fire custom events and you can bind to them on the custom element itself wherever you are using it.
Wow, thank you very much.
This code results in a recursive loop with select2.
I managed to avoid it by adding the following at the start of the event handler:
if (event.originalEvent) return;
So simple, yet so powerful!
Thanks for the tutorial. I already know this will come in handy, and I’ll make a helper method out of the browser-compatible event-creation mechanism.
What boggles me, and please excuse my js-noobishness, is, why to use “bubbles: true” here. If the event has to be fired only at “this.element”, and there is some contract, that the subscriber (e.g. a view using the custom-element) can only listen there, it would be right to set “bubbles: false”, right?
I just tried the method, and even if I’m executing “this.element.dispatchEvent(customEvent)” within and listening directly there with , I need to use “bubbles:true”.
I don’t get it, as why the event has to bubble up in the DOM-hierachy. It all happens on in the end. I guess to take care of performance, I have to make the custom-event cancelable an use “return false” within test().
Maybe I’m switching to EventAggregator.
Sorry for the missing parts. It seems html is filtered out.
First paragraph should be:
I just tried the method, and even if I’m executing “this.element.dispatchEvent(customEvent)” within a custom-element and listing from the view using the custom-element directly on the customelement-tag, I need to use “bubbles:true”
One last lovely can of spam:
I will use aurelia EventAggregator. I tried to stop the bubble to the parent-elements within the event-handler “test(event) { event.stopPropagation(); }”, to no avail. And as I said, if I don’t use bubbling my custom-element customevent.delegate won’t catch the fired events.
Creating custom events can be done using aurelia-pal, see it here. https://github.com/aurelia/templating/issues/38
Hey! Any idea on how to detect the DOM Element on a @containerless Custom Element ?