Clouds: Exploring Prototype, Scriptaculous and RESTful Rails – part 2

This article is the second of two articles about a hobby project in Rails I call Clouds. The concept of the application and the backend are described in part 1, and you can try a demo here. In this article we turn to the most advanced part of Clouds; it’s use of the javascript libraries Prototype and Scriptaculous to allow the user freely add, edit, move and delete topics while keeping the backend database up to date.

From .rhtml to .js
When a user enters the site a new cloud automatically gets created, and the user is redirected to the edit action of clouds_controller. The layout file of the clouds_controller includes required stylesheets as well as the standard javascripts (Prototype, Scriptaculous and application.js where the Cloud-specific code is placed). It also defines the common menu of the new and edit actions, while <%= yield :menu_items %> allows each of the actions to also define their own menu items through <% content_for :menu_items do %>.

Take a look at edit.rhtml. At the top is a javascript block which defines an object called Server, where information about which urls to use when making AJAX calls and the id of the cloud being edited are placed. Also, in the init javascript function a call the to Topic.topic_attach function is generated for each topic belonging directly to the current cloud (inherited topics cannot be edited). Topic.topic_attach sets up events that allows the user to move, edit and delete the topic. The javascript block also calls Cloud.init upon load, which sets up an event that allows the user to create a new cloud by double-clicking anywhere on the page.

The code inside <div id="cloud_body"> illustrates an important choice I had to make: Should I render existing topics and a template for new topics server-side, or should I output the topic details in - say - the call to Topic.topic_attach and generate the html for the existing topics client-side? While the latter might seem be DRY'er, I opted for the former solution because it has two immediate advantages: The browser renders the page faster from server-generated html, and I am able to control the html from partials instead of through messy DOM code.

Javascript structure and naming conventions
The core of Clouds; application.js, consists of four objects with a number of functions. Many functions are invoked by a certain user-generated event, while others serves as helper-functions to these. Only Cloud.init and Topic.topic_attach are invoked from the "outside". The event-handling functions uses .NET naming conventions; the name of source element, and underscore, and the name of the event being handled.

The Cloud object handles only events related directly to the overall "cloud", mainly the creation of new topics - which is passed on to the Topic object - and changing the title of the Cloud the AJAX'y way. The Topic object is where the real action is; creating, moving, editing and deleting topics, and I discuss the functions of this object in detail in the following section.

The Sync object demonstrates how to make AJAX calls to a RESTful Rails backend. The functions all uses the Ajax.Request class, but while topic_create_server and cloud_update_server are able to use request urls directly from the Server object, the functions for updating and destroying topics needs a url with the id of the specific topic being manipulated. I've chosen to let the container div element of each topic have an attribute called update_url, allowing the backend to stay in control of all urls instead of constructing them in javascript. Notice the contents of the parameters argument to each call to Ajax.Request: I use the topic[attribute_name] naming convention of Rails to be able to treat the AJAX call like any other request on the server-side, and I add the RESTful "cheating" variable _method the code>parameters argument when updating (_method=put) and deleting (_method=delete) a topic or a cloud.

Finally the Helper object provides various more or less "generic" helper methods. readAttribute and setAttribute are handy shorthand methods for manipulating html attributes cross-browser, while insertInput and removeInput are handy methods for transforming a div element with some text inside into an input field with that text, and back again.

Manipulating topics
When the user double-clicks anywhere on the page, the function topic_create_client in the Topic object are invoked with the position of the mouse as arguments. This function clones the template generated in clouds/edit.rhtml from the topics/_topic.rhtml partial, assigns a temporary client-side id to the new topic (the topic is not created server-side until the user has written some text and hit enter), positions the topic, adds it the to page, attaches events to it and makes it editable.

When the user hits enter in the input field of the new topic or removes focus from the field, the topic_blur function is invoked. This method transforms the input field back to a div with the new text inside, and makes sure the topic is either created, updated or deleted server-side, if the topic has no id, has one or the text is blank, respectively. When the user double-clicks the text of an existing topic topic_doubleclick once again makes the input field appear, but it also makes sure that the double-click does not bubble up to the parent cloud element by calling Event.stop(e). If the event wasn't stopped here, a new topic would be created on top the one being edited.

The function topic_set_id plays a vital role: As mentioned, new topics are assigned a temporary client-side id, but once the topic is created server-side, we need it to know the server-side id so it can be supplied when the user manipulates the topic. Sync.topic_create_server are invoked when a topic is to be created server-side, and by passing onComplete: Topic.topic_set_id to the request object, this function is called with information about the request (transport) and the data returned by the request (json). Take a look at line 44 of clouds_controller:

format.js   { render_json({ :client_id => params[:client_id], :server_id => @topic.id }.to_json) }

This results in the create action, when invoked with AJAX, returning a very nice JSON object with information about the topic's current id assigned by the client, and it's new, "real" id assigned by the server. The JSON data looks something like this, and by magic of Prototype, it is readily available to use when the function is invoked:

{client_id: "0", server_id: 176}

From that information topic_set_id are able to assign a new id to the topic, decided by the server.

Rounded corners and hovering
I also want to mention a few of the visual aspects of Clouds. First of all I really like to smooth boxes up a bit by giving them rounded corners, and while this as quite easy to achieve using four images, a bunch of div-elements and a considerable amount of CSS, I opted for a easier and more flexible solution. I use the javascript library NiftyCorners which, with just a single javascript call, applies rounded corners to all elements identified by a given selector. The thing that is so nifty about NiftyCorners is that I don't have to create any graphics myself, and I can continuously change the size and the color of the corners - again without having to make new graphics and change a bunch of CSS.

Another visual aspect of Clouds is that the box and the textarea for entering notes only appear when the user hovers his or hers mouse above a topic. Curiously I have opted not to use javascript to handle this, although I have experimented a lot with it. The problem with using javascript is that it is quite complex to figure out which topic that should have it's class changed, since the source element of the mouseover and mouseout event are rather unpredictable. Prototype and Scriptaculous are not able to abstract away the fact that different browsers chooses to fire these events differently, and this makes the CSS alternative very attractive.

Looking at the stylesheet you'll see that I've simply added a bunch of :hover classes which makes the "box" and the textarea for notes appear upon hovering. The only problem I encountered when implementing hovering this way was the corners added by NiftyCorners didn't appear and disappear as they should, but with a little from one of my favorite Firefox plugins - WebDeveloper - I easily found the classes of the added div elements that also needed to have hover classes added:

.rtop,.artop{visibility:hidden}
div.topic:hover .rtop, div.topic:hover .artop, div.topic.active .rtop, div.topic.active .artop {visibility:visible}

Another plugin without which I'd never been able to debug all the AJAX communications and client-side event handling is of course FireBug. If you are developing anything using AJAX or javascript in any other way, you seriously need to install and actively use this plugin. If you are developing web applications and uses Safari or Internet Explorer as your primary browser you seriously need to consider if you are as productive as you could be. Heh.

Conclusion
Back again from that little telling-off (don't know what went into me), I want to conclude this article series by underlining that Clouds in it is current state is not ready for production, as it does not function properly in Internet Explorer. If you decide to go ahead and use some of the code anyway, I would of course love to hear from you. Happy hacking.

Leave a Reply

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

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>