NAME
Rose::HTML::Objects - Object-oriented interfaces for HTML.
SYNOPSIS
#
# HTML form/field abstraction
#
use Rose::HTML::Form;
$form = Rose::HTML::Form->new(action => '/foo',
method => 'post');
$form->add_fields
(
name => { type => 'text', size => 20, required => 1 },
height => { type => 'text', size => 5, maxlength => 5 },
bday => { type => 'datetime' },
);
$form->params(name => 'John', height => '6ft', bday => '01/24/1984');
$form->init_fields();
$bday = $form->field('bday')->internal_value; # DateTime object
print $bday->strftime('%A'); # Tuesday
print $form->field('bday')->html;
#
# Generic HTML objects
#
$obj = Rose::HTML::Object->new('p');
$obj->push_child('hello'); # text node
$obj->add_child(' '); # text node
# Add two children: HTML object with text node child
$obj->add_children(
Rose::HTML::Object->new(element => 'b',
children => [ 'world' ]));
# Serialize to HTML
print $obj->html; # prints: <p>hello <b>world</b></p>
DESCRIPTION
Rose::HTML::Objects is a framework for creating a reusable set of HTML widgets as mutable Perl objects that can be serialized to HTML or XHTML for display purposes.
The Rose::HTML::Object class may be used directly to represent a generic tag with an explicitly set element name and arbitrary attributes. There are also methods for parent/child manipulation.
Though such generic usage is possible, this family of modules is primarily intended as a framework for creating a resuable set of form and field widgets. On the Perl side, these objects are treated as abstract entities that can be fed input and will produce output in the form that is most convenient for the programmer (e.g., pass a DateTime object to a date picker field to initialize it, and get a DateTime object back from the field when asking for its value).
Fields may be simple (one standard HTML form field per Perl field object) or compound (a field object that serializes to an arbitrary number of HTML tags, but can be addressed as a single logical field internally). Likewise, forms themselves can be nested.
Each field has its own customizable validation, input filter, output filter, internal value (a plain value or a Perl object, whichever is most convenient), output value (the value shown when the field is redisplayed), label, associated error, and any other metadata deemed necessary. Each field can also be serialized to the equivalent set of (X)HTML "hidden" fields.
Forms are expected to be initialized with and return an object or list of objects that the form represents. For example, a registration form could be initialized with and return a UserAccount
object.
All labels, errors, and messages used in the bundled form and field widgets are localized in several languages, and you may add your own localized messages and errors using the provided localization framework.
Users are encouraged to create their own libraries of reusable form and field widgets for use on their site. The expectation is that the same kind of field appears in multiple places in any large web application (e.g., username fields, password fields, address forms, etc.) Each field encapsulates a set of values (e.g., options in a pop-up menu), labels, validation constraints, filters, and error messages. Similarly, each form encapsulates a set of fields along with any inter-field validation, error messages, and init-with/object-from methods. Nesting forms and fields preserves this delegation of responsibility, with each higher level having access to its children to perform inter-form/field tasks.
PRIVATE LIBRARIES
The classes that make up the Rose::HTML::Objects distribution can be used as-is to build forms, fields, and other HTML objects. The provided classes may also be subclassed to change their behavior. When subclassing, however, the interconnected nature of these classes may present some surprises. For example, consider the case of subclassing the Rose::HTML::Form::Field::Option class that represents a single option in a select box or pop-up menu.
package My::HTML::Form::Field::Option;
use base 'Rose::HTML::Form::Field::Option';
sub bark
{
print "woof!\n";
}
Now all your options can bark like a dog.
$option = My::HTML::Form::Field::Option->new;
$option->bark; # woof!
This seems great until you make your first select box or pop-up menu, pull out an option object, and ask it to bark.
$color =
Rose::HTML::Form::Field::PopUpMenu->new(
name => 'color',
options => [ 'red', 'green', 'blue' ]);
$option = $color->option('red');
$option->bark; # BOOM: fatal error, no such method!
What you'll get is an error message like this: "Can't locate object method 'bark' via package 'Rose::HTML::Form::Field::Option' - ..." That's because $option
is a plain old Rose::HTML::Form::Field::Option object and not one of your new My::HTML::Form::Field::Option
objects that can bark()
.
This is an example of the aforementioned interconnected nature of HTML objects: pop-up menus and select boxes contain options; radio button groups contain radio buttons; checkbox groups contain checkboxes; forms contain all of the above; and so on. What to do?
Well, one solution is to convince all the Rose::HTML::*
classes that might contain option objects to use your new My::HTML::Form::Field::Option
subclass instead of the standard Rose::HTML::Form::Field::Option class. But globally altering the behavior of the standard Rose::HTML::*
classes is an extremely bad idea. To understand why, imagine that you did so and then tried to incorporate some other code that also uses Rose::HTML::*
classes. That other code certainly doesn't expect the changes you've made. It expects the documented behavior for all the classes it's using, and rightfully so.
That's the problem with making class-wide alterations: every piece of code using those classes will see your changes. It's "anti-social behavior" in the context of code sharing and reuse.
The solution is to subclass not just the single class whose behavior is to be altered, but rather to create an entirely separate namespace for a full hierarchy of classes within which you can make your changes in isolation. This is called a "private library," and the Rose::HTML::Objects class contains methods for creating one, either dynamically in memory, or on disk in the form of actial *.pm
Perl module files.
Let's try the example above again, but this time using a private library. We will use the the make_private_library class method to do this. The reference documentation for this method appears below, but you should get a good idea of its functionality by reading the usage examples here.
First, let's create an in-memory private library to contain our changes. The make_private_library method accepts a hash of class name/code pairs containing customizations to be incorporated into one or more of the classes in the newly created private library. Let's use the My::
prefix for our private library. Here's a hash containing just our custom code:
%code =
(
'My::HTML::Form::Field::Option' => <<'EOF',
sub bark
{
print "woof!\n";
}
EOF
);
Note that the code is provided as a string, not a code reference. Be sure to use the appropriate quoting mechanism (a single-quoted "here document" in this case) to protect your code from unintended variable interpolation.
Next, we'll create the private library in memory:
Rose::HTML::Objects->make_private_library(in_memory => 1,
prefix => 'My::',
code => \%code);
Now we have a full hierarchy of My::
-prefixed classes, one for each public Rose::
class in the Rose::HTML::Objects distribution. Let's try the problematic code from earlier, this time using one of our new classes.
$color =
My::HTML::Form::Field::PopUpMenu->new(
name => 'color',
options => [ 'red', 'green', 'blue' ]);
$option = $color->option('red');
$option->bark; # woof!
Success! Of course, this dynamic in-memory class creation is relatively heavyweight. It necessarily has to have all the classes in memory. Creating a private library on disk allows you to load only the classes you need. It also provides an easier means of making your customizations persistent. Editing the actual *.pm
files on disk means that your changes can be tracked on a per-file basis by your version control system, and so on. We can still use the %code
hash from the in-memory example to "seed" the classes; the make_private_library method will insert our custom code into the initial *.pm
files it generates.
To create a private library on disk, we need to provide a path to the directory where the generated files will be placed. The appropriate directory hierarchy will be created below it (e.g., the path to the My::HTML::Form
Perl module file will be My/HTML/Form.pm
, starting beneath the specified modules_dir
). Let's do it:
Rose::HTML::Objects->make_private_library(
modules_dir => '/home/john/lib',
prefix => 'My::',
code => \%code);
To actually use the generated modules, we must, well, use
(or require
) them. We must also make sure the specified modules_dir
is in our @INC path. Example:
use lib '/home/john/lib';
use My::HTML::Form::Field::PopUpMenu;
$color =
My::HTML::Form::Field::PopUpMenu->new(
name => 'color',
options => [ 'red', 'green', 'blue' ]);
$option = $color->option('red');
$option->bark; # woof!
And it works. Note that if the call to make_private_library that creates the Perl module files on disk was in the same file as the code above, the My::HTML::Form::Field::PopUpMenu
class would have to be require
d rather than use
d. (All use
statements are evaluated at compile time, but the My::HTML::Form::Field::PopUpMenu
class is not created until the make_private_library call is executed, which happens at runtime in this example.)
One final example. Suppose you want to add or override a method in all HTML object classes within your private library. To facilitate this, the make_private_library method will create a mix-in class which will be placed at the front of the inheritence chain (i.e., the first item in the @ISA
array) of all generated subclasses. Given a prefix of My::
as in the example above, this custom class will be called My::HTML::Object::Custom
. It comes pre-populated with an initial set of private-library-wide information such as the object_type_class mapping and the default_localizer (all of which will be populated with your My::*
subclasses, naturally). Simply add your own methods to this module:
package My::HTML::Object::Custom;
...
sub chirp
{
print "tweet!\n";
}
Now the chirp()
method will appear in all other HTML object classes in your private library.
# It's everwhere!
My::HTML::Link->can('chirp'); # true
My::HTML::Form::Field::Date->can('chirp'); # true
My::HTML::Form::Field::CheckboxGroup->can('chirp'); # true
...
I hope this demonstrates the motivation for and utility of private libraries. Please see the make_private_library documentation for a more information on this method.
LOCALIZATION
There are several components of Rose::HTML::Object's localization system: the message and error objects, the classes that manage them, and of course the localizer itself. Using a private library, you get your own private subclasses of all of these. This is extremely important for several reasons, and you should definitely read the PRIVATE LIBRARIES section above before continuing.
The most important actor in the localization process is, predictably, the localizer, and the most important aspect of the localizer is the way in which it's accessed.
The general approach is that each object that is or contains something that needs to be localized has a localizer()
method through which it accesses its localizer object. These methods check for a local localizer object attribute, and if one is not found, the method looks "up the chain" until it finds one. The chain may include parent objects or class hierarchies. Eventually, the assumption is that a localizer will be found and returned.
In the most granular case, this allows each localized object to have its own individual localizer. In the more common (and default) case, there is a single localizer object camped out at some higher point in the chain of lookups, and this localizer serves all objects.
The default localizer class, Rose::HTML::Object::Message::Localizer, reads localized message text from the __DATA__
sections of the Perl module files that make up the Rose::HTML::Objects distribution. This is done mostly because it's the most convenient way to include the "built-in" localized message text in this CPAN module distribution. (See the Rose::HTML::Object::Message::Localizer documentation for more information.) Localized message text is stored in memory within the localizer object itself.
You can change both the source and storage of localized message text by creating your own localizer subclass. The key, of course, is to ensure that your localizer subclass is used instead the default localizer class by all objects. Thankfully, the creation of a private library takes care of that, both creating a localizer subclass and ensuring that it is accessible everywhere.
Here's a simple example of a customized localizer that overrides just one method, get_localized_message_text, to add three stars ***
around the built-in message text.
sub get_localized_message_text
{
my($self) = shift;
# Get message text using the default mechanism
my $text = $self->SUPER::get_localized_message_text(@_);
# Bail out early if no text is defined
return $text unless(defined $text);
# Surround the text with stars and return it
return "*** $text ***";
}
This is a silly example, obviously, but it does demonstrate how easy it is to alter the default behavior. A more useful example might be to look elsewhere for a message first, then fall back to the default mechanism. This requires actually unpacking the method arguments (as opposed to simply passing them on to the superclass call in the example above), but is otherwise not much more complex:
sub get_localized_message_text
{
my($self) = shift;
my %args = @_;
my $id = $args{'id'};
my $name = $args{'name'};
my $locale = $args{'locale'};
my $variant = $args{'variant'};
# Look elsewhere for this localized text: in a database, pull
# from a server, an XML file, whatever.
$text = ...
return $text if($defined $text); #
# Fall back to the default mechanism
return $self->SUPER::get_localized_message_text(@_);
}
By overriding this and othr methods in the Rose::HTML::Object::Message::Localizer class, your localizer subclass could choose to entirely ignore the default mechanism for localized text storage and retrieval.
Here's an example of a new field subclass that uses localized messages and errors. It will use the default localized text mechanism to the sake of simplicity (i.e., text stored in __DATA__
sections of Perl modules). It's a "nickname" field intended to be used as part of a localized form that asks for user information. For the sake of demonstrating validation, let's say we've decided that nicknames may not contain space characters.
The first step is to define our message and error ids. These should be added to the generated My::HTML::Object::Messages
and My::HTML::Object::Errors
classes, respectively. You can do this during private library generation by adding to the code
hash passed to the make_private_library call, or by editing the generated files on disk. (The relevant sections are indicated with comments that make_private_library will place in the generated *.pm
files.) First, the message ids:
package My::HTML::Object::Messages;
...
# Field labels
use constant FIELD_LABEL_NICKNAME => 100_000;
...
# Field errors
use constant FIELD_ERROR_BAD_NICKNAME => 101_000;
...
Now the error ids. Note that the error and message id numbers for each error message (just FIELD_ERROR_BAD_NICKNAME
in this case) should be the same in order to take advantage of the default behavior of the message_for_error_id method.
package My::HTML::Object::Errors;
...
# Field errors
use constant FIELD_ERROR_BAD_NICKNAME => 101_000;
...
Finally, the nickname field class itself. Note that it inherits from and uses classes from our private library, not from Rose::
.
package My::HTML::Form::Field::Nickname;
# Import message and error ids. Note that just the error id for
# FIELD_LABEL_NICKNAME is imported, not the message id. That's
# because we're using it as an error id below, passing it as an
# argument to the error_id() method.
use My::HTML::Object::Messages qw(FIELD_LABEL_NICKNAME);
use My::HTML::Object::Errors qw(FIELD_ERROR_BAD_NICKNAME);
# Inherit from our private library version of a text field
use base qw(My::HTML::Form::Field::Text);
sub init
{
my($self) = shift;
# Set the default label before calling through to the superclass
$self->label_id(FIELD_LABEL_NICKNAME);
$self->SUPER::init(@_);
}
sub validate
{
my($self) = shift;
# Do the default validation first
my $ret = $self->SUPER::validate(@_);
return $ret unless($ret);
#
# Do our custom validation
#
my $nick = $self->internal_value;
# Nicknames may not contain space characters
if($nick =~ /\s/)
{
# Remember, the error_label falls back to the label if no
# explicit error_label is set. (And we set a default
# label_id in init() above.)
my $label = $self->error_label;
# Pass the (also localized!) label as a parameter to this error.
# See the actual localized text in the __DATA__ section below.
$self->error_id(FIELD_ERROR_BAD_NICKNAME, { label => $label });
return 0;
}
return 1;
}
# Standard technique for conditionally loading all localized message
# text from the __DATA__ section below using the default localizer.
# (Alternately, you could remove the conditional and always load all
# the localized message text when this module is loaded.)
if(__PACKAGE__->localizer->auto_load_messages)
{
__PACKAGE__->localizer->load_all_messages;
}
1;
__DATA__
[% LOCALE en %]
FIELD_LABEL_NICKNAME = "Nickname"
FIELD_ERROR_BAD_NICKNAME = "[label] may not contain space characters."
[% LOCALE fr %]
FIELD_LABEL_NICKNAME = "Surnom"
FIELD_ERROR_BAD_NICKNAME = "[label] mai de ne pas contenir des espaces."
(Sorry for the bad French translations. Corrections welcome!)
Finally, let's map the new nickname field class to its own field type name:
package My::HTML::Form;
...
# Add new field type class mappings
__PACKAGE__->add_field_type_classes
(
nickname => 'My::HTML::Form::Field::Nickname',
...
);
Here it is in action:
$field = My::HTML::Form::Field::Nickname->new(name => 'nick');
$field->input_value('bad nickname');
$field->validate;
print $field->error; # "Nickname may not contain space characters."
$field->locale('fr');
print $field->error; # "Surnom mai de ne pas contenir des espaces."
Of course, you'll rarely instantiate a field in isolation. It will usually be part of a form. Similarly, you will rarely set the locale of a field directly. Instead, you will set the locale of the entire form and let the fields use that locale, accessed through the delegation chain searched when the locale method is called on a field object. Example:
$form = My::HTML::Form->new;
$form->add_fields(nick => { type => 'nickname' });
$form->params(nick => 'bad nickname');
$form->validate;
# "Nickname may not contain space characters."
print $form->field('nick')->error;
$form->locale('fr');
# "Surnom mai de ne pas contenir des espaces."
print $form->field('nick')->error;
Or you could set the locale on the localizer itself for a similar effect.
Also note the use of the label within the "bad nickname" error message. In general, incorporating (independently set, remember) labels into messages like this tends to lead to translation issues. (Is the label masculine? Feminine? Singular? Dual? Plural? Etc.) I've done so here to demonstrate that one localized message can be incorporated into another localized message, with both dynamically matching their locales based on the locale set higher up in the object hierarchy.
CLASS METHODS
- make_private_library PARAMS
-
Create a comprehensive collection of
Rose::HTML::*
subclasses, either in memory or as*.pm
files on disk, in order to provide a convenient and isolated location for your customizations. Please read the private libraries section above for more information.Valid PARAMS name/value pairs are:
- class_filter CODEREF
-
A reference to a subroutine that takes a
Rose::HTML::*
class name as its argument and returns true if a subclass should be created for this class, false otherwise. The class name will also be available in$_
. If this parameter is omitted, all classes are subclassed. - code HASHREF
-
A reference to a hash containing code to be added to subclasses. The keys of the hash are the subclass class names (i.e., the names after the application of the
rename
code or thetrim_prefix
/prefix
processing).The value for each key may be either a string containing Perl code or a reference to a hash containing a
code
key whose value is a string containing Perl code and afilter
key whose value is a reference to a subroutine used to filter the code.The
filter
subroutine will be passed a reference to a scalar containing the full Perl code for a subclass and is expected to modify it directly. The Perl code will also be available in$_
. Example:code => { 'My::HTML::Object' => <<'EOF', # code string argument sub my_method { # ... } EOF 'My::HTML::Form' => { filter => sub { s/__FOO__//g }, code => <<'EOF', sub my_other_method__FOO__ { # ... } EOF }, },
This will create
my_method()
in theMy::HTML::Object
class and, with the__FOO__
removed,my_other_method()
will be created in theMy::HTML::Form
class.Note that the use of this parameter is optional. You can always add your code to the Perl module files after they've been generated, or add your code directly into memory after the classes have been created
in_memory
. - code_filter CODEREF
-
A reference to a subroutine used to filter the Perl code for all generated subclasses. This filter will run before any subclass-specific
filter
(see thecode
parameter above for an explanation). This subroutine will be passed a reference to a scalar containing the Perl code and is expected to modify it directly. The Perl code will also be available in$_
. - debug INT
-
Print debugging output to STDERR if INT is creater than 0. Higher numbers produce more output. The maximum useful value is 3.
- in_memory BOOL
-
If true, the classes that make up the private library will be compiled in memory. If false (the default), then a
modules_dir
must be provided. - modules_dir PATH
-
The path to the directory under which all
*.pm
Perl module files will be created. The modules will be created in the expected tree structure. For example, theMy::HTML::Object
class will be in the fileMy/HTML/Object.pm
beneath themodules_dir
PATH. This parameter is ignored if thein_memory
parameter is passed. - overwrite BOOL
-
If true, overwrite any existing files that are located at the same paths as files created by this method call. This option is not applicable if the
in_memory
parameter is passed. - prefix STRING
-
The class name prefix with which to replace the
trim_prefix
in all subclass class names. For example, aprefix
value ofMy::
combined with the (default)trim_prefix
ofRose::
would take a class namedRose::HTML::Whatever
and produce a subclass namedMy::HTML::Whatever
. You must pass this parameter or therename
parameter. - rename CODEREF
-
A reference to a subroutine that takes a
Rose::HTML::*
class name as its argument and returns an appropriate subclass name. The name argument is also available in the$_
variable, enabling code like this:rename => sub { s/^Rose::/Foo::/ },
You must pass this parameter or the
prefix
parameter. - trim_prefix STRING
-
The prefix string to be removed from each
Rose::HTML::*
class name. This parameter is only relevant when theprefix
parameter is passed (and therename
parameter is not). Defaults toRose::
if this parameter is not passed.
DEVELOPMENT POLICY
The Rose development policy applies to this, and all Rose::*
modules. Please install Rose from CPAN and then run perldoc Rose
for more information.
SUPPORT
Any Rose::HTML::Objects questions or problems can be posted to the Rose::HTML::Objects mailing list. To subscribe to the list or search the archives, go here:
http://groups.google.com/group/rose-html-objects
Although the mailing list is the preferred support mechanism, you can also email the author (see below) or file bugs using the CPAN bug tracking system:
http://rt.cpan.org/NoAuth/Bugs.html?Dist=Rose-HTML-Objects
There's also a wiki and other resources linked from the Rose project home page:
CONTRIBUTORS
Tom Heady, Cees Hek, Kevin McGrath, Denis Moskowitz, RJBS, Jacques Supcik, Uwe Voelker
AUTHOR
John C. Siracusa (siracusa@gmail.com)
LICENSE
Copyright (c) 2010 by John C. Siracusa. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.