• 8 juli 2014
  • Leestijd: 4 minuten

Using Events for a Modular JavaScript Architecture

Engineering an architecture for front end code is often the last thing a software engineer thinks about. On the back end, frameworks like Rails can guide you towards a good architecture. On the front end, many JavaScript frameworks are offered, but not all these frameworks tell you something about good architecture.

At Moneybird we decided to use a very small layer of JavaScript in our application. Our views are plain HTML, enhanced with JavaScript behavior. This behavior is often described in a jQuery Widget. Both custom widgets for our projects and open source widgets co-exist in our codebase. The behavior we describe in JavaScript is purely used for enhancing the experience of the end-user, without JavaScript our application would be quite useable.

In this post I want to explain how we keep our JavaScript modular by using events. We write all our JavaScript in CoffeeScript (opent in nieuw tabblad), so the examples will be in this language. Although we use jQuery (opent in nieuw tabblad) and jQuery UI widgets (opent in nieuw tabblad), the techniques described can be applied to vanilla JavaScript and other libraries.

Writing JavaScript widgets #

Mostly, the behavior for a view starts in a simple CoffeeScript file. Once the project grows, some behavior is repeated and we decide to create a widget. For us, a widget is a building block which can be used on any page in our application, as long as the required HTML structure is available. An example of such a widget can be a drop-down menu.


jQuery.widget "moneybird.dropdown",
_create: ->
@element.on "click", =>
if !@element.hasClass("active")
@open()
else
@close()

open: ->
@element.addClass("active")
@element.next().show()

close: ->
@element.removeClass("active")
@element.next().hide()

This widget can be applied to any HTML element on the page, followed by an element that contains the drop-down. When clicking the element, the class is changed and the drop-down is showed.


<a href="javascript:;" data-behavior="dropdown">Options</a>
<div class="dropdown">
<ul>
<li><a href="...">Option 1</a></li>
<li><a href="...">Option 2</a></li>
</ul>
</div>

Instead of using an ID or CSS class, we use the data-behavior attribute in HTML to apply behavior to an element. This increases the separation of style and behavior, allowing a front end engineer to change the class names without affecting the behavior.

$('[data-behavior~=dropdown]').dropdown()

Adding widgets and requirements #

Chances are the widget described above will not be the only widget on a page. For example, the drop-down can be used on a page with a widget for toggling content:


<a href="javascript:;" data-behavior="toggle" data-toggleable="some-content">Toggle</a>
<div data-toggle="some-content">...</div>


jQuery.widget "moneybird.toggleContent", ->
_create: ->
@element.on "click", =>
@toggle()

toggle: ->
$("[data-toggle='#{@element.data("toggleable")}'").toggle()

$('[data-behavior~=toggle]').toggleContent()

The two widgets can operate independently, but sometimes communication between the widgets is required. Such a requirement could be:

When the drop-down is active, it should not be possible to toggle the content

The easiest way to satisfy this dependency, is to add a check to the toggle widget:

toggle: ->
if !$('[data-behavior~=dropdown]').hasClass("active")
@element.toggle()

This check violates our modular architecture, because it reaches beyond the limits of the toggle widget. Suddenly the toggle widget queries something about the page which is not required to be present. Furthermore, the toggle widget knows an implementation detail about the drop-down widget: maybe the developer of the drop-down widget changes the class active to open, breaking the behavior of the toggle widget.

Using events for communication #

JavaScript has a great event handling system. It can be used for events from the browser or end-user, but also for custom events. The interaction between the drop-down and toggle widget should be defined on a higher level and not in the widgets themselves. The first step is to make it possible to disable the toggle widget temporarily:


jQuery.widget "moneybird.toggleContent", ->
_create: ->
@enable()
@element.on "click", =>
@toggle()

toggle: ->
if !@disabled
$("[data-toggle='#{@element.data("toggleable")}'").toggle()

disable: ->
@disabled = true

enable: ->
@disabled = false

jQuery widget allows us to call these methods on elements that have the widget initialized:


$('[data-behavior~=toggle]').toggleContent("disable")
$('[data-behavior~=toggle]').toggleContent("enable")

The next step is to determine when to disable and enable the widget. Therefore we need to know when the drop-down is opened and closed. We do this by triggering an event from the drop-down widget.


jQuery.widget "moneybird.dropdown",
_create: ->
@element.on "click", =>
if !@element.hasClass("active")
@open()
else
@close()

open: ->
@element.addClass("active")
@element.next().show()
@element.trigger("dropdown:open")

close: ->
@element.removeClass("active")
@element.next().hide()
@element.trigger("dropdown:close")

At this point, we can start listening to the events from the drop-down widget. In a CoffeeScript file that is loaded on the page with both widgets, we can listen to the events and change the state of the toggle widget.


$('[data-behavior~=dropdown]').on "dropdown:open", ->
$('[data-behavior~=toggle]').toggle("disable")

$('[data-behavior~=dropdown]').on "dropdown:close", ->
$('[data-behavior~=toggle]').toggle("enable")

Conclusion #

We use custom events in JavaScript to keep our widgets isolated from the page they are used in. Communication between widgets is always implemented via methods and events. This allows us create many widgets and use them independently from each other. More information about event handling in jQuery can be found in the API, read about trigger() (opent in nieuw tabblad) and on() (opent in nieuw tabblad). More about writing your own jQuery UI widgets can be read in the guide: How To Use the Widget Factory. (opent in nieuw tabblad)