ClientJS: render()-Funktion zum noch besseren Chaining
[kivitendo-erp.git] / SL / ClientJS.pm
1 package SL::ClientJS;
2
3 use strict;
4
5 use parent qw(Rose::Object);
6
7 use Carp;
8 use SL::JSON ();
9
10 use Rose::Object::MakeMethods::Generic
11 (
12   'scalar --get_set_init' => [ qw(_actions) ],
13 );
14
15 my %supported_methods = (
16   # Basic effects
17   hide         => 1,
18   show         => 1,
19   toggle       => 1,
20
21   # DOM insertion, around
22   unwrap       => 1,
23   wrap         => 2,
24   wrapAll      => 2,
25   wrapInner    => 2,
26
27   # DOM insertion, inside
28   append       => 2,
29   appendTo     => 2,
30   html         => 2,
31   prepend      => 2,
32   prependTo    => 2,
33   text         => 2,
34
35   # DOM insertion, outside
36   after        => 2,
37   before       => 2,
38   insertAfter  => 2,
39   insertBefore => 2,
40
41   # DOM removal
42   empty        => 1,
43   remove       => 1,
44
45   # DOM replacement
46   replaceAll   => 2,
47   replaceWith  => 2,
48
49   # General attributes
50   attr         => 3,
51   prop         => 3,
52   removeAttr   => 2,
53   removeProp   => 2,
54   val          => 2,
55
56   # Data storage
57   data         => 3,
58   removeData   => 2,
59 );
60
61 sub AUTOLOAD {
62   our $AUTOLOAD;
63
64   my ($self, @args) = @_;
65
66   my $method        =  $AUTOLOAD;
67   $method           =~ s/.*:://;
68   return if $method eq 'DESTROY';
69
70   my $num_args =  $supported_methods{$method};
71   $::lxdebug->message(0, "autoload method $method");
72
73   croak "Unsupported jQuery action: $method"                                                    unless defined $num_args;
74   croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if     scalar(@args) != $num_args;
75
76   if ($num_args) {
77     # Force flattening from SL::Presenter::EscapedText: "" . $...
78     $args[0] =  "" . $args[0];
79     $args[0] =~ s/^\s+//;
80   }
81
82   push @{ $self->_actions }, [ $method, @args ];
83
84   return $self;
85 }
86
87 sub init__actions {
88   return [];
89 }
90
91 sub to_json {
92   my ($self) = @_;
93   return SL::JSON::to_json({ eval_actions => $self->_actions });
94 }
95
96 sub to_array {
97   my ($self) = @_;
98   return $self->_actions;
99 }
100
101 sub render {
102   my ($self, $controller) = @_;
103   return $controller->render(\$self->to_json, { type => 'json' });
104 }
105
106 1;
107 __END__
108
109 =pod
110
111 =encoding utf8
112
113 =head1 NAME
114
115 SL::ClientJS - Easy programmatic client-side JavaScript generation
116 with jQuery
117
118 =head1 SYNOPSIS
119
120 First some JavaScript code:
121
122   // In the client generate an AJAX request whose 'success' handler
123   // calls "eval_json_response(data)":
124   var data = {
125     action: "SomeController/the_action",
126     id:     $('#some_input_field').val()
127   };
128   $.post("controller.pl", data, eval_json_response);
129
130 Now some Perl code:
131
132   # In the controller itself. First, make sure that the "client_js.js"
133   # is loaded. This must be done when the whole side is loaded, so
134   # it's not in the action called by the AJAX request shown above.
135   $::request->layout->use_javascript('client_js.js');
136
137   # Now in that action called via AJAX:
138   sub action_the_action {
139     my ($self) = @_;
140
141     # Create a new client-side JS object and do stuff with it!
142     my $js = SL::ClientJS->new;
143
144     # Show some element on the page:
145     $js->show('#usually_hidden');
146
147     # Set to hidden inputs. Yes, calls can be chained!
148     $js->val('#hidden_id', $self->new_id)
149        ->val('#other_type', 'Unicorn');
150
151     # Replace some HTML code:
152     my $html = $self->render('SomeController/the_action', { output => 0 });
153     $js->html('#id_with_new_content', $html);
154
155     # Finally render the JSON response:
156     $self->render($js);
157   }
158
159 =head1 OVERVIEW
160
161 This module enables the generation of jQuery-using JavaScript code on
162 the server side. That code is then evaluated in a safe way on the
163 client side.
164
165 The workflow is usally that the client creates an AJAX request, the
166 server creates some actions and sends them back, and the client then
167 implements each of these actions.
168
169 There are three things that need to be done for this to work:
170
171 =over 2
172
173 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
174
175 =item 2. The client code needs to call C<eval_json_response()> with the result returned from the server.
176
177 =item 3. The server must use this module.
178
179 =back
180
181 The functions called on the client side are mostly jQuery
182 functions. Other functionality may be added later.
183
184 Note that L<SL::Controller/render> is aware of this module which saves
185 you some boilerplate. The following two calls are equivalent:
186
187   $controller->render($client_js);
188   $controller->render(\$client_js->to_json, { type => 'json' });
189
190 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
191
192 =over 4
193
194 =item C<to_array>
195
196 Returns the actions gathered so far as an array reference. Each
197 element is an array reference containing at least two items: the
198 function's name and what it is called on. Additional array elements
199 are the function parameters.
200
201 =item C<to_json>
202
203 Returns the actions gathered so far as a JSON string ready to be sent
204 to the client.
205
206 =item C<render $controller>
207
208 Renders C<$self> via the controller. Useful for chaining. Equivalent
209 to the following:
210
211   $controller->render(\$self->to_json, { type => 'json' });
212
213 =back
214
215 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
216
217 =head2 JQUERY FUNCTIONS
218
219 The following jQuery functions are supported:
220
221 =over 4
222
223 =item Basic effects
224
225 C<hide>, C<show>, C<toggle>
226
227 =item DOM insertion, around
228
229 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
230
231 =item DOM insertion, inside
232
233 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
234
235 =item DOM insertion, outside
236
237 C<after>, C<before>, C<insertAfter>, C<insertBefore>
238
239 =item DOM removal
240
241 C<empty>, C<remove>
242
243 =item DOM replacement
244
245 C<replaceAll>, C<replaceWith>
246
247 =item General attributes
248
249 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
250
251 =item Data storage
252
253 C<data>, C<removeData>
254
255 =back
256
257 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
258
259 In order not having to maintain two files (this one and
260 C<js/client_js.js>) there's a script that can parse this file's
261 C<%supported_methods> definition and convert it into the appropriate
262 code ready for manual insertion into C<js/client_js.js>. The steps
263 are:
264
265 =over 2
266
267 =item 1. Add lines in this file to the C<%supported_methods> hash. The
268 key is the function name and the value is the number of expected
269 parameters.
270
271 =item 2. Run C<scripts/generate_client_js_actions.pl>
272
273 =item 3. Edit C<js/client_js.js> and replace the type casing code with
274 the output generated in step 2.
275
276 =back
277
278 =head1 BUGS
279
280 Nothing here yet.
281
282 =head1 AUTHOR
283
284 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
285
286 =cut