admin管理员组文章数量:1402985
I'm working on a dynamic form in Angular where I need to use ngComponentOutlet to dynamically load different components based on the field type. However, I'm struggling to bind formControlName to these dynamically loaded components.
Here's a summary of my setup:
this.cardStepForm = new FormGroup({
email: new FormControl(''),
// other form controls
});
HTML:
<div class="form-group" [formGroup]="cardStepForm">
<div *ngFor="let field of dynamicFields">
<ng-container *ngComponentOutlet="COMPONENT_MAP[field.type];
inputs: { label: field.label }"></ng-container>
</div>
</div>
The components being loaded are custom components that implement ControlValueAccessor for working with reactive forms.
My question:
How can I correctly bind formControlName to the dynamic components loaded via ngComponentOutlet? I don't want to pass formControlName directly as an @Input() to the child components, since the form control should be connected to the parent form's FormGroup and the dynamic component should automatically bind to the form control without manual intervention.
What is the best way to ensure that the dynamically loaded components have access to formControlName and can bind to the corresponding FormControl in the parent form?
I'm working on a dynamic form in Angular where I need to use ngComponentOutlet to dynamically load different components based on the field type. However, I'm struggling to bind formControlName to these dynamically loaded components.
Here's a summary of my setup:
this.cardStepForm = new FormGroup({
email: new FormControl(''),
// other form controls
});
HTML:
<div class="form-group" [formGroup]="cardStepForm">
<div *ngFor="let field of dynamicFields">
<ng-container *ngComponentOutlet="COMPONENT_MAP[field.type];
inputs: { label: field.label }"></ng-container>
</div>
</div>
The components being loaded are custom components that implement ControlValueAccessor for working with reactive forms.
My question:
How can I correctly bind formControlName to the dynamic components loaded via ngComponentOutlet? I don't want to pass formControlName directly as an @Input() to the child components, since the form control should be connected to the parent form's FormGroup and the dynamic component should automatically bind to the form control without manual intervention.
What is the best way to ensure that the dynamically loaded components have access to formControlName and can bind to the corresponding FormControl in the parent form?
Share Improve this question asked Mar 27 at 12:58 panagiotis lymperopoulospanagiotis lymperopoulos 211 silver badge2 bronze badges2 Answers
Reset to default 1Pass the form inside the template. If possible pass the form control name also.
<div class="form-group" [formGroup]="cardStepForm">
<div *ngFor="let field of dynamicFields">
<ng-container *ngComponentOutlet="COMPONENT_MAP[field.type];
inputs: { label: field.label, form: cardStepForm, formControlName: field.name }"></ng-container>
</div>
</div>
Then in the dynamically rendered component, use this @Input
form and wrap the control inside a form group.
TS:
@Component({ ... })
export class SomeDynamicComponent {
@Input form!: FormGroup<any>;
@Input formControlName!: string;
...
HTML:
<form [formGroup]="form">
<input [formControlName]="formControlName"/>
</form>
We can't set directives on the dynamically created component because NgComponentOutlet doesn't have an API for doing so.
see this issue which suggests adding directives to the dynamically created components using ViewContainerRef.createComponent.
By the way NgComponentOutlet is the declarative approach to create dynamic components and under the hood it uses ViewContainerRef.createComponent so it's still relevant.
We can use the directive composition API to attach standalone directives statically at compile time, however formControlName is not a standalone directive, so the following code:
@Component({
selector:'app-dynamic'
hostDirectives:[
{
directive:FormControlName,
inputs:['formControlName']
}
]
})
will raise a compilation error TS-992014: Host directive FormControlName must be standalone
So you need a wrapper (Option1, Option2):
Option1: Wrapper component with FormGroup and name inputs
As this answer suggests to make the name and the formGroup as inputs, but the dynamically created component should be a wrapper of your desired components because you want your components (which implement ControlValueAccessor) to be part of your form
some_wrapperponent.ts:
@Component({ selector:'dynamic-wrapper' ... }) export class DynamicWrapperComponent { @Input form!: FormGroup<any>; @Input formControlName!: string; ...
some_wrapperponent.html:
<form [formGroup]="form"> <your-dynamic-component [formControlName]="formControlName"></your-dynamic-component> </form>
Option2: Wrapper component with only a name input
If you want to get rid of the FormGroup input in your ts and let the some_wrapperponent.html contains only this line:
<your-dynamic-component [formControlName]="formControlName"></your-dynamic-component>
You can rely on Dependency Injection to resolve the parent formGroup for your formControlNames by providing ControlContainer in the viewProviders array:
some_wrapperponent.ts:
@Component({ selector:'some-dynamic-wrapper' viewProviders:[ { provide:ControlContainer, useFactory:()=>inject(ControlContainer,{ skipSelf: true }) } ] }) export class SomeDynamicWrapperComponent { @Input formControlName!: string; ...
This works because formControlName directive requesting the ControlContainer as follows :
constructor(@Optional() @Host() @SkipSelf() parent: ControlContainer,...)
The key point here is the @Host() resolution modifier tells angular that the host element view (some-dynamic-wrapper view in our case) is the last stop for searching for providers for the token ControlContainer so that Angular will stop at the viewProviders defined in some-dynamic-wrapper. For more info about @Host resolution modifier and viewProviders, see this document.
- This also means that resolving a dependency restricted with @Host() will be from node injector hierarchy, the DI will never reach the environment injectors *(1)
I put
useFactory:()=>inject(ControlContainer,{ skipSelf: true })
for generality, you could putuseFactory:()=>inject(FormGroupDirective,{ skipSelf: true })
for your specific case but what if the parent wasformArrayName
orformGroupName
.By the way all of these directives extend ControlContainer and provide it as themselves, for example, FormGroupDirective does so as follows:
const formDirectiveProvider: Provider = { provide: ControlContainer, useExisting: forwardRef(() => FormGroupDirective), }; ... @Directive({ selector: '[formGroup]', providers: [formDirectiveProvider], host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'}, exportAs: 'ngForm', standalone: false, }) export class FormGroupDirective extends ControlContainer implements Form, OnChanges, OnDestroy
Option3: No Wrapper component at all, define a standalone directive and use it in the hostDirectives property in each dynamic component
We said that we cannot use the directive composition API to attach formControlName to the host because formControlName is not a standalone directive, but as a workaround we can define a standalone directive and make it inherits FormComtrolName (inspired by this issue):
@Directive({ standalone: true, }) export class SAFormControlNamaDirective extends FormControlName {}
Now we can use this directive on our dynamically created components as follows:
@Component({ ... hostDirectives:[ { directive:SAFormControlNameDirective, inputs:['formControlName'] } ] }) export class DynamicComponent implements ControlValueAccessor{ ...
Problem: The problem here is SAFormControlName inherits FormControlName which requires ControlContainer to be provided in the view of the host element (remember @Host()) but the dynamically created component is treated as a root element meaning it will not have a parent node injector because it's not declared in the host view its inserted there => the ControlContainer will not be resolved from the view where we insert the component, it will not reach the environment injector (remember (1)) => it will be resolved as null due to @Optional().
Does it mean we lost the node injector hierarchy and cannot reach ControlContainer from the view where we insert the dynamically created component?
Actually no, It turns out the supposed parent node injector is chained (wired) with the environment injector through the ChainedInjector.
Solution: So all what we need to reach the ChainedInjector is to remove @Host() restriction.
Then pass the resolved ControlContainer to the parent FormControlName directive.
@Directive({ standalone: true, }) export class SAFormControlNamaDirective extends FormControlName { constructor( @Optional() @SkipSelf() parent: ControlContainer, @Optional() @Self() @Inject(NG_VALIDATORS) validators: (Validator | ValidatorFn)[], @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: (AsyncValidator | AsyncValidatorFn)[], @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[], ){ super(parent,validators,asyncValidators,valueAccessors,null) } }
Optional:
If you want to have statuses classes (.ng-pristine/dirty .ng-vlaid/invalid ..) which are applied automatically in the elements with regular Form-contrl based directives: you can create this directive that extends NgControlStatus( which is responsible for adding/removing those classes on the host element depending on the FormControl model statuses )
@Directive({ standalone: true, }) export class SANgControlStatus extends NgControlStatus {}
And apply it to the hostDirectives array in the DynamicComponent(s):
@Component({ ... hostDirectives:[ { directive:SAFormControlNameDirective, inputs:['formControlName'] }, { directive:SANgControlStatus }, ] }) export class DynamicComponent implements ControlValueAccessor{ ...
but NgControlStatus constructor requies NgControl(FormControlName,FormControl or NgModel) to be on the same node:
constructor(@Self() cd: NgControl)
So we can provide our SAFormControlNamaDirective since its a FormControlName and therefore NgControl:
@Directive({ standalone: true, providers: [ { provide: NgControl, useExisting: forwardRef(() => SAFormControlNamaDirective), }, ], }) export class SAFormControlNamaDirective extends FormControlName { ...
本文标签:
版权声明:本文标题:typescript - How to use formControlName (ng_value_accessor) with ngComponentOutlet in Angular for dynamic form components - Stac 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1744087541a2588808.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论