Merge branch 'master' into dev
[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 1;
102 __END__
103
104 =pod
105
106 =encoding utf8
107
108 =head1 NAME
109
110 SL::ClientJS - Easy programmatic client-side JavaScript generation
111 with jQuery
112
113 =head1 SYNOPSIS
114
115 First some JavaScript code:
116
117   // In the client generate an AJAX request whose 'success' handler
118   // calls "eval_json_response(data)":
119   var data = {
120     action: "SomeController/the_action",
121     id:     $('#some_input_field').val()
122   };
123   $.post("controller.pl", data, eval_json_response);
124
125 Now some Perl code:
126
127   # In the controller itself. First, make sure that the "client_js.js"
128   # is loaded. This must be done when the whole side is loaded, so
129   # it's not in the action called by the AJAX request shown above.
130   $::request->layout->use_javascript('client_js.js');
131
132   # Now in that action called via AJAX:
133   sub action_the_action {
134     my ($self) = @_;
135
136     # Create a new client-side JS object and do stuff with it!
137     my $js = SL::ClientJS->new;
138
139     # Show some element on the page:
140     $js->show('#usually_hidden');
141
142     # Set to hidden inputs. Yes, calls can be chained!
143     $js->val('#hidden_id', $self->new_id)
144        ->val('#other_type', 'Unicorn');
145
146     # Replace some HTML code:
147     my $html = $self->render('SomeController/the_action', { output => 0 });
148     $js->html('#id_with_new_content', $html);
149
150     # Finally render the JSON response:
151     $self->render($js);
152   }
153
154 =head1 OVERVIEW
155
156 This module enables the generation of jQuery-using JavaScript code on
157 the server side. That code is then evaluated in a safe way on the
158 client side.
159
160 The workflow is usally that the client creates an AJAX request, the
161 server creates some actions and sends them back, and the client then
162 implements each of these actions.
163
164 There are three things that need to be done for this to work:
165
166 =over 2
167
168 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
169
170 =item 2. The client code needs to call C<eval_json_response()> with the result returned from the server.
171
172 =item 3. The server must use this module.
173
174 =back
175
176 The functions called on the client side are mostly jQuery
177 functions. Other functionality may be added later.
178
179 Note that L<SL::Controller/render> is aware of this module which saves
180 you some boilerplate. The following two calls are equivalent:
181
182   $controller->render($client_js);
183   $controller->render(\$client_js->to_json, { type => 'json' });
184
185 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
186
187 =over 4
188
189 =item C<to_array>
190
191 Returns the actions gathered so far as an array reference. Each
192 element is an array reference containing at least two items: the
193 function's name and what it is called on. Additional array elements
194 are the function parameters.
195
196 =item C<to_json>
197
198 Returns the actions gathered so far as a JSON string ready to be sent
199 to the client.
200
201 =back
202
203 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
204
205 =head2 JQUERY FUNCTIONS
206
207 The following jQuery functions are supported:
208
209 =over 4
210
211 =item Basic effects
212
213 C<hide>, C<show>, C<toggle>
214
215 =item DOM insertion, around
216
217 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
218
219 =item DOM insertion, inside
220
221 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
222
223 =item DOM insertion, outside
224
225 C<after>, C<before>, C<insertAfter>, C<insertBefore>
226
227 =item DOM removal
228
229 C<empty>, C<remove>
230
231 =item DOM replacement
232
233 C<replaceAll>, C<replaceWith>
234
235 =item General attributes
236
237 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
238
239 =item Data storage
240
241 C<data>, C<removeData>
242
243 =back
244
245 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
246
247 In order not having to maintain two files (this one and
248 C<js/client_js.js>) there's a script that can parse this file's
249 C<%supported_methods> definition and convert it into the appropriate
250 code ready for manual insertion into C<js/client_js.js>. The steps
251 are:
252
253 =over 2
254
255 =item 1. Add lines in this file to the C<%supported_methods> hash. The
256 key is the function name and the value is the number of expected
257 parameters.
258
259 =item 2. Run C<scripts/generate_client_js_actions.pl>
260
261 =item 3. Edit C<js/client_js.js> and replace the type casing code with
262 the output generated in step 2.
263
264 =back
265
266 =head1 BUGS
267
268 Nothing here yet.
269
270 =head1 AUTHOR
271
272 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
273
274 =cut