[Catalyst-commits] r7224 - trunk/examples/CatalystAdvent/root/2007/pen

jasonk at dev.catalyst.perl.org jasonk at dev.catalyst.perl.org
Tue Dec 4 16:59:56 GMT 2007


Author: jasonk
Date: 2007-12-04 16:59:55 +0000 (Tue, 04 Dec 2007)
New Revision: 7224

Modified:
   trunk/examples/CatalystAdvent/root/2007/pen/17.pod
Log:
Committing the "Catalyst with Ext_Ajax: Editable Data Grids" article for the
advent calendar.


Modified: trunk/examples/CatalystAdvent/root/2007/pen/17.pod
===================================================================
--- trunk/examples/CatalystAdvent/root/2007/pen/17.pod	2007-12-04 16:41:16 UTC (rev 7223)
+++ trunk/examples/CatalystAdvent/root/2007/pen/17.pod	2007-12-04 16:59:55 UTC (rev 7224)
@@ -1,5 +1,606 @@
+
 =head1 Catalyst with Ext+Ajax: Editable Data Grids
 
-=head1 AUTHOR
+=head2 Creating an application
 
-jasonk
+This tutorial assumes you already have some Catalyst experience, so we won't
+go into too much detail with the basics of creating an application...
+
+  % catalyst.pl AdventAjaxGrid
+  ...
+  % cd AdventAjaxGrid
+  ...
+  % script/adventajaxgrid_create.pl view TT TTSite
+  ...
+
+=head2 Installing Ext
+
+To install Ext, you first need to download and unzip the distribution.  For
+this tutorial we will be using version 2.0 release candidate 1, which is the
+latest version at the time of this writing.  If you use a different version,
+you will need to adjust some of these commands.  If you prefer to avoid the
+risks of using late-beta software and stick with the latest 1.x release,
+this tutorial should still be helpful, but may require a lot of tweaking.
+
+  % cd root/static
+  % curl -O http://extjs.com/deploy/ext-2.0-rc1.zip
+  ...
+  % unzip ext-2.0-rc1.zip
+  ...
+  % mv ext-2.0-rc1 ext
+
+There are some portions of the Ext framework that we will need in every page,
+so we're going to add those to the head section now.  I like to define a
+macro to do the repetitive stuff for me, so fire up your editor again and
+open root/lib/config/main, then add the following code to the bottom of
+the file:
+
+  [% BLOCK stylesheet_link %][% FILTER collapse %]
+      [% DEFAULT rel="stylesheet" type="text/css" media="all" %]
+      <link
+          rel="[% rel %]"
+          type="[% type %]" 
+          media="[% media %]" 
+          href="[% href %]"
+      />
+  [% END %][% END %]
+  [% BLOCK javascript_link %][% FILTER collapse %]
+      [% DEFAULT type="text/javascript" %]
+      <script
+          src="[% src %]"
+          type="[% type %]"
+      ></script>
+  [% END %][% END %]
+  [% MACRO css_link INCLUDE stylesheet_link %]
+  [% MACRO js_link INCLUDE javascript_link %]
+
+This defines two blocks, one that produces a css link tag, and one that
+produces a javascript script tag.  Then we define a macro to go with each
+one to make their use even shorter.  Now we can very easily add javascript
+or css links whenever we need them.  Let's edit root/lib/site/html, and add
+the main core of the Ext libraries to the header section, just before the
+existing style tag:
+
+  [%
+    css_link( href = "/static/ext/resources/css/ext-all.css" );
+    js_link( src = "/static/ext/adapter/ext/ext-base.js" );
+    js_link( src = "/static/ext/ext-all.js" );
+  %]
+
+Now all we need to do before we can test this first portion, is to create an
+index template.  In root/src create a new template called index.tt2, which
+contains just the following content:
+
+  [% META title = 'Advent AJAX Grid' %]
+  <div id="datagrid"></div>
+
+And then add a method to your root controller to work with it.
+
+  =head2 index
+  
+  =cut
+  
+  sub index : Private {
+    my ( $self, $c ) = @_;
+  
+    $c->stash->{ 'template' } = 'index.tt2';
+  }
+
+Assuming there are no typos in any of that, you should now be able to start
+up the development server and connect to it, to see an empty page with just
+the default TTSite headers and footers.
+
+  % script/adventajaxgrid_server.pl 
+  
+  [info] AdventAjaxGrid powered by Catalyst 5.7011
+  You can connect to your server at http://localhost:3000
+
+=head2 Creating a data source
+
+Before we can get into creating our data grid, we need some data.  For that,
+we're going to set up a very simple L<DBIx::Class::Schema>-based database
+that can automatically deploy itself.  If you don't already have them
+installed, pull out your trusty L<CPAN> or L<CPANPLUS> tools, and install
+L<DBIx::Class> and L<DBICx::Deploy>.  Then create a new schema class in
+your lib directory called AdventDB.pm:
+
+  package AdventDB;
+  use strict;
+  use warnings;
+  use base qw( DBIx::Class::Schema );
+  
+  __PACKAGE__->load_classes();
+  
+  1;
+
+Then make a subdirectory in the same location called AdventDB, and create
+a new result class in there named AdventDB::Person:
+
+  package AdventDB::Person;
+  use strict;
+  use warnings;
+  use base qw( DBIx::Class );
+  
+  __PACKAGE__->load_components(qw( Core ));
+  __PACKAGE__->table( 'people' );
+  __PACKAGE__->add_columns(
+      id      => {
+          data_type           => 'integer',
+          is_nullable         => 0,
+          is_auto_increment   => 1,
+      },
+      name    => {
+          data_type           => 'varchar',
+          size                => 128,
+          is_nullable         => 0,
+      },
+      affiliation => {
+          data_type           => 'varchar',
+          size                => 16,
+          is_nullable         => 1,
+      },
+  );
+  __PACKAGE__->set_primary_key( 'id' );
+  
+  1;
+
+Now you have everything you need to create a database connection.  For the
+sake of example, we are going to use a L<SQLite> database, so pick a suitable
+place to store it, and use the dbicdeploy tool (from the L<DBICx::Deploy>
+package) to create a database from your classes.  From the top-level
+AdventDataGrid directory, run this command:
+
+  % dbicdeploy -l AdventDB dbi:SQLite:dbname=advent.db
+
+=head2 Populating the database
+
+Once the database is created, we need to populate it with some sample data.
+One of the nice things about having the whole database schema built from the
+code, is that during development you can very easily delete the entire
+development database and run dbicdeploy to get a brand-new one at any time.
+To make it even easier on us during development, we'll create a script that
+can then repopulate the database with a collection of suitable sample data.
+
+Create a new script in the script directory called populate-database.pl.
+
+  #!/usr/bin/perl -w
+  ##################
+  use strict;
+  use warnings;
+  use FindBin qw( $Bin );
+  use lib "$Bin/../lib";
+  use AdventDB;
+  
+  my $schema = AdventDB->connect( "dbi:SQLite:dbname=$Bin/../advent.db" );
+  
+  chomp( my @people = <DATA> );
+  my @affiliations = qw( Ninja Pirate );
+  
+  $schema->populate( 'Person', [
+      [ qw( name affiliation ) ],
+      map { [ $_, $affiliations[ rand( @affiliations ) ] ] } @people
+  ] );
+  __DATA__
+  Andy Grundman
+  Andy Wardley
+  Andreas Marienborg
+  Andrew Bramble
+  Andrew Ford
+  Andrew Ruthven
+  Arthur Bergman
+  Autrijus Tang
+  Brian Cassidy
+  Carl Franks
+  Christian Hansen
+  Christopher Hicks
+  Dan Sully
+  Danijel Milicevic
+  David Kamholz
+  David Naughton
+  Drew Taylor
+  Gary Ashton Jones
+  Geoff Richards
+  Jesse Sheidlower
+  Jesse Vincent
+  Jody Belka
+  Johan Lindstrom
+  Juan Camacho
+  Leon Brocard
+  Marcus Ramberg
+  Matt S Trout
+  Robert Sedlacek
+  Sam Vilain
+  Sascha Kiefer
+  Sebastian Willert
+  Tatsuhiko Miyagawa
+  Ulf Edvinsson
+  Yuval Kogman
+
+Now that you have a script to populate the database, run it to populate the
+database.  :)
+
+  % script/populate-database.pl
+
+=head2 Creating the model
+
+The last step in setting up our database environment is to create a model
+that Catalyst can use to access our database.  Since this is the only
+database model our tutorial project will have, we'll just call the model
+'DB':
+
+  % script/adventdatagrid_create.pl model DB DBIC::Schema \
+        AdventDB dbi:SQLite:dbname=advent.db
+
+Note that this command line is split (and a backslash used to escape the
+newline) simply for readability.  When actually running this command, enter
+it as one line and omit the backslash.
+
+=head2 Creating a JSON view
+
+To get data back and forth between the database and the browser, we'll need
+an interface, and since JSON is an easy one to use and setup, that's what
+we'll do.  Make sure you have L<Catalyst::View::JSON> installed, and then...
+
+  % script/adventdatagrid_create.pl view JSON JSON
+
+Now that you have more than one view, you will need to make sure Catalyst
+knows which one to use by default.  You can do this by adding
+'default_view: TT' to the adventajaxgrid.yml configuration file.
+
+Yes, technically using JSON means that our application is actually an
+'AJAJ' powered data grid, rather than AJAX, but it's fairly common for
+AJAX to simply mean 'javascript that interacts with the server in some way',
+as the pointy-haired bosses understand AJAX but not AJAJ or AJAH.  Turning
+this into an actual XML-powered AJAX app is left as an exercise for the
+reader.
+
+=head2 Providing data for the grid
+
+To provide data for the grid to display, we're going to create a new
+method in our root controller...
+
+  =head2 people_data
+  
+  =cut
+  
+  sub people_data : Local {
+      my ( $self, $c ) = @_;
+  
+      my $rs = $c->model( 'DB::Person' );
+      my @people = ();
+      while ( my $person = $rs->next ) {
+          push( @people, {
+              id          => $person->id,
+              name        => $person->name,
+              affiliation => $person->affiliation,
+          } );
+      }
+      $c->stash->{ 'people' } = \@people;
+      $c->detach( $c->view( 'JSON' ) );
+  }
+
+This will load up all of the people from the database, and stick their
+information into a data structure to be fed to the JSON view, which will
+in turn turn it into JSON and send it to the browser.
+
+=head2 Setting up the grid
+
+Now with all that prep-work out of the way (whew!) we can get on to the
+meat of this project, actually setting up our editable data grid.  To do
+that, we need to start out by writing some javascript.  Create a new file
+in root/static called advent.js.  It will be just an empty stub for now,
+but we're going to build on it further as we progress.
+
+  /*
+   * advent.js
+   */
+  
+  Ext.onReady(function(){
+  
+  });
+
+Ext.onReady runs the function given to it as an argument once the page has
+loaded enough for the javascript to work.  This may run before the page is
+completely loaded, while images are still loading (but it runs after the
+entire DOM is available.)  This is generally where Ext applications put their
+initialization.  All the javascript code that follows will go inside this
+function, unless otherwise noted.
+
+=head2 Defining the Column Model
+
+The next step is to define an Ext.grid.ColumnModel object.  The column model
+tells the grid component everything it needs to know about how to handle the
+data in each of the grid cells.  Remember to put this code inside the onReady
+function you just defined.
+
+    var col_model = new Ext.grid.ColumnModel([
+        {
+            id:         'id',
+            header:     'ID',
+            dataIndex:  'id',
+            width:      40
+        },
+        {
+            id:         'name',
+            header:     'Name',
+            dataIndex:  'name'
+        },
+        {
+            id:         'affiliation',
+            header:     'Affiliation',
+            dataIndex:  'affiliation',
+            width:      70
+        }
+    ]); 
+
+Ext's data grid objects are inherently sortable, you can either configure
+each row that you want to be sortable in the column model, or you can change
+the default sorting value for the entire model, to make every field sortable.
+I prefer to change the default, and then disable it as needed for individual
+fields.
+
+    col_model.defaultSortable = true;
+
+=head2 Defining a data record
+
+Ext provides an Ext.data.Record object, which encapsulates information about
+the data records we are working with.  It is possible to define the record
+information inline when we setup the data store in the next step, but
+creating an actual record for it makes it easier to reuse, and we'll need
+that when we add the ability to add a new row to the database.  So the
+next thing to go in that onReady function will be our person record type.
+
+    var Person = Ext.data.Record.create([
+        { name: 'id',           type: 'int' },
+        { name: 'name',         type: 'string' },
+        { name: 'affiliation',  type: 'string' }
+    ]);
+
+=head2 The Data Store
+
+Ext objects that need to access server-side data do so using an appropriate
+data store class.  Since we are going to use JSON for our transport format,
+we need to create an instance of an Ext.data.JsonStore object for our
+grid to consume.  You can find documentation on Ext.data.JsonStore in the
+API Docs at L<http://extjs.com/deploy/dev/docs/?class=Ext.data.JsonStore>.
+
+Because the data store needs to know the URL where it should get data from,
+it would be nice for this url to be created by the Catalyst uri_for method,
+so that our application is relocatable.  But since we want the rest of the
+javascript to be static (so that it can be served quickly) we're going to
+use a little trick.  Open up your index.tt2 template again, and add this
+between the existing META line and the div:
+
+  <script type="text/javascript">
+      var gridurl = '[% Catalyst.uri_for( "/people_data" ) %]';
+  </script>
+  [% js_link( src = '/static/advent.js' ) %]
+
+This way we get a nice relocatable link, without making catalyst process the
+whole javascript library as a template each time it is loaded.  The js_link
+call will include the library we are building now.
+
+Now we are ready to set up the data store.  This code goes in advent.js
+inside the onReady function.
+
+    var store = new Ext.data.JsonStore({
+        url:        gridurl,
+        root:       'people',
+        fields:     Person
+    });
+
+This creates a new Ext.data.JsonStore object, which will load its data from
+the url we just setup.  The 'root' key tells the data store which key in
+the returned data contains the information that should go in the grid, and
+the fields key provides the record constructor we created earlier so that
+the store will know what fields the information it retrieves will have.
+
+=head2 Creating the data grid object (FINALLY!)
+
+Now that we have a data store for our grid to use, we can B<finally> get to
+the point of creating the grid itself.  To do this we are going to create an
+instance of Ext.grid.EditorGridPanel.  Still working inside that advent.js,
+we're now going to add:
+
+    var grid = new Ext.grid.EditorGridPanel({
+        store:              store,
+        cm:                 col_model,
+        title:              'Edit People',
+        width:              600,
+        height:             300,
+        frame:              true,
+        autoExpandColumn:   'name',
+        renderTo:           'datagrid'
+    });
+
+The first few options to the constructor should be self-explanatory.  We
+first give it our store and column model objects, a title, and a width and
+height.  The frame option specifies whether to render the panel with fancy
+rounded borders or not, if it's false then the panel will be surrounded by
+a simple 1 pixel black border.  The autoExpandColumn option takes the id
+of a column which should be expanded to fill any unused space in the grid.
+This allows you to specify widths for those columns where it matters, and
+have the longest column (in our case, the 'name' column) use up any space
+that is left over.  The renderTo option provides the id of a div from our
+HTML template, it will be used as the target container when the grid is
+rendered, allowing for designers to specify exactly where on the page they
+want the grid to appear simply by putting an empty div there.
+
+Now the only thing that is left to do in order to see our grid, is to
+trigger the data store to load it's data and pass it to the grid for
+rendering.
+
+    store.load();
+
+=head2 Try it out!
+
+If all went well, you can restart your development server now, and go to
+http://localhost:3000/ and see the data grid in all it's glory.  Well, maybe
+not B<all> it's glory, as you may have noticed it's not really what one
+would call 'editable'.  We need to do a bit more work yet in order to
+make it B<actually> editable.  First off, we need to expand our column
+model to tell it what type of widget should be used to edit each of the
+different column types.  So open up advent.js again and change the hash
+for the name column to match the following:
+
+        {
+            id:         'name',
+            header:     'Name',
+            dataIndex:  'name',
+            editor:     new Ext.form.TextField({
+                allowBlank:     false,
+            })
+        },
+
+This tells the grid that name should be edited as a textfield, and that it
+is not allowed to be blank.  Next we are going turn the Affiliation field
+into a combobox.  A combobox is like a combination of textfield and popup
+menu, you can popup the menu to select something that already exists from
+the list, or you can type in something new.  Modify the affiliation hash
+like this:
+
+        {
+            header:     'Affiliation',
+            dataIndex:  'affiliation',
+            width:      70,
+            editor:     new Ext.form.ComboBox({
+                typeAhead:      true,
+                triggerAction:  'all',
+                transform:      'affpopup',
+                lazyRender:     true,
+                listClass:      'x-combo-list-small'
+            })
+        }
+
+The one important option to ComboBox here is 'transform'.  This is the lazy
+way of populating the popup list, it provides the id of an existing select
+option that should be turned into the popup.  To create this we need to edit
+the index.tt2 file again, and add a select element.
+
+    <select id="affpopup" style="display: none">
+    <option value="Ninja">Ninja</option>
+    <option value="Pirate">Pirate</option>
+    <option value="Unknown">Unknown</option>
+    </select>
+
+One advantage to doing it this way is that you don't have to change the
+javascript code just to add new items to the list.
+
+Now if you reload the page, you should be able to double-click on a name or
+an affiliation, and it will turn into an editable field so you can change
+the value.  The changes are not sent immediately to the server, however,
+instead they are simply marked in the grid by turning the upper left corner
+of the cell red to indicate that they have been changed.  In order to send
+the changes to the server to be put into the database, we need to create a
+save button.
+
+=head2 Adding a toolbar to the grid
+
+Rather than just a save button, lets add an entire toolbar to our grid.  Since
+the grid inherits from Ext.Panel, it automatically has both top and bottom
+toolbars, which we don't see in our example simply because we haven't told
+it to put anything there, so the empty toolbars are hidden.  Lets add some
+buttons to our toolbar.  We're going to create a toolbar that includes
+'New Person', 'Save Changes', and 'Discard Changes' buttons. 
+
+The last option provided to the EditorGridPanel was 'renderTo', so
+add a comma to the end of the renderTo option, and then follow it with this:
+
+        tbar:               [
+            {               
+                text:           'New Person',
+                handler:        function() {
+                    var p = new Person({
+                        name:           'Unnamed New Person',
+                        affiliation:    'Unknown',
+                    }); 
+                    grid.stopEditing();
+                    store.insert( 0, p );
+                    grid.startEditing( 0, 1 );
+                },
+            },
+            {
+                text:           'Save Changes',
+                handler:        function() {
+                    grid.stopEditing();
+                    var changes = new Array();
+                    var dirty = store.getModifiedRecords();
+                    for ( var i = 0 ; i < dirty.length ; i++ ) {
+                        var id = dirty[i].get( 'id' );
+                        var fields = dirty[i].getChanges();
+                        fields.id = dirty[i].get( 'id' );
+                        changes.push( fields );
+                    }
+                    console.log( changes );
+                    submitChanges( changes );
+                    store.commitChanges();
+                },
+            },
+            {
+                text:           'Discard Changes',
+                handler:        function() {
+                    grid.stopEditing();
+                    store.rejectChanges();
+                },
+            }
+        ]
+
+The first button creates a new Person object, inserts it at the top of the
+grid, and then starts editing that row in column 1 (we skip column 0 because
+it is the id, which isn't editable.)  The second button is the 'Save Changes'
+button, and it's handler collects the modified records from the grid and
+passes them to the submitChanges function to be sent to the server for
+processing.  The third button simply tells the store to reject any pending
+changes and revert the data to it's old values.
+
+In order to get the save button to work, we need to create the submitChanges
+function.  First we'll add another URL variable to the index.tt2 template,
+to indicate where the changes should be submitted to.
+
+    var posturl = '[% Catalyst.uri_for( "/people_data_submit" ) %]';
+
+Then we can use that when we write the submitChanges function, which uses
+Ext.Ajax to submit the changed data asynchronously.  On a successful return,
+we just signal the store to reload, causing it to get the current data from
+the database and update the grid.  Telling the grid to reload from the database
+makes sure that any items that were modified by triggers in the database
+(like our automatic primary keys) will show up correctly in the form.
+
+    function submitChanges( data ) {
+        Ext.Ajax.request({
+            url:        posturl,
+            success:    function() { store.reload() },
+            params:     { changes: Ext.util.JSON.encode( data ) }
+        });
+    }
+
+Now all we need is a new handler in our root controller to process the
+submitted data.
+
+  =head2 people_data_submit
+  
+  =cut
+  
+  use JSON::Any;
+  
+  sub people_data_submit : Local {
+      my ( $self, $c ) = @_;
+  
+      my $rs = $c->model( 'DB::Person' );
+  
+      my $j = JSON::Any->new;
+      my $data = $j->jsonToObj( $c->request->param( 'changes' ) );
+      for my $rec ( @{ $data } ) {
+          $rs->update_or_create( $rec );
+      }
+      $c->detach( $c->view( 'JSON' ) );
+  }
+
+=head3 AUTHOR
+
+Jason Kohles, E<lt>email at jasonkohles.comE<gt>
+
+L<http://www.jasonkohles.com/>
+
+=cut
+




More information about the Catalyst-commits mailing list