start of new widgets for fields
[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
7adfd53f 22 has field_names => (isa => 'ArrayRef', is => 'rw', lazy_build => 1);
f670cfd0 23
7adfd53f 24 has _field_map => (
25 isa => 'HashRef', is => 'rw', init_arg => 'fields',
26 predicate => '_has_field_map', set_or_lazy_build('field_map'),
27 );
f670cfd0 28
7adfd53f 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 );
f670cfd0 36
7adfd53f 37 has on_apply_callback => (
38 isa => 'CodeRef', is => 'rw', required => 0,
39 predicate => 'has_on_apply_callback'
40 );
f670cfd0 41
7adfd53f 42 has ok_label => (
43 isa => 'Str', is => 'rw', required => 1, default => sub { 'ok' }
44 );
f670cfd0 45
7adfd53f 46 has apply_label => (
47 isa => 'Str', is => 'rw', required => 1, default => sub { 'apply' }
48 );
f670cfd0 49
7adfd53f 50 has close_label => (isa => 'Str', is => 'rw', lazy_fail => 1);
f670cfd0 51
7adfd53f 52 has close_label_close => (
53 isa => 'Str', is => 'rw', required => 1, default => sub { 'close' }
54 );
f670cfd0 55
7adfd53f 56 has close_label_cancel => (
57 isa => 'Str', is => 'rw', required => 1, default => sub { 'cancel' }
58 );
f670cfd0 59
7adfd53f 60 sub fields { shift->_field_map }
f670cfd0 61
7adfd53f 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 }
f670cfd0 70
7adfd53f 71 my %field_map = @field_map;
72 my @field_names = @{ $self->sort_by_spec(
73 $args->{column_order}, [keys %field_map] )};
f670cfd0 74
7adfd53f 75 $self->_field_map(\%field_map);
76 $self->field_names(\@field_names);
77 }
78 $self->close_label($self->close_label_close);
79 };
f670cfd0 80
7adfd53f 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 };
f670cfd0 128
7adfd53f 129 implements build_field_map => as {
130 confess "Lazy field map building not supported by default";
131 };
f670cfd0 132
7adfd53f 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 };
f670cfd0 143
7adfd53f 144 implements do_apply => as {
145 my $self = shift;
146 return $self->action->do_apply;
147 };
f670cfd0 148
7adfd53f 149 implements ok => as {
150 my $self = shift;
151 if ($self->apply(@_)) {
152 $self->close(@_);
153 }
154 };
f670cfd0 155
7adfd53f 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);
f670cfd0 161 $self->on_apply_callback->($self => $result) if $self->has_on_apply_callback;
7adfd53f 162 return 1;
163 } else {
164 $self->changed(1);
165 $self->close_label($self->close_label_cancel);
166 return 0;
167 }
168 };
f670cfd0 169
7adfd53f 170 implements close => as {
171 my $self = shift;
172 my ($controller, $name, @args) = @{$self->next_action};
173 $controller->pop_viewport;
174 $controller->$name($self->action->ctx, @args);
175 };
f670cfd0 176
7adfd53f 177 sub can_close { 1 }
f670cfd0 178
7adfd53f 179 override accept_events => sub {
180 (($_[0]->has_next_action ? ('ok', 'close') : ()), 'apply', super());
181 }; # can't do a close-type operation if there's nowhere to go afterwards
f670cfd0 182
7adfd53f 183 override child_event_sinks => sub {
184 my ($self) = @_;
185 return ((grep { ref($_) =~ 'Hidden' } values %{$self->_field_map}),
186 (grep { ref($_) !~ 'Hidden' } values %{$self->_field_map}),
187 super());
188 };
f670cfd0 189
7adfd53f 190 after apply_child_events => sub {
191 # interrupt here because fields will have been updated
192 my ($self) = @_;
193 $self->sync_action_from_fields;
194 };
f670cfd0 195
7adfd53f 196 implements sync_action_from_fields => as {
197 my ($self) = @_;
198 my $field_map = $self->_field_map;
199 my @fields = values %{$field_map};
200 foreach my $field (@fields) {
201 $field->sync_to_action; # get the field to populate the $action if possible
202 }
203 $self->action->sync_all;
204 foreach my $field (@fields) {
205 $field->sync_from_action; # get errors from $action if applicable
206 }
207 };
f670cfd0 208
7adfd53f 209 implements build_simple_field => as {
210 my ($self, $class, $attr, $args) = @_;
211 my $attr_name = $attr->name;
212 my %extra;
213 if (my $config = $args->{Field}{$attr_name}) {
214 %extra = %$config;
215 }
216 my $field = $class->new(
217 action => $self->action,
218 attribute => $attr,
219 name => $attr->name,
f670cfd0 220 location => join('-', $self->location, 'field', $attr->name),
7adfd53f 221 ctx => $self->ctx,
222 %extra
223 );
224 return ($attr_name => $field);
225 };
f670cfd0 226
7adfd53f 227 implements build_fields_for_type_Num => as {
228 my ($self, $attr, $args) = @_;
229 return $self->build_simple_field(Number, $attr, $args);
230 };
f670cfd0 231
7adfd53f 232 implements build_fields_for_type_Int => as {
233 my ($self, $attr, $args) = @_;
234 return $self->build_simple_field(Number, $attr, $args);
235 };
f670cfd0 236
7adfd53f 237 implements build_fields_for_type_Bool => as {
238 my ($self, $attr, $args) = @_;
239 return $self->build_simple_field(Boolean, $attr, $args);
240 };
f670cfd0 241
7adfd53f 242 implements build_fields_for_type_File => as {
243 my ($self, $attr, $args) = @_;
244 return $self->build_simple_field(File, $attr, $args);
245 };
f670cfd0 246
7adfd53f 247 implements build_fields_for_type_Str => as {
248 my ($self, $attr, $args) = @_;
249 if ($attr->has_valid_values) { # There's probably a better way to do this
250 return $self->build_simple_field(ChooseOne, $attr, $args);
251 }
252 return $self->build_simple_field(Text, $attr, $args);
253 };
f670cfd0 254
7adfd53f 255 implements build_fields_for_type_SimpleStr => as {
256 my ($self, $attr, $args) = @_;
257 return $self->build_simple_field(String, $attr, $args);
258 };
f670cfd0 259
7adfd53f 260 implements build_fields_for_type_Password => as {
261 my ($self, $attr, $args) = @_;
262 return $self->build_simple_field(Password, $attr, $args);
263 };
f670cfd0 264
7adfd53f 265 implements build_fields_for_type_DateTime => as {
266 my ($self, $attr, $args) = @_;
267 return $self->build_simple_field(DateTime, $attr, $args);
268 };
f670cfd0 269
7adfd53f 270 implements build_fields_for_type_Enum => as {
271 my ($self, $attr, $args) = @_;
272 return $self->build_simple_field(ChooseOne, $attr, $args);
273 };
f670cfd0 274
275 #implements build_fields_for_type_Reaction_InterfaceModel_Object => as {
7adfd53f 276 implements build_fields_for_type_DBIx_Class_Row => as {
277 my ($self, $attr, $args) = @_;
278 return $self->build_simple_field(ChooseOne, $attr, $args);
279 };
f670cfd0 280
7adfd53f 281 implements build_fields_for_type_ArrayRef => as {
282 my ($self, $attr, $args) = @_;
283 if ($attr->has_valid_values) {
284 return $self->build_simple_field(ChooseMany, $attr, $args)
285 } else {
286 return $self->build_simple_field(HiddenArray, $attr, $args)
287 }
288 };
f670cfd0 289
7adfd53f 290 implements build_fields_for_type_DateTime_Spanset => as {
291 my ($self, $attr, $args) = @_;
292 return $self->build_simple_field(TimeRange, $attr, $args);
293 };
f670cfd0 294
7adfd53f 295 no Moose;
f670cfd0 296
7adfd53f 297 no strict 'refs';
298 delete ${__PACKAGE__ . '::'}{inner};
299
300};
301
3021;
303
304=head1 NAME
305
306Reaction::UI::ViewPort::ActionForm
307
308=head1 SYNOPSIS
309
310 use aliased 'Reaction::UI::ViewPort::ActionForm';
311
312 $self->push_viewport(ActionForm,
313 layout => 'register',
314 action => $action,
315 next_action => [ $self, 'redirect_to', 'accounts', $c->req->captures ],
316 ctx => $c,
317 column_order => [
318 qw / contact_title company_name email address1 address2 address3
319 city country post_code telephone mobile fax/ ],
320 );
321
322=head1 DESCRIPTION
323
324This subclass of viewport is used for rendering a collection of
325L<Reaction::UI::ViewPort::Field> objects for user editing.
326
327=head1 ATTRIBUTES
328
329=head2 action
330
331L<Reaction::InterfaceModel::Action>
332
333=head2 ok_label
334
335Default: 'ok'
336
337=head2 apply_label
338
339Default: 'apply'
340
341=head2 close_label_close
342
343Default: 'close'
344
345=head2 close_label_cancel
346
347This label is only shown when C<changed> is true.
348
349Default: 'cancel'
350
351=head2 fields
352
353=head2 field_names
354
355Returns: Arrayref of field names.
356
357=head2 can_apply
358
359=head2 can_close
360
361=head2 changed
362
363Returns true if a field has been edited.
364
365=head2 next_action
366
367=head2 on_apply_callback
368
369CodeRef.
370
371=head1 METHODS
372
373=head2 ok
374
375Calls C<apply>, and then C<close> if successful.
376
377=head2 close
378
379Pop viewport and proceed to C<next_action>.
380
381=head2 apply
382
383Attempt to save changes and update C<changed> attribute if required.
384
385=head1 SEE ALSO
386
387L<Reaction::UI::ViewPort>
388
389L<Reaction::InterfaceModel::Action>
390
391=head1 AUTHORS
392
393See L<Reaction::Class> for authors.
394
395=head1 LICENSE
396
397See L<Reaction::Class> for the license.
398
399=cut