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