5 use parent qw(Rose::Object);
10 use Rose::Object::MakeMethods::Generic
12 scalar => [ qw(controller) ],
13 'scalar --get_set_init' => [ qw(_actions _flash _error) ],
16 my %supported_methods = (
24 # DOM insertion, around
30 # DOM insertion, inside
38 # DOM insertion, outside
69 focus => 1, # kivi.set_focus(<TARGET>)
71 # Generic Event Handling ## pattern: $(<TARGET>).<FUNCTION>(<ARG1>, kivi.get_function_by_name(<ARG2>))
76 # ## jQuery UI dialog plugin ## pattern: $(<TARGET>).dialog('<FUNCTION>')
78 # Closing and removing the popup
81 # ## jQuery Form plugin ##
82 'ajaxForm' => 1, # pattern: $(<TARGET>).ajaxForm({ success: eval_json_result })
84 # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
86 # Operations on the whole tree
90 # Opening and closing nodes
91 'jstree:open_node' => 2,
92 'jstree:open_all' => 2,
93 'jstree:close_node' => 2,
94 'jstree:close_all' => 2,
95 'jstree:toggle_node' => 2,
96 'jstree:save_opened' => 1,
100 'jstree:create_node' => 4,
101 'jstree:rename_node' => 3,
102 'jstree:delete_node' => 2,
103 'jstree:move_node' => 5,
105 # Selecting nodes (from the 'ui' plugin to jstree)
106 'jstree:select_node' => 2, # $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>, true)
107 'jstree:deselect_node' => 2,
108 'jstree:deselect_all' => 1,
110 # ## ckeditor stuff ##
111 'focus_ckeditor' => 1, # kivi.focus_ckeditor_when_ready(<TARGET>)
114 redirect_to => 1, # window.location.href = <TARGET>
116 flash => 2, # kivi.display_flash(<TARGET>, <ARGS>)
117 reinit_widgets => 0, # kivi.reinit_widgets()
118 run => -1, # kivi.run(<TARGET>, <ARGS>)
119 run_once_for => 3, # kivi.run_once_for(<TARGET>, <ARGS>)
125 my ($self, @args) = @_;
127 my $method = $AUTOLOAD;
129 return if $method eq 'DESTROY';
130 return $self->action($method, @args);
134 my ($self, $method, @args) = @_;
136 $method = (delete($self->{_prefix}) || '') . $method;
137 my $num_args = $supported_methods{$method};
139 croak "Unsupported jQuery action: $method" unless defined $num_args;
142 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
145 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted at least: $num_args)" if scalar(@args) < $num_args;
146 $num_args = scalar @args;
149 foreach my $idx (0..$num_args - 1) {
150 # Force flattening from SL::Presenter::EscapedText and trim leading whitespace for scalars
151 $args[$idx] = "" . $args[$idx] if ref($args[$idx]) eq 'SL::Presenter::EscapedText';
152 $args[$idx] =~ s/^\s+// if !ref($args[$idx]);
155 push @{ $self->_actions }, [ $method, @args ];
161 my ($self, $condition, @args) = @_;
163 return $condition ? $self->action(@args) : $self;
181 return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
182 return SL::JSON::to_json({ eval_actions => $self->_actions });
187 return $self->_actions;
191 my ($self, $controller) = @_;
192 $controller ||= $self->controller;
193 $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
194 return $controller->render(\$self->to_json, { type => 'json' });
199 $self->{_prefix} = 'jstree:';
205 $self->{_prefix} = 'dialog:';
211 $self->{_prefix} = 'ckeditor:';
216 my ($self, $type, @messages) = @_;
218 my $message = join ' ', grep { $_ } @messages;
220 if (!$self->_flash->{$type}) {
221 $self->_flash->{$type} = [ 'flash', $type, $message ];
222 push @{ $self->_actions }, $self->_flash->{$type};
224 $self->_flash->{$type}->[-1] .= ' ' . $message;
231 my ($self, @messages) = @_;
233 $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
247 SL::ClientJS - Easy programmatic client-side JavaScript generation
252 First some JavaScript code:
254 // In the client generate an AJAX request whose 'success' handler
255 // calls "eval_json_result(data)":
257 action: "SomeController/the_action",
258 id: $('#some_input_field').val()
260 $.post("controller.pl", data, eval_json_result);
264 # In the controller itself. First, make sure that the "client_js.js"
265 # is loaded. This must be done when the whole side is loaded, so
266 # it's not in the action called by the AJAX request shown above.
267 $::request->layout->use_javascript('client_js.js');
269 # Now in that action called via AJAX:
270 sub action_the_action {
273 # Create a new client-side JS object and do stuff with it!
274 my $js = SL::ClientJS->new(controller => $self);
276 # Show some element on the page:
277 $js->show('#usually_hidden');
279 # Set to hidden inputs. Yes, calls can be chained!
280 $js->val('#hidden_id', $self->new_id)
281 ->val('#other_type', 'Unicorn');
283 # Replace some HTML code:
284 my $html = $self->render('SomeController/the_action', { output => 0 });
285 $js->html('#id_with_new_content', $html);
287 # Operations on a jstree: rename a node and select it
288 my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
289 $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
290 ->jstree->select_node('#tb-' . $text_block->id);
292 # Close a popup opened by kivi.popup_dialog():
293 $js->dialog->close('#jqueryui_popup_dialog');
295 # Finally render the JSON response:
298 # Rendering can also be chained, e.g.
299 $js->html('#selector', $html)
305 This module enables the generation of jQuery-using JavaScript code on
306 the server side. That code is then evaluated in a safe way on the
309 The workflow is usally that the client creates an AJAX request, the
310 server creates some actions and sends them back, and the client then
311 implements each of these actions.
313 There are three things that need to be done for this to work:
317 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
319 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
321 =item 3. The server must use this module.
325 The functions called on the client side are mostly jQuery
326 functions. Other functionality may be added later.
328 Note that L<SL::Controller/render> is aware of this module which saves
329 you some boilerplate. The following two calls are equivalent:
331 $controller->render($client_js);
332 $controller->render(\$client_js->to_json, { type => 'json' });
334 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
340 Returns the actions gathered so far as an array reference. Each
341 element is an array reference containing at least two items: the
342 function's name and what it is called on. Additional array elements
343 are the function parameters.
347 Returns the actions gathered so far as a JSON string ready to be sent
350 =item C<render [$controller]>
352 Renders C<$self> via the controller. Useful for chaining. Equivalent
355 $controller->render(\$self->to_json, { type => 'json' });
357 The controller instance to use can be set during object creation (see
358 synopsis) or as an argument to C<render>.
362 Tells C<$self> that the next action is to be called on a jQuery UI
363 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
366 $js->dialog->close('#jqueryui_popup_dialog');
370 Tells C<$self> that the next action is to be called on a jstree
371 instance. For example:
373 $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
377 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
379 =head2 GENERIC FUNCTION
381 All of the following functions can be invoked in two ways: either by
382 calling the function name directly on C<$self> or by calling
383 L</action> with the function name as the first parameter. Therefore
384 the following two calls are identical:
386 $js->insertAfter($html, '#some-id');
387 $js->action('insertAfter', $html, '#some-id');
389 The second form, calling L</action>, is more to type but can be useful
390 in situations in which you have to call one of two functions depending
391 on context. For example, when you want to insert new code in a
392 list. If the list is empty you might have to use C<appendTo>, if it
393 isn't you might have to use C<insertAfter>. Example:
395 my $html = $self->render(...);
396 $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
400 my $html = $self->render(...);
401 if ($list_is_empty) {
402 $js->appendTo($html, '#text-block-list');
404 $js->insertAfter($html, '#text-block-' . $self->text_block->id);
407 The first variation is obviously better suited for chaining.
411 =item C<action $method, @args>
413 Call the function with the name C<$method> on C<$self> with arguments
414 C<@args>. Returns the return value of the actual function
415 called. Useful for chaining (see above).
417 =item C<action_if $condition, $method, @args>
419 Call the function with the name C<$method> on C<$self> with arguments
420 C<@args> if C<$condition> is trueish. Does nothing otherwise.
422 Returns the return value of the actual function called if
423 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
426 This function is equivalent to the following:
429 $obj->$method(@args);
432 But it is easier to integrate into a method call chain, e.g.:
434 $js->html('#content', $html)
435 ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
440 =head2 ADDITIONAL FUNCTIONS
444 =item C<flash $type, $message>
446 Display a C<$message> in the flash of type C<$type>. Multiple calls of
447 C<flash> on the same C<$self> will be merged by type.
449 On the client side the flashes of all types will be cleared after each
450 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
452 =item C<error $message>
454 Causes L<to_json> (and therefore L<render>) to output a JSON object
455 that only contains an C<error> field set to this C<$message>. The
456 client will then show the message in the 'error' flash.
458 The messages of multiple calls of C<error> on the same C<$self> will
461 =item C<redirect_to $url>
463 Redirects the browser window to the new URL by setting the JavaScript
464 property C<window.location.href>. Note that
465 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
466 function if the current request is an AJAX request as determined by
467 L<SL::Request/is_ajax>.
471 =head2 KIVITENDO FUNCTIONS
473 The following functions from the C<kivi> namespace are supported:
477 =item Displaying stuff
479 C<flash> (don't call directly, use L</flash> instead)
481 =item Running functions
483 C<run>, C<run_once_for>
491 =head2 JQUERY FUNCTIONS
493 The following jQuery functions are supported:
499 C<hide>, C<show>, C<toggle>
501 =item DOM insertion, around
503 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
505 =item DOM insertion, inside
507 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
509 =item DOM insertion, outside
511 C<after>, C<before>, C<insertAfter>, C<insertBefore>
517 =item DOM replacement
519 C<replaceAll>, C<replaceWith>
521 =item General attributes
523 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
525 =item Class attributes
527 C<addClass>, C<removeClass>, C<toggleClass>
531 C<data>, C<removeData>
537 =item Generic Event Handlers
539 C<on>, C<off>, C<one>
541 These attach/detach event listeners to specific selectors. The first
542 argument is the selector, the second the name of the events and the
543 third argument is the name of the handler function. That function must
544 already exist when the handler is added.
548 =head2 JQUERY POPUP DIALOG PLUGIN
550 Supported functions of the C<popup dialog> plugin to jQuery. They are
551 invoked by first calling C<dialog> in the ClientJS instance and then
554 $js->dialog->close(...);
558 =item Closing and removing the popup
564 =head2 AJAXFORM JQUERY PLUGIN
566 The following functions of the C<ajaxForm> plugin to jQuery are
571 =item All functions by the generic accessor function:
577 =head2 JSTREE JQUERY PLUGIN
579 Supported functions of the C<jstree> plugin to jQuery. They are
580 invoked by first calling C<jstree> in the ClientJS instance and then
583 $js->jstree->open_node(...);
587 =item Operations on the whole tree
591 =item Opening and closing nodes
593 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
594 C<close_all>, C<save_opened>, C<reopen>
596 =item Modifying nodes
598 C<rename_node>, C<delete_node>, C<move_node>
600 =item Selecting nodes (from the 'ui' jstree plugin)
602 C<select_node>, C<deselect_node>, C<deselect_all>
606 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
608 In order not having to maintain two files (this one and
609 C<js/client_js.js>) there's a script that can parse this file's
610 C<%supported_methods> definition and generate the file
611 C<js/client_js.js> accordingly. The steps are:
615 =item 1. Add lines in this file to the C<%supported_methods> hash. The
616 key is the function name and the value is the number of expected
617 parameters. The value can be negative to indicate that the function
618 takes at least the absolute of this value as parameters and optionally
619 more. In such a case the C<E<lt>ARGSE<gt>> format expands to an actual
620 array (and the individual elements if the value is positive>.
622 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
623 generate C<js/client_js.js> automatically.
625 =item 3. Reload the files in your browser (cleaning its cache can also
630 The template file used for generated C<js/client_js.js> is
631 C<scripts/generate_client_js_actions.tpl>.
639 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>