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.
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.
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
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!
Give Your Everyday Typing Some Class
Good Ol’ Shell Alias To The Rescue