Day 19 - HTML::Widget tricks

Custom markup with HTML::Widget

Introduction

Today we'll show you how to generate custom markup using a custom container, and how to add custom elements to your forms.

Let's add a simple feedback form to our Cheer project. First thing, add the Catalyst::Plugin::HTML::Widget (with documentation in HTML::Widget) in the plugin list, in Cheer.pm. Then open Root.pm and type your form:

 sub feedback : Local {
 	my ($self, $c) = @_;
 	my $w = $c->widget('feedback')->method('post')->action($c->req->uri);
 	$w->legend('Tell us what you think!');
 	$w->element('Textfield', 'name')->label('Your name:');
 	$w->element('Textfield', 'email')->label('Your email:');
 	$w->element('Textarea', 'message')->cols(40)->rows(5)->label('Your feedback');
 	$w->element('Hidden','feedback_submitted')->value('1');
 	$w->element('Submit', 'send')->value('Send it');
 	$w->constraint('Email','email')->message($c->localize('Invalid email address'));
 	$w->constraint('All','message')->message('You have to write something');

 	my $result;
 	if ( $c->req->params->{feedback_submitted} ) {
 		$result = $w->process ( $c->request );
 		unless ($result->have_errors) {
 			$c->log->debug("Something should happen here..");
 		}
 	} else {
 		$result = $w->process; 
 	}
 	$c->stash->{feedback_form} = $result->as_xml;
 	$c->stash->{template} = 'feedback.tt';
 }

Don't like the markup? Build a custom container

The generated markup is as follows (whitespace added for easy reading):

 <form action="http://localhost:3000/feedback" id="feedback" method="post">
 <fieldset class="widget_fieldset">

 <legend id="feedback_legend">Tell us what you think!</legend>

 <label for="feedback_name" id="feedback_name_label">Your name:
 <input class="textfield" id="feedback_name" name="name" type="text" />
 </label>

 <label for="feedback_email" id="feedback_email_label">Your email:
 <input class="textfield" id="feedback_email" name="email" type="text" />
 </label>

 <label for="feedback_message" id="feedback_message_label">Your feedback
 <textarea class="textarea" cols="40" id="feedback_message" name="message" rows="5">
 </textarea>
 </label>

 <input class="hidden" id="feedback_feedback_submitted" name="feedback_submitted" type="hidden" value="1" />
 <input class="submit" id="feedback_send" name="send" type="submit" value="Send it" />
 </fieldset>
 </form>

This may not be exactly what you seek , and if you don't want to build the form manually in the template, you'll have to build a custom container.

Let's just make a container that does nothing, but will allow us to continue further. Put this in lib/Cheer/Container.pm . The code is just copied from HTML::Widget::Container, ready for our changes:

 package Cheer::Container;

 use warnings;
 use strict;
 use base 'HTML::Widget::Container';

 sub build_element_label {
     my ( $self, $element, $class ) = @_;

     return $element unless defined $self->label;

     my $l = $self->label->clone;
     my $radiogroup;

     if ( $class eq 'radiogroup_fieldset' ) {
         $element->unshift_content($l);
         $radiogroup = 1;
     }
     elsif ( $self->error && $element->tag eq 'span' ) {

         # it might still be a radiogroup wrapped in an error span
         for my $elem ( $element->content_refs_list ) {
             next unless ref $$elem;
             if ( $$elem->attr('class') eq 'radiogroup_fieldset' ) {
                 $$elem->unshift_content($l);
                 $radiogroup = 1;
             }
         }
     }

     if ( !$radiogroup ) {

         # Do we prepend or append input to label?
         $element =
             ( $class eq 'checkbox' or $class eq 'radio' )
             ? $l->unshift_content($element)
             : $l->push_content($element);
     }

     return $element;
 }

 1;

Now set it as your container class:

 --- Root.pm
 +++ Root.pm
 @@ -47,6 +47,7 @@
  sub feedback : Local {
         my ($self, $c) = @_;
         my $w = $c->widget('feedback')->method('post')->action($c->req->uri);
 +       $w->element_container_class('Cheer::Container');
         $w->legend('Tell us what you think!');




Let's say you want your elements to render like this:

 <div>
  <label for="foo">Foo:</label>
  <input ... />
 </div>

Just edit the container:

 --- Container.pm
 +++ Container.pm
 @@ -34,7 +34,7 @@
          $element =
              ( $class eq 'checkbox' or $class eq 'radio' )
              ? $l->unshift_content($element)
 -            : $l->push_content($element);
 +            : HTML::Element->new('div')->push_content($l,$element);
      }

That's it! You can check the new output immediately. For more funky edits, you'll have to read the manual for HTML::Element.

Add custom tags in your labels and forms

If you want to add some custom elements to your form, just use the Span element as a container:

 my $img = HTML::Element->new('img', src =>'path/to/image.png');
 $w->element('Span','wait')->content($img);

You can also add extra elements in your labels. Here's how you can add an image linking somewhere:

 my $a = HTML::Element->new('a', href => 'http://foobar.com' );
 $a->push_content( HTML::Element->new('img', src =>'/path/to/icon.png'));
 $w->element('Textfield','foobar')->label($a, 'Foobar:');

Just remember the label and Span content is just pushed into the container element, so it can be a simple string, a HTML::Element object or a list of strings and/or objects.

Also keep in mind that doing this in the Controller is a bad idea, but with HTML::Widget it's pretty difficult to avoid. This makes HTML::Widget a poor choice for development teams with non-technical designers, and can lead to messy code for some projects. However for small to medium sized projects HTML::Widget can be a good choice once you understand how to customise its output.

AUTHOR

Bogdan Lucaciu bogdan@sns.ro