Angular - Create a base form component for using within both create and edit page

March 14, 2023

CURD operations implementation is a very familiar job in web development. In many cases, on the client side, we need to use a form template on both the create page and edit page of an object. Especially with a complicated form that we should put in a separate routing component, having a base form component is surely beneficial.

🤟 You can access the demo source code here:
link_preview

Overview

The idea is pretty simple: make a base-form component in which:

  • The component class is used to define and build the form
  • The template is reused in three components: BaseForm, CreatePage, and EditPage.

So, in creating and editing pages, we'll be able to extend BaseForm and manipulate data via API without needing any more templates. We can create more and customize them, though.

It is going to look like this:

Implementation

In create page

@Component({ selector: 'app-<feature>-create-page', templateUrl: '../<feature>-form-base/<feature>-form-base.component.html', styleUrls: ['../<feature>-form-base/<feature>-form-base.component.css'] }) export class <feature>CreatePageComponent extends <feature>FormBaseComponent { constructor(injector: Injector, private service: <feature>Service) { super(injector); } override onSubmit(value: IModel | null): void { super.onSubmit(value); // create item via api call } }

Detail page

@Component({ selector: 'app-<feature>-details-page', templateUrl: '../<feature>-form-base/<feature>-form-base.component.html', styleUrls: ['../<feature>-form-base/<feature>-form-base.component.css'] }) export class <feature>DetailsPageComponent extends <feature>FormBaseComponent { private origin: IModel | null = null; constructor( injector: Injector, private route: ActivatedRoute ) { super(injector); this.route.paramMap .pipe( map((x) => x.get('id') as string), switchMap((id) => /*get data by id*/), takeUntil(this.destroy$) ) .subscribe((item) => { if (!item) return; this.origin = item; this.form.patchValue(item) }); } override ngOnDestroy(): void { super.ngOnDestroy(); } override onSubmit(value: IModel | null): void { super.onSubmit(value); const newItem = { ...this.origin, ...value }; // edit through api and back to list } }

The base form component

@Component({ selector: 'app-<feature>-form-base', template: ` <div class="flex flex-col gap-6 max-w-2xl m-auto"> <h1 class="m-auto text-lg font-semibold text-center"><Feature></h1> <!--form template--> </div>`, styleUrls: ['./<feature>-form-base.component.css'] }) export class <Feature>FormBaseComponent { fBuilder: FormBuilder; router: Router; form: FormGroup; destroy$ = new ReplaySubject<void>(1); constructor(injector: Injector) { this.fBuilder = injector.get(FormBuilder); this.router = injector.get(Router); this.form = this.buildForm(); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } onSubmit(value: IModel | null) { if (!value) { this.router.navigate(['/list']); return; } } protected buildForm() { const newModel: PartialKeyOf<IModel, any> = { //form control's configs }; return this.fBuilder.group(newModel); } }