11.04KiB; Perl | 2016-01-20 21:01:02+01 | Statements 73 | SLOC 251
1
package HTML::FormHandlerX::JQueryRemoteValidator;
2
3
use HTML::FormHandler::Moose::Role;
4
use Method::Signatures::Simple;
5
use JSON ();
6
7
8 2
has_field _validation_scripts => (type => 'JavaScript', set_js_code => '_js_code_for_validation_scripts');
9
10
has validation_endpoint => (is => 'rw', isa => 'Str', default => '/ajax/formvalidator');
11
12
has jquery_validator_link => (is => 'rw', isa => 'Str', default => 'http://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js');
13
14
has skip_remote_validation_types  => (is => 'rw', isa => 'ArrayRef', default => sub { [ qw(Submit Hidden noCAPTCHA Display JSON JavaScript) ] });
15
16
has skip_all_remote_validation => (is => 'rw', isa => 'Bool', default => 0);
17
18
method _js_code_for_validation_scripts () {
19
    return '' if $self->skip_all_remote_validation;
20
    my $spec_data = $self->_data_for_validation_spec;
21
    my $spec = JSON->new->utf8
22
                        ->allow_nonref
23
                        ->pretty(1)
24
                #        ->relaxed(undef)
25
                #        ->canonical(undef)
26
                        ->encode($spec_data)
27
                         || '';
28
29
    my $form_name = $self->name;
30
    $spec =~ s/"${form_name}_data_collector"/${form_name}_data_collector/g;
31
    $spec =~ s/\n$//;
32
    $spec = "\n  var ${form_name}_validation_spec = $spec;\n";
33
    return $self->_data_collector_script . $spec . $self->_run_validator_script;
34
}
35
36
method _data_for_validation_spec () {
37
    my $js_profile = { rules => {}, messages => {} };
38
39
    foreach my $f (@{$self->fields}) {
40
        next if $self->_skip_remote_validation($f);       # don't build rules for these fields
41
        if ($f->isa('HTML::FormHandler::Field::Repeatable')) { # also 'Compound' as well but I don't have one of these lying around just yet
42
            foreach my $subf (sort {$a->name cmp $b->name} map {$_->fields} $f->fields) {
43
                $js_profile->{rules}->{$subf->id}->{remote} = $self->_build_remote_rule($subf);
44
            }
45
        }
46
        else {
47
            $js_profile->{rules}->{$f->id}->{remote} = $self->_build_remote_rule($f);
48
        }
49
    }
50
51
    return $js_profile;
52
}
53
54
method _build_remote_rule ($field) {
55
    my $remote_rule = {
56
        url => sprintf("%s/%s/%s", $self->validation_endpoint, $self->name, $field->id),
57
        type => 'POST',
58
        data => $self->name . "_data_collector",
59
        };
60
61
    return $remote_rule;
62
}
63
64
method _data_collector_script () {
65
    my @func;
66
    foreach my $f (sort {$a->name cmp $b->name} $self->fields) { # the sort is for consistent output in tests
67
        next if $self->_skip_data_collection($f);
68
        if ($f->isa('HTML::FormHandler::Field::Repeatable')) { # also 'Compound' as well but I don't have one of these lying around just yet
69
            foreach my $subf (sort {$a->name cmp $b->name} map {$_->fields} $f->fields) {
70
                push @func, sprintf "    \"%s\": function () { return \$(\"#%s\").val() }",
71
                    $subf->id, _escape_dots($subf->id);
72
            }
73
        }
74
        else {
75
            push @func, sprintf "    \"%s\": function () { return \$(\"#%s\").val() }",
76
                $f->id, _escape_dots($f->id);
77
        }
78
    }
79
80
    my $form_name = $self->name;
81
    return "  var ${form_name}_data_collector = {\n" . join(",\n", @func) . "\n  };\n";
82
}
83
84
sub _escape_dots {
85
    my $str = shift;
86
    $str =~ s/\./\\\\./g;
87
    return $str;
88
}
89
90
method _skip_remote_validation ($field) {
91
    return 1 if $self->skip_all_remote_validation;
92
    my %skip_type  = map {$_=>1} @{$self->skip_remote_validation_types};
93
    return 1 if $field->get_tag('no_remote_validate');
94
    return 1 if $skip_type{$field->type};
95
    return 0;
96
}
97
98
method _skip_data_collection ($field) {
99
    return 0 if $field->type eq 'Hidden'; # collect, but don't validate, hidden fields
100
    return $self->_skip_remote_validation($field);
101
}
102
103
method _run_validator_script () {
104
    my $form_name = $self->name;
105
    my $link = $self->jquery_validator_link;
106
107
    my $opts = join ",\n          ",
108
                    map { sprintf "%s: %s", $_, $self->jquery_validator_opts->{$_} }
109
                    keys %{$self->jquery_validator_opts};
110
111
    $opts = "\n$opts," if $opts;
112
113
    my $script = <<SCRIPT;
114
115
  \$(document).ready(function() {
116
    \$.getScript("$link", function () {
117
      if (typeof ${form_name}_validation_spec !== 'undefined') {
118
        \$('form#$form_name').validate({$opts
119
          rules: ${form_name}_validation_spec.rules,
120
          submitHandler: function(form) { form.submit(); }
121
        });
122
      }
123
    });
124
  });
125
SCRIPT
126
127
    return $script;
128
}
129
130
# http://jqueryvalidation.org/validate/     - start reading with the summary at the *end* of the page
131
has 'jquery_validator_opts' => (is => 'rw', isa => 'HashRef[Str]', required => 0, default => sub {{}});
132
133
=head1 SYNOPSIS
134
135
    package MyApp::Form::Foo;
136
    use HTML::FormHandler::Moose;
137
138
    with 'HTML::FormHandlerX::JQueryRemoteValidator';
139
140
    ...
141
142
    # You need to provide a form validation script at /ajax/formvalidator
143
    # In Poet/Mason, something like this in /ajax/formvalidator.mp -
144
145
    route ':form_name/:field_name';
146
147
    method handle () {
148
        my $form = $.form($.form_name);
149
        $form->process(params => $.args, no_update => 1);
150
151
        my $err = join ' ', @{$form->field($.field_name)->errors};
152
        my $result = $err || 'true';
153
154
        $m->print(JSON->new->allow_nonref->encode($result));
155
    }
156
157
158
=cut
159
160
161
=head1 CONFIGURATION AND SETUP
162
163
The purpose of this package is to build a set of JQuery scripts and inject them
164
into your forms. The scripts send user input to your server where you must
165
provide an endpoint that can validate the fields. Since you already have an
166
HTML::FormHandler form, you can use that.
167
168
The package uses the remote validation feature of the JQuery Validator
169
framework. This also takes care of updating your form to notify the user of
170
errors and successes while they fill in the form. You will most likely want
171
to customise that behaviour for your own situation. An example is given below.
172
173
=head2 What you will need
174
175
=over 4
176
177
=item JQuery
178
179
Load the JQuery library somewhere on your page.
180
181
=item JQuery validator
182
183
See the C<jquery_validator_link> attribute.
184
185
=item Server-side validation endpoint
186
187
See the C<validation_endpoint> attribute.
188
189
=item Some JS fragments to update the form
190
191
192
193
=item CSS to prettify it all
194
195
=back
196
197
=head2 An example using the Bootstrap 3 framework
198
199
=head3 Markup
200
201
    <form ...>
202
203
    <div class="form-group form-group-sm">
204
        <label class="col-xs-3 control-label" for="AddressForm.name"></label>
205
        <div class="col-xs-6">
206
            <input type="text" name="AddressForm.name" id="AddressForm.name"
207
                class="form-control" value="" />
208
        </div>
209
        <label for="AddressForm.name" id="AddressForm.name-error"
210
            class="has-error control-label col-xs-3">
211
        </label>
212
    </div>
213
214
    <div class="form-group form-group-sm">
215
        <label class="col-xs-3 control-label" for="AddressForm.address"></label>
216
        <div class="col-xs-6">
217
            <input type="text" name="AddressForm.address" id="AddressForm.address"
218
                class="form-control" value="" />
219
        </div>
220
        <label for="AddressForm.address" id="AddressForm.address-error"
221
            class="has-error control-label col-xs-3">
222
        </label>
223
    </div>
224
225
    ...
226
227
    </form>
228
229
230
=head3 CSS
231
232
Most of the classes on the form come from Twitter Bootstrap 3. In this example,
233
JQuery validator targets error messages to the second <label> on each
234
form-control. This is the default behaviour but can be changed.
235
236
The default setup will display and remove messages as the user progresses
237
through the form. JQuery Validator offers lots of options. You can read about
238
them at L<http://jqueryvalidation.org/validate/>. You should start by reading
239
the few sentences at the very bottom of that page.
240
241
Some useful additional styling to get started:
242
243
    label.valid {
244
      width: 24px;
245
      height: 24px;
246
      background: url(/static/images/valid.png) center center no-repeat;
247
      display: inline-block;
248
      text-indent: -9999px;
249
    }
250
251
    label.error {
252
      font-weight: normal;
253
      color: red;
254
      padding: 2px 8px;
255
      margin-top: 2px;
256
    }
257
258
=head3 JavaScript
259
260
You can provide extra JavaScript functions to control the behaviour of the error
261
and success messages in the C<jqr_validate_options> attribute:
262
263
    my $jqr_validate_options = {
264
        highlight => q/function(element, errorClass, validClass) {
265
                $(element).closest('.form-group').addClass(errorClass).removeClass(validClass);
266
                $(element).closest('.form-group').find("label").removeClass("valid");
267
            }/,
268
        unhighlight => q/function(element, errorClass, validClass) {
269
                $(element).closest('.form-group').removeClass(errorClass);
270
            }/,
271
        success => q/function(errorLabel, element) {
272
                $(element).closest('.form-group').addClass("has-success");
273
                errorLabel.addClass("valid");
274
            }/,
275
        errorClass => '"has-error"',
276
        validClass => '"has-success"',
277
        errorPlacement => q/function(errorLabel, element) {
278
                errorLabel.appendTo( element.parent("div").parent("div") );
279
            }/,
280
    };
281
282
    has '+jqr_validate_options' => (default => sub {$jqr_validate_options});
283
284
=head2 Class (form) attributes
285
286
=head3 C<validation_endpoint>
287
288
Default: /ajax/formvalidator
289
290
The form data will be POSTed to C<[validation_endpoint]/[form_name]/[field_name]>.
291
292
Note that *all* fields are submitted, not just the field being validated.
293
294
You must write the code to handle this submission. The response should be a JSON
295
string, either C<true> if the field passed its tests, or a message describing
296
the error. The message will be displayed on the form.
297
298
The synopsis has an example for Poet/Mason.
299
300
=head3 C<jquery_validator_link>
301
302
Default: http://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js
303
304
You can leave this as-is, or if you prefer, you can put the file on your own
305
server and modify this setting to point to it.
306
307
=head3 C<jquery_validator_opts>
308
309
Default: {}
310
311
A HashRef, keys being the keys of the C<validate> JQuery validator call documented
312
at L<http://jqueryvalidation.org/validate/>, with values being JavaScript functions
313
etc. as described there.
314
315
=head3 C<skip_remote_validation_types>
316
317
Default: C<[ qw(Submit Hidden noCAPTCHA Display JSON JavaScript) ]>
318
319
A list of field types that should not be included in the validation calls.
320
321
=head3 C<skip_all_remote_validation>
322
323
Boolean, default 0.
324
325
A flag to turn off remote validation altogether, perhaps useful during form development.
326
327
328
=head2 Field attributes
329
330
=head3 Tag C<no_remote_validate> [C<Bool>]
331
332
Default: not set
333
334
Set this tag to a true value on fields that should not be remotely validated:
335
336
    has_field 'foo' => (tags => {no_remote_validate => 1}, ... );
337
338
339
=head1 See also
340
341
=over 4
342
343
=item L<http://www.catalystframework.org/calendar/2012/23>
344
345
=item L<http://alittlecode.com/wp-content/uploads/jQuery-Validate-Demo/index.html>
346
347
=item L<http://jqueryvalidation.org>
348
349
=back
350
351
=cut
352
353
354
=head1 ACKNOWLEDGEMENTS
355
356
This started out as a modification of Aaron Trevana's
357
HTML::FormHandlerX::Form::JQueryValidator
358
359
=cut
360
361
1; # End of HTML::FormHandlerX::JQueryRemoteValidator