Error-Handling: Bei AJAX-Requests Fehler als JSON-Object zurückgeben
[kivitendo-erp.git] / SL / ClientJS.pm
1 package SL::ClientJS;
2
3 use strict;
4
5 use parent qw(Rose::Object);
6
7 use Carp;
8 use SL::JSON ();
9
10 use Rose::Object::MakeMethods::Generic
11 (
12   'scalar --get_set_init' => [ qw(_actions _flash _error) ],
13 );
14
15 my %supported_methods = (
16   # ## Non-jQuery methods ##
17   flash        => 2,            # display_flash(<TARGET>, <ARGS>)
18
19   # ## jQuery basics ##
20
21   # Basic effects
22   hide         => 1,
23   show         => 1,
24   toggle       => 1,
25
26   # DOM insertion, around
27   unwrap       => 1,
28   wrap         => 2,
29   wrapAll      => 2,
30   wrapInner    => 2,
31
32   # DOM insertion, inside
33   append       => 2,
34   appendTo     => 2,
35   html         => 2,
36   prepend      => 2,
37   prependTo    => 2,
38   text         => 2,
39
40   # DOM insertion, outside
41   after        => 2,
42   before       => 2,
43   insertAfter  => 2,
44   insertBefore => 2,
45
46   # DOM removal
47   empty        => 1,
48   remove       => 1,
49
50   # DOM replacement
51   replaceAll   => 2,
52   replaceWith  => 2,
53
54   # General attributes
55   attr         => 3,
56   prop         => 3,
57   removeAttr   => 2,
58   removeProp   => 2,
59   val          => 2,
60
61   # Data storage
62   data         => 3,
63   removeData   => 2,
64
65   # Form Events
66   focus        => 1,
67
68   # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
69
70   # Operations on the whole tree
71   'jstree:lock'          => 1,
72   'jstree:unlock'        => 1,
73
74   # Opening and closing nodes
75   'jstree:open_node'     => 2,
76   'jstree:open_all'      => 2,
77   'jstree:close_node'    => 2,
78   'jstree:close_all'     => 2,
79   'jstree:toggle_node'   => 2,
80   'jstree:save_opened'   => 1,
81   'jstree:reopen'        => 1,
82
83   # Modifying nodes
84   'jstree:rename_node'   => 3,
85   'jstree:delete_node'   => 2,
86   'jstree:move_node'     => 5,
87
88   # Selecting nodes (from the 'ui' plugin to jstree)
89   'jstree:select_node'   => 2,  # $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>, true)
90   'jstree:deselect_node' => 2,
91   'jstree:deselect_all'  => 1,
92 );
93
94 sub AUTOLOAD {
95   our $AUTOLOAD;
96
97   my ($self, @args) = @_;
98
99   my $method        =  $AUTOLOAD;
100   $method           =~ s/.*:://;
101   return if $method eq 'DESTROY';
102   return $self->action($method, @args);
103 }
104
105 sub action {
106   my ($self, $method, @args) = @_;
107
108   $method      =  (delete($self->{_prefix}) || '') . $method;
109   my $num_args =  $supported_methods{$method};
110
111   croak "Unsupported jQuery action: $method"                                                    unless defined $num_args;
112   croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if     scalar(@args) != $num_args;
113
114   if ($num_args) {
115     # Force flattening from SL::Presenter::EscapedText: "" . $...
116     $args[0] =  "" . $args[0];
117     $args[0] =~ s/^\s+//;
118   }
119
120   push @{ $self->_actions }, [ $method, @args ];
121
122   return $self;
123 }
124
125 sub init__actions {
126   return [];
127 }
128
129 sub init__flash {
130   return {};
131 }
132
133 sub init__error {
134   return '';
135 }
136
137 sub to_json {
138   my ($self) = @_;
139
140   return SL::JSON::to_json({ error        => $self->_error   }) if $self->_error;
141   return SL::JSON::to_json({ eval_actions => $self->_actions });
142 }
143
144 sub to_array {
145   my ($self) = @_;
146   return $self->_actions;
147 }
148
149 sub render {
150   my ($self, $controller) = @_;
151   return $controller->render(\$self->to_json, { type => 'json' });
152 }
153
154 sub jstree {
155   my ($self) = @_;
156   $self->{_prefix} = 'jstree:';
157   return $self;
158 }
159
160 sub flash {
161   my ($self, $type, @messages) = @_;
162
163   my $message = join ' ', grep { $_ } @messages;
164
165   if (!$self->_flash->{$type}) {
166     $self->_flash->{$type} = [ 'flash', $type, $message ];
167     push @{ $self->_actions }, $self->_flash->{$type};
168   } else {
169     $self->_flash->{$type}->[-1] .= ' ' . $message;
170   }
171
172   return $self;
173 }
174
175 sub error {
176   my ($self, @messages) = @_;
177
178   $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
179
180   return $self;
181 }
182
183 1;
184 __END__
185
186 =pod
187
188 =encoding utf8
189
190 =head1 NAME
191
192 SL::ClientJS - Easy programmatic client-side JavaScript generation
193 with jQuery
194
195 =head1 SYNOPSIS
196
197 First some JavaScript code:
198
199   // In the client generate an AJAX request whose 'success' handler
200   // calls "eval_json_response(data)":
201   var data = {
202     action: "SomeController/the_action",
203     id:     $('#some_input_field').val()
204   };
205   $.post("controller.pl", data, eval_json_response);
206
207 Now some Perl code:
208
209   # In the controller itself. First, make sure that the "client_js.js"
210   # is loaded. This must be done when the whole side is loaded, so
211   # it's not in the action called by the AJAX request shown above.
212   $::request->layout->use_javascript('client_js.js');
213
214   # Now in that action called via AJAX:
215   sub action_the_action {
216     my ($self) = @_;
217
218     # Create a new client-side JS object and do stuff with it!
219     my $js = SL::ClientJS->new;
220
221     # Show some element on the page:
222     $js->show('#usually_hidden');
223
224     # Set to hidden inputs. Yes, calls can be chained!
225     $js->val('#hidden_id', $self->new_id)
226        ->val('#other_type', 'Unicorn');
227
228     # Replace some HTML code:
229     my $html = $self->render('SomeController/the_action', { output => 0 });
230     $js->html('#id_with_new_content', $html);
231
232     # Operations on a jstree: rename a node and select it
233     my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
234     $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
235        ->jstree->select_node('#tb-' . $text_block->id);
236
237     # Finally render the JSON response:
238     $self->render($js);
239
240     # Rendering can also be chained, e.g.
241     $js->html('#selector', $html)
242        ->render($self);
243   }
244
245 =head1 OVERVIEW
246
247 This module enables the generation of jQuery-using JavaScript code on
248 the server side. That code is then evaluated in a safe way on the
249 client side.
250
251 The workflow is usally that the client creates an AJAX request, the
252 server creates some actions and sends them back, and the client then
253 implements each of these actions.
254
255 There are three things that need to be done for this to work:
256
257 =over 2
258
259 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
260
261 =item 2. The client code needs to call C<eval_json_response()> with the result returned from the server.
262
263 =item 3. The server must use this module.
264
265 =back
266
267 The functions called on the client side are mostly jQuery
268 functions. Other functionality may be added later.
269
270 Note that L<SL::Controller/render> is aware of this module which saves
271 you some boilerplate. The following two calls are equivalent:
272
273   $controller->render($client_js);
274   $controller->render(\$client_js->to_json, { type => 'json' });
275
276 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
277
278 =over 4
279
280 =item C<to_array>
281
282 Returns the actions gathered so far as an array reference. Each
283 element is an array reference containing at least two items: the
284 function's name and what it is called on. Additional array elements
285 are the function parameters.
286
287 =item C<to_json>
288
289 Returns the actions gathered so far as a JSON string ready to be sent
290 to the client.
291
292 =item C<render $controller>
293
294 Renders C<$self> via the controller. Useful for chaining. Equivalent
295 to the following:
296
297   $controller->render(\$self->to_json, { type => 'json' });
298
299 =item C<jstree>
300
301 Tells C<$self> that the next action is to be called on a jstree
302 instance. For example:
303
304   $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
305
306 =back
307
308 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
309
310 =head2 GENERIC FUNCTION
311
312 All of the following functions can be invoked in two ways: either by
313 calling the function name directly on C<$self> or by calling
314 L</action> with the function name as the first parameter. Therefore
315 the following two calls are identical:
316
317   $js->insertAfter($html, '#some-id');
318   $js->action('insertAfter', $html, '#some-id');
319
320 The second form, calling L</action>, is more to type but can be useful
321 in situations in which you have to call one of two functions depending
322 on context. For example, when you want to insert new code in a
323 list. If the list is empty you might have to use C<appendTo>, if it
324 isn't you might have to use C<insertAfter>. Example:
325
326   my $html = $self->render(...);
327   $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
328
329 Instead of:
330
331   my $html = $self->render(...);
332   if ($list_is_empty) {
333     $js->appendTo($html, '#text-block-list');
334   } else {
335     $js->insertAfter($html, '#text-block-' . $self->text_block->id);
336   }
337
338 The first variation is obviously better suited for chaining.
339
340 Additional functions:
341
342 =over 4
343
344 =item C<flash $type, $message>
345
346 Display a C<$message> in the flash of type C<$type>. Multiple calls of
347 C<flash> on the same C<$self> will be merged by type.
348
349 On the client side the flash of this type will be cleared before the
350 message is shown.
351
352 =item C<error $message>
353
354 Causes L<to_json> (and therefore L<render>) to output a JSON object
355 that only contains an C<error> field set to this C<$message>. The
356 client will then show the message in the 'error' flash.
357
358 The messages of multiple calls of C<error> on the same C<$self> will
359 be merged.
360
361 =back
362
363 =head2 JQUERY FUNCTIONS
364
365 The following jQuery functions are supported:
366
367 =over 4
368
369 =item Basic effects
370
371 C<hide>, C<show>, C<toggle>
372
373 =item DOM insertion, around
374
375 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
376
377 =item DOM insertion, inside
378
379 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
380
381 =item DOM insertion, outside
382
383 C<after>, C<before>, C<insertAfter>, C<insertBefore>
384
385 =item DOM removal
386
387 C<empty>, C<remove>
388
389 =item DOM replacement
390
391 C<replaceAll>, C<replaceWith>
392
393 =item General attributes
394
395 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
396
397 =item Data storage
398
399 C<data>, C<removeData>
400
401 =item Form Events
402
403 C<focus>
404
405 =back
406
407 =head2 JSTREE JQUERY PLUGIN
408
409 The following functions of the C<jstree> plugin to jQuery are
410 supported:
411
412 =over 4
413
414 =item Operations on the whole tree
415
416 C<lock>, C<unlock>
417
418 =item Opening and closing nodes
419
420 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
421 C<close_all>, C<save_opened>, C<reopen>
422
423 =item Modifying nodes
424
425 C<rename_node>, C<delete_node>, C<move_node>
426
427 =item Selecting nodes (from the 'ui' jstree plugin)
428
429 C<select_node>, C<deselect_node>, C<deselect_all>
430
431 =back
432
433 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
434
435 In order not having to maintain two files (this one and
436 C<js/client_js.js>) there's a script that can parse this file's
437 C<%supported_methods> definition and generate the file
438 C<js/client_js.js> accordingly. The steps are:
439
440 =over 2
441
442 =item 1. Add lines in this file to the C<%supported_methods> hash. The
443 key is the function name and the value is the number of expected
444 parameters.
445
446 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
447 generate C<js/client_js.js> automatically.
448
449 =item 3. Reload the files in your browser (cleaning its cache can also
450 help).
451
452 =back
453
454 The template file used for generated C<js/client_js.js> is
455 C<scripts/generate_client_js_actions.tpl>.
456
457 =head1 BUGS
458
459 Nothing here yet.
460
461 =head1 AUTHOR
462
463 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
464
465 =cut