Formular mit Floating Labels in Vanilla JS

Floating Labels in Formularen wie beim anstehenden Bootstrap-5-Release lassen sich auch mit gutem, alten Vanilla Javascript realisieren. Benutzt werden kann dabei clientseitig die browsereigene Validation Constraint API.

Tuedo#003

 von  tuedodev

18
Feb 2021
 0

Github Repository des Codes

CodePen Demo

Floating Labels sehen auf Formularen nicht nur schick aus, im Unterschied zu Placeholdern, die bei der Eingabe oder dem Fokuseinfach verschwinden, bleiben sie im Formularfeld sichtbar und teilen dem Nutzer mit, welche Information bei der Eingabe eines bestimmten Felds gewünscht wird. Sie sind also ein wichtiger Faktor in der Usability und User Experience (UX) von Webformularen.

Mit dem neuen 5er-Release (derzeit Beta 2) werden Floating Labels zwar in den Bootstrap-Kanon aufgenommen, diese lassen sich aber auch mit ganz normalem Vanilla Javascript und etwas CSS umsetzen.

HTML-Layout mit Bootstrap 5

Beim Layout werde ich dabei der Einfachheit halber gar nicht auf die Dienste von Bootstrap verzichten. Für die Darstellung des Formulars selbst und der Card-Komponente benutze ich Bootstrap-Klassen. Wichtig für das Layout ist, sich die Grundstruktur für das Feld, das Input, Textarea, Checkbox oder Radio Buttons beinhalten kann, zu überlegen.1

Für die Validierung greife ich dabei auf die im Browser integrierte Constraint Validation API zurück und nutze dabei die Constraints, die seit HTML5 zur Verfügung stehen (z. B. die Types required, pattern oder email, um die wichtigsten zu nennen). Beispielhaft soll das Eingabefeld für den Namen hier angezeigt werden. Zu Testzwecken in der Entwicklung wird hier das Pattern-Attribut gesetzt, das nur Großbuchstaben bei diesem Eingabefeld erlaubt.

<div class="control-wrapper mb-2">
    <div class="label-container">
        <label for="inputName" class="form-label">Your name (CAPITAL LETTERS only)</label>
    </div>
    <input type="text" class="form-control" id="inputName" required pattern="[A-Z]*">
    <div class="display-msg"></div>
</div>

Eine Wrapper-Klasse (control-wrapper) umfasst dabei drei Felder:

  • 1. Zum einen ein Label-Container, der oberhalb des Eingabefelds mit position: relative positioniert ist und das Child Label absolut positioniert. Diese absolute Positionierung des Labels befindet sich direkt auf dem Eingabefeld, wenn das Input-Feld nicht im Fokus steht und kein Textinhalt besitzt, ansonsten wird das Label verkleinert oberhalb des Eingabefelds dargestellt.
  • 2. Das Input-Feld, unter anderem mit den seit HTML5 gängigen Typen text, email, password. Auch die Attribute required oder pattern (um nur bestimmte Regex-Muster zu erlauben) finden hier ihre Anwendung
  • 3. Eine Mitteilungszeile display-msg, die nach der Validierung Platz bietet für den Fehlertext und ihn einblendet.

Beim Sonderfall checkbox ist der Label-Container die Wrapper-Klasse für die Darstellung des Labels, des Input-Felds sowie der Mitteilungszeile. Da keine Texteingabe erfolgt, muss das Label auch nicht floaten. Im Grunde ändert sich bei der Checkbox (und analog auch beim Radio Button) nur die Darstellung des Kästchens durch das Ankreuzen sowie die Anzeige des Fehlertexts.

Absolute Positionierung des Labels innerhalb der relativ positionierten Containerklasse

Der Clou bei der unterschiedlichen absoluten Positionierung des Labels sind die beiden Hilfsklassen field--focused und field--notempty, die je nachdem, ob das Eingabefeld im Fokus steht oder nicht leer ist, zur Containerklasse control-wrapper per Javascript hinzugefügt oder entfernt werden und die absolute Positionerung des Childelements label steuern. Mit CSS-Transitions wird der Übergang zwischen beiden Positionenen (wie auch das Einblenden der Error-Message in der div-Klasse display-msg) sanft vollzogen2.

Validierung in Javascript

Um die Formularlogik und Validierung zu kapseln, wird die Eingabelogik und Validierung in einer Javascript-Klasse namens FormApp umgesetzt. Eine seit ES6 bestehende Möglichkeit, die ich gerne nutze, wenn man wie ich von Java herkommt. Eleganter wäre es natürlich, diese Klasse FormApp modular auszulagern, was aber derzeit flächendeckend nur mit einem Babel-Transcompiler umsetzbar ist, worauf hier aber der Einfachheit halber verzichtet werden soll.

function ready(fn) {
	if (document.readyState != 'loading') {
		fn();
	} else {
		document.addEventListener('DOMContentLoaded', fn);
	}
}

ready(() => {
    const init = () => {
        const myForm = new FormApp("myform");
        myForm.init();
    }    
    init();
});

Mit diesem Code wird zunächst abgewartet, bis der DOM geladen ist und dann eine Objektinstanz der Klasse FormApp erzeugt mit der ID des Formulars als Argument.

constructor(formId){
    this.form = document.getElementById(formId);
    if (this.form){
        let fields = this.form.querySelectorAll(`input:not([disabled]):not([type="submit"]), textarea:not([disabled])`);
        this.fields = Array.prototype.slice.call(fields, 0).filter(x => x !== null);
        this.fields.forEach(field => {
            FormApp.checkIfFieldIsFilled(field);
        })
    }
}

Dem Konstruktor wird die ID übergeben und die einzelnenen aktiven Eingabefelder, Textareas und Checkboxes werden in der Objektvariablen this.fields gespeichert. Beim Aufruf der Objektmethode init() wird dem Formular zunächst das novalidate-Attribut hinzugefügt, das die browsereigene Validierung außer Kraft setzt.

Event-Handler für die Eingabefelder

Danach werden für die drei Events focus (wenn das Feld in den Fokus des Nutzer gerät, er also auf das Feld klickt oder mit der Tabulatortaste auf das Feld kommt), input (wenn er eine Eingabe macht) und blur (wenn der Nutzer das Feld verlässt, es aus dem Fokus verschwindet) dazugehörige Eventlistener registriert. Beim form-Element wird ferner ein Handler für das submit-Event registriert, das gefeuert wird, wenn der Nutzer den Submit-Button klickt oder auf Eingabe drückt.

Der Handler für das focus-Event handleFocus fügt der Parent-Klasse des Eingabefelds die CSS-Hilfsklasse field--focused hinzu. Diese Hilfsklasse sorgt dafür, dass das Label oberhalb des Felds verkleinert positioniert ist.

Der blur-Handler handleBlur macht das genaue Gegenteil. Wenn das Feld also aus dem Fokus verschwindet, wird diese Hilfsklasse wieder entfernt. Ferner wird die Klassenmethode FormApp.checkIfFieldIsFilled(field) aufgerufen, die prüft, ob das übergebene Feld eine Eingabe aufweist bzw. die Checkbox gefüllt ist. Dies entscheidet darüber, ob das Label wieder ins Feld zurückspringt (falls es leer ist) oder bei einer Eingabe an der Stelle oberhalb des Eingabefelds verbleibt. Zum Schluss des blur-Handlers wird die Validierung aufgerufen, und zwar die Objektmethode this.validateField(field).

Auch der input-Handler, der dann aufgerufen wird, wenn der Benutzer eine Eingabe tätigt, ruft wie der blur-Handler die beiden Methoden FormApp.checkIfFieldIsFilled(field) und this.validateField(field). Es wird also auch bei der Eingabe geprüft, ob das Feld noch gefüllt ist (Label bleibt verkleinert) und die Validierungsmethode aufgerufen.

Die Objektmethode validateField zieht aus der browsereigenen Constraint Validation API3 die Eigenschaft validity, die einen ValidityState zurückgibt, eine Art Dictionary, die über eine Reihe von Schlüssel-Wert-Paaren eine bestimmte Eigenschaft des Felds validiert und ein Boolean zurückgibt. Da ein Feld mehrere Fehlermeldungen zurückgeben kann, kumuliere ich die jeweiligen Fehler in einem Array.

let error_arr = []
for(var key in validState){
    if (validState[key]){
        error_arr.push(key);
    }
}

Da diese Fehler-Keys in einer Klassenvariable errorMsg gespeichert sind, lassen sich die jeweiligen Fehlermeldungen für ein Feld mit der eigenen Fehlermeldung befüllen, und zwar dadurch, dass die div-Klasse display-msg selektiert wird und mit dem Text gefüllt wird.

static errorMsg = {
    valueMissing: "Please enter the mandatory input fields.",
    typeMismatch: "Please enter a valid Email address.",
    patternMismatch: "Your input doesn´t match the pattern.",
    tooLong: "Your input is too long.",
    tooShort: "Your input is too short.",
    rangeUnderflow: "Your input is below the allowed range.",
    rangeOverflow: "Your input is beyond the allowed range.",
    stepMismatch: "Your input doesn´t fit the rules.",
    badInput: "Browser cannot handle your input.",
    customError: "A custom error occured."
}

Wenn die Länge des Error-Arrays größer 0 ist, mindestens ein Fehler also vorhanden ist, wird die Form mit der Bootstrap-Hilfsklasse was-validated erweitert, die alle validen Felder grün und alle nicht validen Felder rot umrandet. Warum also das Rad neu erfinden, wenn wir schon ein CSS-Framework wie Bootstrap benutzen?

let setFieldInvalid = () => {
    msg.textContent = err_content;
    cList.add(`field--invalid`);
}

if ( error_arr.length > 0 ){
    this.form.classList.add(`was-validated`);
    if (!msgClist.contains(`slide-out`)){
        setFieldInvalid();
    } else {
        setTimeout(setFieldInvalid, durationNumber);
    }
}

Die CSS-Klasse slide-out sorgt für ein animiertes Herausgleiten des Texts und wird dann der Anzeige-div hinzugefügt, wenn für ein Feld keine Fehlermeldung vorliegt, das Feld also valide ist. War vorher eine Fehlermeldung vorhanden, gleitet diese nun aus dem Sichtbereich heraus. Falls beim Hinzufügen eines Fehlers die Klasse slide-out schon vorhanden ist (eine bereits vorhandene Fehlermeldung also gerade jetzt hinausgleitet) wird mit einem setTimeout das Feld zeitverzögert mit der Fehlermeldung befüllt und der alten Fehlermeldung Zeit gegeben, aus dem Sichtfeld zu gleiten.

return err_content.length > 0 ? false : true;

Das return am Ende der Validierungsgmethode gibt an die aufrufende Methode ein Boolean zurück. Falls der String der Error Message größer 0 ist, das Feld mithin nicht valide ist, wird ein false zurückgegeben, ansonsten true.

Der submit-Handler schließlich wird aufgerufen, wenn der Nutzer den Submit-Button klickt oder die Return-Taste auf der Tastatur betätigt.

handleSubmit = (event) => {
    let evt = event || window.event;
    evt.preventDefault();
    let arr = this.fields.map(field => {
        return this.validateField(field);
    }).filter(field => !field);
    if (arr.length === 0){
        // No Errors client-side: Here you would process the data
        let str = this.fields.map(field => {
            let key = field.labels[0].textContent;
            let value = FormApp.getValueFromInput(field)
            return `${key}: ${value}`;
        }).join(`\n`);
        window.alert("SUBMIT SUCCESSFUL\n\n" + str);
    }
}

Mit evt.preventDefault() wird das übliche Senden der Daten an den Server unterbunden. Nun werden alle im Formular-Objekt hinterlegten aktiven Felder durchlaufen und jeweils validiert und in einem Array hinterlegt.

Bleibt das Array leer, sind also alle Felder valide, dürfen die Daten nun an den Server oder eine API gesendet werden und vor der Weiterverarbeitung serverseitig nochmals überprüft werden. Im vorliegenden Beispiel werden die eingegebenen Daten exemplarisch per Alert-Fenster auf dem Bildschirm ausgegeben.

  1. Auswahlfelder (Select Boxes) lassen wir bei unseren Überlegungen außen vor, da diese in der Regel keine Label benötigen und aufgrund der eingeschränkten Auswahl auch keine Fehlermeldungen erzeugen.
  2. Auf die modernere und performantere Animation-Methode @keyframes wird an der Stelle verzichtet, weil diese ein Overkill wäre für die Abbildung von maximal zwei unterschiedlichen Zuständen.
  3. Mehr Infos zur Constraint Validation API unter diesem Link.

Dieser Beitrag wurde bisher nicht kommentiert.

Kommentar hinzufügen