Monday, September 13, 2010

Extending the Drawing Program with AJAX

Recently I wrote a simple drawing program.  Now I want to be able to save what I drew and then reload it.  I want the data saved on a server somewhere, and I want the act of saving and loading to be done via AJAX. This is the story of how I accomplished this.

Decisions

Ruby on Rails vs. Google App Engine

My first decision was what to use as my server platform.  I've had some experience with Java Servlets, ASP.NET, PHP, and Ruby on Rails.  Ruby on Rails is definitely the coolest of these.  However, given that I am a Google fan-boy, I at least considered the Google App Engine which also looks pretty cool.  However, laziness won out and I stuck to Rails because I know it and I am already tackling a bunch of new things on this project.  Plus I want to know Rails better.  At some point I'll have to come up with a project to try out with Google, and see if I want to change my mind.

JQuery vs. Prototype

As I said above, I want to accomplish the interaction between the client and the server via AJAX.  Well, actually I want to pass JSON messages, not XML, but I think they still call it AJAX.  Anyway, while I could do this myself, it seems to make sense to take advantage of existing libraries.  While there are a ton of JavaScript libraries, the two that seem to be the most popular are jQuery and Prototype.  Prototype is distributed with Rails and so is the natural choice.  But supposedly jQuery is much more lightweight and efficient, so I am going to go with jQuery.

I realize that neither of the choices above seem to be very well researched.  Well, this is just a toy project - if I spend forever researching all the options then I'll never get anything done.  Just by doing something, hopefully I'll be better informed in the future.

What I Did

First I created a rails project:
rails drawing
and then I imported the HTML and JavaScript that I wrote into the public directory of the project.

Of course I needed a database to store the maps in.  I decided to use SQLite for now because it is so easy to set up.  I'll probably move to mysql or something if/when I deploy it for real.  I then used the rails scaffold command to generate the stub rails code.

ruby script/generate scaffold Map name:string content:text

In theory, I don't really need the generated map controller and views.  However, being able to point a browser at <host>/map/ and see all of the uploaded maps provides a very easy way to test if maps are being saved.  I then created my AJAX controller with a save_map and load_map action.  Since these are intended to be used in an AJAX fashion, I didn't create any views for them.

Ruby

My intent was for the drawing program to pass the map as a JSON object to the AJAX call.  The controller could just save that to the database and read it back for a load.  As far as Ruby is concerned it is just a string.  Here is the ruby code that makes that work.
class MyAjaxController < ApplicationController
  
  def save_map
    name = params[:name]
    mapData = params[:map].to_json
    map = Map.find(:first, :conditions=>{:name=>name})
    map.content = mapData if map
    map = Map.new(:name=>name, :content=>mapData) unless map
    map.save!
    render :text=>"ok"
  end

  def load_map
    name = params[:name]
    map = Map.find(:first, :conditions=>{:name=>name})
    render :text=>map.content
  end
end
As you can see, the save method just reads the name and map value from the posted parameters.  Since ruby tries to parse these as objects, I call to_json to get the map contents back into a string.  Then I just save it to the database (either as a new row, or updating an existing row).  load_map just returns the map content that was saved with the given name.  Neither of these methods have much in the way of error checking, so they are definitely not "production" ready, but they work great as a proof of concept.

HTML

Here are the controls I added to the page to allow saving and loading.
<input type="text" id="name"/>
<input type="submit" id="save" value="Save" />
<input type="submit" id="load" value="Load" />

JavaScript

The JavaScript for making the AJAX call looks like this:
  1 $("#save").click(function() {
  2   var data = draw.save(); 
  3   var name = $("#name").get(0).value;
  4   $.post('/my_ajax/save_map', {name:name, map:data}, function() {
  5       alert("Map saved!"); 
  6   });  
  7 }); 
  8  
  9 $("#load").click(function() {
 10   var name = $("#name").get(0).value;
 11   $.getJSON('/my_ajax/load_map', {name:name}, function(data, textStatus) {
 12     control.reset(); 
 13     draw.load(data); 
 14   });  
 15 });
Everything that starts with a $ is a jQuery function.  #save, #load, and #name refer to the HTML controls I've put on the page. The draw variable is the DrawingRecord instance, and control is a ControlLayer instance from the original drawing program.  I added a save and a load method to the DrawingRecord to save and restore the map.  Both Ajax calls $.post(...);   $.getJSON(...); take a URL, an object to pass to the server, and a function which is called when thecall returns (i.e. the A in AJAX).
As for what the draw.save() method returns, originally I just tried using the DrawingRecord.lines object.  Unfortunately this caused problems - apparently jQuery tried to package up all the methods as well as the fields of the object.  So instead I made the save and load methods in DrawingRecord package up all of the points into a single array which can be easily passed back and forth.
  1 DrawingRecord.prototype.save = function(){
  2   var lines = [] 
  3   for (var i in this.lines) {
  4     var line = this.lines[i];
  5     lines.push(line.p1.x, line.p1.y, line.p2.x, line.p2.y); 
  6   } 
  7   return {lines:lines} 
  8 } 
  9 DrawingRecord.prototype.load = function(data) {
 10   this.reset(); 
 11   this.dontDraw = true;
 12   var temp = []; 
 13   for (var index in data.lines) {
 14     temp.push(parseInt(data.lines[index])); 
 15     if (temp.length == 4) {
 16       this.addLine(new Point(temp[0], temp[1]), new Point(temp[2], temp[3]));
 17       temp = []; 
 18     } 
 19   } 
 20   this.dontDraw = false;
 21   this.draw(); 
 22 }
And with that, it basically all works.  Unfortunately, I can't have a live demo of this to put in this blog as I don't have a live server that I want to have committed to serving this project forever.

No comments: