Rixiform Inc.

jQuery Wherefore Doth Thy Includes Go?

July 21st 2009

Whither Javascript

Recently I have been working on a Rails project with lots of Javascript include complexity ( Ashbury Music Hall ). Here’s the challenge:

  • Some larger libraries and plugins only need to appear on single pages (e.g. Thickbox, FCKEditor).
  • Some Ajax page renders depend on core libraries (e.g. jQuery) but shouldn’t be loaded more than once by layouts and partials
  • When Ajax page renders degrade to linked pages they may be in alternate layouts and include different Javascript libraries or have different load orders
  • Rails partials load before layouts and potentially include Javascript files specified in them before core libraries
  • Page and partial specific Javascript (e.g. myview/my_page.js ) presents even more complexity
  • Pages appear to load faster if Javascript is loaded on the bottom of the page

Oh what a tangled web we weave.

Thither

The solution I arrived at is to specify sectional order of libraries, accumulate them, deduplicate them, and include them at the end of the page request. This allows definition of core libraries at the top of a layout page for all partials and inclusion of librararies in plugins on any view or partial without duplicate loading or core library omissions. To do this I defined two Application Helpers as follows:

# Use accumulate_js to accumulate javascript paths throughout the application
# it deduplicates them and then they should be included by calling include_accumulated_js
# at the foot of the page.
def accumulate_js(key, path)
  @javascript_list ||= {} 
  @javascript_list[key] ||= []      

  if path.class == Array || path.class == String
    @javascript_list[key].<<(path)
    @javascript_list[key].flatten! # for some reason   was not working properly for arrays
    @javascript_list[key].uniq!
  else 
    raise ArgumentError
  end
  @javascript_list
end

# call accumulate_js first to use the default @javscript_list instance var
def include_accumulated_js(order_array, path_hash = @javascript_list)
  result = ""

  order_array.each do |key|
    next unless path_hash[key]
    result  = path_hash[key].inject("") do |result, path|
      result  = javascript_include_tag(path)
    end
  end
  result
end

Then wherever Javascript is called for a request (layout, view, or partial) the names of the libraries are accumulated:

accumulate_js(:core, ["jquery-1.3.2.min.js", "jquery-ui-1.7.1.custom.min.js"])
accumulate_js(:plugins, "beautytips/jquery.bt.min.js")
accumulate_js(:application, "application")

All load path strings from the public/javascripts directories are specified. The library order is maintained but additional loads are de-duplicated. Then all of the libraries are loaded at the foot of the document, ordered by type with a command like this:

include_accumulated_js([:core,:plugins,:application,:page,:partial])

For the specifics of what it does, here’s the rspec spec:

describe "javascript library acuumulation" do
  it "accumulates javascripts associated with a specific key" do
    accumulate_js(:core,["a","b"])
    accumulate_js(:core,"c").should == {:core => ["a","b","c"]}
  end

  it "can accumulate a list of javascripts used on a web page" do
    accumulate_js(:core, "library.js")
    accumulate_js(:core, "another/library.js").should == {:core => ["library.js", "another/library.js"]}
  end

  it "can take combinations of arrays and strings" do
    accumulate_js(:foo, ["lib1.js","lib2.js"])
    accumulate_js(:foo, "lib3.js")
    accumulate_js(:foo, ["lib4.js","lib5.js"]).should == {:foo => ["lib1.js","lib2.js","lib3.js","lib4.js","lib5.js"]}
  end

  it "raises an error if it is of an unknown type" do
    lambda do
     accumulate_js(:foo, 1)
    end.should raise_error(ArgumentError)
  end

  it "deduplicates and preserves order" do
    accumulate_js(:baz, ["a","b","a","c","b","c"]).should == {:baz, ["a","b","c"]}
  end
end

describe "including accumulated javascript" do
  before(:each) do
    accumulate_js(:foo, ["foo_1","foo_2"])
    accumulated = accumulate_js(:baz, ["baz_1","baz_2"])
    @result = include_accumulated_js([:baz, :foo], accumulated)
  end

  it "returns javascript includes" do
    @result.should =~ /script.*src.*javascripts\/baz_1.js.*type.*text\/javascript/ 
  end

  it "includes javascripts in the specified order" do
    @result.should =~ /baz_1.*baz_2.*foo_1.*foo_2/m
  end
  it "can handle nil values" do
    accumulated = accumulate_js(:foo, ["foo_1","foo_2"])
    lambda do
      include_accumulated_js([:baz, :not_in_there], accumulated)
    end.should_not raise_error
  end
end

Accolades

Marc Grabanski, excellent jQuery developer that he is, contributed his sage advice to this solution.

Epilogue

I might put this in a plugin and post to github, but just seems a little silly since there are only two helper methods. What think ye? If this blog post is good enough for you, then it’s good enough for me. Obviously, I’m busy enough that I’ve gone bonky and resorted to archaic english – but if you request a plugin or some other container I may be obliged.