=head1 An AJAX CRUD Interface with Catalyst and jQuery In this example, we will develop an AJAX CRUD (Create Read Update Delete) extension to the Catalyst tutorial. AJAX stands for Asynchronous JavaScript And XML (although these days most people use JSON as a data-interchange format instead of XML). The idea is to make a web page, or part of one, something that's less like a traditional web page, where an entire page must be loaded from the server any time information is changed, and closer to a GUI app, where elements of a page can be updated on the fly, without reloading the whole page. =head2 Installing Flexigrid First, get the tarball of the Catalyst Tutorial from L. We're going to use Flexigrid, a jQuery-based data grid, from L. Download and unzip the Flexigrid zip file, and put the contents of its C and C directories into the tutorial's C directory. C goes into the tutorial's C. Edit the file and do an C<< %s!url(images/!url(../images/flexigrid/! >> substitution. C goes into C. Get jQuery from L and save it to the tutorial's C. =head2 Creating a Grid Let's make a controller for AJAX stuff: package MyApp::Controller::AJAX; use strict; use warnings; use parent 'Catalyst::Controller::HTML::FormFu'; sub index : Path Args(0) { my ($self, $c) = @_; $c->stash( no_wrapper => 1, template => 'ajax.tt' ); } sub end : ActionClass('RenderView') {} Edit C and put this at the top: [% IF no_wrapper %] [% content %] [% ELSE %] And an [% END %] at the bottom. This allows us to skip the wrapper for some pages, as we will do here. Now for the HTML: The C icon is from L. Now we need to make an API controller that serves JSON for the grid: package MyApp::Controller::API; use strict; use warnings; use parent 'Catalyst::Controller::REST'; __PACKAGE__->config(default => 'application/json'); sub grid : Local ActionClass('REST') {} sub grid_POST { my ($self, $c) = @_; my ($page, $search_by, $search_text, $rows, $sort_by, $sort_order) = @{ $c->req->params }{qw/page qtype query rp sortname sortorder/}; s/\W*(\w+).*/$1/ for $sort_by, $sort_order, $search_by; # sql injections bad my %data; my $rs = $c->model('DB::Book')->search({}, { order_by => "$sort_by $sort_order", }); $rs = $rs->search_literal("lower($search_by) LIKE ?", lc($search_text)) if $search_by && $search_text; my $paged_rs = $rs->search({}, { page => $page, rows => $rows, }); $data{total} = $rs->count; $data{page} = $page; $data{rows} = [ map { +{ id => $_->id, cell => [ $_->id, $_->title, $_->rating, $_->author_list, ] } } $paged_rs->all ]; $self->status_ok($c, entity => \%data); } 1; Now start the server and go to L and you will see a lovely AJAX grid of books. You can click on the magnifying glass icon at the lower left to search by title or rating. Implementing search by author is left as an excercise for the reader :) =head2 Deleting The Add, Edit, and Delete buttons we put at the top of the grid don't work yet. Let's implement the Delete button. Expand the C JavaScript function stub to: function delete_book(button, grid) { var total_count = $('.trSelected', grid).length; var deleted = 0; $.each($('.trSelected', grid), function() { var id = $('td:nth-child(1) div', this).html(); $.ajax({ url: '/api/book/' + id, type: 'DELETE', data: {}, dataType: 'json', success: function() { deleted++; if (deleted == total_count) { $('#books_grid').flexReload(); } } }); }); } Add to the API controller to support the Delete action: sub book : Local ActionClass('REST') { my ($self, $c, $id) = @_; $c->stash(book => $c->model('DB::Book')->find($id)); } sub book_DELETE { my ($self, $c, $id) = @_; $c->stash->{book}->delete; $self->status_ok($c, entity => { message => 'success' }); } Now restart the server, go back to the grid in your browser and try selecting some rows and pressing the Delete button. Voila! =head2 Adding and Editing For this part we'll need the jQuery Form plugin, which you can get from L. Save it in the tutorial app as C. Add a C<< And above the C<< > tag we'll put a div to hold the popup form. A jQuery UI plugin for a popup would probably be nicer for this purpose. The JavaScript for the Add and Edit buttons is fairly simple: function add_book(button, grid) { var form_div = $('#book_form'); form_div.load('/ajax/book_form_add', null, function() { $('#book_form form').ajaxForm({ url: '/ajax/book_form_add', success: function() { form_div.hide(); $('#books_grid').flexReload(); } }); form_div.show(); }); } function edit_book(button, grid) { var id = $('.trSelected td:nth-child(1) div', grid).html(); var form_div = $('#book_form'); var url = '/ajax/book_form_edit/' + id; form_div.load(url, null, function() { $('#book_form form').ajaxForm({ url: url, success: function() { form_div.hide(); $('#books_grid').flexReload(); } }); form_div.show(); }); } Now we need the AJAX Controller actions, in which we'll use the FormFu stuff that's already a part of the tutorial, but without the wrapper since we're using C<.load> to load the forms as HTML fragments. sub book_form_add : Local Args(0) FormConfig('books/formfu_create.yml') { my ($self, $c) = @_; my $form = $c->stash->{form}; if ($form->submitted_and_valid) { my $book = $c->model('DB::Book')->new_result({}); $form->model->update($book); } else { my @author_objs = $c->model("DB::Author")->all(); my @authors; foreach (sort {$a->last_name cmp $b->last_name} @author_objs) { push(@authors, [$_->id, $_->last_name]); } my $select = $form->get_element({type => 'Select'}); $select->options(\@authors); } $c->stash( no_wrapper => 1, template => 'books/formfu_create.tt2' ); } sub book_form_edit : Local Args(1) FormConfig('books/formfu_create.yml') { my ($self, $c, $id) = @_; my $form = $c->stash->{form}; my $book = $c->model('DB::Book')->find($id); if ($form->submitted_and_valid) { $form->model->update($book); } else { my @author_objs = $c->model("DB::Author")->all(); my @authors; foreach (sort {$a->last_name cmp $b->last_name} @author_objs) { push(@authors, [$_->id, $_->last_name]); } my $select = $form->get_element({type => 'Select'}); $select->options(\@authors); $form->model->default_values($book); } $c->stash( no_wrapper => 1, template => 'books/formfu_create.tt2' ); } Now try out the Add and Edit buttons, and you should see the new entries or your changes in the grid when you click the submit button. Changing the author from Edit doesn't work at the moment; this is a bug in the Tutorial. =head2 Catalyst and AJAX The Catalyst framework with components such as L or L is well suited for making AJAX interfaces, as I hope we've demonstrated. From the Catalyst side, it's just a matter of serving and receiving JSON (or XML) data or serving HTML fragments, preferrably from a well-designed URI API based on REST. Here we used tools for the jQuery JavaScript framework, but other frameworks work just as well, such as ExtJS, Dojo, YUI, or Mootools. You can also use the jQuery selector functionality in combination with these other frameworks, with C mode if they override C<$>. There are also higher-level tools such as L to automate accessing your L schema from JavaScript, and even L which auto-generates a complete AJAX CRUD interface to your L schema using ExtJS. =head1 AUTHOR Caelum: Rafael Kitover