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 thebody
tag - Run a
switch
statement for thatdata
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 theconstructor
- 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:
- A string representing the
data
attribute value - 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 (notnull
), 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 theevent
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.