5 use parent qw(Rose::Object);
10 use Rose::Object::MakeMethods::Generic
12 'scalar --get_set_init' => [ qw(_actions _flash _error) ],
15 my %supported_methods = (
23 # DOM insertion, around
29 # DOM insertion, inside
37 # DOM insertion, outside
70 # Generic Event Handling ## pattern: $(<TARGET>).<FUNCTION>(<ARG1>, kivi.get_function_by_name(<ARG2>))
75 # ## jQuery UI dialog plugin ## pattern: $(<TARGET>).dialog('<FUNCTION>')
77 # Closing and removing the popup
80 # ## jQuery Form plugin ##
81 'ajaxForm' => 1, # pattern: $(<TARGET>).ajaxForm({ success: eval_json_result })
83 # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
85 # Operations on the whole tree
89 # Opening and closing nodes
90 'jstree:open_node' => 2,
91 'jstree:open_all' => 2,
92 'jstree:close_node' => 2,
93 'jstree:close_all' => 2,
94 'jstree:toggle_node' => 2,
95 'jstree:save_opened' => 1,
99 'jstree:create_node' => 4,
100 'jstree:rename_node' => 3,
101 'jstree:delete_node' => 2,
102 'jstree:move_node' => 5,
104 # Selecting nodes (from the 'ui' plugin to jstree)
105 'jstree:select_node' => 2, # $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>, true)
106 'jstree:deselect_node' => 2,
107 'jstree:deselect_all' => 1,
110 redirect_to => 1, # window.location.href = <TARGET>
112 flash => 2, # kivi.display_flash(<TARGET>, <ARGS>)
113 reinit_widgets => 0, # kivi.reinit_widgets()
114 run => -1, # kivi.run(<TARGET>, <ARGS>)
115 run_once_for => 3, # kivi.run_once_for(<TARGET>, <ARGS>)
121 my ($self, @args) = @_;
123 my $method = $AUTOLOAD;
125 return if $method eq 'DESTROY';
126 return $self->action($method, @args);
130 my ($self, $method, @args) = @_;
132 $method = (delete($self->{_prefix}) || '') . $method;
133 my $num_args = $supported_methods{$method};
135 croak "Unsupported jQuery action: $method" unless defined $num_args;
138 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
141 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted at least: $num_args)" if scalar(@args) < $num_args;
142 $num_args = scalar @args;
145 foreach my $idx (0..$num_args - 1) {
146 # Force flattening from SL::Presenter::EscapedText and trim leading whitespace for scalars
147 $args[$idx] = "" . $args[$idx] if ref($args[$idx]) eq 'SL::Presenter::EscapedText';
148 $args[$idx] =~ s/^\s+// if !ref($args[$idx]);
151 push @{ $self->_actions }, [ $method, @args ];
157 my ($self, $condition, @args) = @_;
159 return $condition ? $self->action(@args) : $self;
177 return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
178 return SL::JSON::to_json({ eval_actions => $self->_actions });
183 return $self->_actions;
187 my ($self, $controller) = @_;
188 $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
189 return $controller->render(\$self->to_json, { type => 'json' });
194 $self->{_prefix} = 'jstree:';
200 $self->{_prefix} = 'dialog:';
205 my ($self, $type, @messages) = @_;
207 my $message = join ' ', grep { $_ } @messages;
209 if (!$self->_flash->{$type}) {
210 $self->_flash->{$type} = [ 'flash', $type, $message ];
211 push @{ $self->_actions }, $self->_flash->{$type};
213 $self->_flash->{$type}->[-1] .= ' ' . $message;
220 my ($self, @messages) = @_;
222 $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
236 SL::ClientJS - Easy programmatic client-side JavaScript generation
241 First some JavaScript code:
243 // In the client generate an AJAX request whose 'success' handler
244 // calls "eval_json_result(data)":
246 action: "SomeController/the_action",
247 id: $('#some_input_field').val()
249 $.post("controller.pl", data, eval_json_result);
253 # In the controller itself. First, make sure that the "client_js.js"
254 # is loaded. This must be done when the whole side is loaded, so
255 # it's not in the action called by the AJAX request shown above.
256 $::request->layout->use_javascript('client_js.js');
258 # Now in that action called via AJAX:
259 sub action_the_action {
262 # Create a new client-side JS object and do stuff with it!
263 my $js = SL::ClientJS->new;
265 # Show some element on the page:
266 $js->show('#usually_hidden');
268 # Set to hidden inputs. Yes, calls can be chained!
269 $js->val('#hidden_id', $self->new_id)
270 ->val('#other_type', 'Unicorn');
272 # Replace some HTML code:
273 my $html = $self->render('SomeController/the_action', { output => 0 });
274 $js->html('#id_with_new_content', $html);
276 # Operations on a jstree: rename a node and select it
277 my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
278 $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
279 ->jstree->select_node('#tb-' . $text_block->id);
281 # Close a popup opened by kivi.popup_dialog():
282 $js->dialog->close('#jqueryui_popup_dialog');
284 # Finally render the JSON response:
287 # Rendering can also be chained, e.g.
288 $js->html('#selector', $html)
294 This module enables the generation of jQuery-using JavaScript code on
295 the server side. That code is then evaluated in a safe way on the
298 The workflow is usally that the client creates an AJAX request, the
299 server creates some actions and sends them back, and the client then
300 implements each of these actions.
302 There are three things that need to be done for this to work:
306 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
308 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
310 =item 3. The server must use this module.
314 The functions called on the client side are mostly jQuery
315 functions. Other functionality may be added later.
317 Note that L<SL::Controller/render> is aware of this module which saves
318 you some boilerplate. The following two calls are equivalent:
320 $controller->render($client_js);
321 $controller->render(\$client_js->to_json, { type => 'json' });
323 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
329 Returns the actions gathered so far as an array reference. Each
330 element is an array reference containing at least two items: the
331 function's name and what it is called on. Additional array elements
332 are the function parameters.
336 Returns the actions gathered so far as a JSON string ready to be sent
339 =item C<render $controller>
341 Renders C<$self> via the controller. Useful for chaining. Equivalent
344 $controller->render(\$self->to_json, { type => 'json' });
348 Tells C<$self> that the next action is to be called on a jQuery UI
349 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
352 $js->dialog->close('#jqueryui_popup_dialog');
356 Tells C<$self> that the next action is to be called on a jstree
357 instance. For example:
359 $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
363 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
365 =head2 GENERIC FUNCTION
367 All of the following functions can be invoked in two ways: either by
368 calling the function name directly on C<$self> or by calling
369 L</action> with the function name as the first parameter. Therefore
370 the following two calls are identical:
372 $js->insertAfter($html, '#some-id');
373 $js->action('insertAfter', $html, '#some-id');
375 The second form, calling L</action>, is more to type but can be useful
376 in situations in which you have to call one of two functions depending
377 on context. For example, when you want to insert new code in a
378 list. If the list is empty you might have to use C<appendTo>, if it
379 isn't you might have to use C<insertAfter>. Example:
381 my $html = $self->render(...);
382 $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
386 my $html = $self->render(...);
387 if ($list_is_empty) {
388 $js->appendTo($html, '#text-block-list');
390 $js->insertAfter($html, '#text-block-' . $self->text_block->id);
393 The first variation is obviously better suited for chaining.
397 =item C<action $method, @args>
399 Call the function with the name C<$method> on C<$self> with arguments
400 C<@args>. Returns the return value of the actual function
401 called. Useful for chaining (see above).
403 =item C<action_if $condition, $method, @args>
405 Call the function with the name C<$method> on C<$self> with arguments
406 C<@args> if C<$condition> is trueish. Does nothing otherwise.
408 Returns the return value of the actual function called if
409 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
412 This function is equivalent to the following:
415 $obj->$method(@args);
418 But it is easier to integrate into a method call chain, e.g.:
420 $js->html('#content', $html)
421 ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
426 =head2 ADDITIONAL FUNCTIONS
430 =item C<flash $type, $message>
432 Display a C<$message> in the flash of type C<$type>. Multiple calls of
433 C<flash> on the same C<$self> will be merged by type.
435 On the client side the flashes of all types will be cleared after each
436 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
438 =item C<error $message>
440 Causes L<to_json> (and therefore L<render>) to output a JSON object
441 that only contains an C<error> field set to this C<$message>. The
442 client will then show the message in the 'error' flash.
444 The messages of multiple calls of C<error> on the same C<$self> will
447 =item C<redirect_to $url>
449 Redirects the browser window to the new URL by setting the JavaScript
450 property C<window.location.href>. Note that
451 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
452 function if the current request is an AJAX request as determined by
453 L<SL::Request/is_ajax>.
457 =head2 KIVITENDO FUNCTIONS
459 The following functions from the C<kivi> namespace are supported:
463 =item Displaying stuff
465 C<flash> (don't call directly, use L</flash> instead)
467 =item Running functions
469 C<run>, C<run_once_for>
477 =head2 JQUERY FUNCTIONS
479 The following jQuery functions are supported:
485 C<hide>, C<show>, C<toggle>
487 =item DOM insertion, around
489 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
491 =item DOM insertion, inside
493 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
495 =item DOM insertion, outside
497 C<after>, C<before>, C<insertAfter>, C<insertBefore>
503 =item DOM replacement
505 C<replaceAll>, C<replaceWith>
507 =item General attributes
509 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
511 =item Class attributes
513 C<addClass>, C<removeClass>, C<toggleClass>
517 C<data>, C<removeData>
523 =item Generic Event Handlers
525 C<on>, C<off>, C<one>
527 These attach/detach event listeners to specific selectors. The first
528 argument is the selector, the second the name of the events and the
529 third argument is the name of the handler function. That function must
530 already exist when the handler is added.
534 =head2 JQUERY POPUP DIALOG PLUGIN
536 Supported functions of the C<popup dialog> plugin to jQuery. They are
537 invoked by first calling C<dialog> in the ClientJS instance and then
540 $js->dialog->close(...);
544 =item Closing and removing the popup
550 =head2 AJAXFORM JQUERY PLUGIN
552 The following functions of the C<ajaxForm> plugin to jQuery are
557 =item All functions by the generic accessor function:
563 =head2 JSTREE JQUERY PLUGIN
565 Supported functions of the C<jstree> plugin to jQuery. They are
566 invoked by first calling C<jstree> in the ClientJS instance and then
569 $js->jstree->open_node(...);
573 =item Operations on the whole tree
577 =item Opening and closing nodes
579 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
580 C<close_all>, C<save_opened>, C<reopen>
582 =item Modifying nodes
584 C<rename_node>, C<delete_node>, C<move_node>
586 =item Selecting nodes (from the 'ui' jstree plugin)
588 C<select_node>, C<deselect_node>, C<deselect_all>
592 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
594 In order not having to maintain two files (this one and
595 C<js/client_js.js>) there's a script that can parse this file's
596 C<%supported_methods> definition and generate the file
597 C<js/client_js.js> accordingly. The steps are:
601 =item 1. Add lines in this file to the C<%supported_methods> hash. The
602 key is the function name and the value is the number of expected
603 parameters. The value can be negative to indicate that the function
604 takes at least the absolute of this value as parameters and optionally
605 more. In such a case the C<E<lt>ARGSE<gt>> format expands to an actual
606 array (and the individual elements if the value is positive>.
608 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
609 generate C<js/client_js.js> automatically.
611 =item 3. Reload the files in your browser (cleaning its cache can also
616 The template file used for generated C<js/client_js.js> is
617 C<scripts/generate_client_js_actions.tpl>.
625 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>