package Mojolicious::Plugin::TagHelpers; use Mojo::Base 'Mojolicious::Plugin'; use Mojo::ByteStream; use Mojo::DOM::HTML qw(tag_to_html); use Scalar::Util qw(blessed); sub register { my ($self, $app) = @_; # Text field variations my @time = qw(date 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(datetime_field => sub { _input(@_, type => 'datetime-local') }); my @helpers = ( qw(asset_tag csrf_field form_for hidden_field javascript label_for link_to select_field stylesheet submit_button), qw(tag_with_error text_area) ); $app->helper($_ => __PACKAGE__->can("_$_")) for @helpers; $app->helper(button_to => sub { _button_to(0, @_) }); $app->helper(check_box => sub { _input(@_, type => 'checkbox') }); $app->helper(csrf_button_to => sub { _button_to(1, @_) }); $app->helper(favicon => sub { _favicon(@_) }); $app->helper(file_field => sub { _empty_field('file', @_) }); $app->helper(image => sub { _tag('img', src => _file_url(shift, shift), @_) }); $app->helper(input_tag => sub { _input(@_) }); $app->helper(password_field => sub { _empty_field('password', @_) }); $app->helper(radio_button => sub { _input(@_, type => 'radio') }); # "t" is just a shortcut for the "tag" helper $app->helper($_ => sub { shift; _tag(@_) }) for qw(t tag); } sub _asset_tag { my ($c, $target) = (shift, shift); my $url = $c->url_for_asset($target); return $c->helpers->javascript($url, @_) if $target =~ /\.js$/; return $c->helpers->stylesheet($url, @_) if $target =~ /\.css$/; return $c->helpers->image($url, @_); } sub _button_to { my ($csrf, $c, $text) = (shift, shift, shift); my $prefix = $csrf ? _csrf_field($c) : ''; return _form_for($c, @_, sub { $prefix . _submit_button($c, $text) }); } sub _csrf_field { my $c = shift; return _hidden_field($c, csrf_token => $c->helpers->csrf_token, @_); } sub _empty_field { my ($type, $c, $name) = (shift, shift, shift); return _validation($c, $name, 'input', name => $name, @_, type => $type); } sub _favicon { my ($c, $file) = @_; return _tag('link', rel => 'icon', href => _file_url($c, $file // 'favicon.ico')); } sub _file_url { my ($c, $url) = @_; return blessed $url && $url->isa('Mojo::URL') ? $url : $c->url_for_file($url); } sub _form_for { my ($c, @url) = (shift, shift); push @url, shift if ref $_[0] eq 'HASH'; # Method detection my $r = $c->app->routes->lookup($url[0]); my $method = $r ? $r->suggested_method : 'GET'; my @post = $method ne 'GET' ? (method => 'POST') : (); my $url = $c->url_for(@url); $url->query({_method => $method}) if @post && $method ne 'POST'; return _tag('form', action => $url, @post, @_); } sub _hidden_field { my ($c, $name, $value) = (shift, shift, shift); return _tag('input', name => $name, value => $value, @_, type => 'hidden'); } sub _input { my ($c, $name) = (shift, shift); my %attrs = @_ % 2 ? (value => shift, @_) : @_; if (my @values = @{$c->every_param($name)}) { # Checkbox or radiobutton my $type = $attrs{type} || ''; if ($type eq 'checkbox' || $type eq 'radio') { my $value = $attrs{value} // 'on'; delete $attrs{checked}; $attrs{checked} = undef if grep { $_ eq $value } @values; } # Others else { $attrs{value} = $values[-1] } } return _validation($c, $name, 'input', name => $name, %attrs); } sub _javascript { my $c = shift; my $content = ref $_[-1] eq 'CODE' ? "//() . "\n//]]>" : ''; my @src = @_ % 2 ? (src => _file_url($c, shift)) : (); return _tag('script', @src, @_, sub {$content}); } 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'; my %attrs = (value => $pair->[1], @$pair[2 .. $#$pair]); delete $attrs{selected} if keys %$values; $attrs{selected} = undef if $values->{$pair->[1]}; return _tag('option', %attrs, $pair->[0]); } sub _select_field { my ($c, $name, $options, %attrs) = (shift, shift, shift, @_); my %values = map { $_ => 1 } grep {defined} @{$c->every_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', name => $name, %attrs, sub {$groups}); } sub _stylesheet { my $c = shift; my $content = ref $_[-1] eq 'CODE' ? "/*() . "\n/*]]>*/" : ''; return _tag('style', @_, sub {$content}) unless @_ % 2; return _tag('link', rel => 'stylesheet', href => _file_url($c, shift), @_); } sub _submit_button { my ($c, $value) = (shift, shift // 'Ok'); return _tag('input', value => $value, @_, type => 'submit'); } sub _tag { Mojo::ByteStream->new(tag_to_html(@_)) } 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); my $cb = ref $_[-1] eq 'CODE' ? pop : undef; my $content = @_ % 2 ? shift : undef; $content = $c->param($name) // $content // $cb // ''; return _validation($c, $name, 'textarea', name => $name, @_, $content); } sub _validation { my ($c, $name) = (shift, shift); return _tag(@_) unless $c->helpers->validation->has_error($name); return $c->helpers->tag_with_error(@_); } 1; =encoding utf8 =head1 NAME Mojolicious::Plugin::TagHelpers - Tag helpers plugin =head1 SYNOPSIS # Mojolicious $app->plugin('TagHelpers'); # Mojolicious::Lite plugin 'TagHelpers'; =head1 DESCRIPTION L is a collection of HTML tag helpers for L, based on the 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 L, 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 asset_tag %= asset_tag '/app.js' %= asset_tag '/app.js', async => 'async' Generate 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