In Jetzt Knockout.js lernen: Observables erweitern der Serie zu Knockout.js wurde gezeigt, wie Observables erweitert werden können. Als Beispiel diente eine Validierung auf Basis von einzelnen Observables. Nun hängen diese in der Regel aber in einem Formular zusammen, wodurch eine Gesamtvalidierung und eine damit verbundene Freischaltung von Schaltflächen bzw. weiteren Interaktionsmöglichkeiten einhergeht. Dieser Beitrag greift das Validierungsbeispiel auf und erweitert es im Rahmen dieser Anforderung.
Ausgangspunkt
Den Ausgangspunkt dieses Beitrags bietet dieses Beispiel, zu dem Florian einen sinnvollen Erweiterungswunsch hinterlassen hat:
Interessant wäre noch ein Save-Button, der nur aktiv ist, wenn es keine Fehler auf dem gesamten Formular mehr gibt, sowie ein Cancel- bzw. Undo-Button der bei eventuellen Fehlern, die Werte zurücksetzt auf den ursprünglichen Wert.
Aus meiner Sicht handelt es sich hierbei um eine gängige Anforderung, die ich natürlich aufgegriffen habe. Wer den Hintergrund des Beispiels erfahren möchte, kann dies hier vollständig nachlesen. Als Zusammenfassung: Das Beispiel zeigt, wie ein eigener Extender für Observables unter Knockout.js geschrieben werden kann, um einzelne Observables zu validieren. Als Validierung wurde eine Erweiterung hinsichtlich Pflichtfelder implementiert. Eine Gesamtvalidierung war nicht vorgesehen.
Gesamtvalidierung
Aktuell wird jedes Observable einzeln validiert. Entsprechend der Anforderung sollen nun zwei Schaltflächen für das Speichern, als auch das Zurückstellen auf die zuletzt gültigen Werte (im Fehlerfalle) hinzugekommen. Damit diese auch entsprechend freigeschalten sind, muss das ViewModel bekannt geben, in welchem Status es sich gesamt befindet. Dieser ergibt sich aus den Status jedes einzelnen Observables. Dies können wir uns berechnen lassen (ko.computed) und nennen wir hasErrors.
self.hasErrors = ko.computed(function()
{
return self.title.hasError() ||
self.author.hasError() ||
self.pages.hasError();
});
Hinweis: self ist eine Referenz auf this und wurde im Vergleich zum originalen Beispiel hinzugefügt, um innerhalb der anonymen Funktion auf die Eigenschaften und Funktionen des ViewModels zugreifen zu können.
Damit könnten nun auch bereits die beiden neuen Schaltflächen gesteuert werden:
<input type="button" value="Save" data-bind="disable: hasErrors"></input>
<input type="button" value="Undo" data-bind="enable: hasErrors"></input>
Auf die Speichern-Schaltfläche möchte ich an dieser Stelle nicht weiter eingehen, viel spannender ist hier der Undo-Button. Dieser soll ja – im Fehlerfalle – die zuletzt gültigen Werte hinterlegen. Dies bedeutet, dass diese erfasst werden müssen.
Nun ist es so, dass die einzelnen Eingabefelder eine value-Bindung besitzen und das Durchschreiben der Werte via valueUpdate auf den Wert afterkeydown gesetzt wurde. Das ViewModel wird also in Echtzeit aktualisiert. Darauf können wir also nicht aufbauen, da der Benutzer beispielsweise per Backspace alle Zeichen von “Beispiel” löschen könnte, durch die Echtzeit-Aktualisierung würde der zuletzt valide Wert durch “B” repräsentiert. Worauf wir uns jedoch hängen können ist das Verlassen des Fokus, also dem OnBlur-Ereignis.
Dazu brauchen wir eine Funktion, die uns den beim Verlassen vorhandenen – gültigen – Wert übernimmt:
self.updateLastValidValue = function(observable) {
if (!observable.hasError()) {
observable.lastValidValue(observable());
}
};
Als Parameter wird das an das Eingabefeld gebundene Observable übergeben. So kann auch auf die durch den Extender geschriebenen Eigenschaften (siehe hasError) zugegriffen werden. Läuft die Validierung ohne Fehler, dann ist ein entsprechender Wert vorhanden und kann übernommen werden. Hierzu wird eine neue Eigenschaft namens lastValidValue mit dem Wert gesetzt.
Damit dies nun bei allen Eingabefeldern berücksichtigt wird, ist deren Bindung zu aktualisieren, hier an Hand der Titel-Eingabe:
<input data-bind='value: title, valueUpdate: "afterkeydown", event: { blur: function() { updateLastValidValue(title); }}' />
So werden die Werte nun weiterhin in Echtzeit übernommen und zusätzlich beim Verlassen des Feldes die Funktion updateLastValidValue des ViewModels aufgerufen. Das ist allerdings noch nicht alles, denn ein etwaiger initial gesetzter Wert würde nicht berücksichtig werden. An dieser Stelle bietet es sich an, den bereits existierenden Extender (requireField) zu erweitern:
ko.extenders.requiredField = function(target, message) {
target.hasError = ko.observable();
target.validationMessage = ko.observable();
target.lastValidValue = ko.observable();
function validate(newValue) {
target.hasError(newValue ? false : true);
target.validationMessage(newValue ? "" : message || "* required");
}
function setInitialValueIfValid() {
if (!target.hasError()) {
target.lastValidValue(target());
}
}
validate(target());
setInitialValueIfValid();
target.subscribe(validate);
return target;
};
Neu ist die Funktion setInitialValueIfValid(). Diese wird beim Erstellen von requiredField durchlaufen (siehe den Aufruf direkt nach der initialen Validierung) und, wenn ein Wert vorhanden ist, lastValidValue mit dem entsprechenden Wert beschrieben.
Zu guter Letzt fehlt noch das tatsächliche Zurückschreiben der Werte, wenn die Undo-Schaltfläche geklickt wird. Dazu benötigen wir eine Funktion, nennen wir sie resetToValidValues:
self.resetToValidValues = function() {
self.title(self.title.lastValidValue());
self.author(self.author.lastValidValue());
self.pages(self.pages.lastValidValue());
};
Außerdem muss die click-Bindung auf der Schaltfläche gesetzt werden:
<input type="button" value="Undo" data-bind="enable: hasErrors, click: resetToValidValues"></input>
Fertig ist die Gesamtvalidierung des Formulars, inklusive der Möglichkeit, die zuletzt gültigen Werte zurück zu schreiben.
Ergebnis
An dieser Stelle nun zwei Screenshots, die die Implementierung visuell veranschaulichen. Der erste Screenshot zeigt das gesamte Formular ohne Validierungsfehler:
Und hier nun im Fehlerfalle:
Download / Showcase
Wie immer gibt es auch dieses Beispiel vollständig für eigene Tests. Unter jsfiddle.net/VgvuK/ findet sich die entsprechende Spielwiese. Wer möchte, kann sich dies auch unterhalb ansehen, sollte hierbei allerdings auf die Nutzung eines IE verzichten:
Fazit
Die notwendigen Erweiterungen der “Feld-Validierung” konnte in meinen Augen mit sehr wenig Aufwand auf eine Gesamtvalidierung erweitert werden, wodurch für den Benutzer sehr schnell ersichtlich ist, welche Aktionen zur Verfügung stellen. Ein äußerst sauberer Weg, durch die mögliche Auftrennung.
Feedback
Gerne nehme ich – wie immer – konstruktive Kritik, Anregungen und Anmerkungen entgegen. Einfach einen kurzen Kommentar hinterlassen, es findet sich sicherlich die eine oder andere Verbesserungsmöglichkeit.
Jetzt Knockout.js lernen: Die Serie
- Jetzt Knockout.js lernen: Slides und Beispiele
- Jetzt Knockout.js lernen: Formulare und Listen binden
- Jetzt Knockout.js lernen: Template verwenden
- Jetzt Knockout.js lernen: Benutzerdefinierte Bindungen
- Jetzt Knockout.js lernen: Observables erweitern
- Jetzt Knockout.js lernen: Formularvalidierung mit Undo (dieser Teil)
- Jetzt Knockout.js lernen: Mapping