Beefy Boxes and Bandwidth Generously Provided by pair Networks
Welcome to the Monastery
 
PerlMonks  

Using Perl, jQuery, and JSON for Web development

by bradcathey (Prior)
on Jun 02, 2010 at 12:56 UTC ( [id://842754]=perltutorial: print w/replies, xml ) Need Help??

Many Web sites and applications involve forms. Form validation and error notification is part of good form's etiquette, security, and best practices. There are well-documented security issues with client-side validation via Javascript, so I have always made the trip to the server to validate in Perl. However, I've never liked the perceived latency and obvious screen refresh. Yes, I was able to provide the user with accurate error messages, but as a coder it meant keeping track of lots of things in the callbacks to do polite things like refreshing the HTML fields on the return trip (thank you HTML::FillInForm).

Enter jQuery (or Moo Tools or Prototype). Some good folks at MIT brought us these great libraries that have taken the Javascript out of the HTML coding (no more <a href="#" onclick="dosomething()">Click</a> and legitimized this powerful client-side language. Now sites can have slick new features and effects with little code and without Flash. But we can also have much nicer validation and error handling, and without the delays associated with the screen refresh.

Below I would like to show how I have learned to use jQuery, some Perl plugins, and JSON to still play it safe with server-side validation, no eval javascript, 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.

Try it: Demo

See the original Meditation

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

List of Assets

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; }

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perltutorial [id://842754]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others rifling through the Monastery: (5)
As of 2024-03-28 23:04 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found