Using Catalyst::Controller::REST with jqGrid: Tutorial with Examples for Newbies

  • Introduction
  • Get Started!
  • Using Catalyst::Controller::REST with Catalyst::View::JSON
  • Using Catalyst::Controller::REST without Catalyst::View::JSON
  • MySQL to SQLite
  • What's Next?

Introduction

This is the second in a series of tutorials and examples for Catalyst Newbies. Myself being "over the hill" I naturally feel a kinship with Newbies, and am not ashamed to admit self-identification as a strange old-fashioned geezer-newbie kind of amalgamation. I mean, anybody who would write code in Assembly Language! We'll just stuff those old registers full of stuff, invoke an interrupt or two and be on our way!

In my first tutorial https://blogs.perl.org/users/j0e/2012/09/using-jqgrid-with-catalyst-tutorial-with-examples-for-newbies.html I created a Catalyst application using MySQL, jqGrid, Catalyst::View::JSON and Catalyst::TraitFor::Controller::jQuery::jqGrid.

This second tutorial starts where the final version of the first one left off, and adds a REST controller that uses Catalyst::View::JSON with Catalyst::Controller::REST. After we get the REST controller to work with the JSON view, we will then make it work without it. The purpose of this tutorial is to show you two more ways to communicate with jqGrid: Using a REST controller with and without a JSON view.

Unbeknownst to you—diligent fellow-newbies, I moved from using MySQL to SQLite in these tutorials. I will show you how easily that was done.

Get Started!

We will continue with the final version from the first tutorial and add a controller to use REST. You may want to copy the MyFirstGrid directory from the final version of the first tutorial, to a new location and use that version for this tutorial.

MyFirstGrid.pm:

package MyFirstGrid;
use Moose;
use namespace::autoclean;

use Catalyst::Runtime 5.80;

use Catalyst qw/
    -Debug
    ConfigLoader
    Static::Simple
/;

extends 'Catalyst';

our $VERSION = '0.01';

__PACKAGE__->config(
    name => 'MyFirstGrid',
    # Disable deprecated behavior needed by old applications
    disable_component_resolution_regex_fallback => 1,
    enable_catalyst_header => 1, # Send X-Catalyst header
);

# Specify which stash keys are exposed as a JSON response. 
__PACKAGE__->config({
  'View::JSON' => {
    expose_stash => qw(json_data)
  }
});

# Start the application
__PACKAGE__->setup();

1;

root/index.tt:

<!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>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>My First Grid</title>

<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/cupertino/jquery-ui-1.8.22.custom.css') %]" />
<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/ui.jqgrid.css') %]" />

<style type="text/css">
html, body {
  margin: 0;
  padding: 0;
  font-size: 75%;
}
</style>

<script src="[% c.uri_for('/static/js/jquery-1.7.2.min.js') %]" type="text/javascript"></script>
<script src="[% c.uri_for('/static/js/i18n/grid.locale-en.js') %]" type="text/javascript"></script>
<script src="[% c.uri_for('/static/js/jquery.jqGrid.min.js') %]" type="text/javascript"></script>

<script type="text/javascript">
$(function(){ 

  $("#list").jqGrid({
    url:"[% c.uri_for("getdata") %]",
    datatype:'json',
    mtype:'GET',
    colNames:['Inv No', 'Client ID', 'Amount','Tax','Total','Notes'],
    colModel :[ 
      {name:'inv_id', index:'inv_id', width:55, editable:false, key:true}, 
      {name:'client_id', index:'client_id', width:55, editable:true, editoptions:{size:10}},
      {name:'amount', index:'amount', width:80, align:'right', editable:true, editoptions:{size:10}}, 
      {name:'tax', index:'tax', width:80, align:'right', editable:true, editoptions:{size:10}}, 
      {name:'total', index:'total', width:80, align:'right', editable:true, editoptions:{size:10}}, 
      {name:'note', index:'note', width:150, sortable:false, editable: true, edittype:"textarea", editoptions:{rows:"2",cols:"20"}} 
    ], 
    pager:'#pager',
    rowNum:10,
    rowList:[10,20,30],
    sortname:'inv_id',
    sortorder:'desc',
    viewrecords:true,
    gridview:true,
    caption:'My First Grid',
    editurl:"[% c.uri_for("postrow") %]" 
  }); 

  jQuery("#list").jqGrid('navGrid','#pager',
    {}, //options
    {height:280,reloadAfterSubmit:false}, // edit options
    {height:280,reloadAfterSubmit:false}, // add options
    {reloadAfterSubmit:false}, // del options
    {} // search options
  ); 

}); 
</script>
</head>

<body>
<table id="list"><tr><td/></tr></table> 
<div id="pager"></div> 
</body>
</html>

Controller/Root.pm:

package MyFirstGrid::Controller::Root;
use Moose;
use namespace::autoclean;

BEGIN { extends 'Catalyst::Controller' }
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid'; 

__PACKAGE__->config(namespace => '');

sub index :Path :Args(0) {
  my ($self, $c) = @_;

  $c->detach($c->view("TT"));
}

sub getdata :Local {
  my ($self, $c) = @_;

  my $inv_rs = $c->model('DB::Inventory')->search({});
  $inv_rs = $self->jqgrid_page($c, $inv_rs);
  my @row_data;
  while (my $inv = $inv_rs->next) {
    my $single_row = {
      cell => [
        $inv->inv_id,
        $inv->client_id,
        $inv->amount,
        $inv->tax,
        $inv->total,
        $inv->note,
      ],
    };
    push @row_data, $single_row;
  }
  $c->stash->{json_data}{rows} = \@row_data;
  $c->stash->{current_view} = 'JSON'; 
}

sub postrow :Local {
  my ($self, $c) = @_;

  my $data = $c->request->parameters;
  if ($data->{oper} eq 'edit') { # save row 
    my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
    $inv_rs->update({
      client_id => $data->{client_id},
      amount => $data->{amount},
      tax => $data->{tax}, 
      total => $data->{total}, 
      note => $data->{note}, 
    });
    $c->response->status(204); 
  }
  elsif ($data->{oper} eq 'add') { # add new row
    my $inv_rs = $c->model('DB::Inventory');
    $inv_rs->create({
      client_id => $data->{client_id},
      amount => $data->{amount},
      tax => $data->{tax}, 
      total => $data->{total}, 
      note => $data->{note}, 
    });
    $c->response->status(204); 
  }  
  elsif ($data->{oper} eq 'del') { # delete row
    my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
    $inv_rs->delete();
    $c->response->status(204); 
  }
  else {
    $c->response->body('400 BAD REQUEST: Root/postrow');
    $c->response->status(400); # 400 BAD REQUEST 
  } 
} 

sub default :Path {
  my ($self, $c) = @_;

  $c->response->body('Page not found');
  $c->response->status(404);
}

# Attempt to render a view, if needed.

sub end :ActionClass('RenderView') {}

__PACKAGE__->meta->make_immutable;

1;

Using Catalyst::Controller::REST with Catalyst::View::JSON

Use the Catalyst helper to create a new controller:

$ script/myfirstgrid_create.pl controller Rest

Controller/Rest.pm:

package MyFirstGrid::Controller::Rest;
use Moose;
use namespace::autoclean;

BEGIN {extends 'Catalyst::Controller';}

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;

    $c->response->body('Matched MyFirstGrid::Controller::Rest in Rest.');
}

__PACKAGE__->meta->make_immutable;

1;

We'll extend Catalyst::Controller::REST with Catalyst::TraitFor::Controller::jQuery::jqGrid:

Controller/Rest.pm:

package MyFirstGrid::Controller::Rest;
use Moose;
use namespace::autoclean;

BEGIN {extends 'Catalyst::Controller::REST'}
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid'; 

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;

    $c->response->body('Matched MyFirstGrid::Controller::Rest in Rest.');
}

__PACKAGE__->meta->make_immutable;

1;

Go back and refresh your memory about Catalyst::TraitFor::Controller::jQuery::jqGrid if you don't remember how it can help you:

https://blogs.perl.org/users/j0e/2012/09/using-jqgrid-with-catalyst-tutorial-with-examples-for-newbies.html

We will copy getdata and postrow from the Root controller and use them here in the REST controller. Then we will update url and editurl in our template to use our new REST controller.

Controller/Rest.pm:

package MyFirstGrid::Controller::Rest;
use Moose;
use namespace::autoclean;

BEGIN {extends 'Catalyst::Controller::REST'}
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid'; 

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;

    $c->response->body('Matched MyFirstGrid::Controller::Rest in Rest.');
}

sub getdata :Local :ActionClass('REST') {}

sub getdata_GET { 
  my ($self, $c) = @_;

  my $inv_rs = $c->model('DB::Inventory')->search({});
  $inv_rs = $self->jqgrid_page($c, $inv_rs);
  my @row_data;
  while (my $inv = $inv_rs->next) {
    my $single_row = {
      cell => [
        $inv->inv_id,
        $inv->client_id,
        $inv->amount,
        $inv->tax,
        $inv->total,
        $inv->note,
      ],
    };
    push @row_data, $single_row;
  }
  $c->stash->{json_data}{rows} = \@row_data;
  $c->stash->{current_view} = 'JSON'; 
}

sub postrow :Local :ActionClass('REST') {}

sub postrow_POST {
  my ($self, $c) = @_;

  my $data = $c->request->parameters;
  if ($data->{oper} eq 'edit') { # save row 
    my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
    $inv_rs->update({
      client_id => $data->{client_id},
      amount => $data->{amount},
      tax => $data->{tax}, 
      total => $data->{total}, 
      note => $data->{note}, 
    });
    $c->response->status(204); 
  }
  elsif ($data->{oper} eq 'add') { # add new row
    my $inv_rs = $c->model('DB::Inventory');
    $inv_rs->create({
      client_id => $data->{client_id},
      amount => $data->{amount},
      tax => $data->{tax}, 
      total => $data->{total}, 
      note => $data->{note}, 
    });
    $c->response->status(204); 
  }  
  elsif ($data->{oper} eq 'del') { # delete row
    my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
    $inv_rs->delete();
    $c->response->status(204); 
  }
  else {
    $c->response->body('400 BAD REQUEST: Root/postrow');
    $c->response->status(400); # 400 BAD REQUEST 
  } 
} 

__PACKAGE__->meta->make_immutable;

1;

Update our template to use the REST controller:

root/index.tt:

<!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>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>My First Grid</title>

<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/cupertino/jquery-ui-1.8.22.custom.css') %]" />
<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/ui.jqgrid.css') %]" />

<style type="text/css">
html, body {
  margin: 0;
  padding: 0;
  font-size: 75%;
}
</style>

<script src="[% c.uri_for('/static/js/jquery-1.7.2.min.js') %]" type="text/javascript"></script>
<script src="[% c.uri_for('/static/js/i18n/grid.locale-en.js') %]" type="text/javascript"></script>
<script src="[% c.uri_for('/static/js/jquery.jqGrid.min.js') %]" type="text/javascript"></script>

<script type="text/javascript">
$(function(){ 

  $("#list").jqGrid({
    url:"[% c.uri_for("rest/getdata") %]",
    datatype:'json',
    mtype:'GET',
    colNames:['Inv No', 'Client ID', 'Amount','Tax','Total','Notes'],
    colModel :[ 
      {name:'inv_id', index:'inv_id', width:55, editable:false, key:true}, 
      {name:'client_id', index:'client_id', width:55, editable:true, editoptions:{size:10}},
      {name:'amount', index:'amount', width:80, align:'right', editable:true, editoptions:{size:10}}, 
      {name:'tax', index:'tax', width:80, align:'right', editable:true, editoptions:{size:10}}, 
      {name:'total', index:'total', width:80, align:'right', editable:true, editoptions:{size:10}}, 
      {name:'note', index:'note', width:150, sortable:false, editable: true, edittype:"textarea", editoptions:{rows:"2",cols:"20"}} 
    ], 
    pager:'#pager',
    rowNum:10,
    rowList:[10,20,30],
    sortname:'inv_id',
    sortorder:'desc',
    viewrecords:true,
    gridview:true,
    caption:'My First Grid',
    editurl:"[% c.uri_for("rest/postrow") %]" 
  }); 

  jQuery("#list").jqGrid('navGrid','#pager',
    {}, //options
    {height:280,reloadAfterSubmit:false}, // edit options
    {height:280,reloadAfterSubmit:false}, // add options
    {reloadAfterSubmit:false}, // del options
    {} // search options
  ); 

}); 
</script>
</head>

<body>
<table id="list"><tr><td/></tr></table> 
<div id="pager"></div> 
</body>
</html>

Run it:

$ script/myfirstgrid_server.pl -d -r -k

Open your browser:

http://0.0.0.0:3000

If your results are the same as mine, you will get an empty grid with no data.

Ouch, what went wrong?

This is where my skill-level as a Newbie is confirmed: I know how to fix this, but I don't clearly understand why. For some reason the last line of getdata_GET is causing our problem:

$c->stash->{current_view} = 'JSON';

The Catalyst console gives:

[debug] "GET" request for "rest/getdata" from "127.0.0.1"
[debug] Query Parameters are:
.-------------------------------------+--------------------------------------.
| Parameter                           | Value                                |
+-------------------------------------+--------------------------------------+
| _search                             | false                                |
| nd                                  | 1347648333373                        |
| page                                | 1                                    |
| rows                                | 10                                   |
| sidx                                | inv_id                               |
| sord                                | desc                                 |
'-------------------------------------+--------------------------------------'
[debug] Serializing with Catalyst::Action::Serialize::JSON
[debug] Response Code: 400; Content-Type: text/plain; Content-Length: 182
[info] Request took 0.102985s (9.710/s)
.------------------------------------------------------------+-----------.
| Action                                                     | Time      |
+------------------------------------------------------------+-----------+
| /rest/begin                                                | 0.002102s |
| /rest/getdata                                              | 0.000257s |
| /getdata_GET                                               | 0.069467s |
| /rest/end                                                  | 0.020478s |
'------------------------------------------------------------+-----------'

For some reason we are no longer rendering our JSON view.

Changing the last line of getdata_GET to explicitly render the view fixes it:

$c->detach( $c->view("JSON") );

Run it and we get data in the grid, and a little different feedback from the console:

[debug] "GET" request for "rest/getdata" from "127.0.0.1"
[debug] Query Parameters are:
.-------------------------------------+--------------------------------------.
| Parameter                           | Value                                |
+-------------------------------------+--------------------------------------+
| _search                             | false                                |
| nd                                  | 1347649047150                        |
| page                                | 1                                    |
| rows                                | 10                                   |
| sidx                                | inv_id                               |
| sord                                | desc                                 |
'-------------------------------------+--------------------------------------'
[debug] Response Code: 200; Content-Type: application/json; charset=utf-8; Content-Length: 644
[info] Request took 0.094290s (10.606/s)
.------------------------------------------------------------+-----------.
| Action                                                     | Time      |
+------------------------------------------------------------+-----------+
| /rest/begin                                                | 0.000499s |
| /rest/getdata                                              | 0.000187s |
| /getdata_GET                                               | 0.087410s |
|  -> MyFirstGrid::View::JSON->process                       | 0.001557s |
| /rest/end                                                  | 0.000208s |
'------------------------------------------------------------+-----------'

It seems for some reason our JSON view had not been getting rendered. If you know why, please post a comment with an explanation.

Here is the complete REST controller. It should work correctly:

Controller/Rest.pm:

package MyFirstGrid::Controller::Rest;
use Moose;
use namespace::autoclean;

BEGIN {extends 'Catalyst::Controller::REST'}
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid'; 

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;

    $c->response->body('Matched MyFirstGrid::Controller::Rest in Rest.');
}

sub getdata :Local :ActionClass('REST') {}

sub getdata_GET { 
  my ($self, $c) = @_;

  my $inv_rs = $c->model('DB::Inventory')->search({});
  $inv_rs = $self->jqgrid_page($c, $inv_rs);
  my @row_data;
  while (my $inv = $inv_rs->next) {
    my $single_row = {
      cell => [
        $inv->inv_id,
        $inv->client_id,
        $inv->amount,
        $inv->tax,
        $inv->total,
        $inv->note,
      ],
    };
    push @row_data, $single_row;
  }
  $c->stash->{json_data}{rows} = \@row_data;
#  $c->stash->{current_view} = 'JSON'; 
  $c->detach( $c->view("JSON") ); 
}

sub postrow :Local :ActionClass('REST') {}

sub postrow_POST {
  my ($self, $c) = @_;

  my $data = $c->request->parameters;
  if ($data->{oper} eq 'edit') { # save row 
    my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
    $inv_rs->update({
      client_id => $data->{client_id},
      amount => $data->{amount},
      tax => $data->{tax}, 
      total => $data->{total}, 
      note => $data->{note}, 
    });
    $c->response->status(204); 
  }
  elsif ($data->{oper} eq 'add') { # add new row
    my $inv_rs = $c->model('DB::Inventory');
    $inv_rs->create({
      client_id => $data->{client_id},
      amount => $data->{amount},
      tax => $data->{tax}, 
      total => $data->{total}, 
      note => $data->{note}, 
    });
    $c->response->status(204); 
  }  
  elsif ($data->{oper} eq 'del') { # delete row
    my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
    $inv_rs->delete();
    $c->response->status(204); 
  }
  else {
    $c->response->body('400 BAD REQUEST: Root/postrow');
    $c->response->status(400); # 400 BAD REQUEST 
  } 
} 

__PACKAGE__->meta->make_immutable;

1;

Using Catalyst::Controller::REST without Catalyst::View::JSON

We are now able to use a JSON view with our REST controller. Next we will create a new REST controller, and let it serialize responses itself rather than with a JSON view.

Save Controller/Rest.pm as Controller/Restnv.pm and change class names accordingly:

Controller/Restnv.pm:

package MyFirstGrid::Controller::Restnv;
use Moose;
use namespace::autoclean;

BEGIN {extends 'Catalyst::Controller::REST'}
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid'; 

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;

    $c->response->body('Matched MyFirstGrid::Controller::Restnv in Rest.');
}

sub getdata :Local :ActionClass('REST') {}

sub getdata_GET { 
  my ($self, $c) = @_;

  my $inv_rs = $c->model('DB::Inventory')->search({});
  $inv_rs = $self->jqgrid_page($c, $inv_rs);
  my @row_data;
  while (my $inv = $inv_rs->next) {
    my $single_row = {
      cell => [
        $inv->inv_id,
        $inv->client_id,
        $inv->amount,
        $inv->tax,
        $inv->total,
        $inv->note,
      ],
    };
    push @row_data, $single_row;
  }
  $c->stash->{json_data}{rows} = \@row_data;
#  $c->stash->{current_view} = 'JSON'; 
  $c->detach( $c->view("JSON") ); 
}

sub postrow :Local :ActionClass('REST') {}

sub postrow_POST {
  my ($self, $c) = @_;

  my $data = $c->request->parameters;
  if ($data->{oper} eq 'edit') { # save row 
    my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
    $inv_rs->update({
      client_id => $data->{client_id},
      amount => $data->{amount},
      tax => $data->{tax}, 
      total => $data->{total}, 
      note => $data->{note}, 
    });
    $c->response->status(204); 
  }
  elsif ($data->{oper} eq 'add') { # add new row
    my $inv_rs = $c->model('DB::Inventory');
    $inv_rs->create({
      client_id => $data->{client_id},
      amount => $data->{amount},
      tax => $data->{tax}, 
      total => $data->{total}, 
      note => $data->{note}, 
    });
    $c->response->status(204); 
  }  
  elsif ($data->{oper} eq 'del') { # delete row
    my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
    $inv_rs->delete();
    $c->response->status(204); 
  }
  else {
    $c->response->body('400 BAD REQUEST: Root/postrow');
    $c->response->status(400); # 400 BAD REQUEST 
  } 
} 

__PACKAGE__->meta->make_immutable;

1;

Update the template to use the new controller:

root/index.tt:

<!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>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>My First Grid</title>

<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/cupertino/jquery-ui-1.8.22.custom.css') %]" />
<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/ui.jqgrid.css') %]" />

<style type="text/css">
html, body {
  margin: 0;
  padding: 0;
  font-size: 75%;
}
</style>

<script src="[% c.uri_for('/static/js/jquery-1.7.2.min.js') %]" type="text/javascript"></script>
<script src="[% c.uri_for('/static/js/i18n/grid.locale-en.js') %]" type="text/javascript"></script>
<script src="[% c.uri_for('/static/js/jquery.jqGrid.min.js') %]" type="text/javascript"></script>

<script type="text/javascript">
$(function(){ 

  $("#list").jqGrid({
//    url:"[% c.uri_for("getdata") %]",
//    url:"[% c.uri_for("rest/getdata") %]",
    url:"[% c.uri_for("restnv/getdata") %]",
    datatype:'json',
    mtype:'GET',
    colNames:['Inv No', 'Client ID', 'Amount','Tax','Total','Notes'],
    colModel :[ 
      {name:'inv_id', index:'inv_id', width:55, editable:false, key:true}, 
      {name:'client_id', index:'client_id', width:55, editable:true, editoptions:{size:10}},
      {name:'amount', index:'amount', width:80, align:'right', editable:true, editoptions:{size:10}}, 
      {name:'tax', index:'tax', width:80, align:'right', editable:true, editoptions:{size:10}}, 
      {name:'total', index:'total', width:80, align:'right', editable:true, editoptions:{size:10}}, 
      {name:'note', index:'note', width:150, sortable:false, editable: true, edittype:"textarea", editoptions:{rows:"2",cols:"20"}} 
    ], 
    pager:'#pager',
    rowNum:10,
    rowList:[10,20,30],
    sortname:'inv_id',
    sortorder:'desc',
    viewrecords:true,
    gridview:true,
    caption:'My First Grid',
//    editurl:"[% c.uri_for("postrow") %]" 
//    editurl:"[% c.uri_for("rest/postrow") %]" 
    editurl:"[% c.uri_for("restnv/postrow") %]" 
  }); 

  jQuery("#list").jqGrid('navGrid','#pager',
    {}, //options
    {height:280,reloadAfterSubmit:false}, // edit options
    {height:280,reloadAfterSubmit:false}, // add options
    {reloadAfterSubmit:false}, // del options
    {} // search options
  ); 

}); 
</script>
</head>

<body>
<table id="list"><tr><td/></tr></table> 
<div id="pager"></div> 
</body>
</html>

Run it and the new controller should be working perfectly. You should be able to add, edit and delete rows with no problems, after all the new Restnv controller is identical to the Rest controller except for it's name: They both use Catalyst::Controller::REST with a JSON view.

How do we make the Restnv controller not use the JSON view?

The only place we render a JSON view in the Restnv controller is in the last line of the getrow_GET method:

$c->detach( $c->view("JSON") );

So let's change it to one of the Catalyst::Controller::REST Status Helpers in it's documentation:

  • status_ok: Returns a "200 OK" response. Takes an "entity" to serialize.
  • status_created: Returns a "201 CREATED" response. Takes an "entity" to serialize, and a "location" where the created object can be found.
  • status_accepted: Returns a "202 ACCEPTED" response. Takes an "entity" to serialize. Also takes optional "location" for queue type scenarios.
  • status_no_content: Returns a "204 NO CONTENT" response.
  • status_multiple_choices: Returns a "300 MULTIPLE CHOICES" response. Takes an "entity" to serialize, which should provide list of possible locations. Also takes optional "location" for preferred choice.
  • status_found: Returns a "302 FOUND" response. Takes an "entity" to serialize. Also takes optional "location".
  • status_bad_request: Returns a "400 BAD REQUEST" response. Takes a "message" argument as a scalar, which will become the value of "error" in the serialized response.
  • status_forbidden: Returns a "403 FORBIDDEN" response. Takes a "message" argument as a scalar, which will become the value of "error" in the serialized response.
  • status_not_found: Returns a "404 NOT FOUND" response. Takes a "message" argument as a scalar, which will become the value of "error" in the serialized response.
  • gone: Returns a "41O GONE" response. Takes a "message" argument as a scalar, which will become the value of "error" in the serialized response.

Change getdata_GET to use the status_ok helper:

$self->status_ok(
  $c,
  entity => {
    rows => \@row_data,
  },
);

Controller/Restnv.pm:

package MyFirstGrid::Controller::Restnv;
use Moose;
use namespace::autoclean;

BEGIN {extends 'Catalyst::Controller::REST'}
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid'; 

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;

    $c->response->body('Matched MyFirstGrid::Controller::Restnv in Rest.');
}

sub getdata :Local :ActionClass('REST') {}

sub getdata_GET { 
  my ($self, $c) = @_;

  my $inv_rs = $c->model('DB::Inventory')->search({});
  $inv_rs = $self->jqgrid_page($c, $inv_rs);
  my @row_data;
  while (my $inv = $inv_rs->next) {
    my $single_row = {
      cell => [
        $inv->inv_id,
        $inv->client_id,
        $inv->amount,
        $inv->tax,
        $inv->total,
        $inv->note,
      ],
    };
    push @row_data, $single_row;
  }

  $self->status_ok(
    $c,
    entity => {
      rows => \@row_data,
    },
  );  
}

sub postrow :Local :ActionClass('REST') {}

sub postrow_POST {
  my ($self, $c) = @_;

  my $data = $c->request->parameters;
  if ($data->{oper} eq 'edit') { # save row 
    my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
    $inv_rs->update({
      client_id => $data->{client_id},
      amount => $data->{amount},
      tax => $data->{tax}, 
      total => $data->{total}, 
      note => $data->{note}, 
    });
    $c->response->status(204); 
  }
  elsif ($data->{oper} eq 'add') { # add new row
    my $inv_rs = $c->model('DB::Inventory');
    $inv_rs->create({
      client_id => $data->{client_id},
      amount => $data->{amount},
      tax => $data->{tax}, 
      total => $data->{total}, 
      note => $data->{note}, 
    });
    $c->response->status(204); 
  }  
  elsif ($data->{oper} eq 'del') { # delete row
    my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
    $inv_rs->delete();
    $c->response->status(204); 
  }
  else {
    $c->response->body('400 BAD REQUEST: Root/postrow');
    $c->response->status(400); # 400 BAD REQUEST 
  } 
} 

__PACKAGE__->meta->make_immutable;

1;

Run it and the grid looks good. Data is displayed and you can edit, add and delete rows—you can do everything except view the next page in the grid.

Oops!

I discovered what is wrong by using my browser's developer tools to look at my GET response. First I looked at my current response:

{"rows":[{"cell":[37,2112,139.98,81.34,221.32,"This is record 37."]},{"cell":[36,3632,138.98,80.34,219.32,"This is record 36."]},{"cell":[35,8471,137.98,79.34,217.32,"This is record 35."]},{"cell":[34,7377,136.98,78.34,215.32,"This is record 34."]},{"cell":[33,8221,135.98,77.34,213.32,"This is record 33."]},{"cell":[32,2985,134.98,76.34,211.32,"This is record 32."]},{"cell":[31,9483,133.98,75.34,209.32,"This is record 31."]},{"cell":[30,5232,132.98,74.34,207.32,"This is record 30."]},{"cell":[29,6483,131.98,73.34,205.32,"This is record 29."]},{"cell":[28,2232,130.98,72.34,203.32,"This is record 28."]}]}

Then I compared this to a response that was working earlier:

{"page":"1","records":37,"rows":[{"cell":[37,2112,139.98,81.34,221.32,"This is record 37."]},{"cell":[36,3632,138.98,80.34,219.32,"This is record 36."]},{"cell":[35,8471,137.98,79.34,217.32,"This is record 35."]},{"cell":[34,7377,136.98,78.34,215.32,"This is record 34."]},{"cell":[33,8221,135.98,77.34,213.32,"This is record 33."]},{"cell":[32,2985,134.98,76.34,211.32,"This is record 32."]},{"cell":[31,9483,133.98,75.34,209.32,"This is record 31."]},{"cell":[30,5232,132.98,74.34,207.32,"This is record 30."]},{"cell":[29,6483,131.98,73.34,205.32,"This is record 29."]},{"cell":[28,2232,130.98,72.34,203.32,"This is record 28."]}],"total":4}

Ahh, I am currently not providing the following:

"page":"1","records":37,"total":4

The Catalyst::TraitFor::Controller::jQuery::jqGrid docs state the following:

The jqgrid_page method puts the 'page', 'total' and 'records' information onto the stash.

Ahh-ha, the JSON view uses the stash but Catalyst::Controller::REST does not! We have to change this in getdata_GET:

Change this:

$self->status_ok(
  $c,
  entity => {
    rows => \@row_data,
  },
);

To this:

$self->status_ok($c, entity => {
  page => $c->stash->{json_data}{page},  
  records => $c->stash->{json_data}{records},  
  total => $c->stash->{json_data}{total},  
  rows => \@row_data,
});

Run it and you will find that everything works perfectly, our response is correct and we can page through our data on the grid:

{"page":"1","records":37,"rows":[{"cell":[37,2112,139.98,81.34,221.32,"This is record 37."]},{"cell":[36,3632,138.98,80.34,219.32,"This is record 36."]},{"cell":[35,8471,137.98,79.34,217.32,"This is record 35."]},{"cell":[34,7377,136.98,78.34,215.32,"This is record 34."]},{"cell":[33,8221,135.98,77.34,213.32,"This is record 33."]},{"cell":[32,2985,134.98,76.34,211.32,"This is record 32."]},{"cell":[31,9483,133.98,75.34,209.32,"This is record 31."]},{"cell":[30,5232,132.98,74.34,207.32,"This is record 30."]},{"cell":[29,6483,131.98,73.34,205.32,"This is record 29."]},{"cell":[28,2232,130.98,72.34,203.32,"This is record 28."]}],"total":4}

Controller/Restnv:

package MyFirstGrid::Controller::Restnv;
use Moose;
use namespace::autoclean;

BEGIN {extends 'Catalyst::Controller::REST'}
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid'; 

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;

    $c->response->body('Matched MyFirstGrid::Controller::Restnv in Rest.');
}

sub getdata :Local :ActionClass('REST') {}

sub getdata_GET { 
  my ($self, $c) = @_;

  my $inv_rs = $c->model('DB::Inventory')->search({});
  $inv_rs = $self->jqgrid_page($c, $inv_rs);
  my @row_data;
  while (my $inv = $inv_rs->next) {
    my $single_row = {
      cell => [
        $inv->inv_id,
        $inv->client_id,
        $inv->amount,
        $inv->tax,
        $inv->total,
        $inv->note,
      ],
    };
    push @row_data, $single_row;
  }

  $self->status_ok($c, entity => {
    page => $c->stash->{json_data}{page},  
    records => $c->stash->{json_data}{records},  
    total => $c->stash->{json_data}{total},  
    rows => \@row_data,
  }); 
}

sub postrow :Local :ActionClass('REST') {}

sub postrow_POST {
  my ($self, $c) = @_;

  my $data = $c->request->parameters;
  if ($data->{oper} eq 'edit') { # save row 
    my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
    $inv_rs->update({
      client_id => $data->{client_id},
      amount => $data->{amount},
      tax => $data->{tax}, 
      total => $data->{total}, 
      note => $data->{note}, 
    });
    $c->response->status(204); 
  }
  elsif ($data->{oper} eq 'add') { # add new row
    my $inv_rs = $c->model('DB::Inventory');
    $inv_rs->create({
      client_id => $data->{client_id},
      amount => $data->{amount},
      tax => $data->{tax}, 
      total => $data->{total}, 
      note => $data->{note}, 
    });
    $c->response->status(204); 
  }  
  elsif ($data->{oper} eq 'del') { # delete row
    my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
    $inv_rs->delete();
    $c->response->status(204); 
  }
  else {
    $c->response->body('400 BAD REQUEST: Root/postrow');
    $c->response->status(400); # 400 BAD REQUEST 
  } 
} 

__PACKAGE__->meta->make_immutable;

1;

MySQL to SQLite

As mentioned previously, this tutorial uses SQLite rather than MySQL. I will explain how you can use it too.

Create a SQLite database file:

$ sqlite3 myfirstgrid.db < myfirstgrid.sql

This creates a datebase file (myfirstgrid.db) from myfirstgrid.sql:

myfirstgrid.sql:

CREATE TABLE inventory (                                                     
  inv_id integer primary key autoincrement,                                             
  client_id integer not null,                                                     
  amount real not null default '0.00',                                   
  tax real not null default '0.00',                                      
  total real not null default '0.00',                                    
  note text default null                                 
);
INSERT INTO inventory VALUES (1,2339,103.98,45.34,149.32,"This is record 1.");
INSERT INTO inventory VALUES (2,1171,104.98,46.34,151.32,"This is record 2."); 
INSERT INTO inventory VALUES (3,5232,105.98,47.34,153.32,"This is record 3."); 
INSERT INTO inventory VALUES (4,9110,106.98,48.34,155.32,"This is record 4."); 
INSERT INTO inventory VALUES (5,5232,107.98,49.34,157.32,"This is record 5."); 
INSERT INTO inventory VALUES (6,8471,108.98,50.34,159.32,"This is record 6."); 
INSERT INTO inventory VALUES (7,2339,109.98,51.34,161.32,"This is record 7."); 
INSERT INTO inventory VALUES (8,8471,110.98,52.34,163.32,"This is record 8."); 
INSERT INTO inventory VALUES (9,3632,111.98,53.34,165.32,"This is record 9."); 
INSERT INTO inventory VALUES (10,6483,112.98,54.34,167.32,"This is record 10."); 
INSERT INTO inventory VALUES (11,2333,113.98,55.34,169.32,"This is record 11."); 
INSERT INTO inventory VALUES (12,9110,114.98,56.34,171.32,"This is record 12."); 
INSERT INTO inventory VALUES (13,1005,115.98,57.34,173.32,"This is record 13."); 
INSERT INTO inventory VALUES (14,9987,116.98,58.34,175.32,"This is record 14."); 
INSERT INTO inventory VALUES (15,6483,117.98,59.34,177.32,"This is record 15."); 
INSERT INTO inventory VALUES (16,5249,118.98,60.34,179.32,"This is record 16."); 
INSERT INTO inventory VALUES (17,2333,119.98,61.34,181.32,"This is record 17."); 
INSERT INTO inventory VALUES (18,1171,120.98,62.34,183.32,"This is record 18."); 
INSERT INTO inventory VALUES (19,1005,121.98,63.34,185.32,"This is record 19."); 
INSERT INTO inventory VALUES (20,7377,122.98,64.34,187.32,"This is record 20."); 
INSERT INTO inventory VALUES (21,1171,123.98,65.34,189.32,"This is record 21."); 
INSERT INTO inventory VALUES (22,5232,124.98,66.34,191.32,"This is record 22."); 
INSERT INTO inventory VALUES (23,9110,125.98,67.34,193.32,"This is record 23."); 
INSERT INTO inventory VALUES (24,1728,126.98,68.34,195.32,"This is record 24."); 
INSERT INTO inventory VALUES (25,4732,127.98,69.34,197.32,"This is record 25."); 
INSERT INTO inventory VALUES (26,8221,128.98,70.34,199.32,"This is record 26."); 
INSERT INTO inventory VALUES (27,9110,129.98,71.34,201.32,"This is record 27."); 
INSERT INTO inventory VALUES (28,2232,130.98,72.34,203.32,"This is record 28."); 
INSERT INTO inventory VALUES (29,6483,131.98,73.34,205.32,"This is record 29."); 
INSERT INTO inventory VALUES (30,5232,132.98,74.34,207.32,"This is record 30."); 
INSERT INTO inventory VALUES (31,9483,133.98,75.34,209.32,"This is record 31."); 
INSERT INTO inventory VALUES (32,2985,134.98,76.34,211.32,"This is record 32."); 
INSERT INTO inventory VALUES (33,8221,135.98,77.34,213.32,"This is record 33."); 
INSERT INTO inventory VALUES (34,7377,136.98,78.34,215.32,"This is record 34."); 
INSERT INTO inventory VALUES (35,8471,137.98,79.34,217.32,"This is record 35."); 
INSERT INTO inventory VALUES (36,3632,138.98,80.34,219.32,"This is record 36."); 
INSERT INTO inventory VALUES (37,2112,139.98,81.34,221.32,"This is record 37.");

I don't know if it is necessary, but I deleted the lib/MyFirstGrid/Model and lib/MyFirstGrid/Schema directories before using the helper to recreate them for our new SQLite database. I copied myfirstgrid.db to MyFirstGrid/myfirstgrid.db. Here is the helper command to recreate the Model and Schema:

$ script/myfirstgrid_create.pl model DB DBIC::Schema \
MyFirstGrid::Schema::DB create=static dbi:SQLite:myfirstgrid.db

What's Next?

Catalyst::Plugin::AutoCRUD

5 Comments

Hi! Thanks for posting great tutorials. I was trying to follow this one, and I seem to have gotten lost. My index page, which should show the Jqgrid is just rendering these characters:
--- ~

I wanted to ask if this is the working method to display to the index page:
sub index :Path :Args(0) {
my ( $self, $c ) = @_;

$c->response->body('Matched MyFirstGrid::Controller::Restnv in Rest.');
}

Thanks!

I was still having issues configuring the application for REST. I have a couple of questions that I hope you can help me with:
1) Are you still using the View::JSON config in your MyFirstGrid.pm file
2) Loading the index page is showing as:
-- ~
from the logs, the index page was not properly loaded by the server:

[debug] Serializing with Catalyst::Action::Serialize::YAML::HTML
[debug] Response Code: 200; Content-Type: text/html; Content-Length: 67
[info] Request took 0.014918s (67.033/s)

Thanks again for your help!

Your last response helped a lot. I finally figured it out. The index action I was using was within a REST controller, which is why the response is in YAML. I just had to move it to a non REST controller for it to work.

Thanks again for this great tutorial!

Leave a comment

About j0e

user-pic I have experience and am skilled at maintaining old school Perl 5 and am seeking opportunities to use modern Perl.