ClientJS: AJAX-Form-Submit mit jQuery-Form-Plugin
[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,            # kivi.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   # Class attribute
62   addClass     => 2,
63   removeClass  => 2,
64   toggleClass  => 2,
65
66   # Data storage
67   data         => 3,
68   removeData   => 2,
69
70   # Form Events
71   focus        => 1,
72
73   # Generic Event Handling ## pattern: $(<TARGET>).<FUNCTION>(<ARG1>, kivi.get_function_by_name(<ARG2>))
74   on           => 3,
75   off          => 3,
76   one          => 3,
77
78   # ## jQuery UI dialog plugin ## pattern: $(<TARGET>).dialog('<FUNCTION>')
79
80   # Closing and removing the popup
81   'dialog:close'         => 1,
82
83   # ## jQuery Form plugin ##
84   'ajaxForm'             => 1, # pattern: $(<TARGET>).ajaxForm({ success: eval_json_result })
85
86   # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
87
88   # Operations on the whole tree
89   'jstree:lock'          => 1,
90   'jstree:unlock'        => 1,
91
92   # Opening and closing nodes
93   'jstree:open_node'     => 2,
94   'jstree:open_all'      => 2,
95   'jstree:close_node'    => 2,
96   'jstree:close_all'     => 2,
97   'jstree:toggle_node'   => 2,
98   'jstree:save_opened'   => 1,
99   'jstree:reopen'        => 1,
100
101   # Modifying nodes
102   'jstree:create_node'   => 4,
103   'jstree:rename_node'   => 3,
104   'jstree:delete_node'   => 2,
105   'jstree:move_node'     => 5,
106
107   # Selecting nodes (from the 'ui' plugin to jstree)
108   'jstree:select_node'   => 2,  # $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>, true)
109   'jstree:deselect_node' => 2,
110   'jstree:deselect_all'  => 1,
111
112   # ## other stuff ##
113   redirect_to            => 1,  # window.location.href = <TARGET>
114
115   reinit_widgets         => 0,  # kivi.reinit_widgets()
116 );
117
118 sub AUTOLOAD {
119   our $AUTOLOAD;
120
121   my ($self, @args) = @_;
122
123   my $method        =  $AUTOLOAD;
124   $method           =~ s/.*:://;
125   return if $method eq 'DESTROY';
126   return $self->action($method, @args);
127 }
128
129 sub action {
130   my ($self, $method, @args) = @_;
131
132   $method      =  (delete($self->{_prefix}) || '') . $method;
133   my $num_args =  $supported_methods{$method};
134
135   croak "Unsupported jQuery action: $method"                                                    unless defined $num_args;
136   croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if     scalar(@args) != $num_args;
137
138   foreach my $idx (0..$num_args - 1) {
139     # Force flattening from SL::Presenter::EscapedText and trim leading whitespace for scalars
140     $args[$idx] =  "" . $args[$idx] if  ref($args[$idx]) eq 'SL::Presenter::EscapedText';
141     $args[$idx] =~ s/^\s+//         if !ref($args[$idx]);
142   }
143
144   push @{ $self->_actions }, [ $method, @args ];
145
146   return $self;
147 }
148
149 sub action_if {
150   my ($self, $condition, @args) = @_;
151
152   return $condition ? $self->action(@args) : $self;
153 }
154
155 sub init__actions {
156   return [];
157 }
158
159 sub init__flash {
160   return {};
161 }
162
163 sub init__error {
164   return '';
165 }
166
167 sub to_json {
168   my ($self) = @_;
169
170   return SL::JSON::to_json({ error        => $self->_error   }) if $self->_error;
171   return SL::JSON::to_json({ eval_actions => $self->_actions });
172 }
173
174 sub to_array {
175   my ($self) = @_;
176   return $self->_actions;
177 }
178
179 sub render {
180   my ($self, $controller) = @_;
181   $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
182   return $controller->render(\$self->to_json, { type => 'json' });
183 }
184
185 sub jstree {
186   my ($self) = @_;
187   $self->{_prefix} = 'jstree:';
188   return $self;
189 }
190
191 sub dialog {
192   my ($self) = @_;
193   $self->{_prefix} = 'dialog:';
194   return $self;
195 }
196
197 sub flash {
198   my ($self, $type, @messages) = @_;
199
200   my $message = join ' ', grep { $_ } @messages;
201
202   if (!$self->_flash->{$type}) {
203     $self->_flash->{$type} = [ 'flash', $type, $message ];
204     push @{ $self->_actions }, $self->_flash->{$type};
205   } else {
206     $self->_flash->{$type}->[-1] .= ' ' . $message;
207   }
208
209   return $self;
210 }
211
212 sub error {
213   my ($self, @messages) = @_;
214
215   $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
216
217   return $self;
218 }
219
220 1;
221 __END__
222
223 =pod
224
225 =encoding utf8
226
227 =head1 NAME
228
229 SL::ClientJS - Easy programmatic client-side JavaScript generation
230 with jQuery
231
232 =head1 SYNOPSIS
233
234 First some JavaScript code:
235
236   // In the client generate an AJAX request whose 'success' handler
237   // calls "eval_json_result(data)":
238   var data = {
239     action: "SomeController/the_action",
240     id:     $('#some_input_field').val()
241   };
242   $.post("controller.pl", data, eval_json_result);
243
244 Now some Perl code:
245
246   # In the controller itself. First, make sure that the "client_js.js"
247   # is loaded. This must be done when the whole side is loaded, so
248   # it's not in the action called by the AJAX request shown above.
249   $::request->layout->use_javascript('client_js.js');
250
251   # Now in that action called via AJAX:
252   sub action_the_action {
253     my ($self) = @_;
254
255     # Create a new client-side JS object and do stuff with it!
256     my $js = SL::ClientJS->new;
257
258     # Show some element on the page:
259     $js->show('#usually_hidden');
260
261     # Set to hidden inputs. Yes, calls can be chained!
262     $js->val('#hidden_id', $self->new_id)
263        ->val('#other_type', 'Unicorn');
264
265     # Replace some HTML code:
266     my $html = $self->render('SomeController/the_action', { output => 0 });
267     $js->html('#id_with_new_content', $html);
268
269     # Operations on a jstree: rename a node and select it
270     my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
271     $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
272        ->jstree->select_node('#tb-' . $text_block->id);
273
274     # Close a popup opened by kivi.popup_dialog():
275     $js->dialog->close('#jqueryui_popup_dialog');
276
277     # Finally render the JSON response:
278     $self->render($js);
279
280     # Rendering can also be chained, e.g.
281     $js->html('#selector', $html)
282        ->render($self);
283   }
284
285 =head1 OVERVIEW
286
287 This module enables the generation of jQuery-using JavaScript code on
288 the server side. That code is then evaluated in a safe way on the
289 client side.
290
291 The workflow is usally that the client creates an AJAX request, the
292 server creates some actions and sends them back, and the client then
293 implements each of these actions.
294
295 There are three things that need to be done for this to work:
296
297 =over 2
298
299 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
300
301 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
302
303 =item 3. The server must use this module.
304
305 =back
306
307 The functions called on the client side are mostly jQuery
308 functions. Other functionality may be added later.
309
310 Note that L<SL::Controller/render> is aware of this module which saves
311 you some boilerplate. The following two calls are equivalent:
312
313   $controller->render($client_js);
314   $controller->render(\$client_js->to_json, { type => 'json' });
315
316 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
317
318 =over 4
319
320 =item C<to_array>
321
322 Returns the actions gathered so far as an array reference. Each
323 element is an array reference containing at least two items: the
324 function's name and what it is called on. Additional array elements
325 are the function parameters.
326
327 =item C<to_json>
328
329 Returns the actions gathered so far as a JSON string ready to be sent
330 to the client.
331
332 =item C<render $controller>
333
334 Renders C<$self> via the controller. Useful for chaining. Equivalent
335 to the following:
336
337   $controller->render(\$self->to_json, { type => 'json' });
338
339 =item C<dialog>
340
341 Tells C<$self> that the next action is to be called on a jQuery UI
342 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
343 example:
344
345   $js->dialog->close('#jqueryui_popup_dialog');
346
347 =item C<jstree>
348
349 Tells C<$self> that the next action is to be called on a jstree
350 instance. For example:
351
352   $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
353
354 =back
355
356 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
357
358 =head2 GENERIC FUNCTION
359
360 All of the following functions can be invoked in two ways: either by
361 calling the function name directly on C<$self> or by calling
362 L</action> with the function name as the first parameter. Therefore
363 the following two calls are identical:
364
365   $js->insertAfter($html, '#some-id');
366   $js->action('insertAfter', $html, '#some-id');
367
368 The second form, calling L</action>, is more to type but can be useful
369 in situations in which you have to call one of two functions depending
370 on context. For example, when you want to insert new code in a
371 list. If the list is empty you might have to use C<appendTo>, if it
372 isn't you might have to use C<insertAfter>. Example:
373
374   my $html = $self->render(...);
375   $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
376
377 Instead of:
378
379   my $html = $self->render(...);
380   if ($list_is_empty) {
381     $js->appendTo($html, '#text-block-list');
382   } else {
383     $js->insertAfter($html, '#text-block-' . $self->text_block->id);
384   }
385
386 The first variation is obviously better suited for chaining.
387
388 =over 4
389
390 =item C<action $method, @args>
391
392 Call the function with the name C<$method> on C<$self> with arguments
393 C<@args>. Returns the return value of the actual function
394 called. Useful for chaining (see above).
395
396 =item C<action_if $condition, $method, @args>
397
398 Call the function with the name C<$method> on C<$self> with arguments
399 C<@args> if C<$condition> is trueish. Does nothing otherwise.
400
401 Returns the return value of the actual function called if
402 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
403 (see above).
404
405 This function is equivalent to the following:
406
407   if ($condition) {
408     $obj->$method(@args);
409   }
410
411 But it is easier to integrate into a method call chain, e.g.:
412
413   $js->html('#content', $html)
414      ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
415      ->render($self);
416
417 =back
418
419 =head2 ADDITIONAL FUNCTIONS
420
421 =over 4
422
423 =item C<flash $type, $message>
424
425 Display a C<$message> in the flash of type C<$type>. Multiple calls of
426 C<flash> on the same C<$self> will be merged by type.
427
428 On the client side the flashes of all types will be cleared after each
429 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
430
431 =item C<error $message>
432
433 Causes L<to_json> (and therefore L<render>) to output a JSON object
434 that only contains an C<error> field set to this C<$message>. The
435 client will then show the message in the 'error' flash.
436
437 The messages of multiple calls of C<error> on the same C<$self> will
438 be merged.
439
440 =item C<redirect_to $url>
441
442 Redirects the browser window to the new URL by setting the JavaScript
443 property C<window.location.href>. Note that
444 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
445 function if the current request is an AJAX request as determined by
446 L<SL::Request/is_ajax>.
447
448 =back
449
450 =head2 JQUERY FUNCTIONS
451
452 The following jQuery functions are supported:
453
454 =over 4
455
456 =item Basic effects
457
458 C<hide>, C<show>, C<toggle>
459
460 =item DOM insertion, around
461
462 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
463
464 =item DOM insertion, inside
465
466 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
467
468 =item DOM insertion, outside
469
470 C<after>, C<before>, C<insertAfter>, C<insertBefore>
471
472 =item DOM removal
473
474 C<empty>, C<remove>
475
476 =item DOM replacement
477
478 C<replaceAll>, C<replaceWith>
479
480 =item General attributes
481
482 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
483
484 =item Data storage
485
486 C<data>, C<removeData>
487
488 =item Form Events
489
490 C<focus>
491
492 =item Generic Event Handlers
493
494 C<on>, C<off>, C<one>
495
496 These attach/detach event listeners to specific selectors. The first
497 argument is the selector, the second the name of the events and the
498 third argument is the name of the handler function. That function must
499 already exist when the handler is added.
500
501 =back
502
503 =head2 JSTREE JQUERY PLUGIN
504
505 The following functions of the C<jstree> plugin to jQuery are
506 supported:
507
508 =over 4
509
510 =item Operations on the whole tree
511
512 C<lock>, C<unlock>
513
514 =item Opening and closing nodes
515
516 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
517 C<close_all>, C<save_opened>, C<reopen>
518
519 =item Modifying nodes
520
521 C<rename_node>, C<delete_node>, C<move_node>
522
523 =item Selecting nodes (from the 'ui' jstree plugin)
524
525 C<select_node>, C<deselect_node>, C<deselect_all>
526
527 =back
528
529 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
530
531 In order not having to maintain two files (this one and
532 C<js/client_js.js>) there's a script that can parse this file's
533 C<%supported_methods> definition and generate the file
534 C<js/client_js.js> accordingly. The steps are:
535
536 =over 2
537
538 =item 1. Add lines in this file to the C<%supported_methods> hash. The
539 key is the function name and the value is the number of expected
540 parameters.
541
542 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
543 generate C<js/client_js.js> automatically.
544
545 =item 3. Reload the files in your browser (cleaning its cache can also
546 help).
547
548 =back
549
550 The template file used for generated C<js/client_js.js> is
551 C<scripts/generate_client_js_actions.tpl>.
552
553 =head1 BUGS
554
555 Nothing here yet.
556
557 =head1 AUTHOR
558
559 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
560
561 =cut