Events
DOM events are central to anything interactive on the web. You've probably written element.addEventListener('click', handler)
or $('#button').on('click', handler)
, or similar code, a thousand times.
Step 1
With Ractive.js, events are declarative instead, and you declare an event handler like this:
<button on-click="@global.alert( 'Activating!' )">Activate!</button>
"But wait!", you say. "That looks like some sort of global inline event listener horribleness!". It's not though, I promise. Instead, the
on-
directive will bind a shared callback directly to the element usingaddEventListener
when it is rendered. When the shared callback is triggered, it will evaluate the expression (or list of expressions) that was passed to the event directive. If you inspect the button element in your browser's Dev Tools, you'll notice that there is noon-click
attribute. That's because directives don't render directly to the DOM, but instead control behavior related to rendering like attaching event listeners.
This is generally more convenient - you don't need to pepper the DOM with id and class attributes just you've got a hook to identify elements with. It also takes the guesswork out of when to attach and detach event listeners, since Ractive.js will automatically attach and detach handlers as elements are rendered and unrendered. Since the event directive actually accepts a list of expressions as its argument, so go ahead and log a console message after the alert is acknowledged:
<button on-click="@global.alert( 'Activating!' ), console.log( 'alert was acknowledged' )">Activate!</button>
console
is one of the globals that is exposed to Ractive.js templates, but if you want to get toalert
, you have to go through the@global
special reference.The playground watches for console messages in the output pane and displays them on the
Console
tab, so if you happen to be reading through this in a browser that doesn't have Dev Tools, you can still see most console messages.
Step 2
Okay, we can now annoy users and log debug info. What about something a bit more useful? Back into the bag of contrived examples, and out comes... a number incrementer!
{{number}} <button on-click="@this.add('number')">+</button>
@this
is a reference to the Ractive.js instance that is controlling the template, so you can call any methods on the Ractive.js API with the@this
special reference. Since@this
is a very common reference, it also has an ever so slightly shorter shorthand@
.@this.toggle('visible')
and@.toggle('visible')
are equivalent.
Given that there is a subtract
method in the Ractive.js API, can you add a decrement button to the example as well?
Step 3
To further wangle our incrementer contrivance, suppose we devised a web version of the old traveling game wherein you collect all of the cows that you pass on your side of the car. So we'll need two objects, one for me
and one for my sibling
. Each person will have a property, cows
, which is an integer representing accumulated bovine beasts.
{
me: { cows: 0 },
sibling: { cows: 0 }
}
The template for this game will include a table
containing counts for each person and buttons to increment each person's taurine total.
<table>
<tr>
<th>Me</th><th>Sibling</th>
</tr>
<tr>
<td>
{{#with me}}
{{.cows}}
<button>+</button>
{{/with}}
</td>
<td>
{{#with sibling}}
{{.cows}}
<button>+</button>
{{/with}}
</td>
</tr>
</table>
The event listeners could use the full path to the appropriate cows
property, which is not too large an imposition here, but with a deeper context, it would quickly become inconvenient. They could also do some keypath manipulation using the special reference @keypath
, which resolves to the keypath to the current context at any point in the template. That's a bit painful in most contexts and impossible in others. To address this particular issue, Ractive.js provides a special @context
reference that acts as an API adaptor that is rooted in the current context of the template. @context
objects have most of the same API methods as a Ractive.js instance, but they can resolve relative keypaths. Add this event directive to each of the buttons to see it in action:
<button on-click="@context.add( '.cows' )">+</button>
To complete the rules of the game, when you pass a cemetery on your side of the car, you lose all of your cows. It's a weird game, I know, but I didn't make it up. What would be the easiest way to reset a person's cow count?
Step 4
Suppose you need to track the mouse cursor as it moves across a div
for... reasons. Perhaps you've landed the contract for the frontend of a missile targeting system. Ractive.js provides access to the DOM event object in event directive expressions as the special @event
reference. Any properties and methods available on the event object passed to the addEventListener
callback are available from the @event
reference e.g. @event.clientX
.
<div id="tracker" on-mousemove="@.set({ x: @event.clientX, y: @event.clientY })" on-click="console.log(`firing at (${@event.clientX}, ${@event.clientY})!`)">
({{x}}, {{y}})
</div>
The element on which the event directive is installed is also available within event directive expressions as the special @node
reference. Like the @event
reference, you can access any properties or methods of the DOM element or even pass it as an argument to another function using @node
.
<input value="example" on-focus="@node.select()" />
If you need to cancel an event by calling
stopPropagation
, you can simply makefalse
the last expression in your event directive expression list.<a href="/nope" on-click="doSomething(), false">This will do something rather than /nope.</a>
Step 5
Ractive.js also provides its own instance-level event system, so that you can raise and respond to internal events in a more abstract way. You can name your events however you like, so you can convey more meaning about the intent of the action that triggered the event, such as addUser
as opposed to click
.
To listen to an event, you attach an event listener to your Ractive.js instance with the on
method.
ractive.on( 'activate', function ( context ) {
// `this` is the ractive instance
// `context` is a context object
// any additional event arguments would follow `context`, if supplied
alert( 'Activating!' );
});
To raise an event, you pass the event name and optional context and arguments to the fire
method.
// this will trigger the
ractive.fire( 'activate' );
Update the template and JavaScript to fire and handle an instance event, then execute. Remember, you can access the current Ractive.js instance with the @this
special reference or @
shorthand.
A Ractive.js instance doesn't need to be rendered to update data or fire and handle events.
Step 6
You can subscribe to multiple instance events in one go:
ractive.on({
activate: function () {
alert( 'Activating!' );
},
deactivate: function () {
alert( 'Deactivating!' );
}
});
Add a 'deactivate' button and wire it up.
Step 7
Converting a DOM event into an instance event is a terribly convenient way to handle user actions in a meaningful way. The signature of the fire
method is a little cumbersome to include all over your template, especially if you need to pass the @context
and a few additional arguments. To address that, Ractive.js provides a convenient shorthand method for firing an instance event from an event directive. If there is only one expression in the event directive arguments, that expression returns an array, and that array has a string as the first member, the event directive will fire an internal event with the first array element as the name, the current @context
as the context, and any remaining members of the array as event arguments. This is generally referred to as a "proxy event".
<button on-click="['activate', user]">Activate!</button>
<!-- which is a bit more convenient than -->
<button on-click="@this.fire('activate', @context, user)">Activate!</button>
Depending on your editor and personal tastes, it might be convenient to use an unquoted attribute for your proxied events:
<button on-click=['activate', user]>Activate!</button>
. There is nothing special going on there - Ractive.js supports just about everything that HTML does, and HTML supports unquoted attributes e.g.<input value=green />
.
As with regular event expressions, if a handler for a proxied event returns false
, it will cancel the event.
Step 8
There are a couple of ways to unsubscribe from events. If you've used jQuery, you'll be used to this syntax:
ractive.on( 'select', selectHandler );
// later...
ractive.off( 'select', selectHandler );
That's fine, as long as you stored a reference to selectHandler (i.e. you didn't just use an anonymous function). If you didn't, you can also do this:
// remove ALL 'select' handlers
ractive.off( 'select' );
// remove all handlers of ANY type
ractive.off();
Alternatively, you can do this:
var listener = ractive.on( 'select', selectHandler );
var otherListeners = ractive.on({
activate: function () { alert( 'Activating' ); },
deactivate: function () { alert( 'Deactivating!' ); }
});
// later...
listener.cancel();
otherListeners.cancel();
Try adding a 'stop' button which removes the 'activate' and 'deactivate' handlers.
You can also temporarily disable an event handler or set of event handlers by calling silence
on the handle returned by on
. You can resume processing of the handler or handlers by calling the conveniently named resume
method on the handle.
var listener = ractive.on({
'select', function () { alert( 'Selected!' ); },
'delete', function () { alert( 'Deleted!' ); }
});
// later...
listener.silence();
// no alerts here
ractive.fire( 'select' );
ractive.fire( 'delete' );
// later...
listener.resume();
// alert here
ractive.fire( 'select' );
Try adding a silence button that checks to see if the listener is silenced using the handle's isSilenced
method, and silences or resumes it as appropriate.
If you remove your ractive from the DOM with
ractive.teardown()
, any event handlers will be automatically cleaned up.
Step 9
It's possible to define custom template events in Ractive.js. You use them just like normal events:
<button on-tap='activate'>Activate!</button>
Note that we're using on-tap here instead of on-click
– tap
is a custom event.
Custom events are distributed as plugins, which can be downloaded from here to include in your project.
You can also create your own plugins – just follow the instructions on the docs.
The trouble with click
is that it's nonsense. If you put your mouse down on the 'activate' button, waggle it about, then lift your finger up after a few seconds, the browser will in most cases consider it a 'click'. I don't. Do you?
Furthermore, if your interface needs to work on touch devices, using click
means a 300ms delay between the touchstart
-touchend
sequence event and the simulated mousedown
-mouseup
-click
sequence.
The tap event corrects for both of these anomalies. Try replacing the click proxies in the template.