Real-world template-driven forms require patterns beyond basic single-field validation: editing pre-populated data, handling server errors on form fields, grouping related fields with ngModelGroup, and resetting forms cleanly. These patterns make the difference between a form that works and a form that feels polished and reliable. The edit pattern — loading data, populating the form, submitting changes, and handling server validation errors — is the most common full form lifecycle in the BlogApp.
Form with Pre-Population and Reset
@Component({
standalone: true,
imports: [FormsModule],
template: `
<form #editForm='ngForm' (ngSubmit)="onSubmit(editForm)" novalidate>
<!-- ngModelGroup — groups related fields ─────────────────────────── -->
<fieldset ngModelGroup="author">
<legend>Author Details</legend>
<input name="displayName" [(ngModel)]="post.author.displayName" required>
<input name="bio" [(ngModel)]="post.author.bio">
</fieldset>
<!-- Main post fields ────────────────────────────────────────────── -->
<input name="title"
[(ngModel)]="post.title"
required
minlength="5"
#titleControl="ngModel">
@if (serverFieldErrors()['title']) {
<span class="error">{{ serverFieldErrors()['title'][0] }}</span>
}
<select name="status" [(ngModel)]="post.status" required>
@for (opt of statusOptions; track opt.value) {
<option [value]="opt.value">{{ opt.label }}</option>
}
</select>
<div class="actions">
<button type="submit" [disabled]="isSaving()">
{{ isSaving() ? 'Saving...' : 'Save Changes' }}
</button>
<button type="button" (click)="onReset(editForm)">Reset</button>
</div>
</form>
`,
})
export class PostEditComponent implements OnInit {
@Input({ required: true }) postId!: number;
private api = inject(PostsApiService);
private router = inject(Router);
// The model bound to the form — populated from API
post = { title: '', status: 'draft', author: { displayName: '', bio: '' } };
originalPost = { ...this.post }; // save original for reset
isSaving = signal(false);
serverFieldErrors = signal<Record<string, string[]>>({});
serverError = signal('');
statusOptions = [
{ value: 'draft', label: 'Draft' },
{ value: 'review', label: 'In Review' },
{ value: 'published', label: 'Published' },
];
ngOnInit() {
this.api.getById(this.postId).subscribe(post => {
// Populate the model — ngModel two-way binding updates the template
this.post = {
title: post.title,
status: post.status,
author: { displayName: post.author.displayName, bio: post.author.bio ?? '' },
};
this.originalPost = { ...this.post };
});
}
onSubmit(form: NgForm): void {
if (form.invalid) { form.form.markAllAsTouched(); return; }
this.isSaving.set(true);
this.serverFieldErrors.set({});
this.api.update(this.postId, this.post).subscribe({
next: () => this.router.navigate(['/admin/posts']),
error: (err: HttpErrorResponse) => {
this.isSaving.set(false);
if (err.status === 400 && err.error?.errors) {
this.serverFieldErrors.set(err.error.errors);
} else {
this.serverError.set('Failed to save. Please try again.');
}
},
});
}
onReset(form: NgForm): void {
// Restore original values and reset validation state
this.post = { ...this.originalPost };
form.resetForm(this.post); // resets form state (pristine, untouched) with values
this.serverFieldErrors.set({});
}
}
form.resetForm(values) both resets the form’s validation state (marks all controls as pristine and untouched, clearing error display) and sets new values. form.reset() (without values) clears all values to undefined and resets state. ngForm.setValue({}) sets values without resetting state. For an edit form’s “Cancel” or “Reset” button, use form.resetForm(originalValues) — it restores the saved data and clears any validation errors, giving the user a clean slate.ngModelGroup directive groups related form controls into a nested object in the form’s value. A <fieldset ngModelGroup="author"> with name="displayName" and name="bio" inside produces form.value.author.displayName and form.value.author.bio. The group also has its own validity state — editForm.controls['author'].valid. This mirrors the nested structure of the API request DTO, making form value extraction clean and type-consistent.this.post = { ...apiData } rather than this.post.title = apiData.title; this.post.status = apiData.status;. The spread creates a new object reference which Angular’s change detection reliably detects. Also be careful to populate all bound properties — a missing property means the bound input has an undefined value, which some validators treat as invalid.Common Mistakes
Mistake 1 — Using form.reset() instead of form.resetForm() (clears values to undefined)
❌ Wrong — form.reset() clears all values to null/undefined; edit form shows empty fields after reset.
✅ Correct — form.resetForm(originalValues) restores original data and clears validation state.
Mistake 2 — Not handling server field errors after submit (API validation errors invisible)
❌ Wrong — catching 400 errors with a generic message; specific field errors from the API not shown.
✅ Correct — parse err.error.errors and display per-field in the template using serverFieldErrors()[fieldName].