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 # ## jQuery Form plugin ##
84 'ajaxForm' => 1, # pattern: $(<TARGET>).ajaxForm({ success: eval_json_result })
86 # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
88 # Operations on the whole tree
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,
102 'jstree:create_node' => 4,
103 'jstree:rename_node' => 3,
104 'jstree:delete_node' => 2,
105 'jstree:move_node' => 5,
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,
113 redirect_to => 1, # window.location.href = <TARGET>
115 reinit_widgets => 0, # kivi.reinit_widgets()
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;
136 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
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]);
144 push @{ $self->_actions }, [ $method, @args ];
150 my ($self, $condition, @args) = @_;
152 return $condition ? $self->action(@args) : $self;
170 return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
171 return SL::JSON::to_json({ eval_actions => $self->_actions });
176 return $self->_actions;
180 my ($self, $controller) = @_;
181 $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
182 return $controller->render(\$self->to_json, { type => 'json' });
187 $self->{_prefix} = 'jstree:';
193 $self->{_prefix} = 'dialog:';
198 my ($self, $type, @messages) = @_;
200 my $message = join ' ', grep { $_ } @messages;
202 if (!$self->_flash->{$type}) {
203 $self->_flash->{$type} = [ 'flash', $type, $message ];
204 push @{ $self->_actions }, $self->_flash->{$type};
206 $self->_flash->{$type}->[-1] .= ' ' . $message;
213 my ($self, @messages) = @_;
215 $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
229 SL::ClientJS - Easy programmatic client-side JavaScript generation
234 First some JavaScript code:
236 // In the client generate an AJAX request whose 'success' handler
237 // calls "eval_json_result(data)":
239 action: "SomeController/the_action",
240 id: $('#some_input_field').val()
242 $.post("controller.pl", data, eval_json_result);
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');
251 # Now in that action called via AJAX:
252 sub action_the_action {
255 # Create a new client-side JS object and do stuff with it!
256 my $js = SL::ClientJS->new;
258 # Show some element on the page:
259 $js->show('#usually_hidden');
261 # Set to hidden inputs. Yes, calls can be chained!
262 $js->val('#hidden_id', $self->new_id)
263 ->val('#other_type', 'Unicorn');
265 # Replace some HTML code:
266 my $html = $self->render('SomeController/the_action', { output => 0 });
267 $js->html('#id_with_new_content', $html);
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);
274 # Close a popup opened by kivi.popup_dialog():
275 $js->dialog->close('#jqueryui_popup_dialog');
277 # Finally render the JSON response:
280 # Rendering can also be chained, e.g.
281 $js->html('#selector', $html)
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
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.
295 There are three things that need to be done for this to work:
299 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
301 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
303 =item 3. The server must use this module.
307 The functions called on the client side are mostly jQuery
308 functions. Other functionality may be added later.
310 Note that L<SL::Controller/render> is aware of this module which saves
311 you some boilerplate. The following two calls are equivalent:
313 $controller->render($client_js);
314 $controller->render(\$client_js->to_json, { type => 'json' });
316 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
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.
329 Returns the actions gathered so far as a JSON string ready to be sent
332 =item C<render $controller>
334 Renders C<$self> via the controller. Useful for chaining. Equivalent
337 $controller->render(\$self->to_json, { type => 'json' });
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
345 $js->dialog->close('#jqueryui_popup_dialog');
349 Tells C<$self> that the next action is to be called on a jstree
350 instance. For example:
352 $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
356 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
358 =head2 GENERIC FUNCTION
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:
365 $js->insertAfter($html, '#some-id');
366 $js->action('insertAfter', $html, '#some-id');
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:
374 my $html = $self->render(...);
375 $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
379 my $html = $self->render(...);
380 if ($list_is_empty) {
381 $js->appendTo($html, '#text-block-list');
383 $js->insertAfter($html, '#text-block-' . $self->text_block->id);
386 The first variation is obviously better suited for chaining.
390 =item C<action $method, @args>
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).
396 =item C<action_if $condition, $method, @args>
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.
401 Returns the return value of the actual function called if
402 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
405 This function is equivalent to the following:
408 $obj->$method(@args);
411 But it is easier to integrate into a method call chain, e.g.:
413 $js->html('#content', $html)
414 ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
419 =head2 ADDITIONAL FUNCTIONS
423 =item C<flash $type, $message>
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.
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(...)>.
431 =item C<error $message>
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.
437 The messages of multiple calls of C<error> on the same C<$self> will
440 =item C<redirect_to $url>
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>.
450 =head2 JQUERY FUNCTIONS
452 The following jQuery functions are supported:
458 C<hide>, C<show>, C<toggle>
460 =item DOM insertion, around
462 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
464 =item DOM insertion, inside
466 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
468 =item DOM insertion, outside
470 C<after>, C<before>, C<insertAfter>, C<insertBefore>
476 =item DOM replacement
478 C<replaceAll>, C<replaceWith>
480 =item General attributes
482 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
486 C<data>, C<removeData>
492 =item Generic Event Handlers
494 C<on>, C<off>, C<one>
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.
503 =head2 JSTREE JQUERY PLUGIN
505 The following functions of the C<jstree> plugin to jQuery are
510 =item Operations on the whole tree
514 =item Opening and closing nodes
516 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
517 C<close_all>, C<save_opened>, C<reopen>
519 =item Modifying nodes
521 C<rename_node>, C<delete_node>, C<move_node>
523 =item Selecting nodes (from the 'ui' jstree plugin)
525 C<select_node>, C<deselect_node>, C<deselect_all>
529 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
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:
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
542 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
543 generate C<js/client_js.js> automatically.
545 =item 3. Reload the files in your browser (cleaning its cache can also
550 The template file used for generated C<js/client_js.js> is
551 C<scripts/generate_client_js_actions.tpl>.
559 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>