[Catalyst-commits] r12439 -
trunk/examples/CatalystAdvent/root/2009/pen
caelum at dev.catalyst.perl.org
caelum at dev.catalyst.perl.org
Sun Dec 20 17:02:07 GMT 2009
Author: caelum
Date: 2009-12-20 17:02:06 +0000 (Sun, 20 Dec 2009)
New Revision: 12439
Added:
trunk/examples/CatalystAdvent/root/2009/pen/ajax-crud.pod
Log:
ajax crud article
Added: trunk/examples/CatalystAdvent/root/2009/pen/ajax-crud.pod
===================================================================
--- trunk/examples/CatalystAdvent/root/2009/pen/ajax-crud.pod (rev 0)
+++ trunk/examples/CatalystAdvent/root/2009/pen/ajax-crud.pod 2009-12-20 17:02:06 UTC (rev 12439)
@@ -0,0 +1,354 @@
+=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 instead of XML. The idea is to make something that's closer to
+a GUI app rather than a traditional web page.
+
+=head2 Installing flexigrid
+
+First, get the tutorial tarball from
+L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/examples/Tutorial/MyApp_Chapter9_FormFu.tgz>.
+
+We'll use flexigrid for our grid, from L<http://www.flexigrid.info/>.
+
+From the flexigrid zip, put the contents of C<css/flexigrid/images> and
+C<css/images> into C<root/static/images/flexigrid>.
+
+C<css/flexigrid/flexigrid.css> goes into C<root/static/css>. Edit the file and
+do an C<< %s!url(images/!url(../images/flexigrid/! >> substitution.
+
+C<flexigrid.js> goes into C<root/static/js>.
+
+Get jQuery from L<http://jqueryjs.googlecode.com/files/jquery-1.3.2.js> and
+save it to C<root/static/js/jquery.js>.
+
+=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<root/src/wrapper.tt2> and put this at the top:
+
+ [% IF no_wrapper %]
+ [% content %]
+ [% ELSE %]
+
+And an
+
+ [% END %]
+
+at the bottom.
+
+This allows not using the wrapper for some pages, as we do here.
+
+Now for the HTML:
+
+ <?xml version="1.0" encoding="UTF-8"?>
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+ <html xmlns="http://www.w3.org/1999/xhtml"; xml:lang="en" lang="en">
+ <head>
+ <link rel="stylesheet" href="[% c.uri_for('/static/css/flexigrid.css') %]" />
+ <script language="javascript" src="[% c.uri_for('/static/js/jquery.js') %]">
+ </script>
+ <script language="javascript" src="[% c.uri_for('/static/js/flexigrid.js') %]">
+ </script>
+ </head>
+ <body>
+
+ <table id="books_grid" style="display:none"></table>
+
+ <style>
+ .flexigrid div.fbutton .add
+ {
+ background: url([% c.uri_for('/static/images/flexigrid/add.png') %]) no-repeat center left;
+ }
+ .flexigrid div.fbutton .edit
+ {
+ background: url([% c.uri_for('/static/images/pen-16.gif') %]) no-repeat center left;
+ }
+ .flexigrid div.fbutton .delete
+ {
+ background: url([% c.uri_for('/static/images/flexigrid/close.png') %]) no-repeat center left;
+ }
+ </style>
+
+ <script type="text/javascript">
+ function add_book(button, grid) {}
+ function edit_book(button, grid) {}
+ function delete_book(button, grid) {}
+
+ $("#books_grid").flexigrid({
+ url: '/api/grid',
+ dataType: 'json',
+ colModel : [
+ {display: 'id', name : 'id', width : 0, sortable : false, hide: true},
+ {display: 'Title', name : 'title', width : 200, sortable : true, align: 'left'},
+ {display: 'Rating', name : 'rating', width : 30, sortable : true, align: 'right'},
+ {display: 'Author(s)', name : 'authors', width : 275, sortable : true, align: 'left'},
+ ],
+ searchitems : [
+ {display: 'Title', name: 'title', isdefault: true},
+ {display: 'Rating', name: 'rating' },
+ ],
+ buttons : [
+ {name: 'Add', bclass: 'add', onpress : add_book},
+ {name: 'Edit', bclass: 'edit', onpress : edit_book},
+ {name: 'Delete', bclass: 'delete', onpress : delete_book},
+ {separator: true}
+ ],
+ sortname: "title",
+ sortorder: "asc",
+ usepager: true,
+ title: 'Books',
+ useRp: true,
+ rp: 10,
+ width: 550,
+ height: 245
+ });
+ </script>
+ </body>
+ </html>
+
+The C<pen-16.gif> icon is from L<http://brainstormsandraves.com/zips/icons/skdesigns-30-creamcoffee-blog-icons-16x16-10x10.zip>.
+
+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({}, {
+ page => $page,
+ rows => $rows,
+ order_by => "$sort_by $sort_order",
+ });
+
+ $rs = $rs->search_literal("lower($search_by) LIKE ?", lc($search_text))
+ if $search_by && $search_text;
+
+ $data{total} = $c->model('DB::Book')->count;
+ $data{page} = $page;
+ $data{rows} = [
+ map { +{
+ id => $_->id,
+ cell => [
+ $_->id,
+ $_->title,
+ $_->rating,
+ $_->author_list,
+ ]
+ } } $rs->all
+ ];
+
+ $self->status_ok($c, entity => \%data);
+ }
+
+ 1;
+
+Now start the server and go to L<http://localhost:3000/ajax> 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, 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<delete_book> 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<http://jquery.malsup.com/form/jquery.form.js?2.36>. Save it as
+C<root/static/js/jquery.form.js>.
+
+Add a C<< <script> >> link at the top of C<ajax.tt> for it:
+
+ <script language="javascript" src="[% c.uri_for('/static/js/jquery.form.js') %]">
+ </script>
+
+And above the C<< <table id="books_grid" ... >> tag we'll put a div to hold the
+popup form.
+
+ <div id="book_form" style="display:none"></div>
+
+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 (tutorial bug...)
+
+=head2 Catalyst and jQuery makes AJAX easy
+
+As you can see, the Catalyst framework is well suited for making AJAX
+interfaces, and jQuery is awesome.
+
+=head1 AUTHOR
+
+Caelum: Rafael Kitover <rkitover at cpan.org>
+
More information about the Catalyst-commits
mailing list