Beefy Boxes and Bandwidth Generously Provided by pair Networks
laziness, impatience, and hubris
 
PerlMonks  

comment on

( [id://3333]=superdoc: print w/replies, xml ) Need Help??

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

In reply to RFC:Tutorial: Using jQuery, Json, and Perl for Web development by bradcathey

Title:
Use:  <p> text here (a paragraph) </p>
and:  <code> code here </code>
to format your post; it's "PerlMonks-approved HTML":



  • Are you posting in the right place? Check out Where do I post X? to know for sure.
  • Posts may use any of the Perl Monks Approved HTML tags. Currently these include the following:
    <code> <a> <b> <big> <blockquote> <br /> <dd> <dl> <dt> <em> <font> <h1> <h2> <h3> <h4> <h5> <h6> <hr /> <i> <li> <nbsp> <ol> <p> <small> <strike> <strong> <sub> <sup> <table> <td> <th> <tr> <tt> <u> <ul>
  • Snippets of code should be wrapped in <code> tags not <pre> tags. In fact, <pre> tags should generally be avoided. If they must be used, extreme care should be taken to ensure that their contents do not have long lines (<70 chars), in order to prevent horizontal scrolling (and possible janitor intervention).
  • Want more info? How to link or How to display code and escape characters are good places to start.
Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others perusing the Monastery: (6)
As of 2024-04-24 06:51 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found