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 using addEventListener 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 no on-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 to alert, 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 poroperty, 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.

      {{#with me}}
      {{#with sibling}}

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}})

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="" />

If you need to cancel an event by calling stopPropagation, you can simply make false 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 '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:

  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 and 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="'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... '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 'select' );

// remove all handlers of ANY type;

Alternatively, you can do this:

var listener = ractive.on( 'select', selectHandler );

var otherListeners = ractive.on({
  activate: function () { alert( 'Activating' ); },
  deactivate: function () { alert( 'Deactivating!' ); }

// later...

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...

// no alerts here 'select' ); 'delete' );

// later...

// alert here '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-clicktap 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.