Skip to main content

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() in ngOnInit to ensure view is ready
  • Always call destroy() in ngOnDestroy
  • Use services for shared autosave logic
  • For Reactive Forms, use autoRestore: false and manual restore
  • Subscribe to valueChanges for 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: true with Reactive Forms