Form Validation Teil 3: Ein Validity State API Polyfill

Avatar of Chris Ferdinandi
Chris Ferdinandi am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Im letzten Artikel dieser Serie haben wir ein leichtgewichtiges Skript (6 KB, 2,7 KB minifiziert) mit der Validity State API erstellt, um die native Formularvalidierung zu verbessern. Es funktioniert in allen modernen Browsern und bietet Unterstützung für IE ab IE10. Es gibt jedoch einige Browser-Eigenheiten.

Nicht jeder Browser unterstützt jede Validity State-Eigenschaft. Internet Explorer ist der Hauptschuldige, obwohl Edge die Unterstützung für tooLong vermissen lässt, obwohl IE10+ es unterstützt. Und Chrome, Firefox und Safari haben die vollständige Unterstützung erst kürzlich erhalten.

Heute schreiben wir einen leichtgewichtigen Polyfill, der unsere Browserunterstützung bis zurück zu IE9 erweitert und teilweise unterstützenden Browsern fehlende Eigenschaften hinzufügt, ohne jeglichen Kerncode unseres Skripts zu verändern.

Artikelserie

  1. Constraint-Validierung in HTML
  2. Die Constraint Validation API in JavaScript
  3. Ein Validity State API Polyfill (Sie sind hier!)
  4. Validierung des MailChimp-Anmeldeformulars

Legen wir los.

Unterstützung testen

Als Erstes müssen wir den Browser auf die Unterstützung der Validity State API testen.

Dazu verwenden wir document.createElement('input'), um ein Formularfeld zu erstellen, und prüfen dann, ob die Eigenschaft validity auf diesem Element vorhanden ist.

// Make sure that ValidityState is supported
var supported = function () {
    var input = document.createElement('input');
    return ('validity' in input);
};

Die Funktion supported() gibt in unterstützenden Browsern true und in nicht unterstützenden Browsern false zurück.

Es reicht jedoch nicht aus, nur die Eigenschaft validity zu testen. Wir müssen sicherstellen, dass auch die vollständige Palette der Validity State-Eigenschaften vorhanden ist.

Erweitern wir unsere Funktion supported(), um alle zu testen.

// Make sure that ValidityState is supported in full (all features)
var supported = function () {
    var input = document.createElement('input');
    return ('validity' in input && 'badInput' in input.validity && 'patternMismatch' in input.validity && 'rangeOverflow' in input.validity && 'rangeUnderflow' in input.validity && 'stepMismatch' in input.validity && 'tooLong' in input.validity && 'tooShort' in input.validity && 'typeMismatch' in input.validity && 'valid' in input.validity && 'valueMissing' in input.validity);
};

Browser wie IE11 und Edge werden diesen Test nicht bestehen, obwohl sie viele Validity State-Eigenschaften unterstützen.

Eingabegültigkeit prüfen

Als Nächstes schreiben wir unsere eigene Funktion, um die Gültigkeit eines Formularfeldes zu prüfen und ein Objekt mit der gleichen Struktur wie die Validity State API zurückzugeben.

Unsere Prüfungen einrichten

Zuerst richten wir unsere Funktion ein und übergeben das Feld als Argument.

// Generate the field validity object
var getValidityState = function (field) {
    // Run our validity checks...
};

Als Nächstes richten wir einige Variablen für einige Dinge ein, die wir wiederholt in unseren Gültigkeitsprüfungen verwenden müssen.

// Generate the field validity object
var getValidityState = function (field) {

    // Variables
    var type = field.getAttribute('type') || field.nodeName.toLowerCase(); // The field type
    var isNum = type === 'number' || type === 'range'; // Is the field numeric
    var length = field.value.length; // The field value length

};

Gültigkeit testen

Nun erstellen wir das Objekt, das alle unsere Gültigkeitsprüfungen enthalten wird.

// Generate the field validity object
var getValidityState = function (field) {

    // Variables
    var type = field.getAttribute('type') || input.nodeName.toLowerCase();
    var isNum = type === 'number' || type === 'range';
    var length = field.value.length;

    // Run validity checks
    var checkValidity = {
        badInput: false, // value does not conform to the pattern
        rangeOverflow: false, // value of a number field is higher than the max attribute
        rangeUnderflow: false, // value of a number field is lower than the min attribute
        stepMismatch: false, // value of a number field does not conform to the stepattribute
        tooLong: false, // the user has edited a too-long value in a field with maxlength
        tooShort: false, // the user has edited a too-short value in a field with minlength
        typeMismatch: false, // value of a email or URL field is not an email address or URL
        valueMissing: false // required field without a value
    };

};

Sie werden feststellen, dass die Eigenschaft valid im Objekt checkValidity fehlt. Wir können nur wissen, was sie ist, nachdem wir unsere anderen Tests durchgeführt haben.

Wir werden jede einzelne durchlaufen. Wenn eine davon true ist, setzen wir unseren valid-Status auf false. Andernfalls setzen wir ihn auf true. Dann geben wir das gesamte checkValidity zurück.

// Generate the field validity object
var getValidityState = function (field) {

    // Variables
    var type = field.getAttribute('type') || input.nodeName.toLowerCase();
    var isNum = type === 'number' || type === 'range';
    var length = field.value.length;

    // Run validity checks
    var checkValidity = {
        badInput: false, // value does not conform to the pattern
        rangeOverflow: false, // value of a number field is higher than the max attribute
        rangeUnderflow: false, // value of a number field is lower than the min attribute
        stepMismatch: false, // value of a number field does not conform to the stepattribute
        tooLong: false, // the user has edited a too-long value in a field with maxlength
        tooShort: false, // the user has edited a too-short value in a field with minlength
        typeMismatch: false, // value of a email or URL field is not an email address or URL
        valueMissing: false // required field without a value
    };

    // Check if any errors
    var valid = true;
    for (var key in checkValidity) {
        if (checkValidity.hasOwnProperty(key)) {
            // If there's an error, change valid value
            if (checkValidity[key]) {
                valid = false;
                break;
            }
        }
    }

    // Add valid property to validity object
    checkValidity.valid = valid;

    // Return object
    return checkValidity;

};

Tests schreiben

Jetzt müssen wir jeden unserer Tests schreiben. Die meisten davon beinhalten die Verwendung eines Regex-Musters mit der Methode test() gegen den Feldwert.

badInput

Für badInput gilt: Wenn das Feld numerisch ist, mindestens ein Zeichen enthält und mindestens eines der Zeichen *keine* Zahl ist, geben wir true zurück.

badInput: (isNum && length > 0 && !/[-+]?[0-9]/.test(field.value))
patternMismatch

Die Eigenschaft patternMismatch ist eine der einfacheren zu testen. Diese Eigenschaft ist true, wenn das Feld ein pattern-Attribut hat, mindestens ein Zeichen enthält und der Feldwert dem enthaltenen pattern-Regex nicht entspricht.

patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false)
rangeOverflow

Die Eigenschaft rangeOverflow sollte true zurückgeben, wenn das Feld ein max-Attribut hat, numerisch ist und mindestens ein Zeichen enthält, das über dem max-Wert liegt. Wir müssen den Stringwert von max mit der Methode parseInt() in eine Ganzzahl umwandeln.

rangeOverflow: (field.hasAttribute('max') && isNum && field.value > 1 && parseInt(field.value, 10) > parseInt(field.getAttribute('max'), 10))
rangeUnderflow

Die Eigenschaft rangeUnderflow sollte true zurückgeben, wenn das Feld ein min-Attribut hat, numerisch ist und mindestens ein Zeichen enthält, das unter dem min-Wert liegt. Wie bei rangeOverflow müssen wir den Stringwert von min mit der Methode parseInt() in eine Ganzzahl umwandeln.

rangeUnderflow: (field.hasAttribute('min') && isNum && field.value > 1 && parseInt(field.value, 10) < parseInt(field.getAttribute('min'), 10))
stepMismatch

Für die Eigenschaft stepMismatch gilt: Wenn das Feld numerisch ist, das Attribut step hat und der Wert des Attributs *nicht* any ist, verwenden wir den Modulo-Operator (%), um sicherzustellen, dass der Feldwert, geteilt durch step, keinen Rest hat. Wenn es einen Rest gibt, geben wir true zurück.

stepMismatch: (field.hasAttribute('step') && field.getAttribute('step') !== 'any' && isNum && Number(field.value) % parseFloat(field.getAttribute('step')) !== 0)
tooLong

Bei tooLong geben wir true zurück, wenn das Feld ein maxLength-Attribut größer als 0 hat und die length des Feldwerts größer ist als der Attributwert.

 0 && length > parseInt(field.getAttribute('maxLength'), 10))
tooShort

Umgekehrt geben wir bei tooShort true zurück, wenn das Feld ein minLength-Attribut größer als 0 hat und die length des Feldwerts kleiner ist als der Attributwert.

tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10))
typeMismatch

Die Eigenschaft typeMismatch ist am kompliziertesten zu validieren. Wir müssen zuerst sicherstellen, dass das Feld nicht leer ist. Dann müssen wir einen Regex-Test ausführen, wenn der type des Feldes email ist, und einen anderen, wenn es url ist. Wenn es einer dieser Werte ist *und* der Feldwert nicht mit unserem Regex-Muster übereinstimmt, geben wir true zurück.

typeMismatch: (length > 0 && ((type === 'email' && !/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test(field.value)) || (type === 'url' && !/^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*)(?::\d{2,5})?(?:[\/?#]\S*)?$/.test(field.value))))
valueMissing

Die Eigenschaft valueMissing ist ebenfalls etwas kompliziert. Zuerst prüfen wir, ob das Feld das Attribut required hat. Wenn ja, müssen wir je nach Feldtyp einen von drei verschiedenen Tests durchführen.

Wenn es sich um ein Kontrollkästchen oder einen Radio-Button handelt, müssen wir sicherstellen, dass es ausgewählt ist. Wenn es sich um ein Auswahlmenü handelt, müssen wir sicherstellen, dass ein Wert ausgewählt ist. Wenn es sich um einen anderen Eingabetyp handelt, müssen wir sicherstellen, dass er einen Wert hat.

valueMissing: (field.hasAttribute('required') && (((type === 'checkbox' || type === 'radio') && !field.checked) || (type === 'select' && field.options[field.selectedIndex].value < 1) || (type !=='checkbox' && type !== 'radio' && type !=='select' && length < 1)))

Der vollständige Satz von Tests

Hier sehen Sie, wie das abgeschlossene Objekt checkValidity mit all seinen Tests aussieht.

// Run validity checks
var checkValidity = {
    badInput: (isNum && length > 0 && !/[-+]?[0-9]/.test(field.value)), // value of a number field is not a number
    patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // value does not conform to the pattern
    rangeOverflow: (field.hasAttribute('max') && isNum && field.value > 1 && parseInt(field.value, 10) > parseInt(field.getAttribute('max'), 10)), // value of a number field is higher than the max attribute
    rangeUnderflow: (field.hasAttribute('min') && isNum && field.value > 1 && parseInt(field.value, 10) < parseInt(field.getAttribute('min'), 10)), // value of a number field is lower than the min attribute
    stepMismatch: (field.hasAttribute('step') && field.getAttribute('step') !== 'any' && isNum && Number(field.value) % parseFloat(field.getAttribute('step')) !== 0), // value of a number field does not conform to the stepattribute
    tooLong: (field.hasAttribute('maxLength') && field.getAttribute('maxLength') > 0 && length > parseInt(field.getAttribute('maxLength'), 10)), // the user has edited a too-long value in a field with maxlength
    tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10)), // the user has edited a too-short value in a field with minlength
    typeMismatch: (length > 0 && ((type === 'email' && !/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test(field.value)) || (type === 'url' && !/^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*)(?::\d{2,5})?(?:[\/?#]\S*)?$/.test(field.value)))), // value of a email or URL field is not an email address or URL
    valueMissing: (field.hasAttribute('required') && (((type === 'checkbox' || type === 'radio') && !field.checked) || (type === 'select' && field.options[field.selectedIndex].value < 1) || (type !=='checkbox' && type !== 'radio' && type !=='select' && length < 1))) // required field without a value
};

Besondere Überlegungen für Radio-Buttons

In unterstützenden Browsern schlägt required bei einem Radio-Button nur fehl, wenn keine Elemente in der Gruppe ausgewählt wurden. Unser Polyfill gibt derzeit valueMissing als true für einen nicht ausgewählten Radio-Button zurück, auch wenn ein anderer Button in der Gruppe ausgewählt *ist*.

Um dies zu beheben, müssen wir jeden Button in der Gruppe ermitteln. Wenn einer von ihnen ausgewählt ist, validieren wir diesen Radio-Button anstelle des, der den Fokus verloren hat.

// Generate the field validity object
var getValidityState = function (field) {

        // Variables
        var type = field.getAttribute('type') || input.nodeName.toLowerCase(); // The field type
        var isNum = type === 'number' || type === 'range'; // Is the field numeric
        var length = field.value.length; // The field value length

        // If radio group, get selected field
        if (field.type === 'radio' && field.name) {
                var group = document.getElementsByName(field.name);
                if (group.length > 0) {
                        for (var i = 0; i < group.length; i++) {
                                if (group[i].form === field.form && field.checked) {
                                        field = group[i];
                                        break;
                                }
                        }
                }
        }

        ...

};

Hinzufügen der validity-Eigenschaft zu Formularfeldern

Schließlich, wenn die Validity State API nicht vollständig unterstützt wird, möchten wir die Eigenschaft validity hinzufügen oder überschreiben. Dies tun wir mit der Methode Object.defineProperty().

// If the full set of ValidityState features aren't supported, polyfill
if (!supported()) {
    Object.defineProperty(HTMLInputElement.prototype, 'validity', {
        get: function ValidityState() {
            return getValidityState(this);
        },
        configurable: true,
    });
}

Alles zusammenfügen

Hier ist der Polyfill in seiner Gesamtheit. Um unsere Funktionen aus dem globalen Geltungsbereich herauszuhalten, habe ich ihn in eine IIFE (Immediately Invoked Function Expression) verpackt.

;(function (window, document, undefined) {

    'use strict';

    // Make sure that ValidityState is supported in full (all features)
    var supported = function () {
        var input = document.createElement('input');
        return ('validity' in input && 'badInput' in input.validity && 'patternMismatch' in input.validity && 'rangeOverflow' in input.validity && 'rangeUnderflow' in input.validity && 'stepMismatch' in input.validity && 'tooLong' in input.validity && 'tooShort' in input.validity && 'typeMismatch' in input.validity && 'valid' in input.validity && 'valueMissing' in input.validity);
    };

    /**
     * Generate the field validity object
     * @param  {Node]} field The field to validate
     * @return {Object}      The validity object
     */
    var getValidityState = function (field) {

        // Variables
        var type = field.getAttribute('type') || input.nodeName.toLowerCase();
        var isNum = type === 'number' || type === 'range';
        var length = field.value.length;
        var valid = true;

        // Run validity checks
        var checkValidity = {
            badInput: (isNum && length > 0 && !/[-+]?[0-9]/.test(field.value)), // value of a number field is not a number
            patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // value does not conform to the pattern
            rangeOverflow: (field.hasAttribute('max') && isNum && field.value > 1 && parseInt(field.value, 10) > parseInt(field.getAttribute('max'), 10)), // value of a number field is higher than the max attribute
            rangeUnderflow: (field.hasAttribute('min') && isNum && field.value > 1 && parseInt(field.value, 10) < parseInt(field.getAttribute('min'), 10)), // value of a number field is lower than the min attribute
            stepMismatch: (field.hasAttribute('step') && field.getAttribute('step') !== 'any' && isNum && Number(field.value) % parseFloat(field.getAttribute('step')) !== 0), // value of a number field does not conform to the stepattribute
            tooLong: (field.hasAttribute('maxLength') && field.getAttribute('maxLength') > 0 && length > parseInt(field.getAttribute('maxLength'), 10)), // the user has edited a too-long value in a field with maxlength
            tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10)), // the user has edited a too-short value in a field with minlength
            typeMismatch: (length > 0 && ((type === 'email' && !/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test(field.value)) || (type === 'url' && !/^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*)(?::\d{2,5})?(?:[\/?#]\S*)?$/.test(field.value)))), // value of a email or URL field is not an email address or URL
            valueMissing: (field.hasAttribute('required') && (((type === 'checkbox' || type === 'radio') && !field.checked) || (type === 'select' && field.options[field.selectedIndex].value < 1) || (type !=='checkbox' && type !== 'radio' && type !=='select' && length < 1))) // required field without a value
        };

        // Check if any errors
        for (var key in checkValidity) {
            if (checkValidity.hasOwnProperty(key)) {
                // If there's an error, change valid value
                if (checkValidity[key]) {
                    valid = false;
                    break;
                }
            }
        }

        // Add valid property to validity object
        checkValidity.valid = valid;

        // Return object
        return checkValidity;

    };

    // If the full set of ValidityState features aren't supported, polyfill
    if (!supported()) {
        Object.defineProperty(HTMLInputElement.prototype, 'validity', {
            get: function ValidityState() {
                return getValidityState(this);
            },
            configurable: true,
        });
    }

})(window, document);

Das Hinzufügen dieses Skripts zu Ihrer Website erweitert die Validity State API bis zurück zu IE9 und fügt teilweise unterstützenden Browsern fehlende Eigenschaften hinzu. (Sie können den Polyfill auch auf GitHub herunterladen.)

Das Formularvalidierungs-Skript, das wir im letzten Artikel geschrieben haben, nutzte auch die classList API, die in allen modernen Browsern und ab IE10 unterstützt wird. Um wirklich IE9+-Unterstützung zu erhalten, sollten wir auch den classList.js Polyfill von Eli Grey einbeziehen.

Artikelserie

  1. Constraint-Validierung in HTML
  2. Die Constraint Validation API in JavaScript
  3. Ein Validity State API Polyfill (Sie sind hier!)
  4. Validierung des MailChimp-Anmeldeformulars