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.
[:haiku, :ruby] do |type| formalized poetry out of Japan; end
class OpenSource has_and_belongs_to_many :developers; end
WebFramework.find(:best) from the hand of DHH Rails belongs_to :all
This article is part 1 in a series of articles about an “open source project” I’ve been working on. I’ve put it in quotes because it’s not a real open source project in the sense that many people has worked together – it’s a hobby project with me as the only developer, and now I’m choosing to make its source public because I feel like contributing something back to the community. I call this project Clouds, and you can try it right now, right here.
The idea behind Clouds
Basically, Clouds is some kind of generic notice board. I wrote Clouds because I wanted to use it make a wish list application with a different approach, but I also wanted to make it generic enough to be able to reuse it in other applications. Perhaps even transform it into an engine to make it easier for other people to use it in their applications.
Upon entering the Clouds for the first time, a blank cloud is created automatically. The user is presented to an entirely white page (except for discreet menu bar at the top) and are able to add topics anywhere on the page by double-clicking and entering a text in the appearing box. The user can also add notes to topics, move topics around and even delete them again.
To further enhance the users ability to express themselves freely in their clouds, one could add features such as choosing the color, size and font of topics, but that is not essential to the real function of the cloud: To let users build lists in two dimensions instead of just one (typically vertical). In its bare form, Clouds can be used in many ways and taken in many directions, which is why I wanted people to be able to extend the application to suit specific needs, like a wish list, a mind map or …
As I had this basic functionality, I felt Clouds needed a bit more functionality to be usable to anyone, and yet I still wanted it to be basic and generic. So added the ability to protect editing and/or viewing with a password (not a registration and login – too much work for the user), and the ability to extend any list into a new list. The extension feature would be vital in, say, the wish list application, because it would allow people to extend a wish list and then coordinate who is buying which presents through their new list without the gift receiver knowing, while still maintaining the link to the original wish list.
I still have several ideas for things I want to add or improve, i.e. something as basic as getting the whole thing to work properly in Internet Explorer (no, Prototype and Scriptacolous cannot save you from all the headaches of IE), but also the fancy notion of synchronizing lists live between users, so multiple users working on the same cloud will see each others changes immediately.
The rest of this article and the one(s) to follow will dip into the technical aspects of Clouds, especially highlighting the parts where I’ve run into trouble. You can review and download the source code right here, but remember to check the license if you want to reuse any substantial parts of the code.
Creating a RESTful Rails application
What does it mean for a Rails application to be RESTful? To me it means to use the scaffold_resource as a basis for most entities in the application, and to strive to keep the application normalized by having only the CRUD actions generated by scaffold_resource in the controllers. Of course, these actions will also give the application a RESTful web service API for free, but that’s just an added bonus to me.
This article is a not a tutorial, but I do want to walk you through creating a RESTful Rails application as it will help you to build cleaner applications, also when it comes to database structure. To build Clouds, my first task was to generate the two central entities: The Cloud and the Topic.
./script/generate scaffold_resource Cloud title:string, created_on:datetime, updated_on:datetime, password:string, settings:integer, parent_id:integer ./script/generate scaffold_resource Topic cloud_id:integer, text:string, notes:text, posx:integer, posy:integer
The scaffold_resource generates everything you need to get started with your new entity: Model, controller, views, migration, tests and route. No need to worry about when to use singular or plural names, lowercase or uppercase, underscores or camel case. All files are created and named according to current Rails conventions. All you need to do is run rake db:migrate and restart the server.
Take a look at config/routes.rb. The generator has added map.resources :topics and map.resources :clouds. This enables the user to browse your Rails application using the nice RESTful urls, meaning that the same urls are reused for different actions, depending on the HTTP method used. For instance, if you do a GET (opening the page in your browser) on the url /clouds/1 the controllers show action is invoked, while a PUT on the same url will invoke the update action.
As you can probably read out of the scaffold_resource statements, a Cloud has a title, a password, some settings and a parent (plus two timestamps automatically updated by Rails), but for the basic functionality of Clouds, none of these attributes are required. A Cloud is nothing but a collection of Topics, and a topic has a text (short “headline” visible at all time), notes (longer text only visible on mouse over) and a position on the screen.
Take a look at the index action in the clouds_controller. After determining if the user already has a cloud to edit (by reading a cookie), the action utilizes either the edit_cloud_path(cloud) or the new_cloud_path methods to redirect the user. There is nothing new about named routes in Rails, but as a part of using RESTful Rails we get these named routes for free. Another interesting point in the clouds_controller is the calls to an authorize method located in application_controller, which is used to check if the user is required to enter a password to either view or edit the cloud. This method is invoked with a symbol as its third parameter, which is used to ask the cloud if either the show or edit action are protected (cloud.send(method)). I think that’s quite clever use of Ruby’s fantastic reflection abilities.
Using bitflags
A last thing I want to highlight in this first part in my article series about Clouds, is my use of bitflags. I believe the term bitflags is used and understood differently by different people, but in this context I talk about storing multiple true/false values in a single integer. In Clouds I needed to store some settings per cloud, specifically if the cloud’s show or edit actions where password protected. I could have added two fields to the clouds table called protect_edit and protect_show with the data type boolean (bit), but what if I needed further settings later on? Should I just continue adding more fields to my clouds table?
No. I believe bitflags is a cleaner solution for this, and to my enjoyment I was quickly able to find a small plugin providing me with this exact functionality called has_flags. In a nutshell, bitflags uses the power of 2 to combine multiple numbers into a single integer, while still being able to tell exactly which power of 2′s are in the integer. So the bitflag 22 are the numbers 2, 4 and 16 combined, while 8 is not present in that bitflag.
Take a look at the cloud model:
# Utilizes the has_flags plugin for easy bitflag usage has_flags [ :protect_edit, :protect_show ], [ :column => 'settings' ]
This single line of code automagically adds protect_edit and protect_show as boolean attributes on the cloud model, while [ :column => 'settings' ] tells the plugin to store the bitflags in a field called settings. This is an easy, elegant and first and foremost “Railsy” way of using bitflags in your models.
In part 2 of this article series on Clouds we jump into the core part of the application: The javascript.
Hello, I'm Casper Fabricius. I have developed for the web for 10 years, and have been enjoying Ruby on Rails for the past 5.
My experience covers communities, shopping solutions, multi-language sites, heavy back-end lifting and a wide selection of more traditional websites. I like to integrate Ruby with Java and .NET through JRuby and IronRuby when it makes sense. I am passionate about test- and behavior-driven development, but at the same time I am pragmatic and believe in getting things done.
I live in Copenhagen, Denmark, where I work for a fantastic company: Podio. I do not currently take on freelance assignments.