package Mojolicious::Plugin::TagHelpers; use Mojo::Base 'Mojolicious::Plugin'; use Mojo::ByteStream; use Mojo::Util 'xml_escape'; use Scalar::Util 'blessed'; sub register { my ($self, $app) = @_; # Text field variations my @time = qw(date datetime month time week); for my $name (@time, qw(color email number range search tel text url)) { $app->helper("${name}_field" => sub { _input(@_, type => $name) }); } $app->helper(check_box => sub { _input(shift, shift, value => shift, @_, type => 'checkbox') }); $app->helper(csrf_field => \&_csrf_field); $app->helper(file_field => sub { shift; _tag('input', name => shift, @_, type => 'file') }); $app->helper(form_for => \&_form_for); $app->helper(hidden_field => \&_hidden_field); $app->helper(image => sub { _tag('img', src => shift->url_for(shift), @_) }); $app->helper(input_tag => sub { _input(@_) }); $app->helper(javascript => \&_javascript); $app->helper(label_for => \&_label_for); $app->helper(link_to => \&_link_to); $app->helper(password_field => \&_password_field); $app->helper(radio_button => sub { _input(shift, shift, value => shift, @_, type => 'radio') }); $app->helper(select_field => \&_select_field); $app->helper(stylesheet => \&_stylesheet); $app->helper(submit_button => \&_submit_button); # "t" is just a shortcut for the "tag" helper $app->helper($_ => sub { shift; _tag(@_) }) for qw(t tag); $app->helper(tag_with_error => \&_tag_with_error); $app->helper(text_area => \&_text_area); } sub _csrf_field { my $c = shift; return _hidden_field($c, csrf_token => $c->csrf_token, @_); } sub _form_for { my ($c, @url) = (shift, shift); push @url, shift if ref $_[0] eq 'HASH'; # POST detection my @post; if (my $r = $c->app->routes->lookup($url[0])) { my %methods = (GET => 1, POST => 1); do { my @via = @{$r->via || []}; %methods = map { $_ => 1 } grep { $methods{$_} } @via if @via; } while $r = $r->parent; @post = (method => 'POST') if $methods{POST} && !$methods{GET}; } return _tag('form', action => $c->url_for(@url), @post, @_); } sub _hidden_field { my $c = shift; return _tag('input', name => shift, value => shift, @_, type => 'hidden'); } sub _input { my ($c, $name) = (shift, shift); my %attrs = @_ % 2 ? (value => shift, @_) : @_; # Special selection value my @values = $c->param($name); my $type = $attrs{type} || ''; if (@values && $type ne 'submit') { # Checkbox or radiobutton my $value = $attrs{value} // ''; if ($type eq 'checkbox' || $type eq 'radio') { $attrs{value} = $value; $attrs{checked} = 'checked' if grep { $_ eq $value } @values; } # Others else { $attrs{value} = $values[0] } } return _validation($c, $name, 'input', %attrs, name => $name); } sub _javascript { my $c = shift; # CDATA my $cb = sub {''}; if (ref $_[-1] eq 'CODE' && (my $old = pop)) { $cb = sub { "//() . "\n//]]>" } } # URL my $src = @_ % 2 ? $c->url_for(shift) : undef; return _tag('script', @_, $src ? (src => $src) : (), $cb); } sub _label_for { my ($c, $name) = (shift, shift); my $content = ref $_[-1] eq 'CODE' ? pop : shift; return _validation($c, $name, 'label', for => $name, @_, $content); } sub _link_to { my ($c, $content) = (shift, shift); my @url = ($content); # Content unless (ref $_[-1] eq 'CODE') { @url = (shift); push @_, $content; } # Captures push @url, shift if ref $_[0] eq 'HASH'; return _tag('a', href => $c->url_for(@url), @_); } sub _option { my ($values, $pair) = @_; $pair = [$pair => $pair] unless ref $pair eq 'ARRAY'; # Attributes my %attrs = (value => $pair->[1]); $attrs{selected} = 'selected' if exists $values->{$pair->[1]}; %attrs = (%attrs, @$pair[2 .. $#$pair]); return _tag('option', %attrs, sub { xml_escape $pair->[0] }); } sub _password_field { my ($c, $name) = (shift, shift); return _validation($c, $name, 'input', @_, name => $name, type => 'password'); } sub _select_field { my ($c, $name, $options, %attrs) = (shift, shift, shift, @_); my %values = map { $_ => 1 } $c->param($name); my $groups = ''; for my $group (@$options) { # "optgroup" tag if (blessed $group && $group->isa('Mojo::Collection')) { my ($label, $values, %attrs) = @$group; my $content = join '', map { _option(\%values, $_) } @$values; $groups .= _tag('optgroup', label => $label, %attrs, sub {$content}); } # "option" tag else { $groups .= _option(\%values, $group) } } return _validation($c, $name, 'select', %attrs, name => $name, sub {$groups}); } sub _stylesheet { my $c = shift; # CDATA my $cb; if (ref $_[-1] eq 'CODE' && (my $old = pop)) { $cb = sub { "/*() . "\n/*]]>*/" } } # "link" or "style" tag my $href = @_ % 2 ? $c->url_for(shift) : undef; return $href ? _tag('link', rel => 'stylesheet', href => $href, @_) : _tag('style', @_, $cb); } sub _submit_button { my $c = shift; return _tag('input', value => shift // 'Ok', @_, type => 'submit'); } sub _tag { my $name = shift; # Content my $cb = ref $_[-1] eq 'CODE' ? pop : undef; my $content = @_ % 2 ? pop : undef; # Start tag my $tag = "<$name"; # Attributes my %attrs = @_; if ($attrs{data} && ref $attrs{data} eq 'HASH') { while (my ($key, $value) = each %{$attrs{data}}) { $key =~ y/_/-/; $attrs{lc("data-$key")} = $value; } delete $attrs{data}; } $tag .= qq{ $_="} . xml_escape($attrs{$_} // '') . '"' for sort keys %attrs; # Empty element unless ($cb || defined $content) { $tag .= ' />' } # End tag else { $tag .= '>' . ($cb ? $cb->() : xml_escape($content)) . "" } # Prevent escaping return Mojo::ByteStream->new($tag); } sub _tag_with_error { my ($c, $tag) = (shift, shift); my ($content, %attrs) = (@_ % 2 ? pop : undef, @_); $attrs{class} .= $attrs{class} ? ' field-with-error' : 'field-with-error'; return _tag($tag, %attrs, defined $content ? $content : ()); } sub _text_area { my ($c, $name) = (shift, shift); # Make sure content is wrapped my $cb = ref $_[-1] eq 'CODE' ? pop : sub {''}; my $content = @_ % 2 ? shift : undef; $cb = sub { xml_escape $content } if defined($content = $c->param($name) // $content); return _validation($c, $name, 'textarea', @_, name => $name, $cb); } sub _validation { my ($c, $name) = (shift, shift); return _tag(@_) unless $c->validation->has_error($name); return $c->tag_with_error(@_); } 1; =encoding utf8 =head1 NAME Mojolicious::Plugin::TagHelpers - Tag helpers plugin =head1 SYNOPSIS # Mojolicious $self->plugin('TagHelpers'); # Mojolicious::Lite plugin 'TagHelpers'; =head1 DESCRIPTION L is a collection of HTML tag helpers for L. Most form helpers can automatically pick up previous input values and will show them as default. You can also use L to set them manually and let necessary attributes always be generated automatically. % param country => 'germany' unless param 'country'; <%= radio_button country => 'germany' %> Germany <%= radio_button country => 'france' %> France <%= radio_button country => 'uk' %> UK For fields that failed validation with L the C class will be automatically added through the C helper, to make styling with CSS easier. This is a core plugin, that means it is always enabled and its code a good example for learning how to build new plugins, you're welcome to fork it. See L for a list of plugins that are available by default. =head1 HELPERS L implements the following helpers. =head2 check_box %= check_box employed => 1 %= check_box employed => 1, disabled => 'disabled' Generate C tag of type C. Previous input values will automatically get picked up and shown as default. =head2 color_field %= color_field 'background' %= color_field background => '#ffffff' %= color_field background => '#ffffff', id => 'foo' Generate C tag of type C. Previous input values will automatically get picked up and shown as default. =head2 csrf_field %= csrf_field Generate C tag of type C with L. =head2 date_field %= date_field 'end' %= date_field end => '2012-12-21' %= date_field end => '2012-12-21', id => 'foo' Generate C tag of type C. Previous input values will automatically get picked up and shown as default. =head2 datetime_field %= datetime_field 'end' %= datetime_field end => '2012-12-21T23:59:59Z' %= datetime_field end => '2012-12-21T23:59:59Z', id => 'foo' Generate C tag of type C. Previous input values will automatically get picked up and shown as default. =head2 email_field %= email_field 'notify' %= email_field notify => 'nospam@example.com' %= email_field notify => 'nospam@example.com', id => 'foo' Generate C tag of type C. Previous input values will automatically get picked up and shown as default. =head2 file_field %= file_field 'avatar' %= file_field 'avatar', id => 'foo' Generate C tag of type C. =head2 form_for %= form_for login => begin %= text_field 'first_name' %= submit_button % end %= form_for login => {format => 'txt'} => (method => 'POST') => begin %= text_field 'first_name' %= submit_button % end %= form_for '/login' => (enctype => 'multipart/form-data') => begin %= text_field 'first_name', disabled => 'disabled' %= submit_button % end %= form_for 'http://example.com/login' => (method => 'POST') => begin %= text_field 'first_name' %= submit_button % end Generate portable C
tag with L. For routes that allow C but not C, a C attribute will be automatically added.
=head2 hidden_field %= hidden_field foo => 'bar' %= hidden_field foo => 'bar', id => 'bar' Generate C tag of type C. =head2 image %= image '/images/foo.png' %= image '/images/foo.png', alt => 'Foo' Generate portable C tag. Foo =head2 input_tag %= input_tag 'first_name' %= input_tag first_name => 'Default name' %= input_tag 'employed', type => 'checkbox' Generate C tag. Previous input values will automatically get picked up and shown as default. =head2 javascript %= javascript '/script.js' %= javascript begin var a = 'b'; % end Generate portable C =head2 label_for %= label_for first_name => 'First name' %= label_for first_name => 'First name', class => 'user' %= label_for first_name => begin First name % end %= label_for first_name => (class => 'user') => begin First name % end Generate C