http://qs321.pair.com?node_id=842126

I use Perl almost exclusively for Web development: Web sites and Web applications. The former usually involves a proprietary CMS that saves to a MySQL database and then displays the pages as requested by the visitor through a templating system. The second uses pretty standard BREAD method, and is usually an add-on feature for the public facing site.

Many of my Web apps involve forms. Though I understand the security issues of client-side validation via Javascript, I've never liked the trip to server with the obvious screen refresh. There was the obvious visual feedback, but as a coder it meant keeping track of lots of things in the callbacks, especially in proper error handler so necessary for good forms etiquette .

Enter jQuery (or Moo Tools or Prototype). I've been hanging around the Monastery long enough to know Javascript has not always been appreciated around here. But that's before some good folks at MIT gave us these great libraries that took the Javascript out of the HTML and legitimized this potentially clever language. Suddenly I can give my sites new features and effects, with little code and no Flash (never a fan).

So, while I'm sure that Javascript still has its detractors here in the Monastery, even clothed with these great new wrappings, I'm a now a believer. Below I would like to show how I have learned to use jQuery, some plugins, and JSON to still play it safe with server-side validation, no eval javascript (thank you jQuery 1.4.2), but to achieve the crisper response expected by today's Web users. I hope that it will be a help to those Web developers who are diehard Perl users, but want to try libraries like jQuery. As always, there are many ways to approach it. This is mine. I hope it helps someone out there.

Try it: Demo

Note: my example will show a typical LAMP setup with CGI::Application, and few of it's plugins.

List of Assets

Update 1: Added the CSS code (very bottom)

Update 2: Thanks to prompting by dws, a clue from afoken, and an example by ikegami [id://8E4324|here] I've added escaping of column names in record(), and learned something.

Update 3: Added <readmore> tags

Flow of "Application:"

  1. User fills out form, but when they click their choice of "Ethnicity" radio button, jQuery fires off a request:
    <script type="text/javascript"> $(function(){ $(".ethnicity").click(function(){ $.getJSON('menu.cgi', { rm:'g', ethnicity:$(this).val() }, ....
  2. A request is then sent to the server's Perl module that queries the database for related entrees, which are returned as a JSON object,
    my $stmt = qq~SELECT id, entrees FROM menu WHERE ethnicity = ?~; my $entrees = $self->dbh->selectall_arrayref($stmt, {Slice => {}}, $se +lf->query->param('ethnicity')); return $self->json_body( $entrees); #using CGI::App::Plugin::JSON
  3. The returned result is parsed, and a set of options is built for the Entree select tag:
    function(result){ // returns: [{"entrees":"Lasagna","id":"1"},{"entrees":"Spaghetti","id +":"2"},{"entrees":"Pizza","id":"3"}] // build options under select var options = '<option value="">Select one...</option>'; for ( i = 0; i < result.length; i++ ) { options = options+ '<option value="'+result[i].id+'">'+result[+i] +.entrees+'</option>'; } $('#entree').html(options);
  4. The form is submitted and jQuery handles the submit $('#meals').submit(function() {
  5. jQuery posts the form data to the server by firing a instance of the Perl script:
    $('#menu').submit(function() { $.ajax({ type: "POST", url: $(this).attr('action'), datatype: "json", data: $(this).serialize(), //function of jquery.form.js success: function(result){ //pass JSON object, form name, name of success div // to external .js library used by all forms parse_results(result,'menu','msgs'); } }); return false; });
  6. The data is validated by calling the save_form runmode in the Perl script:
    sub save_form { my $self = shift; $self->validate_form(); if ( $self->param('error_list')) { my $result = [{ 'messages' => $self->param('error_list') }]; return $self->json_body( $result ); }
    • if there are errors, the error messages are returned as a JSON object and displayed in the label tag above the offending form element:
      $.ajax({ type: "POST", url: $(this).attr('action'), datatype: "json", data: $(this).serialize(), success: function(result){ //pass JSON object, form name, name of success div // to external .js library used by all forms parse_results(result,'menu','msgs'); } }); ... function parse_results(result,form, msgdiv) { var success = ''; var msgArray = result[0].messages; $.each(msgArray, function(i,o) { for (var p in o) { var val = o[p]; if (p == 'success') { success += '<p class="success">' + val + '</p>'; } else { $($("label[for='"+p+"']")).addClass('error').append(' '+val +); } } });
    • if there are no errors, the form data is recorded, a "success" message created and returned as a JSON object:
      $self->record(); $self->param('success_list' => [{'success' => 'Record added'}]); my $result = [{ 'messages' => $self->param('success_list') }]; return $self->json_body( $result);
      where it is displayed in a once hidden div:
      if (success) { $('#'+form).resetForm();//jquery.form.js feature $('#'+form).hide("fast"); //hide the form if you want $('#'+msgdiv).css('display','block'); //display the success $('#'+msgdiv).append(success); // div and message }

Full HTML:

<!--include common header for all pages --> <tmpl_include header.tmpl> <h2>Meal Choice</h2> <!-- later, for success messages --> <div id="msgs"> </div> <div class="form"> <form id="menu" action="menu.cgi" method="post"> <input type="hidden" name="rm" value="s" /> <p class="wrap"> <label for="name" class="blabel">Name</label> <input type="text" name="name" id="name" value="" /> <p class="wrap"> <label for="ethnicity" class="blabel">Ethnicity</label> <input type="radio" name="ethnicity" class="ethnicity" value= +"italian" /> Italian <input type="radio" name="ethnicity" class="ethnicity" value= +"chinese" /> Chinese </p> <p class="wrap"> <label for="entree" class="blabel">Entree</label> <select name="entree" id="entree"> </select> </p> <p class="wrap"> <label for="email" class="blabel">Email address</label> <input type="text" name="email" id="email" value="" /> </p> <p class="wrap"> <input type="submit" name="submitBtn" value="Submit" /> </p> </form> </div> </readmore> <p><b>jQuery in HTML:</b></p> <readmore> <script type="text/javascript" language="javascript"> $(function() { $(".ethnicity").click(function(){ // jQuery 1.4 introduced getJSON to simplify the call // $.getJSON(instancescript, key:values, process_result) $.getJSON('menu.cgi', { rm:'g', ethnicity:$(this).val() }, function(result){ // returns: [{"entrees":"Lasagna","id":"1"},{"entrees": +"Spaghetti","id":"2"},{"entrees":"Pizza","id":"3"}] // build options under select var options = '<option value="">Select one...</option>' +; for ( i = 0; i < result.length; i++ ) { options = options+ '<option value="'+result[i].id+'" +>'+result[+i].entrees+'</option>'; } $('#entree').html(options); } ); }); $('#menu').submit(function() { //removes error messages so they don't double up // on a resubmit still with errors normalize_labels(this,'000'); $.ajax({ type: "POST", url: $(this).attr('action'), datatype: "json", data: $(this).serialize(), success: function(result){ //pass JSON object, form name, name of success div // to external .js library used by all forms parse_results(result,'menu','msgs'); } }); return false; }); }); </script> </script> <!--pull in a standard footer for all pages --> <tmpl_include footer.tmpl>

External Javascript (forms.js)

//called in HTML <head> with: //<script type="text/javascript" src="forms.js"></script> function normalize_labels(element,color) { $('label').each(function(){ //there might be a better way but these next 5 lines // get the original text for the label from the "for" // attribute, makes it presentable, and then // places it back in the form var lab = $(this).attr('for'); lab = lab.slice(0,1).toUpperCase() + lab.slice(1); lab = lab.replace(/_/g, " "); $(this).css({'color':'#'+color}); $(this).text(lab); }); } function parse_results(result,form, msgdiv) { var success = ''; var msgArray = result[0].messages; $.each(msgArray, function(i,o) { for (var p in o) { var val = o[p]; //p is the key (id) in this case, and // val is the message if (p == 'success') { //build html for a success message success += '<p class="success">' + val + '</p>'; } else { //display errors where labels were $($("label[for='"+p+"']")).addClass('error').append(' ' ++val); } } });//each if (success) { $('#'+form).resetForm();//jquery.form.js feature $('#'+form).hide("fast"); //hide the form if you want $('#'+msgdiv).css('display','block'); //display the success $('#'+msgdiv).append(success); // div and message } }

Perl to handle in-place population

#--- Get entrees on the fly sub get_entrees { my $self = shift; my $stmt = qq~SELECT id, entrees FROM menu WHERE ethnicity = ?~; my $entrees = $self->dbh->selectall_arrayref($stmt, {Slice => {}}, + $self->query->param('ethnicity')); return $self->json_body( $entrees); }

PERL to process form:

use CGI::Application::Plugin::DBH (qw/dbh_config dbh/); use CGI::Application::Plugin::JSON ':all'; #--- Save sub save_form { my $self = shift; $self->validate_form(); if ( $self->param('error_list')) { my $result = [{ 'messages' => $self->param('error_list') }]; return $self->json_body( $result ); } $self->record(); my $result = [{ 'messages' => $self->param('success_list') }]; return $self->json_body( $result); } #--- Validate sub validate_form { my $self = shift; my (%sql, $error, @error_list); ($sql{'name'}, $error) = $self->val_input(1, 32, $self->query->para +m('name') ); if ( $error-> { msg } ) { push @error_list, { "name" => $error-> +{ msg } }; } ($sql{'ethnicity'}, $error) = $self->val_input( 1, 16, $self->query +->param('ethnicity') ); if ( $error-> { msg } ) { push @error_list, { "ethnicity" => $er +ror->{ msg } }; } ($sql{'entree'}, $error) = $self->val_selected ($self->query->param +('entree') ); if ( $error-> { msg } ) { push @error_list, { "entree" => $error +->{ msg } }; } ($sql{'email'}, $error) = $self->val_email( 1, $self->query->param( +'email') ); if ( $error-> { msg } ) { push @error_list, { "email" => $error- +>{ msg } }; } if (@error_list) { $self->param('error_list' => \@error_list) } $self->param('sql' => \%sql); } #--- Record sub record { my $self = shift; my %sql = %{ $self->param('sql') }; my @cols = map $self->dbh->quote_identifier($_), keys %sql; my $stmt = 'INSERT INTO entrees (created_on,' . join(',', @cols) . +') VALUES (NOW(),' . join(',', ('?') x @cols) . ')'; $self->dbh->do( $stmt, undef, values %sql); + $self->param('success_list' => [{'success' => 'Record added'}]); }

External Validation.pm

use Email::Valid; sub val_input { my $self = shift; my ($mand, $len, $value) = @_; if (!$value && $mand) { return (undef, { msg => 'cannot be blank' }); } elsif ($len && (length($value) > $len) ) { return (undef, { msg => 'is limited to '.$len.' characters' }); } elsif ($value && $value !~ /^([\w \.\,\-\(\)\?\:\;\"\!\'\/\n\r]*) +$/) { return (undef, { msg => 'can only use letters, numbers, spaces a +nd -.,&:\'' }); } else { my $tf = new HTML::TagFilter; return ($tf->filter($1)); } } sub val_email { my $self = shift; my ($mand, $value) = @_; if ( !Email::Valid->address($value) && $mand ) { return ( undef, { msg => 'address does not appear to be valid or + is blank' } ); } elsif ( !Email::Valid->address($value) && $value ) { return ( undef, { msg => 'address does not appear to be valid or + is blank' } ); } else { return $value; } } sub val_selected { my $self = shift; my ($value) = @_; if (!$value) { return (undef, { msg => 'must be selected' }); } else { return $value; } }

External CSS (just for the curious)

.form { float: left; width: 100%; margin: 10px 0 40px 0px; } label { display: block; } input:focus, textarea:focus { background: #F5F5DC; } p.wrap { font: 11px/15px verdana, sans-serif; margin: 6px 0 0 0; width: 100%; clear:both; } input, textarea,.label, .blabel { font: 11px/15px verdana, sans-serif; } input[type="text"] { width: 240px; margin-top: 0 } input[type="radio"] {margin: 5px 2px 0 0 } input[id=submitBtn] {margin: 10px 0 0 } .label, .blabel { line-height: 18px; padding-right: 7px; margin: 0; } .blabel { font-weight: bold; } #msgs { display: none; margin: 0 0 0 20px; } p.success { margin: 20px 0 0 20px; padding: 0 0 0 20px; font: bold 12px verdana, sans-serif; color: green; background: url(/images/success.png) no-repeat 5px; }

Replies are listed 'Best First'.
Re: RFC:Tutorial: Using jQuery, Json, and Perl for Web development
by dws (Chancellor) on May 29, 2010 at 04:22 UTC
    Great end-to-end example, with at least problem. In record, you are escaping the values in the query that you're constructing, but not the keys. That opens the door to an injection attack.

      Point taken, but not sure how to satisfy. I just spent the last hour or so trying to find out exactly what needs to happen.

      I learned this technique from chromatic's venerable piece DBI is OK several years ago. Just came across Updating my database, where keys are escaped, but not quote as elegantly as I had hoped. Also looked again at $dbh->quote() but not seeing how that works with keys.

      Can you point me in the right direction on this one? I'd appreciate it because I use this approach a lot. Thanks.

      —Brad
      "The important work of moving the world forward does not wait to be done by perfect men." George Eliot

        $dbh->quote_identifier() should be sufficient. $dbh->quote() is only for values, and only for those rare cases where placeholders cannot be used.

        Alexander

        --
        Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)
Re: RFC:Tutorial: Using jQuery, Json, and Perl for Web development
by Your Mother (Archbishop) on May 29, 2010 at 14:07 UTC

      Yeah, you beat me to the punch ;) Glad to see other examples of jQuery and Perl getting together.

      My goal with this tutorial was to show a 1) complete example (HTML, jQuery, Perl, CSS), and 2) the UX-friendly, IMHO, approach to the way I'm handling errors these days.

      —Brad
      "The important work of moving the world forward does not wait to be done by perfect men." George Eliot
Re: RFC:Tutorial: Using jQuery, Json, and Perl for Web development
by kejohm (Hermit) on May 29, 2010 at 13:41 UTC

    An excellent post. I'm not a web programmer by trade, but I do like to occasionally tinker with Perl-based web apps and pages. JQuery and associated Perl modules look like they could be quite useful for when I do. Anything that allows more use of Perl when developing code is always a good thing.

    You have given me a lot to ponder...