Globbing up javascript
posted by andy, Mon Mar 16 12:00:00 UTC 2009
The Motivation
When you first started with Ruby on Rails you were probably intrigued by the simplicity of adding AJAX to your applications by simply including a line like this:
<%= javascript_include_tag :defaults %>
It was short, concise, and invisible. All the Rails "magic" took over and you didn't need to worry much about which files were included because everything just worked.
Then you ran into jquery. Either because you did not want to be outdone by the "cool kids" or you really were motivated by the clean separation between view and code even in the browser you bought in. Then you discovered a few nifty plugins for paging and ajax forms. And there was a nifty auto-complete that you just had to have. Before you knew it, there was more javascript_include_tag logic in your application layout than anything else.
Compounding the Issue
For me it really started with the ExtJs library. Once we bought into it at work we quickly realized that we were now developing a client-server app over http and that our javascript views needed to be just as neatly organized as our html views would have otherwise been. We developed a scheme for organize our javascript views that looked remarkably like the Rails organization:
public
javascripts
my_company
components
...files...
views
area1
...files...
area2
...files...
The good news for us was that the code was pretty well organized. We know where to find a file that needs some love when things go awry. The bad news is that means we also need to write a lot of includes. Too many includes to keep up with really. There was also the not-so-minor inconvenience of working hard on a new view, refreshing the browser, and throwing javascript errors (or worse, silently failing) because the new file had not been included. Argh!
Glob to the rescue
This is where 'globbing' came in very handy. If you are not familiar with it, glob is a class method of Dir that searches your directory tree for files that match a pattern that you supply. The pattern for the file names is similar to but not quite the same as regex. As the documentation explains, it's closer to the shell's glob.
For this problem, though, that's all we need. The pattern matching matches literals as you might expect and it includes a pattern for recursing subdirectories ('**'). That fits perfectly well with our problem since what we want to do is find all the javascript files in subfolders of /public/javascripts.
Of course, there's a catch. glob begins it's work in the current directory and it returns file names relative to that directory. The javascript_include_tag, however, expects file references to be relative to /public/javascripts. That leaves you with two options: change working directories or alter the file paths. We chose the former path. The results looked like this:
<% javascript_include_tag
*(Dir.chdir( File.join(Rails.root, 'public', 'javascripts') {
Dir.glob("my_company/**/*.js").sort
}
) %>
Okay, so what's going on there??? Simple. First, we change directory (Dir.chdir) to start our directory globbing relative to the javascripts folder. By using the block-form of chdir we allow Ruby to reset the working directory for us when the block exits. Within the block we simply invoke Dir.glob asking for all the javascript files ("*.js") in subdirectories of the "my_company" directory. The great thing here is that glob really does recurse the subdirectories so we can add as many organizational layers as is necessary.
Don't miss the splat ('*') and parentheses around all that. It's pretty important. Dir.glob is going to return an array of strings describing file paths. javascript_include_tag, however, is still looking for a list of arguments. The splat comes in handy here, expanding the array.
The Net Result
The net result of all that is a single javascript_include_tag statement that includes all of the nested javascript files we've created. Even better, since they are included together in a single line when we can turn on javascript caching they will be compressed into one js bundle before being delivered to the client. Even better than that, we can drop in new files, reorganize existing files, and drop out that crummy javascript file we always hated without ever having to re-build the list of includes.