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, # display_flash(<TARGET>, <ARGS>)
26 # DOM insertion, around
32 # DOM insertion, inside
40 # DOM insertion, outside
73 # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
75 # Operations on the whole tree
79 # Opening and closing nodes
80 'jstree:open_node' => 2,
81 'jstree:open_all' => 2,
82 'jstree:close_node' => 2,
83 'jstree:close_all' => 2,
84 'jstree:toggle_node' => 2,
85 'jstree:save_opened' => 1,
89 'jstree:create_node' => 4,
90 'jstree:rename_node' => 3,
91 'jstree:delete_node' => 2,
92 'jstree:move_node' => 5,
94 # Selecting nodes (from the 'ui' plugin to jstree)
95 'jstree:select_node' => 2, # $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>, true)
96 'jstree:deselect_node' => 2,
97 'jstree:deselect_all' => 1,
103 my ($self, @args) = @_;
105 my $method = $AUTOLOAD;
107 return if $method eq 'DESTROY';
108 return $self->action($method, @args);
112 my ($self, $method, @args) = @_;
114 $method = (delete($self->{_prefix}) || '') . $method;
115 my $num_args = $supported_methods{$method};
117 croak "Unsupported jQuery action: $method" unless defined $num_args;
118 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
121 # Force flattening from SL::Presenter::EscapedText: "" . $...
122 $args[0] = "" . $args[0];
123 $args[0] =~ s/^\s+//;
126 push @{ $self->_actions }, [ $method, @args ];
132 my ($self, $condition, @args) = @_;
134 return $condition ? $self->action(@args) : $self;
152 return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
153 return SL::JSON::to_json({ eval_actions => $self->_actions });
158 return $self->_actions;
162 my ($self, $controller) = @_;
163 return $controller->render(\$self->to_json, { type => 'json' });
168 $self->{_prefix} = 'jstree:';
173 my ($self, $type, @messages) = @_;
175 my $message = join ' ', grep { $_ } @messages;
177 if (!$self->_flash->{$type}) {
178 $self->_flash->{$type} = [ 'flash', $type, $message ];
179 push @{ $self->_actions }, $self->_flash->{$type};
181 $self->_flash->{$type}->[-1] .= ' ' . $message;
188 my ($self, @messages) = @_;
190 $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
204 SL::ClientJS - Easy programmatic client-side JavaScript generation
209 First some JavaScript code:
211 // In the client generate an AJAX request whose 'success' handler
212 // calls "eval_json_response(data)":
214 action: "SomeController/the_action",
215 id: $('#some_input_field').val()
217 $.post("controller.pl", data, eval_json_response);
221 # In the controller itself. First, make sure that the "client_js.js"
222 # is loaded. This must be done when the whole side is loaded, so
223 # it's not in the action called by the AJAX request shown above.
224 $::request->layout->use_javascript('client_js.js');
226 # Now in that action called via AJAX:
227 sub action_the_action {
230 # Create a new client-side JS object and do stuff with it!
231 my $js = SL::ClientJS->new;
233 # Show some element on the page:
234 $js->show('#usually_hidden');
236 # Set to hidden inputs. Yes, calls can be chained!
237 $js->val('#hidden_id', $self->new_id)
238 ->val('#other_type', 'Unicorn');
240 # Replace some HTML code:
241 my $html = $self->render('SomeController/the_action', { output => 0 });
242 $js->html('#id_with_new_content', $html);
244 # Operations on a jstree: rename a node and select it
245 my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
246 $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
247 ->jstree->select_node('#tb-' . $text_block->id);
249 # Finally render the JSON response:
252 # Rendering can also be chained, e.g.
253 $js->html('#selector', $html)
259 This module enables the generation of jQuery-using JavaScript code on
260 the server side. That code is then evaluated in a safe way on the
263 The workflow is usally that the client creates an AJAX request, the
264 server creates some actions and sends them back, and the client then
265 implements each of these actions.
267 There are three things that need to be done for this to work:
271 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
273 =item 2. The client code needs to call C<eval_json_response()> with the result returned from the server.
275 =item 3. The server must use this module.
279 The functions called on the client side are mostly jQuery
280 functions. Other functionality may be added later.
282 Note that L<SL::Controller/render> is aware of this module which saves
283 you some boilerplate. The following two calls are equivalent:
285 $controller->render($client_js);
286 $controller->render(\$client_js->to_json, { type => 'json' });
288 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
294 Returns the actions gathered so far as an array reference. Each
295 element is an array reference containing at least two items: the
296 function's name and what it is called on. Additional array elements
297 are the function parameters.
301 Returns the actions gathered so far as a JSON string ready to be sent
304 =item C<render $controller>
306 Renders C<$self> via the controller. Useful for chaining. Equivalent
309 $controller->render(\$self->to_json, { type => 'json' });
313 Tells C<$self> that the next action is to be called on a jstree
314 instance. For example:
316 $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
320 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
322 =head2 GENERIC FUNCTION
324 All of the following functions can be invoked in two ways: either by
325 calling the function name directly on C<$self> or by calling
326 L</action> with the function name as the first parameter. Therefore
327 the following two calls are identical:
329 $js->insertAfter($html, '#some-id');
330 $js->action('insertAfter', $html, '#some-id');
332 The second form, calling L</action>, is more to type but can be useful
333 in situations in which you have to call one of two functions depending
334 on context. For example, when you want to insert new code in a
335 list. If the list is empty you might have to use C<appendTo>, if it
336 isn't you might have to use C<insertAfter>. Example:
338 my $html = $self->render(...);
339 $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
343 my $html = $self->render(...);
344 if ($list_is_empty) {
345 $js->appendTo($html, '#text-block-list');
347 $js->insertAfter($html, '#text-block-' . $self->text_block->id);
350 The first variation is obviously better suited for chaining.
354 =item C<action $method, @args>
356 Call the function with the name C<$method> on C<$self> with arguments
357 C<@args>. Returns the return value of the actual function
358 called. Useful for chaining (see above).
360 =item C<action_if $condition, $method, @args>
362 Call the function with the name C<$method> on C<$self> with arguments
363 C<@args> if C<$condition> is trueish. Does nothing otherwise.
365 Returns the return value of the actual function called if
366 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
369 This function is equivalent to the following:
372 $obj->$method(@args);
375 But it is easier to integrate into a method call chain, e.g.:
377 $js->html('#content', $html)
378 ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
383 =head2 ADDITIONAL FUNCTIONS
387 =item C<flash $type, $message>
389 Display a C<$message> in the flash of type C<$type>. Multiple calls of
390 C<flash> on the same C<$self> will be merged by type.
392 On the client side the flash of this type will be cleared before the
395 =item C<error $message>
397 Causes L<to_json> (and therefore L<render>) to output a JSON object
398 that only contains an C<error> field set to this C<$message>. The
399 client will then show the message in the 'error' flash.
401 The messages of multiple calls of C<error> on the same C<$self> will
406 =head2 JQUERY FUNCTIONS
408 The following jQuery functions are supported:
414 C<hide>, C<show>, C<toggle>
416 =item DOM insertion, around
418 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
420 =item DOM insertion, inside
422 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
424 =item DOM insertion, outside
426 C<after>, C<before>, C<insertAfter>, C<insertBefore>
432 =item DOM replacement
434 C<replaceAll>, C<replaceWith>
436 =item General attributes
438 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
442 C<data>, C<removeData>
450 =head2 JSTREE JQUERY PLUGIN
452 The following functions of the C<jstree> plugin to jQuery are
457 =item Operations on the whole tree
461 =item Opening and closing nodes
463 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
464 C<close_all>, C<save_opened>, C<reopen>
466 =item Modifying nodes
468 C<rename_node>, C<delete_node>, C<move_node>
470 =item Selecting nodes (from the 'ui' jstree plugin)
472 C<select_node>, C<deselect_node>, C<deselect_all>
476 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
478 In order not having to maintain two files (this one and
479 C<js/client_js.js>) there's a script that can parse this file's
480 C<%supported_methods> definition and generate the file
481 C<js/client_js.js> accordingly. The steps are:
485 =item 1. Add lines in this file to the C<%supported_methods> hash. The
486 key is the function name and the value is the number of expected
489 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
490 generate C<js/client_js.js> automatically.
492 =item 3. Reload the files in your browser (cleaning its cache can also
497 The template file used for generated C<js/client_js.js> is
498 C<scripts/generate_client_js_actions.tpl>.
506 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>