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 = (
16 # ## Non-jQuery methods ##
17 flash => 2, # kivi.display_flash(<TARGET>, <ARGS>)
26 # DOM insertion, around
32 # DOM insertion, inside
40 # DOM insertion, outside
73 # Generic Event Handling ## pattern: $(<TARGET>).<FUNCTION>(<ARG1>, kivi.get_function_by_name(<ARG2>))
78 # ## jQuery UI dialog plugin ## pattern: $(<TARGET>).dialog('<FUNCTION>')
80 # Closing and removing the popup
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 reinit_widgets => 0, # kivi.reinit_widgets()
118 my ($self, @args) = @_;
120 my $method = $AUTOLOAD;
122 return if $method eq 'DESTROY';
123 return $self->action($method, @args);
127 my ($self, $method, @args) = @_;
129 $method = (delete($self->{_prefix}) || '') . $method;
130 my $num_args = $supported_methods{$method};
132 croak "Unsupported jQuery action: $method" unless defined $num_args;
133 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
135 foreach my $idx (0..$num_args - 1) {
136 # Force flattening from SL::Presenter::EscapedText and trim leading whitespace for scalars
137 $args[$idx] = "" . $args[$idx] if ref($args[$idx]) eq 'SL::Presenter::EscapedText';
138 $args[$idx] =~ s/^\s+// if !ref($args[$idx]);
141 push @{ $self->_actions }, [ $method, @args ];
147 my ($self, $condition, @args) = @_;
149 return $condition ? $self->action(@args) : $self;
167 return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
168 return SL::JSON::to_json({ eval_actions => $self->_actions });
173 return $self->_actions;
177 my ($self, $controller) = @_;
178 $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
179 return $controller->render(\$self->to_json, { type => 'json' });
184 $self->{_prefix} = 'jstree:';
190 $self->{_prefix} = 'dialog:';
195 my ($self, $type, @messages) = @_;
197 my $message = join ' ', grep { $_ } @messages;
199 if (!$self->_flash->{$type}) {
200 $self->_flash->{$type} = [ 'flash', $type, $message ];
201 push @{ $self->_actions }, $self->_flash->{$type};
203 $self->_flash->{$type}->[-1] .= ' ' . $message;
210 my ($self, @messages) = @_;
212 $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
226 SL::ClientJS - Easy programmatic client-side JavaScript generation
231 First some JavaScript code:
233 // In the client generate an AJAX request whose 'success' handler
234 // calls "eval_json_result(data)":
236 action: "SomeController/the_action",
237 id: $('#some_input_field').val()
239 $.post("controller.pl", data, eval_json_result);
243 # In the controller itself. First, make sure that the "client_js.js"
244 # is loaded. This must be done when the whole side is loaded, so
245 # it's not in the action called by the AJAX request shown above.
246 $::request->layout->use_javascript('client_js.js');
248 # Now in that action called via AJAX:
249 sub action_the_action {
252 # Create a new client-side JS object and do stuff with it!
253 my $js = SL::ClientJS->new;
255 # Show some element on the page:
256 $js->show('#usually_hidden');
258 # Set to hidden inputs. Yes, calls can be chained!
259 $js->val('#hidden_id', $self->new_id)
260 ->val('#other_type', 'Unicorn');
262 # Replace some HTML code:
263 my $html = $self->render('SomeController/the_action', { output => 0 });
264 $js->html('#id_with_new_content', $html);
266 # Operations on a jstree: rename a node and select it
267 my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
268 $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
269 ->jstree->select_node('#tb-' . $text_block->id);
271 # Close a popup opened by kivi.popup_dialog():
272 $js->dialog->close('#jqueryui_popup_dialog');
274 # Finally render the JSON response:
277 # Rendering can also be chained, e.g.
278 $js->html('#selector', $html)
284 This module enables the generation of jQuery-using JavaScript code on
285 the server side. That code is then evaluated in a safe way on the
288 The workflow is usally that the client creates an AJAX request, the
289 server creates some actions and sends them back, and the client then
290 implements each of these actions.
292 There are three things that need to be done for this to work:
296 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
298 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
300 =item 3. The server must use this module.
304 The functions called on the client side are mostly jQuery
305 functions. Other functionality may be added later.
307 Note that L<SL::Controller/render> is aware of this module which saves
308 you some boilerplate. The following two calls are equivalent:
310 $controller->render($client_js);
311 $controller->render(\$client_js->to_json, { type => 'json' });
313 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
319 Returns the actions gathered so far as an array reference. Each
320 element is an array reference containing at least two items: the
321 function's name and what it is called on. Additional array elements
322 are the function parameters.
326 Returns the actions gathered so far as a JSON string ready to be sent
329 =item C<render $controller>
331 Renders C<$self> via the controller. Useful for chaining. Equivalent
334 $controller->render(\$self->to_json, { type => 'json' });
338 Tells C<$self> that the next action is to be called on a jQuery UI
339 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
342 $js->dialog->close('#jqueryui_popup_dialog');
346 Tells C<$self> that the next action is to be called on a jstree
347 instance. For example:
349 $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
353 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
355 =head2 GENERIC FUNCTION
357 All of the following functions can be invoked in two ways: either by
358 calling the function name directly on C<$self> or by calling
359 L</action> with the function name as the first parameter. Therefore
360 the following two calls are identical:
362 $js->insertAfter($html, '#some-id');
363 $js->action('insertAfter', $html, '#some-id');
365 The second form, calling L</action>, is more to type but can be useful
366 in situations in which you have to call one of two functions depending
367 on context. For example, when you want to insert new code in a
368 list. If the list is empty you might have to use C<appendTo>, if it
369 isn't you might have to use C<insertAfter>. Example:
371 my $html = $self->render(...);
372 $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
376 my $html = $self->render(...);
377 if ($list_is_empty) {
378 $js->appendTo($html, '#text-block-list');
380 $js->insertAfter($html, '#text-block-' . $self->text_block->id);
383 The first variation is obviously better suited for chaining.
387 =item C<action $method, @args>
389 Call the function with the name C<$method> on C<$self> with arguments
390 C<@args>. Returns the return value of the actual function
391 called. Useful for chaining (see above).
393 =item C<action_if $condition, $method, @args>
395 Call the function with the name C<$method> on C<$self> with arguments
396 C<@args> if C<$condition> is trueish. Does nothing otherwise.
398 Returns the return value of the actual function called if
399 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
402 This function is equivalent to the following:
405 $obj->$method(@args);
408 But it is easier to integrate into a method call chain, e.g.:
410 $js->html('#content', $html)
411 ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
416 =head2 ADDITIONAL FUNCTIONS
420 =item C<flash $type, $message>
422 Display a C<$message> in the flash of type C<$type>. Multiple calls of
423 C<flash> on the same C<$self> will be merged by type.
425 On the client side the flashes of all types will be cleared after each
426 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
428 =item C<error $message>
430 Causes L<to_json> (and therefore L<render>) to output a JSON object
431 that only contains an C<error> field set to this C<$message>. The
432 client will then show the message in the 'error' flash.
434 The messages of multiple calls of C<error> on the same C<$self> will
437 =item C<redirect_to $url>
439 Redirects the browser window to the new URL by setting the JavaScript
440 property C<window.location.href>. Note that
441 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
442 function if the current request is an AJAX request as determined by
443 L<SL::Request/is_ajax>.
447 =head2 JQUERY FUNCTIONS
449 The following jQuery functions are supported:
455 C<hide>, C<show>, C<toggle>
457 =item DOM insertion, around
459 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
461 =item DOM insertion, inside
463 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
465 =item DOM insertion, outside
467 C<after>, C<before>, C<insertAfter>, C<insertBefore>
473 =item DOM replacement
475 C<replaceAll>, C<replaceWith>
477 =item General attributes
479 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
483 C<data>, C<removeData>
489 =item Generic Event Handlers
491 C<on>, C<off>, C<one>
493 These attach/detach event listeners to specific selectors. The first
494 argument is the selector, the second the name of the events and the
495 third argument is the name of the handler function. That function must
496 already exist when the handler is added.
500 =head2 JSTREE JQUERY PLUGIN
502 The following functions of the C<jstree> plugin to jQuery are
507 =item Operations on the whole tree
511 =item Opening and closing nodes
513 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
514 C<close_all>, C<save_opened>, C<reopen>
516 =item Modifying nodes
518 C<rename_node>, C<delete_node>, C<move_node>
520 =item Selecting nodes (from the 'ui' jstree plugin)
522 C<select_node>, C<deselect_node>, C<deselect_all>
526 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
528 In order not having to maintain two files (this one and
529 C<js/client_js.js>) there's a script that can parse this file's
530 C<%supported_methods> definition and generate the file
531 C<js/client_js.js> accordingly. The steps are:
535 =item 1. Add lines in this file to the C<%supported_methods> hash. The
536 key is the function name and the value is the number of expected
539 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
540 generate C<js/client_js.js> automatically.
542 =item 3. Reload the files in your browser (cleaning its cache can also
547 The template file used for generated C<js/client_js.js> is
548 C<scripts/generate_client_js_actions.tpl>.
556 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>