kivitendo/SL/ClientJS.pm @ 4d9d7d51
7af2b128 | Moritz Bunkus | package SL::ClientJS;
|
||
use strict;
|
||||
use parent qw(Rose::Object);
|
||||
use Carp;
|
||||
use SL::JSON ();
|
||||
use Rose::Object::MakeMethods::Generic
|
||||
(
|
||||
'scalar --get_set_init' => [ qw(_actions) ],
|
||||
);
|
||||
my %supported_methods = (
|
||||
# Basic effects
|
||||
hide => 1,
|
||||
show => 1,
|
||||
toggle => 1,
|
||||
# DOM insertion, around
|
||||
unwrap => 1,
|
||||
wrap => 2,
|
||||
wrapAll => 2,
|
||||
wrapInner => 2,
|
||||
# DOM insertion, inside
|
||||
append => 2,
|
||||
appendTo => 2,
|
||||
html => 2,
|
||||
prepend => 2,
|
||||
prependTo => 2,
|
||||
text => 2,
|
||||
# DOM insertion, outside
|
||||
after => 2,
|
||||
before => 2,
|
||||
insertAfter => 2,
|
||||
insertBefore => 2,
|
||||
# DOM removal
|
||||
empty => 1,
|
||||
remove => 1,
|
||||
# DOM replacement
|
||||
replaceAll => 2,
|
||||
replaceWith => 2,
|
||||
# General attributes
|
||||
attr => 3,
|
||||
prop => 3,
|
||||
removeAttr => 2,
|
||||
removeProp => 2,
|
||||
val => 2,
|
||||
# Data storage
|
||||
data => 3,
|
||||
removeData => 2,
|
||||
);
|
||||
sub AUTOLOAD {
|
||||
our $AUTOLOAD;
|
||||
my ($self, @args) = @_;
|
||||
my $method = $AUTOLOAD;
|
||||
$method =~ s/.*:://;
|
||||
return if $method eq 'DESTROY';
|
||||
my $num_args = $supported_methods{$method};
|
||||
$::lxdebug->message(0, "autoload method $method");
|
||||
croak "Unsupported jQuery action: $method" unless defined $num_args;
|
||||
croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
|
||||
if ($num_args) {
|
||||
# Force flattening from SL::Presenter::EscapedText: "" . $...
|
||||
$args[0] = "" . $args[0];
|
||||
$args[0] =~ s/^\s+//;
|
||||
}
|
||||
push @{ $self->_actions }, [ $method, @args ];
|
||||
return $self;
|
||||
}
|
||||
sub init__actions {
|
||||
return [];
|
||||
}
|
||||
sub to_json {
|
||||
my ($self) = @_;
|
||||
return SL::JSON::to_json({ eval_actions => $self->_actions });
|
||||
}
|
||||
sub to_array {
|
||||
my ($self) = @_;
|
||||
return $self->_actions;
|
||||
}
|
||||
4d9d7d51 | Moritz Bunkus | sub render {
|
||
my ($self, $controller) = @_;
|
||||
return $controller->render(\$self->to_json, { type => 'json' });
|
||||
}
|
||||
7af2b128 | Moritz Bunkus | 1;
|
||
__END__
|
||||
=pod
|
||||
=encoding utf8
|
||||
=head1 NAME
|
||||
SL::ClientJS - Easy programmatic client-side JavaScript generation
|
||||
with jQuery
|
||||
=head1 SYNOPSIS
|
||||
First some JavaScript code:
|
||||
// In the client generate an AJAX request whose 'success' handler
|
||||
// calls "eval_json_response(data)":
|
||||
var data = {
|
||||
action: "SomeController/the_action",
|
||||
id: $('#some_input_field').val()
|
||||
};
|
||||
$.post("controller.pl", data, eval_json_response);
|
||||
Now some Perl code:
|
||||
# In the controller itself. First, make sure that the "client_js.js"
|
||||
# is loaded. This must be done when the whole side is loaded, so
|
||||
# it's not in the action called by the AJAX request shown above.
|
||||
$::request->layout->use_javascript('client_js.js');
|
||||
# Now in that action called via AJAX:
|
||||
sub action_the_action {
|
||||
my ($self) = @_;
|
||||
# Create a new client-side JS object and do stuff with it!
|
||||
my $js = SL::ClientJS->new;
|
||||
# Show some element on the page:
|
||||
$js->show('#usually_hidden');
|
||||
# Set to hidden inputs. Yes, calls can be chained!
|
||||
$js->val('#hidden_id', $self->new_id)
|
||||
->val('#other_type', 'Unicorn');
|
||||
# Replace some HTML code:
|
||||
my $html = $self->render('SomeController/the_action', { output => 0 });
|
||||
$js->html('#id_with_new_content', $html);
|
||||
# Finally render the JSON response:
|
||||
$self->render($js);
|
||||
}
|
||||
=head1 OVERVIEW
|
||||
This module enables the generation of jQuery-using JavaScript code on
|
||||
the server side. That code is then evaluated in a safe way on the
|
||||
client side.
|
||||
The workflow is usally that the client creates an AJAX request, the
|
||||
server creates some actions and sends them back, and the client then
|
||||
implements each of these actions.
|
||||
There are three things that need to be done for this to work:
|
||||
=over 2
|
||||
=item 1. The "client_js.js" has to be loaded before the AJAX request is started.
|
||||
=item 2. The client code needs to call C<eval_json_response()> with the result returned from the server.
|
||||
=item 3. The server must use this module.
|
||||
=back
|
||||
The functions called on the client side are mostly jQuery
|
||||
functions. Other functionality may be added later.
|
||||
Note that L<SL::Controller/render> is aware of this module which saves
|
||||
you some boilerplate. The following two calls are equivalent:
|
||||
$controller->render($client_js);
|
||||
$controller->render(\$client_js->to_json, { type => 'json' });
|
||||
=head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
|
||||
=over 4
|
||||
=item C<to_array>
|
||||
Returns the actions gathered so far as an array reference. Each
|
||||
element is an array reference containing at least two items: the
|
||||
function's name and what it is called on. Additional array elements
|
||||
are the function parameters.
|
||||
=item C<to_json>
|
||||
Returns the actions gathered so far as a JSON string ready to be sent
|
||||
to the client.
|
||||
4d9d7d51 | Moritz Bunkus | =item C<render $controller>
|
||
Renders C<$self> via the controller. Useful for chaining. Equivalent
|
||||
to the following:
|
||||
$controller->render(\$self->to_json, { type => 'json' });
|
||||
7af2b128 | Moritz Bunkus | =back
|
||
=head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
|
||||
=head2 JQUERY FUNCTIONS
|
||||
The following jQuery functions are supported:
|
||||
=over 4
|
||||
=item Basic effects
|
||||
C<hide>, C<show>, C<toggle>
|
||||
=item DOM insertion, around
|
||||
C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
|
||||
=item DOM insertion, inside
|
||||
C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
|
||||
=item DOM insertion, outside
|
||||
C<after>, C<before>, C<insertAfter>, C<insertBefore>
|
||||
=item DOM removal
|
||||
C<empty>, C<remove>
|
||||
=item DOM replacement
|
||||
C<replaceAll>, C<replaceWith>
|
||||
=item General attributes
|
||||
C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
|
||||
=item Data storage
|
||||
C<data>, C<removeData>
|
||||
=back
|
||||
=head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
|
||||
In order not having to maintain two files (this one and
|
||||
C<js/client_js.js>) there's a script that can parse this file's
|
||||
C<%supported_methods> definition and convert it into the appropriate
|
||||
code ready for manual insertion into C<js/client_js.js>. The steps
|
||||
are:
|
||||
=over 2
|
||||
=item 1. Add lines in this file to the C<%supported_methods> hash. The
|
||||
key is the function name and the value is the number of expected
|
||||
parameters.
|
||||
=item 2. Run C<scripts/generate_client_js_actions.pl>
|
||||
=item 3. Edit C<js/client_js.js> and replace the type casing code with
|
||||
the output generated in step 2.
|
||||
=back
|
||||
=head1 BUGS
|
||||
Nothing here yet.
|
||||
=head1 AUTHOR
|
||||
Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
|
||||
=cut
|