Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 7af2b128

Von Moritz Bunkus vor mehr als 11 Jahren hinzugefügt

  • ID 7af2b12887c4b1cb0cb427960c57f5b777b85315
  • Vorgänger 8817f25b
  • Nachfolger 3e2ecde7

Serverseitiges Erzeugen von im Client ausgeführten JavaScript-Befehlen

Unterschiede anzeigen:

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