yii.activeForm.js 24.1 KB
Newer Older
Qiang Xue committed
1 2 3 4 5 6 7 8 9 10 11 12 13
/**
 * Yii form widget.
 *
 * This is the JavaScript widget used by the yii\widgets\ActiveForm widget.
 *
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
(function ($) {

Qiang Xue committed
14 15 16 17 18 19 20 21 22 23
    $.fn.yiiActiveForm = function (method) {
        if (methods[method]) {
            return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
        } else if (typeof method === 'object' || !method) {
            return methods.init.apply(this, arguments);
        } else {
            $.error('Method ' + method + ' does not exist on jQuery.yiiActiveForm');
            return false;
        }
    };
Qiang Xue committed
24

25 26 27 28 29 30
    var events = {
        /**
         * beforeValidate event is triggered before validating the whole form and each attribute.
         * The signature of the event handler should be:
         *     function (event, messages, deferreds, attribute)
         * where
Qiang Xue committed
31
         *  - event: an Event object.
32 33 34 35 36 37 38
         *  - messages: error messages. When attribute is undefined, this parameter is an associative array
         *    with keys being attribute IDs and values being error messages for the corresponding attributes.
         *    When attribute is given, this parameter is an array of the error messages for that attribute.
         *  - deferreds: an array of Deferred objects. You can use deferreds.add(callback) to add a new deferred validation.
         *  - attribute: an attribute object. Please refer to attributeDefaults for the structure.
         *    If this is undefined, it means the event is triggered before validating the whole form.
         *    Otherwise it means the event is triggered before validating the specified attribute.
Qiang Xue committed
39 40
         *
         * If the handler returns a boolean false, it will stop further validation after this event.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
         */
        beforeValidate: 'beforeValidate',
        /**
         * afterValidate event is triggered after validating the whole form and each attribute.
         * The signature of the event handler should be:
         *     function (event, messages, attribute)
         * where
         *  - event: an Event object.
         *  - messages: error messages. When attribute is undefined, this parameter is an associative array
         *    with keys being attribute IDs and values being error messages for the corresponding attributes.
         *    When attribute is given, this parameter is an array of the error messages for that attribute.
         *    If the array length is greater than 0, it means the attribute has validation errors.
         *  - attribute: an attribute object. Please refer to attributeDefaults for the structure.
         *    If this is undefined, it means the event is triggered before validating the whole form.
         *    Otherwise it means the event is triggered before validating the specified attribute.
         */
        afterValidate: 'afterValidate',
        /**
         * beforeSubmit event is triggered before submitting the form (after all validations pass).
         * The signature of the event handler should be:
         *     function (event)
         * where event is an Event object.
Qiang Xue committed
63 64
         *
         * If the handler returns a boolean false, it will stop further validation after this event.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
         */
        beforeSubmit: 'beforeSubmit',
        /**
         * ajaxBeforeSend event is triggered before sending an AJAX request for AJAX-based validation.
         * The signature of the event handler should be:
         *     function (event, jqXHR, settings)
         * where
         *  - event: an Event object.
         *  - jqXHR: a jqXHR object
         *  - settings: the settings for the AJAX request
         */
        ajaxBeforeSend: 'ajaxBeforeSend',
        /**
         * ajaxComplete event is triggered after completing an AJAX request for AJAX-based validation.
         * The signature of the event handler should be:
         *     function (event, jqXHR, textStatus)
         * where
         *  - event: an Event object.
         *  - jqXHR: a jqXHR object
         *  - settings: the status of the request ("success", "notmodified", "error", "timeout", "abort", or "parsererror").
         */
        ajaxComplete: 'ajaxComplete'
    };

89
    // NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveForm::getClientOptions() as well
Qiang Xue committed
90
    var defaults = {
91 92
        // whether to encode the error summary
        encodeErrorSummary: true,
Qiang Xue committed
93
        // the jQuery selector for the error summary
94
        errorSummary: '.error-summary',
Qiang Xue committed
95 96 97
        // whether to perform validation before submitting the form.
        validateOnSubmit: true,
        // the container CSS class representing the corresponding attribute has validation error
98
        errorCssClass: 'has-error',
Qiang Xue committed
99
        // the container CSS class representing the corresponding attribute passes validation
100
        successCssClass: 'has-success',
Qiang Xue committed
101 102
        // the container CSS class representing the corresponding attribute is being validated
        validatingCssClass: 'validating',
103 104 105 106
        // the GET parameter name indicating an AJAX-based validation
        ajaxParam: 'ajax',
        // the type of data that you're expecting back from the server
        ajaxDataType: 'json',
Qiang Xue committed
107
        // the URL for performing AJAX-based validation. If not set, it will use the the form's action
108
        validationUrl: undefined
Qiang Xue committed
109
    };
Qiang Xue committed
110

111
    // NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveField::getClientOptions() as well
Qiang Xue committed
112
    var attributeDefaults = {
113 114
        // a unique ID identifying an attribute (e.g. "loginform-username") in a form
        id: undefined,
Qiang Xue committed
115 116 117 118
        // attribute name or expression (e.g. "[0]content" for tabular input)
        name: undefined,
        // the jQuery selector of the container of the input field
        container: undefined,
119
        // the jQuery selector of the input field under the context of the container
Qiang Xue committed
120
        input: undefined,
121 122
        // the jQuery selector of the error tag under the context of the container
        error: '.help-block',
123 124
        // whether to encode the error
        encodeError: true,
Qiang Xue committed
125
        // whether to perform validation when a change is detected on the input
126
        validateOnChange: true,
127
        // whether to perform validation when the input loses focus
128
        validateOnBlur: true,
Qiang Xue committed
129 130 131 132 133 134 135 136 137 138 139 140 141
        // whether to perform validation when the user is typing.
        validateOnType: false,
        // number of milliseconds that the validation should be delayed when a user is typing in the input field.
        validationDelay: 200,
        // whether to enable AJAX-based validation.
        enableAjaxValidation: false,
        // function (attribute, value, messages), the client-side validation function.
        validate: undefined,
        // status of the input field, 0: empty, not entered before, 1: validated, 2: pending validation, 3: validating
        status: 0,
        // the value of the input
        value: undefined
    };
Qiang Xue committed
142

Qiang Xue committed
143 144 145 146 147 148 149
    var methods = {
        init: function (attributes, options) {
            return this.each(function () {
                var $form = $(this);
                if ($form.data('yiiActiveForm')) {
                    return;
                }
Qiang Xue committed
150

Qiang Xue committed
151 152 153 154
                var settings = $.extend({}, defaults, options || {});
                if (settings.validationUrl === undefined) {
                    settings.validationUrl = $form.prop('action');
                }
155

Qiang Xue committed
156 157
                $.each(attributes, function (i) {
                    attributes[i] = $.extend({value: getValue($form, this)}, attributeDefaults, this);
158
                    watchAttribute($form, attributes[i]);
Qiang Xue committed
159
                });
160

Qiang Xue committed
161 162 163 164 165 166
                $form.data('yiiActiveForm', {
                    settings: settings,
                    attributes: attributes,
                    submitting: false,
                    validated: false
                });
Qiang Xue committed
167

Qiang Xue committed
168 169 170 171 172
                /**
                 * Clean up error status when the form is reset.
                 * Note that $form.on('reset', ...) does work because the "reset" event does not bubble on IE.
                 */
                $form.bind('reset.yiiActiveForm', methods.resetForm);
Qiang Xue committed
173

Qiang Xue committed
174 175 176 177
                if (settings.validateOnSubmit) {
                    $form.on('mouseup.yiiActiveForm keyup.yiiActiveForm', ':submit', function () {
                        $form.data('yiiActiveForm').submitObject = $(this);
                    });
178
                    $form.on('submit.yiiActiveForm', methods.submitForm);
Qiang Xue committed
179 180 181
                }
            });
        },
Qiang Xue committed
182

183 184 185 186 187 188 189 190 191 192 193 194 195 196
        // add a new attribute to the form dynamically.
        // please refer to attributeDefaults for the structure of attribute
        add: function (attribute) {
            var $form = $(this);
            attribute = $.extend({value: getValue($form, attribute)}, attributeDefaults, attribute);
            $form.data('yiiActiveForm').attributes.push(attribute);
            watchAttribute($form, attribute);
        },

        // remove the attribute with the specified ID from the form
        remove: function (id) {
            var $form = $(this),
                attributes = $form.data('yiiActiveForm').attributes,
                index = -1,
197
                attribute = undefined;
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
            $.each(attributes, function (i) {
                if (attributes[i]['id'] == id) {
                    index = i;
                    attribute = attributes[i];
                    return false;
                }
            });
            if (index >= 0) {
                attributes.splice(index, 1);
                unwatchAttribute($form, attribute);
            }
            return attribute;
        },

        // find an attribute config based on the specified attribute ID
        find: function (id) {
214 215
            var attributes = $(this).data('yiiActiveForm').attributes,
                result = undefined;
216 217 218 219 220 221 222 223 224
            $.each(attributes, function (i) {
                if (attributes[i]['id'] == id) {
                    result = attributes[i];
                    return false;
                }
            });
            return result;
        },

Qiang Xue committed
225 226
        destroy: function () {
            return this.each(function () {
Qiang Xue committed
227
                $(this).unbind('.yiiActiveForm');
Qiang Xue committed
228 229 230
                $(this).removeData('yiiActiveForm');
            });
        },
Qiang Xue committed
231

Qiang Xue committed
232 233 234
        data: function () {
            return this.data('yiiActiveForm');
        },
Qiang Xue committed
235

236
        validate: function () {
Qiang Xue committed
237
            var $form = $(this),
238 239 240 241 242 243
                data = $form.data('yiiActiveForm'),
                needAjaxValidation = false,
                messages = {},
                deferreds = deferredArray();

            if (data.submitting) {
Qiang Xue committed
244
                var event = $.Event(events.beforeValidate);
245
                $form.trigger(event, [messages, deferreds]);
Qiang Xue committed
246
                if (event.result === false) {
247 248
                    data.submitting = false;
                    return;
Qiang Xue committed
249 250
                }
            }
Qiang Xue committed
251

252 253 254 255 256 257 258 259 260
            // client-side validation
            $.each(data.attributes, function () {
                // perform validation only if the form is being submitted or if an attribute is pending validation
                if (data.submitting || this.status === 2 || this.status === 3) {
                    var msg = messages[this.id];
                    if (msg === undefined) {
                        msg = [];
                        messages[this.id] = msg;
                    }
Qiang Xue committed
261
                    var event = $.Event(events.beforeValidate);
262
                    $form.trigger(event, [msg, deferreds, this]);
Qiang Xue committed
263
                    if (event.result !== false) {
264 265 266 267 268 269
                        if (this.validate) {
                            this.validate(this, getValue($form, this), msg, deferreds);
                        }
                        if (this.enableAjaxValidation) {
                            needAjaxValidation = true;
                        }
Qiang Xue committed
270
                    }
271
                }
272 273 274 275 276 277 278 279
            });

            // ajax validation
            $.when.apply(this, deferreds).always(function() {
                // Remove empty message arrays
                for (var i in messages) {
                    if (0 === messages[i].length) {
                        delete messages[i];
Qiang Xue committed
280
                    }
281 282 283 284 285 286 287 288 289
                }
                if (needAjaxValidation && (!data.submitting || $.isEmptyObject(messages))) {
                    // Perform ajax validation when at least one input needs it.
                    // If the validation is triggered by form submission, ajax validation
                    // should be done only when all inputs pass client validation
                    var $button = data.submitObject,
                        extData = '&' + data.settings.ajaxParam + '=' + $form.prop('id');
                    if ($button && $button.length && $button.prop('name')) {
                        extData += '&' + $button.prop('name') + '=' + $button.prop('value');
Qiang Xue committed
290
                    }
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
                    $.ajax({
                        url: data.settings.validationUrl,
                        type: $form.prop('method'),
                        data: $form.serialize() + extData,
                        dataType: data.settings.ajaxDataType,
                        complete: function (jqXHR, textStatus) {
                            $form.trigger(events.ajaxComplete, [jqXHR, textStatus]);
                        },
                        beforeSend: function (jqXHR, settings) {
                            $form.trigger(events.ajaxBeforeSend, [jqXHR, settings]);
                        },
                        success: function (msgs) {
                            if (msgs !== null && typeof msgs === 'object') {
                                $.each(data.attributes, function () {
                                    if (!this.enableAjaxValidation) {
                                        delete msgs[this.id];
                                    }
                                });
                                updateInputs($form, $.extend(messages, msgs));
                            } else {
                                updateInputs($form, messages);
                            }
                        },
                        error: function () {
                            data.submitting = false;
                        }
                    });
                } else if (data.submitting) {
                    // delay callback so that the form can be submitted without problem
                    setTimeout(function () {
                        updateInputs($form, messages);
                    }, 200);
                } else {
                    updateInputs($form, messages);
Qiang Xue committed
325 326
                }
            });
327 328 329 330 331 332 333
        },

        submitForm: function () {
            var $form = $(this),
                data = $form.data('yiiActiveForm');

            if (data.validated) {
Qiang Xue committed
334
                var event = $.Event(events.beforeSubmit);
335
                $form.trigger(event, [$form]);
Qiang Xue committed
336
                if (event.result === false) {
337 338 339 340 341 342 343 344 345 346 347 348 349
                    data.validated = false;
                    data.submitting = false;
                    return false;
                }
                return true;   // continue submitting the form since validation passes
            } else {
                if (data.settings.timer !== undefined) {
                    clearTimeout(data.settings.timer);
                }
                data.submitting = true;
                methods.validate.call($form);
                return false;
            }
Qiang Xue committed
350
        },
Qiang Xue committed
351

Qiang Xue committed
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
        resetForm: function () {
            var $form = $(this);
            var data = $form.data('yiiActiveForm');
            // Because we bind directly to a form reset event instead of a reset button (that may not exist),
            // when this function is executed form input values have not been reset yet.
            // Therefore we do the actual reset work through setTimeout.
            setTimeout(function () {
                $.each(data.attributes, function () {
                    // Without setTimeout() we would get the input values that are not reset yet.
                    this.value = getValue($form, this);
                    this.status = 0;
                    var $container = $form.find(this.container);
                    $container.removeClass(
                        data.settings.validatingCssClass + ' ' +
                            data.settings.errorCssClass + ' ' +
                            data.settings.successCssClass
                    );
                    $container.find(this.error).html('');
                });
                $form.find(data.settings.summary).hide().find('ul').html('');
            }, 1);
        }
    };
Qiang Xue committed
375

376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
    var watchAttribute = function ($form, attribute) {
        var $input = findInput($form, attribute);
        if (attribute.validateOnChange) {
            $input.on('change.yiiActiveForm',function () {
                validateAttribute($form, attribute, false);
            });
        }
        if (attribute.validateOnBlur) {
            $input.on('blur.yiiActiveForm', function () {
                if (attribute.status == 0 || attribute.status == 1) {
                    validateAttribute($form, attribute, !attribute.status);
                }
            });
        }
        if (attribute.validateOnType) {
            $input.on('keyup.yiiActiveForm', function () {
                if (attribute.value !== getValue($form, attribute)) {
                    validateAttribute($form, attribute, false);
                }
            });
        }
    };

    var unwatchAttribute = function ($form, attribute) {
        findInput($form, attribute).off('.yiiActiveForm');
    };

Qiang Xue committed
403 404
    var validateAttribute = function ($form, attribute, forceValidate) {
        var data = $form.data('yiiActiveForm');
Qiang Xue committed
405

Qiang Xue committed
406 407 408 409 410 411 412 413 414 415 416 417
        if (forceValidate) {
            attribute.status = 2;
        }
        $.each(data.attributes, function () {
            if (this.value !== getValue($form, this)) {
                this.status = 2;
                forceValidate = true;
            }
        });
        if (!forceValidate) {
            return;
        }
Qiang Xue committed
418

Qiang Xue committed
419 420 421 422 423 424 425 426 427 428 429 430 431
        if (data.settings.timer !== undefined) {
            clearTimeout(data.settings.timer);
        }
        data.settings.timer = setTimeout(function () {
            if (data.submitting || $form.is(':hidden')) {
                return;
            }
            $.each(data.attributes, function () {
                if (this.status === 2) {
                    this.status = 3;
                    $form.find(this.container).addClass(data.settings.validatingCssClass);
                }
            });
432
            methods.validate.call($form);
Qiang Xue committed
433 434
        }, data.settings.validationDelay);
    };
Alex-Code committed
435 436 437 438 439 440 441 442 443
    
    /**
     * Returns an array prototype with a shortcut method for adding a new deferred.
     * The context of the callback will be the deferred object so it can be resolved like ```this.resolve()```
     * @returns Array
     */
    var deferredArray = function () {
        var array = [];
        array.add = function(callback) {
Alex-Code committed
444
            this.push(new $.Deferred(callback));
Alex-Code committed
445 446 447
        };
        return array;
    };
448

Qiang Xue committed
449
    /**
450 451 452
     * Updates the error messages and the input containers for all applicable attributes
     * @param $form the form jQuery object
     * @param messages array the validation error messages
Qiang Xue committed
453
     */
454 455
    var updateInputs = function ($form, messages) {
        var data = $form.data('yiiActiveForm');
Qiang Xue committed
456

457 458 459 460 461
        if (data.submitting) {
            var errorInputs = [];
            $.each(data.attributes, function () {
                if (updateInput($form, this, messages)) {
                    errorInputs.push(this.input);
Qiang Xue committed
462
                }
463
            });
Qiang Xue committed
464

465 466 467 468 469 470 471 472 473
            $form.trigger(events.afterValidate, [messages]);

            updateSummary($form, messages);

            if (errorInputs.length) {
                var top = $form.find(errorInputs.join(',')).first().offset().top;
                var wtop = $(window).scrollTop();
                if (top < wtop || top > wtop + $(window).height) {
                    $(window).scrollTop(top);
Alex-Code committed
474
                }
475
                data.submitting = false;
Alex-Code committed
476
            } else {
477 478 479 480 481 482 483 484 485
                data.validated = true;
                var $button = data.submitObject || $form.find(':submit:first');
                // TODO: if the submission is caused by "change" event, it will not work
                if ($button.length) {
                    $button.click();
                } else {
                    // no submit button in the form
                    $form.submit();
                }
Alex-Code committed
486
            }
487 488 489 490 491 492 493
        } else {
            $.each(data.attributes, function () {
                if (this.status === 2 || this.status === 3) {
                    updateInput($form, this, messages);
                }
            });
        }
Qiang Xue committed
494
    };
Qiang Xue committed
495

Qiang Xue committed
496 497 498 499 500 501 502 503 504 505 506
    /**
     * Updates the error message and the input container for a particular attribute.
     * @param $form the form jQuery object
     * @param attribute object the configuration for a particular attribute.
     * @param messages array the validation error messages
     * @return boolean whether there is a validation error for the specified attribute
     */
    var updateInput = function ($form, attribute, messages) {
        var data = $form.data('yiiActiveForm'),
            $input = findInput($form, attribute),
            hasError = false;
Qiang Xue committed
507

508 509
        if (!$.isArray(messages[attribute.id])) {
            messages[attribute.id] = [];
Qiang Xue committed
510
        }
511 512
        $form.trigger(events.afterValidate, [messages[attribute.id], attribute]);

Qiang Xue committed
513 514
        attribute.status = 1;
        if ($input.length) {
515
            hasError = messages[attribute.id].length > 0;
Qiang Xue committed
516 517 518
            var $container = $form.find(attribute.container);
            var $error = $container.find(attribute.error);
            if (hasError) {
519 520 521 522 523
                if (attribute.encodeError) {
                    $error.text(messages[attribute.id][0]);
                } else {
                    $error.html(messages[attribute.id][0]);
                }
Qiang Xue committed
524 525 526
                $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass)
                    .addClass(data.settings.errorCssClass);
            } else {
527
                $error.empty();
Qiang Xue committed
528 529 530 531 532 533 534
                $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ')
                    .addClass(data.settings.successCssClass);
            }
            attribute.value = getValue($form, attribute);
        }
        return hasError;
    };
Qiang Xue committed
535

Qiang Xue committed
536 537 538 539 540 541 542 543
    /**
     * Updates the error summary.
     * @param $form the form jQuery object
     * @param messages array the validation error messages
     */
    var updateSummary = function ($form, messages) {
        var data = $form.data('yiiActiveForm'),
            $summary = $form.find(data.settings.errorSummary),
544
            $ul = $summary.find('ul').empty();
Qiang Xue committed
545

Qiang Xue committed
546 547
        if ($summary.length && messages) {
            $.each(data.attributes, function () {
548
                if ($.isArray(messages[this.id]) && messages[this.id].length) {
549 550 551 552 553 554 555
                    var error = $('<li/>');
                    if (data.settings.encodeErrorSummary) {
                        error.text(messages[this.id][0]);
                    } else {
                        error.html(messages[this.id][0]);
                    }
                    $ul.append(error);
Qiang Xue committed
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584
                }
            });
            $summary.toggle($ul.find('li').length > 0);
        }
    };

    var getValue = function ($form, attribute) {
        var $input = findInput($form, attribute);
        var type = $input.prop('type');
        if (type === 'checkbox' || type === 'radio') {
            var $realInput = $input.filter(':checked');
            if (!$realInput.length) {
                $realInput = $form.find('input[type=hidden][name="' + $input.prop('name') + '"]');
            }
            return $realInput.val();
        } else {
            return $input.val();
        }
    };

    var findInput = function ($form, attribute) {
        var $input = $form.find(attribute.input);
        if ($input.length && $input[0].tagName.toLowerCase() === 'div') {
            // checkbox list or radio list
            return $input.find('input');
        } else {
            return $input;
        }
    };
Qiang Xue committed
585

586
})(window.jQuery);