Form Guardian for Angular
Complete guide to using Form Guardian with Angular applications (Reactive Forms and Template-driven Forms).
🚀 Quick Start
Installation
npm install @form-guardian/dom
Template-Driven Forms
import { Component, ViewChild, ElementRef, OnInit, OnDestroy } from '@angular/core';
import { attachFormAutosave, FormAutosaveHandle } from '@form-guardian/dom';
@Component({
selector: 'app-contact-form',
template: `
<form #contactForm (ngSubmit)="onSubmit()">
<input name="name" [(ngModel)]="form.name" placeholder="Name" />
<input name="email" [(ngModel)]="form.email" type="email" placeholder="Email" />
<textarea name="message" [(ngModel)]="form.message" placeholder="Message"></textarea>
<button type="submit">Submit</button>
</form>
`
})
export class ContactFormComponent implements OnInit, OnDestroy {
@ViewChild('contactForm') formElement!: ElementRef<HTMLFormElement>;
private autosave?: FormAutosaveHandle;
form = {
name: '',
email: '',
message: ''
};
ngOnInit() {
// Wait for view to initialize
setTimeout(() => {
this.autosave = attachFormAutosave({
formId: 'contact-form',
root: this.formElement.nativeElement,
autoRestore: true,
debounceMs: 500,
});
});
}
ngOnDestroy() {
this.autosave?.destroy();
}
async onSubmit() {
await this.autosave?.clear();
console.log('Submitting:', this.form);
// Submit logic...
}
}
Reactive Forms
import { Component, ViewChild, ElementRef, OnInit, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { attachFormAutosave, FormAutosaveHandle } from '@form-guardian/dom';
@Component({
selector: 'app-registration-form',
template: `
<form #formElement [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
<div *ngIf="hasDraft" class="draft-alert">
Draft saved at {{ draftTime | date:'short' }}
</div>
<div>
<input formControlName="name" placeholder="Name" />
<div *ngIf="registrationForm.get('name')?.errors?.['required']">
Name is required
</div>
</div>
<div>
<input formControlName="email" type="email" placeholder="Email" />
<div *ngIf="registrationForm.get('email')?.errors?.['email']">
Invalid email
</div>
</div>
<div>
<textarea formControlName="message" placeholder="Message"></textarea>
</div>
<button type="submit" [disabled]="registrationForm.invalid">Submit</button>
</form>
`
})
export class RegistrationFormComponent implements OnInit, OnDestroy {
@ViewChild('formElement') formElement!: ElementRef<HTMLFormElement>;
private autosave?: FormAutosaveHandle;
registrationForm: FormGroup;
hasDraft = false;
draftTime: Date | null = null;
constructor(private fb: FormBuilder) {
this.registrationForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
message: ['']
});
}
ngOnInit() {
setTimeout(async () => {
this.autosave = attachFormAutosave({
formId: 'registration-form',
root: this.formElement.nativeElement,
autoRestore: false, // Manual restore for reactive forms
onAfterSave: () => {
this.hasDraft = true;
this.draftTime = new Date();
}
});
// Check and restore draft
const hasDraft = await this.autosave.hasDraft();
if (hasDraft) {
await this.autosave.restore();
this.hasDraft = true;
const meta = await this.autosave.getDraftMeta();
this.draftTime = new Date(meta.updatedAt);
}
// Auto-save on value changes
this.registrationForm.valueChanges.subscribe(() => {
this.autosave?.saveValues();
});
});
}
ngOnDestroy() {
this.autosave?.destroy();
}
async onSubmit() {
if (this.registrationForm.valid) {
await this.autosave?.clear();
this.hasDraft = false;
console.log('Submitting:', this.registrationForm.value);
// Submit logic...
}
}
}
📋 Complete Service Example
Autosave Service
// autosave.service.ts
import { Injectable } from '@angular/core';
import { attachFormAutosave, FormAutosaveHandle } from '@form-guardian/dom';
export interface AutosaveOptions {
formId: string;
root: HTMLFormElement;
autoRestore?: boolean;
debounceMs?: number;
onAfterSave?: () => void;
onAfterRestore?: () => void;
}
@Injectable({
providedIn: 'root'
})
export class AutosaveService {
private handles = new Map<string, FormAutosaveHandle>();
attach(options: AutosaveOptions): FormAutosaveHandle {
const handle = attachFormAutosave(options);
this.handles.set(options.formId, handle);
return handle;
}
get(formId: string): FormAutosaveHandle | undefined {
return this.handles.get(formId);
}
async clear(formId: string): Promise<void> {
const handle = this.handles.get(formId);
if (handle) {
await handle.clear();
handle.destroy();
this.handles.delete(formId);
}
}
destroy(formId: string): void {
const handle = this.handles.get(formId);
if (handle) {
handle.destroy();
this.handles.delete(formId);
}
}
destroyAll(): void {
this.handles.forEach(handle => handle.destroy());
this.handles.clear();
}
}
Usage:
import { Component, ViewChild, ElementRef, OnInit, OnDestroy } from '@angular/core';
import { AutosaveService } from './autosave.service';
@Component({
selector: 'app-my-form',
template: `<form #formElement><!-- fields --></form>`
})
export class MyFormComponent implements OnInit, OnDestroy {
@ViewChild('formElement') formElement!: ElementRef<HTMLFormElement>;
private readonly formId = 'my-form';
constructor(private autosaveService: AutosaveService) {}
ngOnInit() {
setTimeout(() => {
this.autosaveService.attach({
formId: this.formId,
root: this.formElement.nativeElement,
autoRestore: true,
onAfterSave: () => console.log('Draft saved'),
});
});
}
ngOnDestroy() {
this.autosaveService.destroy(this.formId);
}
async onSubmit() {
await this.autosaveService.clear(this.formId);
// Submit...
}
}
🔧 Advanced Patterns
With Draft Status Observable
import { Component, OnInit, OnDestroy } from '@angular/core';
import { BehaviorSubject, interval, Subscription } from 'rxjs';
import { attachFormAutosave, FormAutosaveHandle } from '@form-guardian/dom';
@Component({
selector: 'app-form-with-status',
template: `
<div *ngIf="draftStatus$ | async as status" class="draft-status">
<span *ngIf="status.hasDraft">
📝 Draft saved {{ status.timeAgo }}
</span>
</div>
<form #formElement><!-- fields --></form>
`
})
export class FormWithStatusComponent implements OnInit, OnDestroy {
@ViewChild('formElement') formElement!: ElementRef<HTMLFormElement>;
private autosave?: FormAutosaveHandle;
private subscription?: Subscription;
draftStatus$ = new BehaviorSubject<{
hasDraft: boolean;
timestamp: number | null;
timeAgo: string;
}>({
hasDraft: false,
timestamp: null,
timeAgo: ''
});
ngOnInit() {
setTimeout(async () => {
this.autosave = attachFormAutosave({
formId: 'my-form',
root: this.formElement.nativeElement,
autoRestore: true,
onAfterSave: async () => {
await this.updateDraftStatus();
}
});
await this.updateDraftStatus();
// Update time ago every 30 seconds
this.subscription = interval(30000).subscribe(() => {
this.updateDraftStatus();
});
});
}
ngOnDestroy() {
this.autosave?.destroy();
this.subscription?.unsubscribe();
}
private async updateDraftStatus() {
const hasDraft = await this.autosave?.hasDraft();
if (hasDraft) {
const meta = await this.autosave?.getDraftMeta();
this.draftStatus$.next({
hasDraft: true,
timestamp: meta.updatedAt,
timeAgo: this.getTimeAgo(meta.updatedAt)
});
} else {
this.draftStatus$.next({
hasDraft: false,
timestamp: null,
timeAgo: ''
});
}
}
private getTimeAgo(timestamp: number): string {
const seconds = Math.floor((Date.now() - timestamp) / 1000);
if (seconds < 60) return 'just now';
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
return `${Math.floor(hours / 24)}d ago`;
}
}
Multi-Step Wizard
// wizard.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { saveDraftCore, loadDraftCore, clearDraft } from '@form-guardian/core';
export interface WizardStep {
id: string;
data: any;
}
@Injectable({
providedIn: 'root'
})
export class WizardService {
private currentStep$ = new BehaviorSubject<number>(0);
private steps: WizardStep[] = [];
async saveStep(stepId: string, data: any) {
await saveDraftCore(stepId, data);
}
async loadStep(stepId: string): Promise<any> {
const draft = await loadDraftCore(stepId);
return draft?.values;
}
async clearAllSteps() {
for (const step of this.steps) {
await clearDraft(step.id);
}
}
registerStep(stepId: string) {
this.steps.push({ id: stepId, data: null });
}
nextStep() {
this.currentStep$.next(this.currentStep$.value + 1);
}
prevStep() {
this.currentStep$.next(this.currentStep$.value - 1);
}
getCurrentStep() {
return this.currentStep$.asObservable();
}
}
💡 Best Practices
✅ DO
- Use
setTimeout()inngOnInitto ensure view is ready - Always call
destroy()inngOnDestroy - Use services for shared autosave logic
- For Reactive Forms, use
autoRestore: falseand manual restore - Subscribe to
valueChangesfor auto-saving Reactive Forms
❌ DON'T
- Don't attach autosave before view initialization
- Don't forget to unsubscribe from observables
- Don't save password fields (already excluded by default)
- Don't use
autoRestore: truewith Reactive Forms