By Diego Liascovich
Full-Stack Developer | Angular | Clean Architecture | WebDev
π Overview
Reactive Forms in Angular are powerful β but building large or dynamic forms manually in every component can get messy.
In this tutorial, weβll create a generic Angular service that:
- Dynamically generates a
FormGroup
from a JSON file. - Maps string-based validators to Angularβs built-in validators.
- Exposes helper methods for managing and observing the form.
Useful for multi-step forms, admin dashboards, dynamic form builders, or simply keeping your components cleaner.
π§± JSON Configuration Example
{
"name": {
"value": "",
"validators": ["required"]
},
"email": {
"value": "",
"validators": ["required", "email"]
},
"age": {
"value": 0,
"validators": []
}
}
Save this file as:
π src/assets/form-definitions/form1.json
βοΈ The Generic Form Service
// generic-form.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup, Validators, AbstractControl } from '@angular/forms';
import { BehaviorSubject, Observable, map } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class GenericFormService {
private form!: FormGroup;
private formValueSubject = new BehaviorSubject<any>({});
constructor(private fb: FormBuilder, private http: HttpClient) {}
loadFormConfig(path: string): Observable<FormGroup> {
return this.http.get<{ [key: string]: any }>(path).pipe(
map(config => {
const group: { [key: string]: any } = {};
for (const key in config) {
const field = config[key];
const value = field.value;
const validators = this.mapValidators(field.validators || []);
group[key] = [value, validators];
}
this.form = this.fb.group(group);
this.listenToChanges();
return this.form;
})
);
}
private mapValidators(validatorNames: string[]): any[] {
const validatorMap: { [key: string]: any } = {
required: Validators.required,
email: Validators.email,
min: Validators.min(0),
max: Validators.max(100),
};
return validatorNames.map(name => validatorMap[name]).filter(Boolean);
}
getForm(): FormGroup {
return this.form;
}
getControl(name: string): AbstractControl {
return this.form.get(name)!;
}
getValues(): any {
return this.form.value;
}
patch(values: any): void {
this.form.patchValue(values);
}
get valueChanges$(): Observable<any> {
return this.formValueSubject.asObservable();
}
private listenToChanges(): void {
this.form.valueChanges.subscribe(value => {
this.formValueSubject.next(value);
});
}
}
π§ͺ Using the Service in a Component
@Component({
selector: 'app-dynamic-form',
templateUrl: './dynamic-form.component.html'
})
export class DynamicFormComponent implements OnInit {
form!: FormGroup;
constructor(private formService: GenericFormService) {}
ngOnInit(): void {
this.formService
.loadFormConfig('assets/form-definitions/form1.json')
.subscribe(form => {
this.form = form;
this.formService.valueChanges$.subscribe(values => {
console.log('Live form values:', values);
});
});
}
submit() {
if (this.form.valid) {
console.log('Submitted:', this.formService.getValues());
}
}
}
β Benefits
- Centralizes form logic and configuration
- Enables loading forms dynamically at runtime
- Keeps components clean and testable
- Supports reactive state via
BehaviorSubject
π¬ Conclusion
This approach is especially useful when:
- Your forms are complex or deeply nested
- You want to build a dynamic form builder tool
- You maintain multiple form versions/configs
Next steps:
- Add support for
FormArray
- Extend validator parser to support custom rules or parameters
- Load form structure from API
π If you found this helpful, follow me for more Angular and full-stack development content!
Top comments (0)