Autosave Form
Debounced auto-save with a status indicator. The isDirty flag is derived from a snapshot of the last saved state, so the UI can never drift out of sync with reality.
Live Demo
Draft Editor
Source
HTML + JavaScript
<div data-component="autosave-form">
<span data-bind="saveStatusText"
data-bind-class="saveStatusClass"></span>
<input data-model="title" data-model-trim
placeholder="Untitled Document">
<textarea data-model="content"></textarea>
<input data-model="tags" data-model-trim>
</div>
<script>
wildflower.component('autosave-form', {
state: {
title: '',
content: '',
tags: '',
saving: false,
lastSaved: null,
// Snapshot of the last successfully saved state.
// `isDirty` compares current state against this.
snapshot: { title: '', content: '', tags: '' }
},
computed: {
isDirty() {
return this.title !== this.snapshot.title
|| this.content !== this.snapshot.content
|| this.tags !== this.snapshot.tags;
},
saveStatusText() {
if (this.saving) return 'Saving...';
if (this.isDirty) return 'Unsaved changes';
return 'Saved';
},
saveStatusClass() {
if (this.saving) return 'save-indicator save-saving';
if (this.isDirty) return 'save-indicator save-dirty';
return 'save-indicator save-saved';
},
lastSavedText() {
return this.lastSaved
? 'Last saved: ' + this.lastSaved
: 'Not yet saved';
}
},
// onUpdate fires after any state change. data-model updates
// state synchronously on every keystroke, so this runs on every
// edit. We coalesce rapid calls into one save via setTimeout.
onUpdate() {
if (!this.isDirty || this.saving) return;
clearTimeout(this._saveTimer);
this._saveTimer = setTimeout(() => {
this.saving = true;
setTimeout(() => {
// "Save" completed: snapshot current state so isDirty
// flips back to false until the user types again.
this.snapshot = {
title: this.title,
content: this.content,
tags: this.tags
};
this.saving = false;
this.lastSaved = new Date().toLocaleTimeString();
}, 500);
}, 800);
},
destroy() {
clearTimeout(this._saveTimer);
}
});
</script>
Key Points
data-modelupdates state synchronously on every keystroke, so the "Unsaved changes" label appears instantly (theisDirtycomputed recomputes as soon as state changes)onUpdate()is a component lifecycle hook that fires after any state change; we debounce the save inside it withsetTimeoutso rapid typing coalesces into one save 800ms after the user stopsisDirtyis derived from a snapshot of the last saved state, not a manual flag you set, so it can never drift out of sync with reality- Snapshotting on save completion is the whole mechanism: once
snapshotmatches current state,isDirtyflips back to false automatically destroy()clears the save timer so a pending save doesn't fire against a removed componentplaceholder="Untitled Document"on the title field disappears on first keystroke; state stays clean (empty string) instead of carrying boilerplate text the user has to delete