Catalyst AJAX With MochiKit
In this Advent entry I want to show how MochiKit (http://www.mochikit.com) can be used to add AJAX (or my favorite under-used buzzword "Web 2.0") functionality to the Catalyst Tutorial on CPAN.
What You Need
You need the Tutorial example app Part 4 (Basic CRUD), which can be downloaded from http://www.catalystframework.org/calendar/static/2008/mochikit/MyApp_Part4.tgz. Important: during the course of writing this article, the examples in the tutorial changed, so this download is of the older version than the one currently online.
You need the MochiKit JavaScript Library which can be downloaded from http://www.mochikit.com/dist/MochiKit-1.4.2.zip. This zip file contains some other files and documentation as well.
Why MochiKit?
Without going into JavaScript library wars, MochiKit has some great features which should make it a candidate for any project that is going to need a lot of JavaScript development. It will save you from writing without controlling you. Some of the main reasons I think it should be considered are:
- Lightweight; the various functional libraries can be cut out if not needed
- Does not alter JavaScript to the point where its not JavaScript anymore, like some libraries. I find it has the Perl mentality of letting you do things any way you want, and can be used with other UI libraries
- Saves a ton of JavaScript coding without giving away control
- It integrates well with prebuilt applications as we will see in this example. You don't need to re-write the whole UI, as in some other libraries' widgets, to get good functionality out of it
- Comes with a non-browser-dependent JavaScript debugging console in case you're not using something like FireBug (http://getfirebug.com/) (which is highly recommended in any case)
Now Let's Begin
The first thing is to download the Tutorial Part 4 and extract it where you want it; in my case, it's on my Desktop.
Now, let's start the server and make sure it's working.
Now, let's browse to our books controller's list action at http://localhost:3000/books/list, and you should see the default data from the downloaded example like the image below.
This is where we will be adding the AJAX functionality. I think it would be sooooo Web 2.0-ish if we could create, delete, and modify the books and have the UI updated without a page refresh. However, I will only be showing creates.
Let's load the MochiKit library into our templates so it's loaded on all pages. To do that, first create a js directory inside of root/static/ to hold our JaveScript files. Then let's copy the packed (white space removed) MochiKit file, which includes all the libraries in one file for the sake of simplicity. In a production environment you will probably only want to load the necessary libraries.
Now let's modify the current MyApp/root/lib/site/html file to include the MochiKit library on all pages. Just add the following line after the css section in the file.
<script type="text/javascript" src="[% Catalyst.uri_for('/static/js/MochiKit.js') %]"></script>
Now make sure that when you restart your server and reload the http://localhost:3000/books/list page, the MochiKit library file is being loaded on the page. You can do this by browsing to http://localhost:3000/static/js/MochiKit.js
So Here Is The Plan
Let's add the ability to create as many books as we want without needing to refresh. Just for fun, let's also do some simple input validation. The basic idea is we will have some input taken from the user, then posted to Catalyst by JavaScript. Catalyst will process and give us a standarized response, and our JavaScript will process the response and update the page as needed.
First thing is first: let's create the needed UI elements in the
template. I think all we would need is an empty Title, Rating, and
Author ID text field. Change the top of the table in the template
MyApp/root/src/books/list.tt2 to include a thead
, tbody
, and an
id
for the tbody
. MochiKit has some DOM Coercion rules and requires
tbody
in tables if we want to manipulate them. Add the following to the
bottom of MyApp/root/src/books/list.tt2:
<table> <thead> <tr><td>Title</td><td>Rating</td><td>Author(s)</td><td>Links</td></tr> </thead> <tbody id="list_body">
Modify the bottom of MyApp/root/src/books/list.tt2 to look like this.
</tbody> </table> <table> <tr><td>Title:</td><td><input type="text" name="title" id="title"></td></tr> <tr><td>Rating:</td><td><input type="text" name="rating" id="rating"></td></tr> <tr><td>Author ID:</td><td><input type="text" name="author_id" id="author_id"></td></tr> </table> <input type="button" name="add" value="add" id="add_book"> <script type="text/javascript" src="[% Catalyst.uri_for('/static/js/list.js') %]"></script>
We also want to edit the MyApp/root/lib/site/layout
file and add an
id that we can reference for both span elements, so that we can update
them live. We will also remove the stash references so that it only gets
updated via JavaScript, and not through the template processing.
<span class="message" id="message"></span> <span class="error" id="error"></span>
Now we are ready to start writing the JavaScript for the page. Let's
create a file MyApp/root/static/js/list.js
. For this example, the
first thing we want to do is create variables that will be references to
things we know we need.
//Creates MochiKit logging pane. Remove "true" if you want it popped out in its own window createLoggingPane(true); var message = getElement('message'); var error = getElement('error'); var list_body = getElement('list_body'); var title = getElement('title'); var rating = getElement('rating'); var author_id = getElement('author_id'); var add_book = getElement('add_book');
If you are already pretty familiar with JavaScript, you probably
noticed I am using a getElement() instead of getElementById() which is
the standard JavaScript function for getting a handle on an
element. This getElement() function is a MochiKit helper function
which saves you 4 keystrokes!!! Seriously, it's really useful because
it does some checks on input and is more flexible than the standard
getElementById(). Remember that the MochiKit logging console I was talking
about earlier? The createLoggingPane(true)
call creates this
window at the bottom of the page; if you want it as a popup window,just
call createLoggingPane()
without the "true".
The next thing we want to do is to read the input and submit it when the Add button is clicked.
But I want to explain a few things first. With MochiKit you can use the
typical event handlers like onClick, onMouseUp, etc. However MochiKit
has a better cross-browser event handling system they call Signals
(http://www.mochikit.com/doc/html/MochiKit/Signal.html) which does
not leak memory. However you can't have both. And there is a price to
pay: load events are not supported at the same time Signals are. The
only real side affect of that is you can't create a JavaScript function
that gets called by <body onload="my_javascript_function()"
> which
means you will need to have that called at the bottom of the page or
after you know things are loaded. Trust me, it's worth going with the
signals.
So, add the following lines to your list.js file:
connect ( add_book, 'onclick', function () { log("I have been clicked"); log("Title: ", title.value); log("Rating: ", rating.value); log("Author_ID: ", author_id.value); } );
Restart the server and refresh the page and you should see the logging
window at the bottom, and when you click on the add button you should
see the "I have been clicked"
text and whatever values are in the
input text fields. Now tell me that's not cool!
Now to explain some more MochiKit before we add our AJAX stuff. MochiKit uses an Async (http://www.mochikit.com/doc/html/MochiKit/Async.html) framework which has deferred objects for asynchronous calls. Defererred objects basically have callbacks and other associated properties. You can add Async functionality to almost anything with MochiKit but in this case it will be for our XMLHttpRequests. Deferred objects are cool and very powerful if you want to add a lot of flexibility to your application. You can cancel deferred objects, set errors, have error callback functions, etc. Add the following lines of code to your list.js under the log() calls.
//Creating our params object to hold our arguments that we will be posting var params = { title: title.value, rating: rating.value, author_id: author_id.value }; //Calling MochiKit's doXHR which makes XMLHttpRequests painless var d = doXHR ( '/books/create_do', { 'method': 'POST', 'sendContent': queryString(params), 'headers': {'Content-Type':'application/x-www-form-urlencoded'} } );
We created an object named params
to hold our arguments that we post
to our Catalyst app. Then we call the MochiKit function doXHR
(http://www.mochikit.com/doc/html/MochiKit/Async.html#fn-doxhr),
which creates a deferred object and returns that deferred object which
we call 'd'. Then the params object is converted to a query string
that is URL encoded using the MochiKit function queryString()
. Another
thing you might notice is the URL the post is happening to is
/books/create_do
which means we need to create that action. Before we
can create that action, however, we need to do a few things, like setup a
Catalyst view for JSON and configure that. We will be using
Catalyst::View::JSON so make sure it's installed before you
continue.
Once we created our view, let's configure it. It's good practice to set a default view once a Catalyst app has more than one view. In our case we want the default to be TT. We also want to set the json_driver to JSON::XS and we want to only expose the named 'json' hash in the stash. This is how I modified my config call in MyApp/lib/MyApp.pm.
__PACKAGE__->config ( name => 'MyApp', 'View::JSON' => { expose_stash => 'json', json_driver => 'JSON::XS' }, default_view => 'TT' );
Finally, here is my /books/create_do
action.
sub create_do : Local { my ($self, $c) = @_; # creating default values in object that will be serialized my $ret = { status => 'Unsuccessful', error => {Unknown => ''}, data => {} }; # Retrieve the values from the form my $title = $c->request->params->{title}; my $rating = $c->request->params->{rating}; my $author_id = $c->request->params->{author_id}; $c->log->info("title: $title"); $c->log->info("rating: $rating"); $c->log->info("author_id: $author_id"); # Validate input first if(!defined($title) || $title =~ m/[^a-zA-Z0-9 \-\.\/\,]/) { $c->log->info("title not defined or matches invalid characters rejecting"); delete($ret->{error}->{'Unknown'}) if defined($ret->{error}->{'Unknown'}); $ret->{error}->{"Invalid Characters"} = 'Title contains unsupported characters or is not defined'; } elsif(!defined($rating) || $rating =~ m/[^1-5]/) { $c->log->info("rating not defined or matches invalid characters rejecting"); delete($ret->{error}->{'Unknown'}) if defined($ret->{error}->{'Unknown'}); $ret->{error}->{"Invalid Characters"} = 'Rating contains unsupported characters must be value of 1-5 or is not defined'; } elsif(!defined($author_id) || $author_id =~ m/[^0-9]/ || $author_id == 0) { $c->log->info("author_id not defined or matches invalid characters rejecting"); delete($ret->{error}->{'Unknown'}) if defined($ret->{error}->{'Unknown'}); $ret->{error}->{"Invalid Characters"} = 'Author_ID contains unsupported characters must be valid id or is not defined'; } else { $c->log->info("No invalid characters or undefined values in the input"); # Create the book my $book = $c->model('DB::Books')->create({ title => $title, rating => $rating, }); # Handle relationship with author $book->add_to_book_authors({author_id => $author_id}); $c->log->info("Created book_id: ",$book->id); my $authors = []; foreach my $author ($book->authors) { $c->log->info("last name: ", $author->last_name); push(@$authors, $author->last_name); } delete($ret->{error}->{'Unknown'}) if defined($ret->{error}->{'Unknown'}); $ret->{status} = 'Successful'; $ret->{data} = { book_id => $book->id, title => $book->title, rating => $book->rating, authors => $authors }; } # Putting our return data into the json stash to get serialized into json $c->stash->{json} = $ret; $c->forward('MyApp::View::JSON'); }
I am not going to go into details here about what every line of code
does but I am going to point out the specific things I am doing for an
AJAX-y JSON response. Notice that I define my $ret
variable with some
default values. This is important because on the JavaScript side we
need to add code that will handle this response and we need to have
some clear indications if things worked. So having standard values are
good practices; just trusting that the XMLHttpRequest returned a 200 is
not very good in cases where you are processing data.
Now we just need to update our JavaScript code to handle the response
and manipulate the DOM. In our case our JavaScript code does what our
TT templates do: read from a defined data structure and
render our page. Below is the complete JavaScript file. Notice after
our doXHR()
call we add a callback to that deferred object. Without
creating this callback, once the XMLHttpRequest is complete nothing
will happen, but in our case we need to process that returned data so
we create an anonymous function to handle that data. We call
MochiKit's evalJSONRequest()
and that takes care of creating our
JavaScript object. The advantage here is that you don't need to do
anything hard or annoying like parsing values out or stripping
characters: you're working with a Object with a structure you can access
in very easy and defined ways. Doing things this way makes JavaScript
painless and to me feels very Perlish. You can see in the code I
check to make sure status is Successful, and if it is I start creating
DOM Elements and storing them into variables that I can reference
later. I usually wouldn't store them, I would probably just create that
whole structure annonomously, but I purposely did different things so
that you could see some different examples. You can see with MochiKit
creating and manipulating the DOM on the fly is a piece of cake. All
those MochiKit calls like A(), TD(), TR(), etc are Partials which I
will explain below.
//Creates MochiKit logging pane. Remove "true" if you want it popped out in its own window createLoggingPane(true); var message = getElement('message'); var error = getElement('error'); var list_body = getElement('list_body'); var title = getElement('title'); var rating = getElement('rating'); var author_id = getElement('author_id'); var add_book = getElement('add_book'); //function to update error or message spans var update_user = function (type, txt) { var p_txt; if(type == 'message') { p_txt = P({'style':'display:none'},'Status: '+txt); replaceChildNodes(message, [p_txt]); } else if (type == 'error') { p_txt = P({'style':'display:none'},'Error: '+txt); replaceChildNodes(error, [p_txt]); } appear(p_txt,{'speed':0.1}); } //Creating a partial for updating the message and error spans for example purposes var u_message = partial(update_user,'message'); var u_error = partial(update_user,'error'); connect ( add_book, 'onclick', function () { log("I have been clicked"); log("Title: ", title.value); log("Rating: ", rating.value); log("Author_ID: ", author_id.value); //Creating our params object to hold our arguments that we will be posting var params = { title: title.value, rating: rating.value, author_id: author_id.value }; //Calling MochiKits doXHR which makes XMLHttpRequests painless var d = doXHR ( '/books/create_do', { 'method': 'POST', 'sendContent': queryString(params), 'headers': {'Content-Type':'application/x-www-form-urlencoded'} } ); //Creating a callback on success to process our json response d.addCallback ( function (req) { //eval'ing and assigning our returned json data to resp variable var resp = evalJSONRequest(req); //logging to firebug as an example comment out if not installed console.log(resp); //Checking to see we have a successful response in our returned data if(resp.status == 'Successful') { log('Response has status of successful'); //calling our partial function u_message(resp.status); //creating dom elements. first arg pass is named args for attributes //second arg passed is data inside element. can be string or array of more //elements consult mochikit docs for full details var td_title = TD(null, resp.data.title); var td_rating = TD(null, resp.data.rating); var td_authors = TD(null, '(' + resp.data.authors.length + ') ' + resp.data.authors.join(', ')); var a_link = A({'href':resp.data.link}, ['Delete']); var td_links = TD(null, [a_link]); //creating our tr which holds all of our previously created elements var tr_book = TR ( null, [ td_title, td_rating, td_authors, td_links ] ); //you can log this to FireBug and actually inspect the dom //its like a hackish Data::Dump::dump() for JavaScript console.log(tr_book); //Calling MochiKit's appendChildNodes() to only the fly update the DOM appendChildNodes(list_body, [tr_book]); } else { log('Response has status of NON successful'); //calling our partial function u_message(resp.status); //getting error reason and txt and updating user for (i in resp.error) { log('Error is:',i); log('Reason is:',resp.error[i]); u_error(i+': '+resp.error.i); } } } ) } );
Partials
(http://www.mochikit.com/doc/html/MochiKit/Base.html#fn-partial) are
really cool and are one of the reasons why MochiKit can save a lot of
coding. Partials are basically wrappers of partially applied
functions. This is really useful for creating re-usable functions that
could be wrapping several functions. You might argue, "Why
wouldn't I just call functions with parameters?" Well you can if you
want, but a Partial is meant to wrap that for you. In the example above I
created a simple function for updating the messages to the user. Now
each time I call that function I could do
update_user("message",resp.status); or I can create a partially applied
function for both "message" and "error" so that I can call them and just
pass in one argument like u_message(resp.status)
. Another good use for
Partials is if you're creating a lot of UI components. For example, if you
want to create a UI component, you will always re-use that wraps a
<a></a>
inside of a DIV with certain style
attributes set a Partial would be perfect candidate to save you a lot of
typing each time you created that.
Before I finish I would just like to show off a few benefits of FireBug for this type of AJAX development. Below I show screenshots of FireBug console window after I click the Add button. FireBug logs XMLHttpRequests in the console window by default. It shows the HTTP Method (POST, GET), the URL, parameters passed, and the response from the server. This is really useful while you are in the development process because you can see the parameters passed to the server from the client's point of view. Its also very useful to see the raw response from the server to see if the data structure you're expecting is really being returned or not. Of course if needed you can also see the request and response headers.
Another FireBug feature I wanted to show is the easy ability to log DOM
elements to the console window so you can inspect them further. With
FireBug enabled, you can right-click any element and select "Inspect
Element" and view the element with its HTML, CSS, and DOM
properties. What's even cooler is that you can further modify any of
those aforementioned properties on the fly and see the result live! In
the screenshot above you can see Object status=Successful error=Object
data=Object
in the console window. This is because in my list.js I have
console.log(resp) being called in my callback for my deferred
object. When you call console.log() and pass in an object you can
inspect that object in the DOM. This is very useful if you want to
inspect what type of data structure an object is and what values it
contains. You can do this for any DOM element or variable. The
screenshot below shows the result of our "resp" object:
The final FireBug cool feature I wanted to show is the JavaScript
console. This console allows full interaction with your page on the
fly. All your functions and libraries that are loaded on the page are
accessible via FireBug's console. I usually start prototyping my
JavaScript code in FireBug's console window before I put it into my JS
file because it's faster and doesn't need to have the page refreshed to
load the updated JS file. Below you see a screenshot of calling our
partial u_message()
function from the console.
The full finished app can be downloaded here http://http://www.catalystframework.org/calendar/static/2008/mochikit/MyApp_MochiKit.tar.gz. Hopefully this article helps anyone who wants to start doing AJAX with Catalyst.
Author
Ali Mesdaq (amesdaq@websense.com), is a Senior Security Researcher with Websense Security Labs (http://securitylabs.websense.com/).
- Previous
- Next