The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Template::Refine::Cookbook::Attributes - learn how to annotate a form

DESCRIPTION

You have a form, and some fields are required. You want to annotate the labels of the required fields with a * so that the user knows they're required. (You also don't want the designer to do this manually, since you may change the requirements in your code at some point.)

Template::Refine to the rescue!

We'll start by defining a Moose class that is the "result" of submitting the form:

    package Person;
    use Moose;

    has 'name' => ( is => 'ro', isa => 'Str', required => 1 );
    has 'bio'  => ( is => 'ro', isa => 'Str' );
    has 'age'  => ( is => 'ro', isa => 'Int', required => 1 );

Hopefully you're using Moose by now, but if not, I think the code is pretty easy to understand. Person has three fields, name, bio, and age. Name and age are required. Bio is optional.

Now let's see what the designer gave us as HTML:

    lang:HTML
    <form>
        <div id="name">
            <span class="label">Name</span>: <input />
        </div>
        <div id="bio">
            <span class="label">Biography</span>: <input />
        </div>
        <div id="age">
            <span class="label">Age</span>: <input />
        </div>
    </form>

You can see that each field has a region that can be selected with <//div[@id='<name']>>. Inside that region, the label can be selected with <//span[@class='label']> or similar.

Here's how to write a Template::Refine rule for a two-stage scheme like this:

    lang:Perl
    sub transform {
        my $frag = shift;
        for my $attribute (Person->meta->compute_all_applicable_attributes) {

            my $attribute_id = $attribute->name;
            $frag = $frag->process(
                simple_replace {
                    my $n = shift;
                    my $sub_fragment = Template::Refine::Fragment->new_from_string(
                        $n->toString,
                    );
                    return annotate_required_field($sub_fragment, $attribute)->fragment;
                } "//*[\@id='$attribute_id']"
            );
        }
        return $frag
    }

    sub annotate_required_field {
        my ($fragment, $attribute) = @_;
        return $fragment unless $attribute->is_required;
        return $fragment->process(
            simple_replace {
                my $n = shift;
                return replace_text $n, $n->textContent . ' *';
            } q|//*[@class='label']|,
        );
    }

The transform subroutine processes the entire form. For each attribute, it creates a rule that finds that attribute's region (the div). In the coderef that generates the replacement, we generate a Template::Refine::Fragment that represents only that region. Then we process the "sub fragment", this time looking for the labels (via annotate_required_field). For each label found, we get the existing text and add a * if the attribute's metaclass indicates that it is required.

The whole script (minus those two functions) looks like this:

    use Person;
    use Template::Refine::Fragment;
    use Template::Refine::Utils qw(replace_text simple_replace);

    my $frag = Template::Refine::Fragment->new_from_file('person_form.html');
    print transform($frag)->render;

And the output is what you would expect from looking at the HTML and Moose class:

    lang:HTML
    <form>
        <div id="name">
            <span class="label">Name *</span>: <input/></div>
        <div id="bio">
            <span class="label">Biography</span>: <input/></div>
        <div id="age">
            <span class="label">Age *</span>: <input/></div>
    </form>