Revision 7af2b128
Von Moritz Bunkus vor mehr als 11 Jahren hinzugefügt
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 |
SL/Controller/Base.pm | ||
---|---|---|
60 | 60 |
my $template = shift; |
61 | 61 |
my ($options, %locals) = (@_ && ref($_[0])) ? @_ : ({ }, @_); |
62 | 62 |
|
63 |
# Special handling/shortcut for an instance of SL::ClientJS: |
|
64 |
return $self->render(\$template->to_json, { type => 'json' }) if ref($template) eq 'SL::ClientJS'; |
|
65 |
|
|
63 | 66 |
# Set defaults for all available options. |
64 | 67 |
my %defaults = ( |
65 | 68 |
type => 'html', |
js/client_js.js | ||
---|---|---|
1 |
// NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE: |
|
2 |
|
|
3 |
// Generate the dispatching lines in this script by running |
|
4 |
// "scripts/generate_client_js_actions.pl". See the documentation for |
|
5 |
// SL/ClientJS.pm for instructions. |
|
6 |
|
|
7 |
function eval_json_result(data) { |
|
8 |
if (!data) |
|
9 |
return; |
|
10 |
|
|
11 |
if ((data.js || '') != '') |
|
12 |
eval(data.js); |
|
13 |
|
|
14 |
if (data.eval_actions) |
|
15 |
$(data.eval_actions).each(function(idx, action) { |
|
16 |
// console.log("ACTION " + action[0] + " ON " + action[1]); |
|
17 |
|
|
18 |
// Basic effects |
|
19 |
if (action[0] == 'hide') $(action[1]).hide(); |
|
20 |
else if (action[0] == 'show') $(action[1]).show(); |
|
21 |
else if (action[0] == 'toggle') $(action[1]).toggle(); |
|
22 |
|
|
23 |
// DOM insertion, around |
|
24 |
else if (action[0] == 'unwrap') $(action[1]).unwrap(); |
|
25 |
else if (action[0] == 'wrap') $(action[1]).wrap(action[2]); |
|
26 |
else if (action[0] == 'wrapAll') $(action[1]).wrapAll(action[2]); |
|
27 |
else if (action[0] == 'wrapInner') $(action[1]).wrapInner(action[2]); |
|
28 |
|
|
29 |
// DOM insertion, inside |
|
30 |
else if (action[0] == 'append') $(action[1]).append(action[2]); |
|
31 |
else if (action[0] == 'appendTo') $(action[1]).appendTo(action[2]); |
|
32 |
else if (action[0] == 'html') $(action[1]).html(action[2]); |
|
33 |
else if (action[0] == 'prepend') $(action[1]).prepend(action[2]); |
|
34 |
else if (action[0] == 'prependTo') $(action[1]).prependTo(action[2]); |
|
35 |
else if (action[0] == 'text') $(action[1]).text(action[2]); |
|
36 |
|
|
37 |
// DOM insertion, outside |
|
38 |
else if (action[0] == 'after') $(action[1]).after(action[2]); |
|
39 |
else if (action[0] == 'before') $(action[1]).before(action[2]); |
|
40 |
else if (action[0] == 'insertAfter') $(action[1]).insertAfter(action[2]); |
|
41 |
else if (action[0] == 'insertBefore') $(action[1]).insertBefore(action[2]); |
|
42 |
|
|
43 |
// DOM removal |
|
44 |
else if (action[0] == 'empty') $(action[1]).empty(); |
|
45 |
else if (action[0] == 'remove') $(action[1]).remove(); |
|
46 |
|
|
47 |
// DOM replacement |
|
48 |
else if (action[0] == 'replaceAll') $(action[1]).replaceAll(action[2]); |
|
49 |
else if (action[0] == 'replaceWith') $(action[1]).replaceWith(action[2]); |
|
50 |
|
|
51 |
// General attributes |
|
52 |
else if (action[0] == 'attr') $(action[1]).attr(action[2], action[3]); |
|
53 |
else if (action[0] == 'prop') $(action[1]).prop(action[2], action[3]); |
|
54 |
else if (action[0] == 'removeAttr') $(action[1]).removeAttr(action[2]); |
|
55 |
else if (action[0] == 'removeProp') $(action[1]).removeProp(action[2]); |
|
56 |
else if (action[0] == 'val') $(action[1]).val(action[2]); |
|
57 |
|
|
58 |
// Data storage |
|
59 |
else if (action[0] == 'data') $(action[1]).data(action[2], action[3]); |
|
60 |
else if (action[0] == 'removeData') $(action[1]).removeData(action[2]); |
|
61 |
|
|
62 |
else console.log("Unknown action: " + action[0]); |
|
63 |
}); |
|
64 |
|
|
65 |
console.log("current_content_type " + $('#current_content_type').val() + ' ID ' + $('#current_content_id').val()); |
|
66 |
} |
scripts/generate_client_js_actions.pl | ||
---|---|---|
1 |
#!/usr/bin/perl |
|
2 |
|
|
3 |
use strict; |
|
4 |
use warnings; |
|
5 |
|
|
6 |
use File::Slurp; |
|
7 |
use List::Util qw(first max); |
|
8 |
|
|
9 |
my $file_name = (first { -f } qw(SL/ClientJS.pm ../SL/ClientJS.pm)) || die "ClientJS.pm not found"; |
|
10 |
my @actions; |
|
11 |
|
|
12 |
foreach (read_file($file_name)) { |
|
13 |
chomp; |
|
14 |
|
|
15 |
next unless (m/^my \%supported_methods/ .. m/^\);/); |
|
16 |
|
|
17 |
push @actions, [ 'action', $1, $2 ] if m/^\s+([a-zA-Z]+)\s*=>\s*(\d+),$/; |
|
18 |
push @actions, [ 'comment', $1 ] if m/^\s+#\s+(.+)/; |
|
19 |
} |
|
20 |
|
|
21 |
my $longest = max map { length($_->[1]) } grep { $_->[0] eq 'action' } @actions; |
|
22 |
my $first = 1; |
|
23 |
my $output; |
|
24 |
|
|
25 |
# else if (action[0] == 'hide') $(action[1]).hide(); |
|
26 |
foreach my $action (@actions) { |
|
27 |
if ($action->[0] eq 'comment') { |
|
28 |
print "\n" unless $first; |
|
29 |
print " // ", $action->[1], "\n"; |
|
30 |
|
|
31 |
} else { |
|
32 |
my $args = $action->[2] == 1 ? '' : join(', ', map { "action[$_]" } (2..$action->[2])); |
|
33 |
|
|
34 |
printf(' %s if (action[0] == \'%s\')%s $(action[1]).%s(%s);' . "\n", |
|
35 |
$first ? ' ' : 'else', |
|
36 |
$action->[1], |
|
37 |
' ' x ($longest - length($action->[1])), |
|
38 |
$action->[1], |
|
39 |
$args); |
|
40 |
$first = 0; |
|
41 |
} |
|
42 |
} |
|
43 |
|
|
44 |
printf "\n else\%sconsole.log('Unknown action: ' + action[0]);\n", ' ' x (4 + 2 + 6 + 3 + 4 + 2 + $longest + 1); |
Auch abrufbar als: Unified diff
Serverseitiges Erzeugen von im Client ausgeführten JavaScript-Befehlen