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
dataattribute from thebodytag - Run a
switchstatement for thatdataattribute - 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
dataattribute for the active page - The name of the
dataattribute 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
initmethod from theconstructor - Merge defaults and passed-in options into a
_settingsobject
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_setDefaultsmethod for our default options to be merged with any overrides - We set a jQuery object with a class of
.js-dispatcheras the element - We set the name of the
dataattribute on the element - We set an empty array of
events - And we call a
dispatchmethod
The events array is an array of objects, and each object contains two parts:
- A string representing the
dataattribute 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
eventhas been directly called (notnull), we loop through the events in@_settings.events - We run a
switchstatement to check matches on the current page - If it matches, we execute the
runfunction in theeventobject - If the
event.pageis'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.