AutoSave für AJAX-Formulare Timo Holzherr AJAX in ACTION 2007, 06.11.2007, Frankfurt
Web Application Developer bei der Nero AG / Karlsbad Timo Holzherr Web Application Developer bei der Nero AG / Karlsbad Web-Community My Nero (PHP5, AJAX) Online Services für verschiedene Nero-Applikationen Lehrauftrag bei der Berufsakademie Stuttgart Vorlesung Software-Engineering 2 Betreuung von Studienarbeiten, Thema Compiler-Entwicklung
Agenda Was ist AutoSave? Praktische Anwendungen Formulareingaben überwachen Zeitintervalle steuern, Eingaben zählen AJAX-Request absenden Formular-Validierung
Was ist AutoSave? Auf AJAX basierendes Feature Für HTML-Formulare Automatisches Speichern der Benutzer-Eingaben Eingaben überwachen Änderungen regelmäßig zum Server übertragen Recovery-Feature Browser-Absturz Versehentliches Schließen des Browserfensters
AutoSave – Praktische Anwendungen Vor AJAX nur in Desktop-Applikationen Microsoft Office … Heute bei vielen Online-Services My Nero (http://my.nero.com/) Google Mail (http://mail.google.com/)
AutoSave bei My Nero
Regelmäßiges Speichern im Hintergrund AutoSave bei My Nero Blog-Editor Regelmäßiges Speichern im Hintergrund
Recovery-Feature auf dem Login-Screen AutoSave bei My Nero Recovery-Feature auf dem Login-Screen
Autosave bei Google Mail
AutoSave bei Google Mail Während der Eingabe der E-Mail: Regelmäßiges Speichern der Eingaben
AutoSave bei Google Mail Der Benutzer findet die automatisch gespeicherte E-Mail im Ordner Entwürfe
Entwicklung eines AutoSave-Frameworks Trennung der Aufgaben Formulareingaben überwachen Zeitintervalle steuern Formulardaten absenden Verschiedene Klassen FormObservable.js AutoSave.js Note.js Vorteil der Trennung Komponenten können einzeln verwendet werden Bsp.: Live-Validierung von Formularen durch FormObservable
Formulareingaben überwachen Eingaben des Benutzers müssen überwacht werden Registrieren für Events aller Form-Elemente onkeyup (Nach einem Tastendruck) onmouseup (Nach einem Mausklick) onblur (Verlassen eines Formularfeldes) onchange (Ändern einer Check- oder Dropdown-Box) Beim Auftreten dieser Events Prüfen, ob sich die Formularwerte geändert haben Mithilfe des Observer-Patterns Änderungen signalisieren
Klasse FormObservable Speichert die Liste der Observer Initialisiert die Klasse an einer Form-Node Sendet ein Event an alle Observer Fügt einen Observer hinzu
Klasse FormObservable var observable = new FormObservable( document.forms[0] ); observable.addObserver( { onupdate: function() { alert(“Form element has been updated.“); } } );
Klasse FormObservable: Initialisierung FormObservable = function( formNode ) { var events = [ 'keyup', 'mouseup', 'blur', 'change' ]; var call = this._onFieldChange.bind( this ); for( var i=0; i < formNode.elements.length; i++ ) { var elem = formNode.elements.item( i ); for( var j=0; j < events.length; j++ ) { elem.addEventListener( events[j], call, true ); } Der Konstruktor erwartet ein HTML-Form-Objekt als Parameter
Klasse FormObservable: Initialisierung FormObservable = function( formNode ) { var events = [ 'keyup', 'mouseup', 'blur', 'change' ]; var call = this._onFieldChange.bind( this ); for( var i=0; i < formNode.elements.length; i++ ) { var elem = formNode.elements.item( i ); for( var j=0; j < events.length; j++ ) { elem.addEventListener( events[j], call, true ); } Events, die überwacht werden sollen
Klasse FormObservable: Initialisierung FormObservable = function( formNode ) { var events = [ 'keyup', 'mouseup', 'blur', 'change' ]; var call = this._onFieldChange.bind( this ); for( var i=0; i < formNode.elements.length; i++ ) { var elem = formNode.elements.item( i ); for( var j=0; j < events.length; j++ ) { elem.addEventListener( events[j], call, true ); } Methode, die später die Events verarbeitet
Klasse FormObservable: Initialisierung FormObservable = function( formNode ) { var events = [ 'keyup', 'mouseup', 'blur', 'change' ]; var call = this._onFieldChange.bind( this ); for( var i=0; i < formNode.elements.length; i++ ) { var elem = formNode.elements.item( i ); for( var j=0; j < events.length; j++ ) { elem.addEventListener( events[j], call, true ); } bind: Methode aus der Bibliothek Prototype [1] Setzt den Kontext der Methode auf die Instanz this JS-Event-Handling: Ansonsten Verlust des Kontextes
Klasse FormObservable: Initialisierung FormObservable = function( formNode ) { var events = [ 'keyup', 'mouseup', 'blur', 'change' ]; var call = this._onFieldChange.bind( this ); for( var i=0; i < formNode.elements.length; i++ ) { var elem = formNode.elements.item( i ); for( var j=0; j < events.length; j++ ) { elem.addEventListener( events[j], call, true ); } Jedem Formular-Element wird call als Event-Listener hinzugefügt
Klasse FormObservable: Initialisierung FormObservable = function( formNode ) { var events = [ 'keyup', 'mouseup', 'blur', 'change' ]; var call = this._onFieldChange.bind( this ); for( var i=0; i < formNode.elements.length; i++ ) { var elem = formNode.elements.item( i ); for( var j=0; j < events.length; j++ ) { elem.addEventListener( events[j], call, true ); } Hier: Event-Implementierung nur für Mozilla Firefox Empfohlen: Bibliothek für Browser-Abstraktion Bsp.: Prototype [1]
Klasse FormObservable: _onFieldChange FormObservable.prototype._onFieldChange = function( ) { … for( var i=0; i < formNode.elements.length; i++ ) { var elem = formNode.elements.item(i); var oldVal = this._getStoredValue( elem ); var newVal = elem.value; if( newVal != oldVal ) { this.notifyObservers( 'update' ); this._setStoredValue( elem, newVal ); } Wird vom JS-Event-Handling aufgerufen, wenn eines der überwachten Ereignisse eintritt keyup mouseup blur change
Klasse FormObservable: _onFieldChange FormObservable.prototype._onFieldChange = function( ) { … for( var i=0; i < formNode.elements.length; i++ ) { var elem = formNode.elements.item(i); var oldVal = this._getStoredValue( elem ); var newVal = elem.value; if( newVal != oldVal ) { this.notifyObservers( 'update' ); this._setStoredValue( elem, newVal ); } Dabei wird jedes Formularelement auf eine Änderung geprüft
Klasse FormObservable: _onFieldChange FormObservable.prototype._onFieldChange = function( ) { … for( var i=0; i < formNode.elements.length; i++ ) { var elem = formNode.elements.item(i); var oldVal = this._getStoredValue( elem ); var newVal = elem.value; if( newVal != oldVal ) { this.notifyObservers( 'update' ); this._setStoredValue( elem, newVal ); } FormObservable: Merkt sich lokal für jedes Element den letzten Wert
Klasse FormObservable: _onFieldChange FormObservable.prototype._onFieldChange = function( ) { … for( var i=0; i < formNode.elements.length; i++ ) { var elem = formNode.elements.item(i); var oldVal = this._getStoredValue( elem ); var newVal = elem.value; if( newVal != oldVal ) { this.notifyObservers( 'update' ); this._setStoredValue( elem, newVal ); } Observers über eine Änderung informieren Wertänderung lokal merken
Klasse FormObservable: Default Values Problem Formularelemente mit Default-Values Beim ersten Klick würde das System vermuten, dass sich Elemente mit Default-Values geändert haben Abhilfe Default-Values zu Beginn als Startwert merken
Klasse FormObservable: _loadDefaultValues FormObservable = function( formNode ) { … // Initialisierung der Event-Handler this._loadDefaultValues(); } Nach der bereits erwähnten Initialisierung: Laden der Default Values
Klasse FormObservable: _loadDefaultValues FormObservable = function( formNode ) { … // Initialisierung der Event-Handler this._loadDefaultValues(); } FormObservable.prototype._loadDefaultValues = function( ) { for( var i=0; i < this.form.elements.length; i++ ) { var elem = this.form.elements.item(i); this._setStoredValue( elem, elem.value ); } };
Beispiel Observable [2] var observable = new FormObservable( document.forms[0] ); observable.addObserver( { onupdate: function() { alert(“Form element has been updated.“); } } ); Beispiel Observable [2]
Observer-Notifications empfangen Empfangen der Observer-Notifications AJAX-Request muss an FormObservable gekoppelt werden Ansatz: var observable = new FormObservable( document.forms[0] ); observable.addObserver( { onupdate: function() { // AJAX Request senden } } );
Observer-Notifications empfangen Problem Bei jedem Tastaturanschlag Übertragung der Änderung Viele HTTP-Requests Überlastung des Web-Servers Lösung Zeitverzögertes Senden der Daten Einstellbares Timeout
Klasse AutoSave Hält eine Instanz der Klasse FormObservable vor Instanziert die Klasse an einer Form-Node, Timeout konfigurierbar Fügt einen Observer hinzu, delegiert FormObservable Setzt alle Timeouts zurück
Klasse AutoSave var as = new AutoSave( document.forms[0], 5 ); as.addObserver( { ontimeout: function(){ /* AJAX Request senden */ }, onupdate: function() { /* aktivieren */ } } );
Klasse AutoSave: Initialisierung AutoSave = function( form, saveTime ) { this._options = { form: form, saveTime: saveTime * 1000, // convert into milliseconds }; this._interval = null; this._observable = new FormObservable( form ); this._observable.addObserver( { onupdate: this._updateInterval.bind( this ), ontimeout: this.reset.bind( this ) } ); Die übergebenen Parameter werden gespeichert
Klasse AutoSave: Initialisierung AutoSave = function( form, saveTime ) { this._options = { form: form, saveTime: saveTime * 1000, // convert into milliseconds }; this._interval = null; this._observable = new FormObservable( form ); this._observable.addObserver( { onupdate: this._updateInterval.bind( this ), ontimeout: this.reset.bind( this ) } ); Klassenvariable _interval wird initialisiert. Referenziert Timeout, um es wieder zu stoppen var to = window.setTimeout( callback, zeit ); window.clearTimeout( to );
Klasse AutoSave: Initialisierung AutoSave = function( form, saveTime ) { this._options = { form: form, saveTime: saveTime * 1000, // convert into milliseconds }; this._interval = null; this._observable = new FormObservable( form ); this._observable.addObserver( { onupdate: this._updateInterval.bind( this ), ontimeout: this.reset.bind( this ) } ); FormObservable instanzieren und Observer hinzufügen
Klasse AutoSave: _updateInterval AutoSave.prototype._updateInterval = function( ) { this._resetInterval(); this._interval = window.setTimeout( this._timeExceeded.bind( this ), this._options.saveTime ); }; Wird bei einem Update-Event des FormObservable aufgerufen (Änderungen am Formular)
Klasse AutoSave: _updateInterval AutoSave.prototype._updateInterval = function( ) { this._resetInterval(); this._interval = window.setTimeout( this._timeExceeded.bind( this ), this._options.saveTime ); }; AutoSave.prototype._resetInterval = function() { if( this._interval ) { window.clearTimeout( this._interval ); this._interval = null; }
Klasse AutoSave: _updateInterval AutoSave.prototype._updateInterval = function( ) { this._resetInterval(); this._interval = window.setTimeout( this._timeExceeded.bind( this ), this._options.saveTime ); }; Timeout starten Aufgerufene Methode: AutoSave._timeExceeded() Setzt this._interval zurück Löst mit notifyObservers das Event „timeout“ aus
Beispiel AutoSave[2] var as = new AutoSave( document.forms[0] ); as.addObserver( { onupdate: function() { document.forms[0].draft.disabled = null; }, ontimeout: function() { alert(“Saving form data.“); document.forms[0].draft.disabled = 'disabled'; } } ); Beispiel AutoSave[2]
Durchgehendes Tippen Problem Ansatz Regelmäßiges Eingeben in ein Textfeld Das Formular wird nie automatisch abgeschickt Ansatz Eingaben zählen (Maus, Tastatur) Ab gewisser Anzahl an Änderungen das Speichern erzwingen
Klasse AutoSave: HitsCounter
Klasse AutoSave mit HitsCounter Neuer Parameter hitsLimit Spezifiziert die maximale Anzahl ungespeicherter Änderungen Observer-Events: update: Element hat sich geändert timeout: Timeout wurde erreicht hitslimit: Maximale Anz. an Änderungen erreicht
Konkrete Anwendung von AutoSave.js Verwendet AutoSave Observer-Events Klasse Note Konkrete Anwendung von AutoSave.js Verwendet AutoSave Observer-Events update: aktivieren timeout: Daten senden, deaktivieren hitslimit: Daten senden, deaktivieren
Speicherung der Daten (1) Implementierung des AJAX-Requests Abhängig vom eingesetzten AJAX-Framework Datenbank-Schema zum Ablegen der AutoSave-Daten Abhängig von der Anwendung Extra-Tabelle „autosaves“ Inmitten der Nutzdaten: flag: „draft“
Speicherung der Daten (2) Fall: Speicherung der Daten inmitten der Nutzdaten 1. AutoSave-Request würde einen neuen Eintrag anlegen INSERT INTO … Folge-Requests müssen diesen Datensatz aktualisieren UPDATE … WHERE ID = … Deshalb AutoSave-Request muss den PK zurückgeben Notizen id INT(10) PRIMARY KEY title VARCHAR(50) body TEXT lang VARCHAR(5) draft TINYINT(1)
Speicherung der Daten (2)
Beim Empfangen von Formularen Formularvalidierung Beim Empfangen von Formularen Formularvalidierung Sind alle Felder ausgefüllt Überschreiten die Felder nicht die Maximallänge Bei AutoSave-Requests Keine oder nur einfache Validierung! Sonst: Keine AutoSave der Eingaben obwohl evtl nur ein Feld fehlt
Demonstration Ergebnis Framework für AutoSaving Kann universell eingesetzt werden AJAX-Requests und Persistence müssen selbst implementiert werden
[2] Folien und Beispiele dieses Vortrags Verweise [1] Prototype http://www.prototypejs.org/ JS-Bibliothek für einfache DOM-Manipulation und AJAX-Requests [2] Folien und Beispiele dieses Vortrags http://timo.holzherr.de/aia2007