File Coverage

blib/lib/Mojolicious/Plugin/NamespaceForm.pm
Criterion Covered Total %
statement 43 45 95.5
branch 8 14 57.1
condition 9 11 81.8
subroutine 8 9 88.8
pod 1 1 100.0
total 69 80 86.2


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::NamespaceForm;
2              
3             =head1 NAME
4              
5             Mojolicious::Plugin::NamespaceForm - Support foo.0.bar params
6              
7             =head1 VERSION
8              
9             0.01
10              
11             =head1 DESCRIPTION
12              
13             This plugin makes it easier to work with multiple forms on a webpages.
14              
15             This plugins solves the problem related to validation and automatic form
16             filling. That logic is based on the name of the form field, meaning you
17             need to provide unique names for each form of the same type, unless
18             you want confusing error messages displayed to the user.
19              
20             =head2 Example
21              
22             The forms below is supposed to illustrate the problem:
23              
24            
25            

New product

26             Product name:
27            
28            

Existing product

29             Product name:
30            
31            
32              
33             =head2 How does it work?
34              
35             This plugin works by wrapping around most of the built in form helpers with
36             extra logic for generating the name of the input field. The helpers below is
37             overridden by default, but the list will probably get longer in the future.
38              
39             check_box
40             hidden_field
41             label_for
42             number_field
43             password_field
44             radio_button
45             select_field
46             text_area
47             text_field
48             url_field
49              
50             =head1 SYNOPSIS
51              
52             =head2 Single object
53              
54             Application/controller logic:
55              
56             use Mojolicious::Lite;
57             plugin 'Mojolicious::Plugin::NamespaceForm';
58              
59             post '/user' => sub {
60             my $self = shift;
61             my $user = $self->namespace_params('user')->single;
62              
63             # $user = { email => '...', name => '...', _index => 42 }
64             };
65              
66             Template:
67              
68             % stash field_namespace => 'user';
69             % stash field_index => 42; # optional
70             %= text_field 'email';
71             %= text_field 'name';
72              
73             Output:
74              
75            
76            
77              
78             =head2 Multiple objects
79              
80             post '/users' => sub {
81             my $self = shift;
82             my @users = @{ $self->namespace_params('user') };
83              
84             # @users = (
85             # { email => '...', name => '...', _index => 0 },
86             # { email => '...', name => '...', _index => 1 },
87             # );
88              
89             for my $user (@users) {
90             # ...
91             }
92             };
93              
94             Template:
95              
96             % stash field_namespace => 'user';
97             % stash field_index => 0;
98             % for my $user (@$users) {
99             %= text_field 'email';
100             %= text_field 'name';
101             % stash->{field_index}++;
102             % }
103              
104             Output:
105              
106            
107            
108            
109            
110             ...
111              
112             =cut
113              
114 1     1   2071 use Mojo::Base 'Mojolicious::Plugin';
  1         2  
  1         8  
115              
116             our $VERSION = '0.01';
117              
118             my @FORM_HELPERS = qw(
119             check_box
120             hidden_field
121             label_for
122             number_field
123             password_field
124             radio_button
125             select_field
126             text_area
127             text_field
128             url_field
129             );
130              
131             =head1 HELPERS
132              
133             =head2 namespace_params
134              
135             $obj = $self->namespace_params($namespace);
136             @list_of_hashes = @$obj;
137             $hash_ref = $obj->single; # might die
138             $hash_ref = $obj->get($index); # might return undef
139              
140             The C<$obj> is overloaded in list context: It will return a list of hash-refs
141             ordered by C<_index>.
142              
143             See L for more details.
144              
145             =head1 METHODS
146              
147             =head2 register
148              
149             $self->register(helpers => [qw( input_tag )]);
150              
151             Will register L and override tag helpers.
152              
153             =cut
154              
155             sub register {
156 1     1 1 54 my($self, $app, $config) = @_;
157 1 50       7 my @helpers = $config->{helpers} ? @{ $config->{helpers} } : @FORM_HELPERS;
  0         0  
158 1         36 my $r = $app->renderer;
159              
160 1         22 $app->defaults(field_index => 0, field_namespace => '');
161              
162 1         34 for my $name (@FORM_HELPERS) {
163 10 50       582 my $original = delete $r->helpers->{$name} or die "No such helper: $name";
164             $r->add_helper($name => sub {
165 6     6   106169 my($c, $name, @args) = @_;
166 6         25 my $namespace = $c->stash('field_namespace');
167 6 50       78 return $c->$original($name, @args) unless $namespace;
168 6   100     19 my $index = $c->stash('field_index') || 0;
169 6         102 return $c->$original("$namespace.$index.$name", @args);
170 10         173 });
171             }
172              
173             $r->add_helper(namespace_params => sub {
174 3     3   111429 my($self, $namespace) = @_;
175 3         19 my $validated = $self->validation->output;
176 3   66     2019 my $only_validated = $self->validation->has_error || $self->validation->is_valid;
177 3         883 my %data;
178              
179 3         48 $namespace = qr{^$namespace\.(\d+)\.(.+)$};
180              
181 3         19 for my $name ($self->param) {
182 5 50       698 next unless $name =~ $namespace;
183 5 100 100     102 next if $only_validated and !defined $validated->{$name};
184 4         23 $data{$1}{_index} = int $1;
185 4   66     24 $data{$1}{$2} = $validated->{$name} // $self->param($name);
186             }
187              
188 3         348 return Mojolicious::Plugin::NamespaceForm::Data->new(data => \%data);
189 1         44 });
190             }
191              
192             =head1 NAMESPACE OBJECT METHODS
193              
194             =cut
195              
196             package
197             Mojolicious::Plugin::NamespaceForm::Data;
198              
199 1     1   917 use Mojo::Base -base;
  1         2  
  1         8  
200             use overload (
201             fallback => 1,
202             q(@{}) => sub {
203             [
204 1         12 sort { $a->{_index} <=> $b->{_index} }
  1         9  
205 1     1   70 values %{ $_[0]->{data} }
206             ];
207             },
208 1     1   243 );
  1         2  
  1         16  
209              
210             =head2 get
211              
212             $hash_ref = $self->get($index);
213              
214             Return a given hash ref by index, or undef if no such index is defined.
215              
216             =cut
217              
218             sub get {
219 0     0   0 $_[0]->{data}{$_[1]};
220             }
221              
222             =head2 single
223              
224             $hash_ref = $self->single;
225              
226             This method will die if no data exists for form namespace or if there are more
227             than one item. The index does not matter.
228              
229             =cut
230              
231             sub single {
232 2     2   32 my $self = shift;
233 2         4 my $n = keys %{ $self->{data} };
  2         65  
234              
235 2 50       8 die "No elements in form namespace" unless $n;
236 2 50       6 die "Too many elements in form namespace ($n)" if $n > 1;
237 2         21 values %{ $self->{data} };
  2         20  
238             }
239              
240             =head1 AUTHOR
241              
242             Jan Henning Thorsen - C
243              
244             =cut
245              
246             1;