0a413db356bf708495050d38babb065b5933be5a
[catagits/Reaction.git] / lib / Reaction / UI / ViewPort / ActionForm.pm
1 package Reaction::UI::ViewPort::ActionForm;
2
3 use Reaction::Class;
4
5 use aliased 'Reaction::UI::ViewPort::Field::Text';
6 use aliased 'Reaction::UI::ViewPort::Field::Number';
7 use aliased 'Reaction::UI::ViewPort::Field::Boolean';
8 use aliased 'Reaction::UI::ViewPort::Field::File';
9 use aliased 'Reaction::UI::ViewPort::Field::String';
10 use aliased 'Reaction::UI::ViewPort::Field::Password';
11 use aliased 'Reaction::UI::ViewPort::Field::DateTime';
12 use aliased 'Reaction::UI::ViewPort::Field::ChooseOne';
13 use aliased 'Reaction::UI::ViewPort::Field::ChooseMany';
14 use aliased 'Reaction::UI::ViewPort::Field::HiddenArray';
15 use aliased 'Reaction::UI::ViewPort::Field::TimeRange';
16
17 class ActionForm is 'Reaction::UI::ViewPort', which {
18   has action => (
19     isa => 'Reaction::InterfaceModel::Action', is => 'ro', required => 1
20   );
21   
22   has field_names => (isa => 'ArrayRef', is => 'rw', lazy_build => 1);
23   
24   has _field_map => (
25     isa => 'HashRef', is => 'rw', init_arg => 'fields',
26     predicate => '_has_field_map', set_or_lazy_build('field_map'),
27   );
28   
29   has changed => (
30     isa => 'Int', is => 'rw', reader => 'is_changed', default => sub { 0 }
31   );
32
33   has next_action => (
34     isa => 'ArrayRef', is => 'rw', required => 0, predicate => 'has_next_action'
35   );
36   
37   has on_apply_callback => (
38     isa => 'CodeRef', is => 'rw', required => 0,
39     predicate => 'has_on_apply_callback'
40   );
41   
42   has ok_label => (
43     isa => 'Str', is => 'rw', required => 1, default => sub { 'ok' }
44   );
45   
46   has apply_label => (
47     isa  => 'Str', is => 'rw', required => 1, default => sub { 'apply' }
48   );
49   
50   has close_label => (isa => 'Str', is => 'rw', lazy_fail => 1);
51   
52   has close_label_close => (
53     isa => 'Str', is => 'rw', required => 1, default => sub { 'close' }
54   );
55   
56   has close_label_cancel => (
57     isa => 'Str', is => 'rw', required => 1, default => sub { 'cancel' }
58   );
59   
60   sub fields { shift->_field_map }
61   
62   implements BUILD => as {
63     my ($self, $args) = @_;
64     unless ($self->_has_field_map) {
65       my @field_map;
66       my $action = $self->action;
67       foreach my $attr ($action->parameter_attributes) {
68         push(@field_map, $self->build_fields_for($attr => $args));
69       }
70   
71       my %field_map = @field_map;
72       my @field_names = @{ $self->sort_by_spec(
73           $args->{column_order}, [keys %field_map] )};
74   
75       $self->_field_map(\%field_map);
76       $self->field_names(\@field_names);
77     }
78     $self->close_label($self->close_label_close);
79   };
80   
81   implements build_fields_for => as {
82     my ($self, $attr, $args) = @_;
83     my $attr_name = $attr->name;
84     #TODO: DOCUMENT ME!!!!!!!!!!!!!!!!!
85     my $builder = "build_fields_for_name_${attr_name}";
86     my @fields;
87     if ($self->can($builder)) {
88       @fields = $self->$builder($attr, $args); # re-use coderef from can()
89     } elsif ($attr->has_type_constraint) {
90       my $constraint = $attr->type_constraint;
91       my $base_name = $constraint->name;
92       my $tried_isa = 0;
93       CONSTRAINT: while (defined($constraint)) {
94         my $name = $constraint->name;
95         if (eval { $name->can('meta') } && !$tried_isa++) {
96           foreach my $class ($name->meta->class_precedence_list) {
97             my $mangled_name = $class;
98             $mangled_name =~ s/:+/_/g;
99             my $builder = "build_fields_for_type_${mangled_name}";
100             if ($self->can($builder)) {
101               @fields = $self->$builder($attr, $args);
102               last CONSTRAINT;
103             }
104           }
105         }
106         if (defined($name)) {
107           unless (defined($base_name)) {
108             $base_name = "(anon subtype of ${name})";
109           }
110           my $mangled_name = $name;
111           $mangled_name =~ s/:+/_/g;
112           my $builder = "build_fields_for_type_${mangled_name}";
113           if ($self->can($builder)) {
114             @fields = $self->$builder($attr, $args);
115             last CONSTRAINT;
116           }
117         }
118         $constraint = $constraint->parent;
119       }
120       if (!defined($constraint)) {
121         confess "Can't build field ${attr_name} of type ${base_name} without $builder method or build_fields_for_type_<type> method for type or any supertype";
122       }
123     } else {
124       confess "Can't build field ${attr} without $builder method or type constraint";
125     }
126     return @fields;
127   };
128   
129   implements build_field_map => as {
130     confess "Lazy field map building not supported by default";
131   };
132   
133   implements can_apply => as {
134     my ($self) = @_;
135     foreach my $field (values %{$self->_field_map}) {
136       return 0 if $field->needs_sync;
137         # if e.g. a datetime field has an invalid value that can't be re-assembled
138         # into a datetime object, the action may be in a consistent state but
139         # not synchronized from the fields; in this case, we must not apply
140     }
141     return $self->action->can_apply;
142   };
143   
144   implements do_apply => as {
145     my $self = shift;
146     return $self->action->do_apply;
147   };
148   
149   implements ok => as {
150     my $self = shift;
151     if ($self->apply(@_)) {
152       $self->close(@_);
153     }
154   };
155   
156   implements apply => as {
157     my $self = shift;
158     if ($self->can_apply && (my $result = $self->do_apply)) {
159       $self->changed(0);
160       $self->close_label($self->close_label_close);
161       if ($self->has_on_apply_callback) {
162         $self->on_apply_callback->($self => $result);
163       }
164       return 1;
165     } else {
166       $self->changed(1);
167       $self->close_label($self->close_label_cancel);
168       return 0;
169     }
170   };
171   
172   implements close => as {
173     my $self = shift;
174     my ($controller, $name, @args) = @{$self->next_action};
175     $controller->pop_viewport;
176     $controller->$name($self->action->ctx, @args);
177   };
178   
179   sub can_close { 1 }
180   
181   override accept_events => sub {
182     (($_[0]->has_next_action ? ('ok', 'close') : ()), 'apply', super());
183   }; # can't do a close-type operation if there's nowhere to go afterwards
184   
185   override child_event_sinks => sub {
186     my ($self) = @_;
187     return ((grep { ref($_) =~ 'Hidden' } values %{$self->_field_map}),
188             (grep { ref($_) !~ 'Hidden' } values %{$self->_field_map}),
189             super());
190   };
191   
192   after apply_child_events => sub {
193     # interrupt here because fields will have been updated
194     my ($self) = @_;
195     $self->sync_action_from_fields;
196   };
197   
198   implements sync_action_from_fields => as {
199     my ($self) = @_;
200     my $field_map = $self->_field_map;
201     my @fields = values %{$field_map};
202     foreach my $field (@fields) {
203       $field->sync_to_action; # get the field to populate the $action if possible
204     }
205     $self->action->sync_all;
206     foreach my $field (@fields) {
207       $field->sync_from_action; # get errors from $action if applicable
208     }
209   };
210   
211   implements build_simple_field => as {
212     my ($self, $class, $attr, $args) = @_;
213     my $attr_name = $attr->name;
214     my %extra;
215     if (my $config = $args->{Field}{$attr_name}) {
216       %extra = %$config;
217     }
218     my $field = $class->new(
219                   action => $self->action,
220                   attribute => $attr,
221                   name => $attr->name,
222                   location => join('-', $self->location, 'field', $attr->name),
223                   ctx => $self->ctx,
224                   %extra
225                 );
226     return ($attr_name => $field);
227   };
228   
229   implements build_fields_for_type_Num => as {
230     my ($self, $attr, $args) = @_;
231     return $self->build_simple_field(Number, $attr, $args);
232   };
233   
234   implements build_fields_for_type_Int => as {
235     my ($self, $attr, $args) = @_;
236     return $self->build_simple_field(Number, $attr, $args);
237   };
238   
239   implements build_fields_for_type_Bool => as {
240     my ($self, $attr, $args) = @_;
241     return $self->build_simple_field(Boolean, $attr, $args);
242   };
243   
244   implements build_fields_for_type_File => as {
245     my ($self, $attr, $args) = @_;
246     return $self->build_simple_field(File, $attr, $args);
247   };
248   
249   implements build_fields_for_type_Str => as {
250     my ($self, $attr, $args) = @_;
251     if ($attr->has_valid_values) { # There's probably a better way to do this
252       return $self->build_simple_field(ChooseOne, $attr, $args);
253     }
254     return $self->build_simple_field(Text, $attr, $args);
255   };
256   
257   implements build_fields_for_type_SimpleStr => as {
258     my ($self, $attr, $args) = @_;
259     return $self->build_simple_field(String, $attr, $args);
260   };
261   
262   implements build_fields_for_type_Password => as {
263     my ($self, $attr, $args) = @_;
264     return $self->build_simple_field(Password, $attr, $args);
265   };
266   
267   implements build_fields_for_type_DateTime => as {
268     my ($self, $attr, $args) = @_;
269     return $self->build_simple_field(DateTime, $attr, $args);
270   };
271   
272   implements build_fields_for_type_Enum => as {
273     my ($self, $attr, $args) = @_;
274     return $self->build_simple_field(ChooseOne, $attr, $args);
275   };
276   
277   implements build_fields_for_type_DBIx_Class_Row => as {
278     my ($self, $attr, $args) = @_;
279     return $self->build_simple_field(ChooseOne, $attr, $args);
280   };
281   
282   implements build_fields_for_type_ArrayRef => as {
283     my ($self, $attr, $args) = @_;
284     if ($attr->has_valid_values) {
285       return $self->build_simple_field(ChooseMany, $attr, $args)
286     } else {
287       return $self->build_simple_field(HiddenArray, $attr, $args)
288     }
289   };
290   
291   implements build_fields_for_type_DateTime_Spanset => as {
292     my ($self, $attr, $args) = @_;
293     return $self->build_simple_field(TimeRange, $attr, $args);
294   };
295   
296   no Moose;
297   
298   no strict 'refs';
299   delete ${__PACKAGE__ . '::'}{inner};
300
301 };
302
303 1;
304
305 =head1 NAME
306
307 Reaction::UI::ViewPort::ActionForm
308
309 =head1 SYNOPSIS
310
311   use aliased 'Reaction::UI::ViewPort::ActionForm';
312
313   $self->push_viewport(ActionForm,
314     layout => 'register',
315     action => $action,
316     next_action => [ $self, 'redirect_to', 'accounts', $c->req->captures ],
317     ctx => $c,
318     column_order => [
319       qw / contact_title company_name email address1 address2 address3
320            city country post_code telephone mobile fax/ ],
321   );
322
323 =head1 DESCRIPTION
324
325 This subclass of viewport is used for rendering a collection of
326 L<Reaction::UI::ViewPort::Field> objects for user editing.
327
328 =head1 ATTRIBUTES
329
330 =head2 action
331
332 L<Reaction::InterfaceModel::Action>
333
334 =head2 ok_label
335
336 Default: 'ok'
337
338 =head2 apply_label
339
340 Default: 'apply'
341
342 =head2 close_label_close
343
344 Default: 'close'
345
346 =head2 close_label_cancel
347
348 This label is only shown when C<changed> is true.
349
350 Default: 'cancel'
351
352 =head2 fields
353
354 =head2 field_names
355
356 Returns: Arrayref of field names.
357
358 =head2 can_apply
359
360 =head2 can_close
361
362 =head2 changed
363
364 Returns true if a field has been edited.
365
366 =head2 next_action
367
368 =head2 on_apply_callback
369
370 CodeRef.
371
372 =head1 METHODS
373
374 =head2 ok
375
376 Calls C<apply>, and then C<close> if successful.
377
378 =head2 close
379
380 Pop viewport and proceed to C<next_action>.
381
382 =head2 apply
383
384 Attempt to save changes and update C<changed> attribute if required.
385
386 =head1 SEE ALSO
387
388 L<Reaction::UI::ViewPort>
389
390 L<Reaction::InterfaceModel::Action>
391
392 =head1 AUTHORS
393
394 See L<Reaction::Class> for authors.
395
396 =head1 LICENSE
397
398 See L<Reaction::Class> for the license.
399
400 =cut