Smart DOMReady

Concatenating and minifying JavaScript files into a single one is a pretty common doing among web developers. A tool like the Rails Asset pipeline makes it even more transparent as you can develop locally into separate files and they will automatically be merged in production. But then how do you efficiently execute page-specific scripts?

Personally, I’ve always been doing something like this:

# contact.coffee
class Contact
  constructor: ->
    console.log 'Contact script'

$contact = $('#contact')
new Contact if $contact.length
# home.coffee
class Home
  constructor: ->
    console.log 'Home script'

$home = $('#home')
new Home if $home.length

It has proven to be a good way, yet I wasn’t totally satisfied with that behaviour, especially under certain circumstances. What if you have another #contact somewhere — a kitten died by the way — ? It shouldn’t be the case, but it feels a little bit too hardcoded for me. Didn’t bother to find a solution as it wasn’t really broken until I started digging a bit deeper into ajax content loading.

The problem

Ajax navigation is amazing. When properly setup, you’ll reach a performance level that you wouldn’t think possible. But what happens when you load the content of a page and the said page requires some JavaScript action? JavaScript is not being re-executed because it has already been loaded and manually adding <script> tags into the loaded content really is a bad idea.

I know what you’re thinking. Mark my words; I will not go that way…

# main.coffee
$container = $('#container')

$('a.ajax').on 'click', (e) ->
  e.preventDefault()
  $.ajax
    url: this.href
    success: (data) ->
      $container.html data

      # if contact
      $contact = $(data).find '#contact'
      new Contact if $contact.length

      # if home
      $home = $(data).find '#home'
      new Home if $home.length

This little piece of code works well at loading content via ajax, but the success callback is a mess. Good luck with that as you scale your web app up.

The solution

Custom events galore!

# contact.coffee
class Contact
  constructor: ->
    console.log 'Contact script'

$(document).on 'contact:ready', (e) ->
  new Contact
# home.coffee
class Home
  constructor: ->
    console.log 'Home script'

$(document).on 'home:ready', (e) ->
  new Home

From this point, the only thing left to do to initialize your classes is to dispatch contact:ready or home:ready events anywhere in your code.

Thou shall not hardcode that event. —Etienne

Make it reachable anywhere in your dom. Personally I print it in a data- attribute on the body tag like this:

<!-- contact.html -->
<body data-view="contact">
<!-- index.html -->
<body data-view="home">

The data-view value is either a variable set by my controller and printed in my layout or hardcoded in my .html pages like the examples above. When in Rails, I like to print controller_name & action_name.

<%# index.html.erb %>
<body data-view="<%= "#{controller_name}-#{action_name}" %>">

And then, I can easily dispatch my custom view event:

# main.coffee
$(document).trigger "#{$('body').data('view')}:ready" # dispatch event on domready
# => "contact:ready"

There, an event is triggered and classes listening to that event will be initialized. No error will be thrown if the event isn’t being listened to, it will bubble its way up. Forever. But wait! It still doesn’t solve the view change via ajax problem. To fix that, I prepend a meta tag in my html only when my request is an XHR one. This meta tag contains my view name.

<!-- http://foo.com -->
<!DOCTYPE html>
<html>
  <head>
    <title>Foo.com</title>
  </head>
  <body data-view="home">
    <div id="container">
      <h1>Home</h1>
    </div>
  </body>
</html>
<!-- http://foo.com/contact -->
<!DOCTYPE html>
<html>
  <head>
    <title>Contact | Foo.com</title>
  </head>
  <body data-view="contact">
    <div id="container">
      <h1>Contact</h1>
    </div>
  </body>
</html>
<!-- xhr request to http://foo.com/contact -->
<meta name="data-view" content="contact">
<h1>Contact</h1>

Then on ajax success you can overwrite your body data- attribute and dispatch your ready event.

# main.coffee
dispatchViewReady = ->
  $(document).trigger "#{$body).data('view')}:ready"

dispatchViewReady() # dispatch event on domready

$body = $('body')
$container = $('#container')

$('a.ajax').on 'click', (e) ->
  e.preventDefault()
  $.ajax
    url: this.href
    success: (data) ->
      $container.html data
      $metaView = $container.find 'meta[name="data-view"]'

      if $metaView.length
        $body.data 'view', $metaView.attr('content')
        dispatchViewReady() # dispatch event after ajax request

Sky is the limit

You could have multiple values in your data-view and instead of dispatching the attribute value itself you could split it $body.data('view').split(' '), loop in that array and dispatch all the values. It would allow certain classes to listen to a more generic event if you need it in more than one page.

Happy custom events dispatching!