So, I have the issue that I have a base component that has a form. But I would like to be able to add another field to it. Why? Because I have four pages, with a very similar structure, that differ only in one field at most. I've got a stackblitz that should get the idea across as well. The rendering part is working nice and well.
But to also give a general idea:
@Component({
selector: 'app-base-form',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `<form
style="display: flex; flex-direction: column; gap: 1rem"
[formGroup]="form"
>
<input formControlName="search" type="text" placeholder="Search something" />
<ng-template
[ngTemplateOutletContext]="{ $implicit: form }"
[ngTemplateOutlet]="seachOptionalField()!"
></ng-template>
</form>`,
})
export class BaseFormComponent {
seachOptionalField = input<TemplateRef<any>>();
fb = inject(UntypedFormBuilder);
form = this.fb.group({
search: this.fb.control(['']),
});
}
The idea was initially to pass the form via the outlet context, but programmatically adding a control didn't really impact the original form. Also, there was the question on how to apply the rendered (to be used) form control.
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, BaseFormComponent],
template: `
<app-base-form [seachOptionalField]="searchOptionalField"></app-base-form>
<ng-template #searchOptionalField let-form>
<input type="text" placeholder="We just added this" />
{{form.value | json}}
</ng-template>
`,
})
export class App {}
You need to provide a corresponding formControlName
on the new controls, also wrap them on a form element inside the template.
You can also add an extra parameter, which specifies the new form controls configuration on a object which we can destructure into our form group when initializing.
Working example below with stackblitz!
Full Code:
main.ts
import { Component, inject } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import 'zone.js';
import { BaseFormComponent } from './app/base-form/base-form.component';
import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, BaseFormComponent, ReactiveFormsModule],
template: `
<app-base-form [seachOptionalField]="searchOptionalField" [extraFormParams]="extraFormParams"></app-base-form>
<ng-template #searchOptionalField let-form>
<form [formGroup]="form">
<input type="text" placeholder="We just added this" formControlName="search2"/>
</form>
{{form.value | json}}
</ng-template>
`,
})
export class App {
fb = inject(UntypedFormBuilder);
extraFormParams = {
search2: this.fb.control(['']),
};
}
bootstrapApplication(App);
base-form.ts
import { CommonModule } from '@angular/common';
import { Component, TemplateRef, inject, input } from '@angular/core';
import {
FormGroup,
ReactiveFormsModule,
UntypedFormBuilder,
} from '@angular/forms';
@Component({
selector: 'app-base-form',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `<form
style="display: flex; flex-direction: column; gap: 1rem"
[formGroup]="form"
>
<input formControlName="search" type="text" placeholder="Search something" />
<ng-template
[ngTemplateOutletContext]="{ $implicit: form }"
[ngTemplateOutlet]="seachOptionalField()!"
></ng-template>
</form>`,
})
export class BaseFormComponent {
seachOptionalField = input<TemplateRef<any>>();
extraFormParams = input<Object>({});
fb = inject(UntypedFormBuilder);
form!: FormGroup;
ngOnInit() {
this.form = this.fb.group({
search: this.fb.control(['']),
...this.extraFormParams(),
});
}
}