Pothibo

Event based response in JavaScript

What? Event based response?

This is something that I’ve been using for a few months now on different project. The goal is that instead of JSON, a response is a custom event that is fired from the element that initiated the request and bubbles up to the document.

By building custom events right from the response, the views become a place where client-side and server-side code transfert knowledge, like osmosis. We’ll get to this later.

When it comes down to it, JSON and events are just JavaScript objects. On the outside, the difference seems minimal. However, there’s a huge difference between the two and if you understand the difference and leverage it in your project, you’ll find how natural using events is compared to JSON.

Natural propagation

When an event is dispatched in a browser and its set to bubble, it will start from the targeted element and then bubble up the DOM from parent element to parent element until it reaches the document. This means that any JavaScript object can listen to that propagation through addEventListener(). To get this behavior, you need to dispatch that event to an element. Usually, this element is the one that called the request.

This comes at the opposite of how a JSON object is handled on the client side. It needs to be inspected in order to push it to the right object. Maybe you’ll have a router that handles that inspection, maybe it’s something else. But the important part of this is that if it’s not you who’s doing the work, your framework of choice will have to do it for you.

A simple todo

The ubiquitous todo application! Well, if I want you to understand what I mean, I need to start somewhere, right?

When creating a new todo, here’s how a server could send your response.


(function(target) {
  var e = new CustomEvent("todos:create", {bubbles: true})
  e.html = "<li class=\'todo\' data-oid=\"5\">\n  test\n    <form action=\"/todos/5\" class=\"button_to\" data-remote=\"true\" method=\"post\"><div><input name=\"_method\" type=\"hidden\" value=\"delete\" /><input type=\"submit\" value=\"✓\" /><input name=\"authenticity_token\" type=\"hidden\" value=\"s7+hU9ioPzPCJgaiweHvI76Bk5Zse5jSMDMVsp7JSQc=\" /><\/div><\/form>\n<\/li>\n".toHTML()

  target.dispatchEvent(e)
})

The inline HTML could be JSON. Personally, I prefer HTML over JSON because it's already what I wanted to inject. I know this because it's a response to a specific query. I also prefer HTML because it's queryable which means I can either insert the HTML as is, and I can also look if an instance of the same HTML exists by querying both the DOM and this HTML fragment.

I said earlier that views were like osmosis in this system. The reason is because the HTML I render is built with Rails' view system. But instead of just sending pure HTML down the wire, I assign it to a JavaScript object — an event object which will be evaluated in the browser — that will the be propagated through the DOM once in the client's browser.

Still vague?

Let me break it down for you.

 # apps/views/layout/application.js.erb
(function(target) {
  var e = new CustomEvent("<%= j "#{controller_name}:#{action_name}" %>", {bubbles: true})
  <%= yield %>
  target.dispatchEvent(e)
})
 # apps/views/todos/create.js.erb
e.html = "<%= j render @todo %>".toHTML()
 # apps/views/todos/_todo.html.erb
<li class='todo' data-oid="<%= todo.id%>">
  <%= todo.title %>
  <% unless todo.done? %>
    <%= button_to "✓", todo_path(todo), method: :delete, remote: true %>
  <% end %>
</li>

I'm using Rails view system to build the partial for the todo. Then, I assign it to the event's html property (arbitrary property). At this stage, it's not javascript yet. It's a simple string that needs to be sent through the wire and injected in the client's browser.

The next logical question is, how to you make the browser evaluate this event and dispatch it to the target.

This is the place where you need to configure your library to pass everything through a special callback so it can evaluate the response from the server and run it inside the browser.

 # apps/assets/javascripts/xhr.js.coffee
  script = "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
  document.addEventListener 'submit', (e) ->
    if e.target.getAttribute('disabled')? || e.target.dataset['remote'] != 'true'
      return

    XHR.Form(e.target)

    e.preventDefault()
    return false

  document.addEventListener 'click', (e) ->

    if e.target.getAttribute('disabled')? || e.target.dataset['remote'] != 'true'
      return

    xhr = new XHR(e.target)
    xhr.send(e.target.getAttribute('href'))

    e.preventDefault()
    return false

class XHR
  constructor: (el) ->
    @element(el)
    @request = new XMLHttpRequest()
    @request.addEventListener('load', @completed)


  element: (el) ->
    @element = ->
      el

  completed: (e) =>
    if e.target.responseText.length > 1
      eval(e.target.responseText)(@element())

  send: (src, method = 'GET', data) =>
    @request.open(method, src)
    @request.setRequestHeader('accept', "*/*;q=0.5, #{@script}")
    @request.setRequestHeader('X-Requested-With', "XMLHttpRequest")

    @request.send(data)

  @Form: (element) =>
    xhr = new XHR(element)
    data = new FormData(element)
    param = document.querySelector('meta[name=csrf-param]').getAttribute('content')
    token = document.querySelector('meta[name=csrf-token]').getAttribute('content')
    data.append(param, token)
    xhr.send(element.getAttribute('action'), element.getAttribute('method'), data)
    xhr

What this code does is that it takes any target in the DOM that has the attribute remote and will submit the request to the server. When the server responds, the query — which is a function — is evaluated and passed the calling element as the argument.

Any response you get from the server is now an event that is automatically propagated from the the target element up to the document.

Get more ideas like this to your inbox

You will never receive spam, ever.