Spellbook Dispatcher

by Drew Barontini

Dispatcher is a JavaScript class that determines which page is active, and runs page-specific initialization code based on that determination.

Our team first discovered the pattern within the GitLab repository. They created a JavaScript class (written in CoffeeScript) to:

  • Get a data attribute from the body tag
  • Run a switch statement for that data attribute
  • And run page-specific initialization code based on which page is active
page = $( 'body' ).data( 'page' )

switch page
  when 'home'  then new Home()
  when 'about' then new About()
  # ...

And, in this instance, Home and About are other CoffeeScript classes that run setup code for the given pages.

class Home

  constructor : ->
    # ...
class About

  constructor : ->
    # ...

In their version, though, the switch statement is set up to check for each page within the same Dispatcher class. We thought this would be perfect to abstract, so we pulled it out into Spellbook.

If you’d like to learn more about Spellbook, you can read my article on it.

Setting the data attribute

In order to pull the correct page, the data attribute has to be configured correctly for each page. I’ll illustrate how to do this in Ruby on Rails, but the concept should be similar in other environments.

I did not write this. This is a result of our amazing Rails developers at Code School. I take no credit here.

In app/helpers/application_helper.rb:

def body_data_attribute(options)
  @body_data_attributes ||= {}

  @body_data_attributes.merge!(options)
end

def body_data_attributes
  @body_data_attributes
end

def body_data_page
  path      = controller_path.split('/')
  namespace = path.first if path.second

  [namespace, controller_name, action_name].compact.join(':')
end

In app/layouts/application.html.haml:

- body_data_attribute 'page' => body_data_page

%body{ class: 'js-dispatcher', data: body_data_attributes }

This will generate:

<body class='js-dispatcher' data-page='users:show'>
  <!-- ... -->
</body>

And the data-page attribute will change based on the current page that’s being shown.

Structure

Let’s talk about the structure of the Dispatcher class.

We need configurable settings for:

  • Which element has the data attribute for the active page
  • The name of the data attribute on said element
  • An array of events for running the page-specific code

All code examples are written in CoffeeScript for brevity and alignment with Spellbook’s conventions. However, the same principles can be applied to vanilla JavaScript.

class @Spellbook.Classes.Dispatcher extends @Spellbook.Classes.Base

  # ...

Wait! What is the extends @Spellbook.Classes.Base bit? That’s something that my coworker, John D. Jameson, added to Spellbook. It abstracts the standard, boilerplate code into its own class.

class @Spellbook.Classes.Base

  _settings : {}

  constructor : ( @options ) -> @init?()

  _setDefaults : ( defaults ) ->
    @_settings = $.extend( defaults, @options )

This abstracts the setup code we typically write to:

  • Call an init method from the constructor
  • Merge defaults and passed-in options into a _settings object

Defaults

Within the Dispatcher class, we create our init method.

init : ->
  @_setDefaults
    $element : $( '.js-dispatcher' )
    dataAttr : 'dispatcher-page'
    events   : []

  @dispatch()
  • We call the @Spellbook.Classes.Base _setDefaults method for our default options to be merged with any overrides
  • We set a jQuery object with a class of .js-dispatcher as the element
  • We set the name of the data attribute on the element
  • We set an empty array of events
  • And we call a dispatch method

The events array is an array of objects, and each object contains two parts:

  1. A string representing the data attribute value
  2. A function to run the initialization code for that page
events = [
  { page : 'home', run : -> console.log( 'Hello, home page!') }
  # ...
]

Dispatch

Let’s look at the dispatch method in its entirety before we break it down.

dispatch : ( event = null ) ->
  page = @_getCurrentPage()

  return false unless page

  unless event?
    for event in @_settings.events
      switch event.page
        when page  then event.run()
        when 'all' then event.run()

      if event.match
        event.run() if page.match( event.match )

  else
    switch event.page
      when page  then event.run()
      when 'all' then event.run()

Now let’s look at it piece by piece.

dispatch : ( event = null ) ->
  # ...

We have a single argument of event, which has a default value of null, if nothing is passed in.

page = @_getCurrentPage()

We call a _getCurrentPage method to determine the current page. That method looks like this:

_getCurrentPage : ->
    @_settings.$element.data( @_settings.dataAttr )

All it’s doing is getting the data attribute value off of the element (generally the body) and returning it.

Back in our dispatch method:

return false unless page

If there isn’t a value for page (as returned by _getCurrentPage), we just want to return and stop execution of the dispatch method.

unless event?
  for event in @_settings.events
    switch event.page
      when page  then event.run()
      when 'all' then event.run()

    if event.match
      event.run() if page.match( event.match )
  • Unless event has been directly called (not null), we loop through the events in @_settings.events
  • We run a switch statement to check matches on the current page
  • If it matches, we execute the run function in the event object
  • If the event.page is 'all', run the function
if event.match
  event.run() if page.match( event.match )

This block is a special circumstance when we want to match multiple pages that share a similar string. For example, users:index, users:show, users:edit.

events : [
  { match : 'users', run : -> new Users() }
]

The new Users() class instantiation will be run on any page where the element’s data attribute contains the string 'users'.

Back in the dispatch method:

else
  switch event.page
    when page  then event.run()
    when 'all' then event.run()

If the event argument of the dispatch method is not null (meaning an argument of value was passed in), run the same switch as earlier.

This is for one-off instances to pass an event object directly to the dispatch method:

dispatcher = new Spellbook.Classes.Dispatcher()
dispatcher.dispatch
  page: 'home'
  run: -> Home.init()

And that covers all the functionality the Dispatcher class provides. Now, with it in place, we can instantiate the object and set up our calls:

new Spellbook.Classes.Dispatcher
  events: [
    {
      page : 'home',
      run  : -> new Home() # Run page-specific JS on the Home page.
    },
    {
      page : 'about',
      run  : -> new About() # Run page-specific JS on the About page.
    }
  ]

As you can see, the Dispatcher provides organization and structure to a codebase with multiple pages requiring setup code. Hopefully this can help you organize non-JS-framework codebases that rely on more traditional, “from scratch” JavaScript structure.