Dear Railsists, Please Don’t be Obtrusive

obtrusive_or_not.png
Update: thanks to Jon Wood aka jellybob, a prototype demonstration has been added, which is even better than my original jQuery btw as it degrades gracefully. Check it out in the ‘prototype-unobtrusive’ directory.

I am guessing 9 out of 10 of you reading the title is prepared for yet-another Rails drama on some obtrusive community members, and because everyone is tired of Rails dramas, I am risking that some of you won’t care to read the article – but I couldn’t resist :-). Actually I’d like to talk about usage of (un)obtrusive Javascript – why is it a bad idea to be obtrusive, especially given that (as you will learn from the article) writing unobtrusive Javascript is not harder, and you get the warm, fuzzy feeling of writing nice and clean code!

The Drill

To demonstrate the differences, I’ll lead you through the creation of a quick AJAXy shout wall both the default/standard (and obtrusive) way, then do the same with unobtrusive Javascript to show you that contrary to the popular belief, you don’t need to memorize the “Tome of Javascript Black Magick Tricks” by heart, use obscure libraries or special coding techniques to achieve clean, unobtrusive code. The shout wall is simply a form for posting a new message, and a list of messages below it, like so:


shout_wall.png


(You can check out the code used in this post from it’s github repository).

The Standard Way

Note: If you’d like to follow along, please use the provided pastie links – do not try to cut & paste multiple lines from the page (single lines are OK), as it will be b0rk3d.

  1. Creating a new Rails application

    rails obtrusive-shout-wall
      
  2. Get into the Rails dir

    cd obtrusive-shout-wall
      
  3. Generate the resource message

      
    script/generate resource message
      
  4. Add this the following to the generated migration (some_timestamp_create_messages (Get it from pastie):

    t.string :author
    t.text :message	
    
  5. Run the migrations:

    rake db:migrate
    
  6. Because we want to view the messages in reverse order (newest one first), we add a default scope to the Message model (in message.rb):

        default_scope :order => 'created_at DESC'
    
  7. Create the application layout – create a new file in app/views/layouts called application.html.erb, and fill it with the following content (Get it from pastie):

    
      
        <%= stylesheet_link_tag "application" %>
    		<%= javascript_include_tag :defaults %>		
      
    	
        <%= yield %>
    	
    
    
  8. Create a file application.css and drop it into public/stylesheets. Add the following content (Get it from pastie):

    body {
    	background-color:#FFFFFF;
    	color:#333333;
    	font-family:"Lucida Grande",verdana,arial,helvetica,sans-serif;
    	margin:0 auto;
    	padding:0;
    	text-align:center;
    	width:960px;
    }
    
    #messages {
    	text-align: left;
    	margin-left: 80px;
    	margin-top: 50px;
    }
    
    #message-form {
    	text-align: left;
    }
    
    #message-form dl {
    	margin:10px 0 0 80px;
    }
    
    #message-form dd {
    color:#666666;
    font-size:11px;
    line-height:24px;
    margin:0 0 5px 80px;
    }
    
    #message-form dt {
    	float:left;
    	font-size:14px;
    	line-height:24px;
    	width:80px;
      text-align: left;	
    }
    
    #author {
      margin-right: 640px;
    }
    
    #message {
      width: 600px;
    	height: 200px;
      margin-right: 194px;
    }
    
    .message {
      margin-bottom: 20px;
    }
    
    .first_row {
      padding-bottom: 10px;
    }
    
    .message-meta {
    	font-size: 12px;
    }
    
    .author {
      color: #FF5050;
    	font-weight: bold;
    }
    
    .new-message-label {
      text-align: left;
      padding-top: 30px;
      margin-left: 80px;
    }
    
    #submit-button {
      float : right;
      margin-right: 195px;
      margin-top: 10px;
    }
    
  9. Create a new action, index in MessagesController (Get it from pastie):

    def index
      @messages = Message.all    
    end
    
  10. This goes into app/views/messages/index.html.erb (Get it from pastie):

    Enter new message!

    <% remote_form_for :message, :html => {:id => "message-form"} do |form| %>
    Author:
    <%= text_field_tag 'author' %>
    Message:
    <%= text_area_tag 'message' %>
    <%= submit_tag "Submit!", :id => "submit-button"%> <% end %>
    <%= render :partial => 'message', :collection => @messages %>

    We are showing the form for the messages and list the already exiting messages below the list.
    Note that we are using the _remote_form_for_ Rails helper to create an AJAXy form. This is already obtrusive, since if you observe the generated HTML, you will see that the form has an onsubmit parameter with some horribly looking code attached to it.:


    Obtrusive helper.png


    Sure, you can go ‘meh’ all the way, but slinging Javascript code all over the place is just as bad idea as writing inline CSS (or even worse, using HTML code for styling) or putting Rails code into views. It will work without any problems – but it’s just not the right way of doing things, especially if your code is going to hit a certain size.
  11. You probably noticed that we are rendering a message as a partial – so create a partial file app/views/messages/_message.html.erb with the following content (Get it from pastie):

    on <%= message.created_at.to_formatted_s(:long_ordinal) %>, <%= message.author %> said:
    <%= message.message %>
  12. We need a ‘create’ action in MessagesController in order to process the form submission (Get it from pastie):

    def create
      @message = Message.create(:author => params[:author], :message => params[:message])
    end
    
  13. And obviously we’ll need to render something to respond to the create action. Using the standard Rails way, RJS, we might come up with something like this (in app/views/messages/create.js.rjs – Get it from pastie):

    page.insert_html :top, "messages", :partial => 'message', :object => @message
    page.visual_effect  :highlight, "message-#{@message.id}"
    

    Here we insert the “messages” partial, using the just created @message, and throw a splash of yellow fade into the mix for good measure. Easy peasy.

  14. We are done! Fire up script/server, hit localhost:3000/messages and voila!

The Good Way

Here I am presenting only the steps that are different from the above – i.e. if step 3 is skipped, use the one from above.

  1. Creating a new Rails application

      rails unobtrusive-shout-wall
      
  2. Get into the Rails dir

    cd unobtrusive-shout-wall
      
  3. Same as above
  4. Same as above
  5. Same as above
  6. Same as above
  7. Since we are going to use jQuery (unobtrusiveness is *not* a property of jQuery, you can be just as unobtrusive with Prorotype – but I switched to jQuery just before learning how, and now I am lazy to go back check out how in the ‘prototype unobtrusive’ directory in the github repository), you have to download jQuery with some basic effects, as well as an AJAX form handling library (still from the directory unobtrusive-shout-wall – Get it from pastie):
  8. curl http://jqueryjs.googlecode.com/files/jquery-1.3.1.min.js > public/javascripts/jquery.js
    curl http://www.malsup.com/jquery/form/jquery.form.js?2.28 > public/javascripts/jquery-form.js
    curl http://view.jquery.com/tags/ui/latest/ui/effects.core.js > public/javascripts/effects.core.js
    curl http://view.jquery.com/tags/ui/latest/ui/effects.highlight.js > public/javascripts/effects.highlight.js
    

    and replace

    <%= javascript_include_tag :defaults %>
    

    with

    <%= javascript_include_tag 'jquery' %>		
    <%= javascript_include_tag 'jquery-form' %>
    <%= javascript_include_tag 'application' %>
    <%= javascript_include_tag 'effects.core' %>
    <%= javascript_include_tag 'effects.highlight' %>		
    

    in the layout file.

  9. Same as above
  10. Same as above
  11. Same as above – just delete “remote” from the name of the helper, i.e. use a standard Rails view helper, form_for
  12. Same as above
  13. Since we are not relying on Rails to do the rendering for as via a template file, we return the html chunk that we will render from Javascipt. So your create action should look like (Get it from pastie):
    def create
      @message = Message.create(:author => params[:author], :message => params[:message])
      render :partial => 'message', :object => @message
    end
    
  14. Now comes the fundamentally different part – instead of using RJS to respond to the create action, we move all our code to application.js (Get if from pastie):
    $(document).ready(function() {      
      $("#message-form").ajaxForm({success: handleNewMessage});
    
      function handleNewMessage(response, statusText) {
        $("#messages").prepend(response).effect("highlight", {}, 1500);
      }    
    });
    

    I don’t think so that this code is particularly more complicated or hard to understand that the RJS one. Everything is inside the ready() function, which means that it’s only run once the document is properly loaded. Then we declare that “#message-form” is an AJAX form, and that upon successful submission, the handleNewMessage() method should be called. And if that happens, we add the response (which is the return value of the “create” action) to the “#messages” div, just as we did in RJS. Then we apply the yellow fade! w00t!

  15. Same as above

(You can check out the code used in this post from it’s github repository).

Conclusion

As you can see, the only real difference between the obtrusive and non-obtrusive version is in the last 2 points (downloading and including the jQuery header files can be easily solved with Rails templates): instead of leaving the rendering part to Rails, we return the response as a string and dynamically insert it from jQuery. With about the same effort, we kept all the Javascript code in application.js, which is much cleaner this way (you can open up 1 file and check out all the JS/AJAX behavior in one place), especially after introducing a lot of Javascript functionality into your code – in other words, for the same amount of work we got something much better. Please try to keep this in mind when you are working with Javascript and Rails the next time – believe me, it can save you from a lot of pain!

82 thoughts on “Dear Railsists, Please Don’t be Obtrusive

  1. I tend to stay away from javascript for my Rails apps, but just because I don’t like how rjs works. After having read this though, I think I might now start adding some jquery magic to my apps.

    Good work!

  2. Sweet post, Peter. I already knew about unobtrusive JS, but had a bit of trouble integrating it into Rails. This is a nice explanation.

  3. You should check up on the stuff being done for rails 3 with html5 data-* attributes and unobtrusive JS. It’s a huge step in the right direction, and I’m guessing you’d approve.

  4. Thanks guys – I actually expected resistance rather than approval 😉

    @Matt Thanks for the pointer – will check it out. I heard that a big goal of Rails 3 is Javascript framework agnosticism and clean Javascript but didn’t know about data-* attributes.

    Can’t wait for Rails 3 – if half of the things will work out as promised, I’ll be already very happy 🙂

  5. The real problem with unobstrusive javascript emerges in big proyects. When yo have tens of views with javascript, application.js can be a mess. If you split the file application.js in several files, the problem is still there: when you want to modify a view and the javascript associated to it, you have to browse and search in application.js (or the splitted files) to find the wich javascript function you have to modify.

    Using obstrusive javascript is dirty, but you have all the code in the same place. I prefer UNobstrubsive javascript, but we need some conventions to order the code, not only application.js.

  6. “Using obstrusive javascript is dirty, but you have all the code in the same place”

    What do you mean? It’s exactly the opposite: you have it all over the place in views, RJS files, maybe inline RJS in action responses and even application.js. How is that one place?

    As for the big application.js file – sure, split it up, if you are using meaningful names and separation that makes sense (you could even create a directory structure similar to views/* inside javasripts or something) I think it’s OK.

    Obviously, the more files, the bigger mess – but I doubt this is specific to Javacript (obtrusive or not).

  7. I mean the same place as view code, all together view and javascript.

    I agree using a directory similar to views/* could be the solution, but we need a convention telling to do this way, because every people do it in a different manner. Anyway, great post 🙂

  8. @Jim: Lol, was thinking about that, but it seems like an overkill to waste a passenger for this 😉

    @Francisco: Yeah, but for a large project, the ‘view code’ is a pretty all-over-the-place thing too 😉

  9. This is how I do it with PHP:

    remoteformfor ..

    =>

    automatically generates and attaches javascript file with

    $(“.remote_01”).submit(function(){ … });

    🙂

  10. I didn’t see the explanation for why obtrusive js is bad, beyond: “It will work without any problems – but it’s just not the right way of doing things, especially if your code is going to hit a certain size.” Saying something is bad because it isn’t the right way (without explaining why it’s not the right way) isn’t an argument – it’s an assertion.

    What are the actual, real-world disadvantages? IOW, what specific problems will I avoid by being unobtrusive?

    I’m not saying there aren’t any. I’d just like to hear what they are.

  11. Rails seriously needs to refactor its RJS and ajax helpers and become more jQuery friendly. I try to use the RJS and ajax helpers as little as possible because I feel a little dirty every time I do. I’m hoping Rails 3 will adopt unobtrusive javascript and maybe give a jQuery option.

  12. Hey, just a quick problem. Your code is not what we can call unobtrusive as when you disable your javascript, you code will now work as expected.

    You need to have 2 formats in respond_to in order to achieve that. One responding to the JS another one responding to the HTML.

  13. Pingback: Double Shot #458 « A Fresh Cup

  14. I’ve forked the git repository for this so I can add a Prototype implementation, largely because I wanted something to do for half an hour before going home!

    As well as implementing it in Prototype, it also demonstrates using respond_to in the create action so that users without Javascript turned on will still be able to use the wall.

  15. ewww, that is nasty. Surely there is a better way to do this?

    How do i know if a form is an ajax form? Do I have to look in the application.js to see if a function has been attached to the form class? are you kidding me?

  16. “when you want to modify a view and the javascript associated to it, you have to browse and search in application.js”

    I’m just learning to do Js the right way myself, but our front-end guys tend to use named yields to organize our js. Classes that are specific to a particular view go in a named yield, and the rest go in a global js file. That plus strict namespacing tends to keep things a bit more organized in bigger projects — or so they tell me.

    Also, for those who are new to javascript (like me), I would recommend looking at the module pattern (http://yuiblog.com/blog/2007/06/12/module-pattern/). I can’t really explain why, but for me using this patten to structure your javascript really helped me wrap my mind around it for the first time.

  17. Give me ONE good reason why obtrusive JavaScript is bad other than “it’s ugly when I click view source”. It’s much easier to code and maintain, and since it’s contained in helper methods it’s DRY and if there’s ever a bug or upgrade I don’t have to change any of my application code because updating rails will do it for me. Remember, it absolutely doesn’t matter what the HTML looks like that’s sent over the wire, and what DOES matter is the source code that programmers are actually writing. Obtrusive JavaScript is vastly superior in every way, shape, and form to some hackish un-obtrusive method. Never forget that “theory” will only get you so far, what really matters is having applications that work and code that is easy to maintain.

  18. @Philip: its important to separate Behaviour From Structure.Few reasons why you should be as unobtrusive as possible:

    • it gives you the ability to make incremental modifications to the HTML markup structure or the behaviour (ie. Javascript ) code independently without having to modify the other.
    • behaviour code can easily be shared across multiple pages
    • better user experience ( if javascript is turned off )
    • it makes it easier for teams of web designers (who handle HTML and CSS) and Javascript developers to work together.
  19. Pingback: Ennuyer.net » Blog Archive » 2009-05-25- Today’s Ruby/Rails Reading

  20. @Francisco J. Yáñez: The way I do it, instead of writing js at application.js, I do the content_for trick for my javascripts at the view itself. In a rails project, I put a yield block especially for custom javascripts at my main layout. So whenever I write a javascript, I just write it at the associated view. So now, I won’t worry if this form is ajaxify or not because I could see them at the view code.

  21. oh hai guise! Wow, this is an old rotten corpse you’ve brought back to daylight!

    Unobtrusive Javascript might be cool for that small trendy little shopping-list application that you want to access from your iPhone, Lynx browser, web scraper and Tamagotchi. But trust me on this, as soon as your application grows big it will be unmaintainable. Last 1,5 years or so I’ve been in a team building a huge application in Rails that relies heavily on AJAX. I’m certain that it would been a real suffering without the Prototype helpers provided by Rails. Sure, more complex javascript we’ve had to move out to a javascript file. But for all those zillions of forms and minor page updates we’ll find the code where we expect and don’t have to wade through miles of .js files to find some attached method.

    Rails Prototype helpers might give you ugly HTML source output, but I’m a pragmatist, I don’t really care about that.

    And putting the response in application.js is a violation against the MVC-structure! It belongs there just as well as your HTML, XML or JSON response! I’m also pretty certain that those who dismiss RJS templates have not yet understood it’s awesomeness. They are small, powerful and very easy to maintain. Each response has it’s own place. If you have more than four-five controllers and do a little more than slide a div to the right, putting all your response in hand coded methods in application.js — to me that sound like a recipe for a great mess!

    (uhm, sorry for the harsh tone. It was not my intent.)

  22. I agree fundamentally here, but I think the message is muddied a bit by the prototype/jquery dichotomy, and also by the inclusion of so many inconsequential setup details. If you were to strip down the examples to just the RJS version vs an equivalent pure javascript implementation it would be easier to compare.

    I have to admit that in the early days of Rails I was really enamoured with the Prototype integration and (when it was release later) RJS. In retrospect I think this is simply because Javascript seemed difficult and error-prone, so better to encapsulate as much logic into well-tested Rails components. However since then I’ve actually taken the time to learn Javascript properly (Javascript: The Good Parts by Douglas Crockford should be mandatory reading for any web developer period). The fact is that the Prototype helpers in Rails give you some handy defaults, but the abstraction leaks horribly as soon as you need to customize anything. You end up having to mentally translate the Ruby to Javascript and trace the AJAX flow through a bunch of different files anyway which negates any benefit you got in the first place.

    If instead you embrace Javascript and learn how to properly engineer Javascript code you will have a lot more freedom of how to construct the interface between your Javascript and your Ruby. I recently converted an AJAX function from RJS to pure Javascript, and was able to realize incredible benefits to front-end performance, memory usage, clarity, integration to other AJAX features. I replaced 200 lines of Ruby with 50 lines of Javascript. That’s not to say RJS doesn’t have its place, but unless you really know Javascript you will be tempted to misuse it.

    Having significant Javascript code in templates, strings, or helpers is a definite smell.

  23. Как обычно, написавший кошерно накреативил!

  24. В принципе, вебмастер оригинально накропал.

  25. Не поспоришь, отменная новость

  26. Dear Peter,

    I share the thoughts and concerns of creating views that are not obstructed by any code, especially JavaScript, at all. I have been applying the techniques you describe here for a long while and can give my testimony that they get their job done!

    Now the only problem I am seeing in this code is the lack of flexibility towards non-JavaScript enabled browsers. What if someone tries to reach your code without JavaScript? Let’s put it into a more realistic world: what if you are specifying an API?

    In my opinion your code could use some header detection to determine whether the client wants a JSON answer (e.g. if it is an AJAX client), html (e.g. a simple post done by a form that informs your data was submitted properly), etc. Luckily, and as you may already be aware of, this could be achieved by using the respond_to provided in rails as follows:

    def create
    @message = Message.create(:author => params[:author], :message => params[:message])
    respond_to do |format|
    format.json {render :partial => ‘message’, :object => @message}
    format.html
    end
    end

    Even though I particularly dislike sending raw html through a JSON request -I think it should be a proper JSON object- this is a better approach to handle both kinds of requests nicely.

    I understand the purpose of the example was to prove how unobstrusive JavaScript coding can be done. However, we should not forget that a similar concept should be bore in mind when defining the endpoints of our applications.

    Regards,
    Darío

  27. Должен признать, автор уныло отжег.

  28. If you do in such risky situations every time thee monsters visit Florida–but kids
    books not on lions! Is this you too. Listen to what Jauja is meant to be very angry.

    Also visit mmy web site – ebook (Kevin)

Leave a Reply

Your email address will not be published. Required fields are marked *