Rails on the Run

Rails experiments by Matt Aimonetti

Browsing Posts tagged javascript

I was recently asked by Rubyist friend (Josh Knowles) how I was organizing my Javascript files when using LowPro.

LowPro is the best solution for doing Unobtrusive Javascript using Prototype.

With the help of LowPro, you define behaviors that get triggered by the user. This is great, however, you’ll notice that some behaviors are used all over the place (a date chooser for instance) and some complicated behaviors only get used on very specific pages.

First things first, let’s look at the header in my application.html.erb file (located in app/views/layouts).
This is the default layout used by all my views, I rarely use more than 5 layouts per app and always use a default layout.

Please note that I’m using Rails 2.0 so some features you’ll see in my file won’t work in Rails 1.2.x. (if you want to know about all the new Rails sexiness, check on this awesome Peepcode PDF.

Here we go:

1
2
3
  <%= javascript_include_tag 'prototype', 'effects', :cache => true %>
  <%= javascript_include_tag 'lowpro', :cache => true %>
  <%= javascript_include_tag  'application', :cache => true %>

The first thing you might notice is that I don’t use


  <%= javascript_include_tag :defaults %>

The reason? I don’t want to load prototype.js, effects.js, controls.js, dragdrop.js, and application.js all at once. I almost never use drag’n'drop and seldom use Autocompleter and InPlaceEditor so why loading them in each and every single page of my apps? I’m not saying they are bad libraries, I’m just saying that in more than 80% of my page, I don’t use them, so they should not be in my default page load.

The second thing you might notice, I use :cache => true. Asset caching is a new feature in Rails 2.0 which combines related assets into a single file (works with css and js and only in production mode)
Note that the above code is untested, but everything should be loaded properly, otherwise, make sure proto gets loaded first, then lowpro, then application. (and you can probably create a one-liner)

All the default behaviors are defined in the application.js file, so they get loaded on all page. However to handle action specific behaviors, I use another Rails trick right in the header:


 <%= yield :javascript %>

Why? Very simple, I want to load some custom JS in the header depending on the action that is used. For instance, when a visitor goes to my fancy ajax photo editor, I want to load the content editor javascript right in the header where it belongs.

For that, I simply need to add the following to my view:

1
2
3
  <% content_for :javascript do %>
    <%= javascript_include_tag "photos_show" %>
  <% end %>

If content_for :javascript isn’t define, noting is yield in my header and therefore nothing is included but whenever I need, I can access my header directly from the view and insert javascript code in a very clean way.

photos_show.js is the javascript defining all the behaviors related to the photos controller and the show action. I usually only have few actions with a lot of custom behaviors so, these structure works well for me.

1
2
3
4
5
  javascripts
    \controller_action.js
    \controller_another_action.js
    \another_controller_action.js
    ...

However in the case of an app with a lot of custom behaviors, I recommend using the following structure:

1
2
3
4
5
6
  javascripts
    \ controller_name_
            \action_name_.js
      \another_controller_name
              \action_name_.js
  ...

That’s it folks, I’m not a javascript expert, and if you know better, don’t hesitate to leave me a comment. What I know for sure, is that since I started using LowPro and behavior driven with Prototype, I have much more fun. Adding a bit of structure is a simple way for me to keep my code clear and help other people who have to work with me. (more on that later)

If you’ve read my post on Ajax pagination you know that I’m a big fan of Dan Webb’s LowPro unobtrusive javascript library.

Doing Unobtrusive Javascript (UJS) is basically registering event handlers programmatically using CSS selectors to select the elements to register. In other words : keeping things separate and avoiding inline javascript.

If you’ve been using LowPro 0.4 and recently tried to upgrade to Prototype 1.6 you probably noticed that things don’t work as they used to.

The first thing you want to do, is to update to the latest version of Lowpro.

So what’s new in 0.5 trunk?

First off, you need to know that a lot of lowpro features were moved in Prototype 1.6 core :)

  • You now get a warning via firebug if you try to use Low Pro with a version of Prototype that its not designed to work with.
  • Alternative event system ripped out: uses core events
  • DOM method mixins ripped out: alternatives all in prototype
  • Event.onReady delegates to the new dom:loaded event. However this doesn’t fire immediately if the dom is already loaded like Event.onReady did. (might be patched to work as before)
  • DOMBuilder is staying but now is a thin shell around the new proto 1.6 Element stuff.
  • You can still return false from event handlers in addEvent and Behaviors to stop the event but now if you use Event.observe raw you don’t get this.
  • Behavior.create now works like Class.create in 1.6 so behaviors can have full inheritance:
1
2
3
4
5
6
7
8
9
10
11
12
  Basic = Behavior.create({
   onclick: function() {
    alert('woo');
   }
  });

  SuppedUp = Behavior.create({
   onclick: function($super) {
    alert('wee');
    $super();
   }
  });

Works really nicely.

  • core behaviors : Remote and Observed are now moved into the lowpro core (you don’t need to include the external files).

  • Event.addBehavior.reassignAfterAjax defaults to false. If you want re assign behaviors after an ajax call, you need to turn this option to true.

  • Event.addBehavior.reload(); added to reload/re assign behaviors. Very useful if you dynamically insert elements you want to observe!

  • new website has been set up and will contain documentation and tips – Full API docs coming soon. There’s also a dedicated google group.

Here is a quick example with real life code. (which could be refactored, I know)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Make sure the behaviors still work even after navigating to another page using the ajax navigation.
Event.addBehavior.reassignAfterAjax = true;

// Behaviors
Event.addBehavior({
  // Pagination links  
  'div.pagination a' : Remote.Link,

  // Reset the list when a user clicks on cancel.
  'a.cancel_button:click' : function() {
    $('list_of_things').update("");
  },

  // carousel navigation prev
  'a#carousel_prev:click' : function() {
    moveCarousel('prev'); return false;
  },

  // carousel navigation next
  'a#carousel_next:click' : function() {
    moveCarousel('next'); return false;
  },

  'div.panel_pic:click' : function() {
    removePanelPic(this);
  },

  'div.photo_from_row img:click' : function() {
    // Get the div holding the pic and use it as a target
    var target = this.up();
    addPicToPanel(target);
    new Effect.Highlight(target);
  }
});

function addPicToPanel(target){
  new Insertion.Bottom('control_panel_photos', "<div id='edit_"+ target.id +"' class='panel_pic'><img class='panel_pic' src='" +  target.immediateDescendants()[0].src + "'/></div>");
  // Reload the behaviors so the new inserted pic can be monitored 
  // and the 'div.panel_pic:click' behavior can be triggered
  Event.addBehavior.reload();
}
  

LowPro is a great way of keeping your code really clean and your views very accessible.

If you are interested in knowing more about UJS, come to our SDruby group meeting on Dec 6 @ 7:30pm (directions). And if you don’t care about UJS, come later to hear about Facebook API. Don’t forger to bring your questions for our first Rails roundtable.

updated Nov 26 to reflect the recent low pro changes. (please use low pro 0.5 and Prototype 1.6)

Recently one of my client asked me to add ‘ajax’ pagination to his application. His site already had a very nice pagination using the excellent will_paginate from Mislav and the guys(PJ & Chris) from err the blog but since my client had a special need where he had to have Ajax.

It took me virtually no time to convert the standard pagination into an Ajax navigation while still degrading gracefully.(it works even without Javascript)

I really enjoy using will_paginate, it’s very well written and the authors keep up with the bugs and new features.

Start by installing will_paginate:

ruby script/plugin install svn://errtheblog.com/svn/plugins/will_paginate

Then go watch the Railcast screencast about will paginate.

Once you have your pagination working, we will do some ‘progressive enhancement‘.

What we want is to add a behavior to the pagination link. The behavior would make the same call than the normal link but via an ajax(Javascript) call.

Add lowpro

To do that, you simply need to add the excellent ‘lowpro’ Prototype extension from Dan Webb

You can get the files directly from the lowpro’s repository.

Add lowpro.js to your public/javascript folder.

Don’t forget to include the javascript in your page. (

1
2

  <%= javascript_include_tag 'lowpro' %>

Create a behavior

Now open your application.js file (or whichever Javascript file you’re using) and add the following:

1
2
3
4
5

  Event.addBehavior.reassignAfterAjax = true;
  Event.addBehavior({
    'div.pagination a' : Remote.Link
  })

Refresh your cache, reload your page, and test the link. It will probably look like it doesn’t do anything but if you are using firebug or if you are checking your logs, you’ll notice something happened. The problem is that we didn’t tell our action to send an Ajax response so we get the html full page all over again.

Setup a response for javascript requests

Got to your action handling the pagination and let’s setup a response for Javascript:

1
2
3
4
5
6
7
8
9
10
11
12

  def index
  @photos = Photo.paginate(:all, :conditions => ["photos.user_id = ?", current_user.id], :page => params[:page])
    respond_to do |format|
      format.html # index.html.erb
      format.js do
        render :update do |page|
          page.replace_html 'photos', :partial => "photos"
        end
      end
    end
  end

Perfect! Now when a visitor clicks on my pagination links, only the photos are paginated, the rest of the page stays the same. Note that my navigation bar is inside the partial so it gets ‘updated’ after a visitor clicks on any pagination link.

Read more and convert to UJS (unobtrusive javascript)

Read more about UJS and think about replacing all your nasty inline javascript snippets by pretty behaviors :) (Think about stopping using the obtrusive rails helpers)