I'm building a generic Angular list component that integrates with forms using value accessors. I need to dynamically render form controls with custom theming inside this list. The following is a sand box example.
<a-component>
<ng-template let-name="name">
<div *provideControlContainer>
<div>{{ name }}</div>
<div>
<input [formControlName]="name">
</div>
</div>
</ng-template>
</a-component>
And
<div [formGroup]="form">
<input formControlName="name">
<b-component name='alias' />
<hr/>
<ng-container
*provideControlContainer
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ name: 'surname'}"
/>
</div>
The problem is that FormControlName
only searches for parent ControlContainer
instances within the host component, due to the @Host()
decorator:
TypeScript
constructor(
@Optional() @Host() @SkipSelf() parent: ControlContainer,
)
This prevents dynamically generated form controls within my list component's template (using ng-template
) from correctly registering with the parent form.
I've tried injecting and proxying the parent ControlContainer
, but this only works with component transclusion, not ng-template
.
I'm also considering these solutions:
FormControlName
directive to remove the @Host()
limitation.Are there existing solutions or best practices for this scenario? I'd prefer to use a template or projection approach if possible. Any suggestions or alternative approaches would be greatly appreciated. There is stackblitz to play with.
P.S. It was built with AI tools.
Collecting workspace informationSure, here is a summary of the four variants of attaching control to the control group in Angular, based on the files imported in main.ts.
In this project, there are four different approaches to attaching a control to a control group. Each approach is demonstrated in a separate file. Below is a summary of each approach:
In this approach, the control is passed as an input to a child component, and the child component uses ngTemplateOutlet
to render the control.
File: pass-container-instance.ts
Key Components:
ComponentA
receives a control as an input and uses ngTemplateOutlet
to render it.ComponentB
passes a control to ComponentA
and provides a template for the control.FrameComponent
creates a form group and passes a control to ComponentB
.@Component({
selector: 'component-a',
template: `
<div>component-a</div>
<div>
<ng-container *ngTemplateOutlet="template; context: { control: control }"></ng-container>
</div>
`,
})
class ComponentA {
@Input() control: FormControl | null = null;
@ContentChild(TemplateRef) template!: TemplateRef<any>;
}
@Component({
selector: 'component-b',
template: `
<div>component-b</div>
<div>
<component-a [control]=control>
<ng-template let-control="control">
<input [formControl]="control" ngDefaultControl>
</ng-template>
</component-a>
</div>
`,
})
class ComponentB {
@Input() control: FormControl | null = null;
}
@Component({
selector: 'app-frame',
template: `
<div>
<h1>FrameComponent</h1>
<form [formGroup]="form">
<input formControlName="name" placeholder="Name">
<component-b [control]="surnameControl"/>
</form>
{{ form.value | json }}
</div>
`,
})
class FrameComponent {
surnameControl = new FormControl('Surname');
form = new FormGroup({
name: new FormControl('Name'),
surname: this.surnameControl,
});
}
In this approach, the entire form group is passed to a child component, and the child component uses ngTemplateOutlet
to render the control within the form group.
File: pass-control-instance.ts
Key Components:
ComponentA
receives a form group as an input and uses ngTemplateOutlet
to render the control.ComponentB
passes the form group to ComponentA
and provides a template for the control.FrameComponent
creates a form group and passes it to ComponentB
.@Component({
selector: 'component-a',
template: `
<div>component-a</div>
<div>
<ng-container *ngTemplateOutlet="template; context: { control: control }"></ng-container>
</div>
`,
})
class ComponentA {
@Input() control: FormControl | null = null;
@ContentChild(TemplateRef) template!: TemplateRef<any>;
}
@Component({
selector: 'component-b',
template: `
<div>component-b</div>
<div>
<component-a [form]=form>
<ng-template let-control="control">
<div [formGroup]="form">
<input formControlName="surname" ngDefaultControl>
</div>
</ng-template>
</component-a>
</div>
`,
})
class ComponentB {
@Input() form: FormGroup | null = null;
}
@Component({
selector: 'app-frame',
template: `
<div>
<h1>FrameComponent</h1>
<form [formGroup]="form">
<input formControlName="name" placeholder="Name">
<component-b [form]="form"/>
</form>
{{ form.value | json }}
</div>
`,
})
class FrameComponent {
form = new FormGroup({
name: new FormControl('Name'),
surname: new FormControl('Surname'),
});
}
In this approach, a custom directive is used to inject the parent control container into the child component.
File: cc-injecting-directive.ts
Key Components:
ParentControlContainer
directive injects the parent control container.ComponentA
and ComponentB
use the ParentControlContainer
directive.FrameComponent
creates a form group and uses ComponentB
.@Directive({
selector: '[parentControlContainer]',
providers: [
{
provide: ControlContainer,
useFactory: () => inject(ControlContainer, { skipSelf: true }),
}
]
})
export class ParentControlContainer {
constructor(
@Optional()
@SkipSelf()
parent: ControlContainer,
private injector: Injector
) {
this.logParentChain();
}
private logParentChain() {
let currentInjector: Injector | null = this.injector;
while (currentInjector) {
const parentControlContainer = currentInjector.get(ControlContainer, null);
console.log('Parent ControlContainer:', parentControlContainer);
currentInjector = (currentInjector as unknown as {parent: Injector}).parent;
}
}
}
@Component({
selector: 'component-b',
template: `
<div>component-b</div>
<div>
<component-a>
<ng-template>
<ng-container parentControlContainer>
<input formControlName="surname" ngDefaultControl>
</ng-container>
</ng-template>
</component-a>
</div>
`,
})
class ComponentB {
@Input() name!: string;
}
@Component({
selector: 'component-a',
template: `
<div>component-a</div>
<div>
<ng-container *ngTemplateOutlet="template"></ng-container>
</div>
`,
})
class ComponentA {
@Input() name!: string;
@ContentChild(TemplateRef) template!: TemplateRef<any>;
}
@Component({
selector: 'app-frame',
template: `
<div>
<h1>FrameComponent</h1>
<form [formGroup]="form">
<input formControlName="name" placeholder="Name">
<component-b />
</form>
{{ form.value | json }}
</div>
`,
})
class FrameComponent {
form = new FormGroup({
name: new FormControl('Name'),
surname: new FormControl('Surname'),
});
}
In this approach, a custom directive is used to override the default FormControlName
directive.
File: main-dirrective-override.ts
Key Components:
OutControlForm
directive overrides the default FormControlName
directive.ComponentA
and ComponentB
use the OutControlForm
directive.FrameComponent
creates a form group and uses ComponentB
.const controlNameBinding: Provider = {
provide: NgControl,
useExisting: forwardRef(() => OutControlForm),
};
@Directive({
selector: '[outFormControlName]',
providers: [controlNameBinding],
})
export class OutControlForm extends FormControlName {
@Input('outFormControlName') override name: string | number | null = null;
constructor(
@Optional()
@SkipSelf()
parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: (Validator | ValidatorFn)[],
@Optional()
@Self()
@Inject(NG_ASYNC_VALIDATORS)
asyncValidators: (AsyncValidator | AsyncValidatorFn)[],
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[],
private injector: Injector
) {
super(parent, validators, asyncValidators, valueAccessors, null);
this.logParentChain();
}
private logParentChain() {
let currentInjector: Injector | null = this.injector;
while (currentInjector) {
const parentControlContainer = currentInjector.get(ControlContainer, null);
console.log('Parent ControlContainer:', parentControlContainer);
currentInjector = (currentInjector as unknown as {parent: Injector}).parent;
}
}
}
@Component({
selector: 'component-a',
template: `
<div>component-a</div>
<div>
<ng-container *ngTemplateOutlet="template"></ng-container>
</div>
`,
})
class ComponentA {
@Input() name!: string;
@ContentChild(TemplateRef) template!: TemplateRef<any>;
}
@Component({
selector: 'component-b',
template: `
<div>component-b</div>
<div>
<component-a>
<ng-template>
<input outFormControlName="surname" ngDefaultControl>
</ng-template>
</component-a>
</div>
`,
})
class ComponentB {
@Input() name!: string;
}
@Component({
selector: 'app-frame',
template: `
<div>
<h1>FrameComponent</h1>
<form [formGroup]="form">
<input formControlName="name" placeholder="Name">
<component-b />
</form>
{{ form.value | json }}
</div>
`,
})
class FrameComponent {
form = new FormGroup({
name: new FormControl('Name'),
surname: new FormControl('Surname'),
});
}
Each of these approaches demonstrates a different way to attach a control to a control group in Angular. Depending on your specific requirements, you can choose the approach that best fits your needs.
This summary provides an overview of the four different approaches used in the project. You can refer to the respective files for more details on each implementation.
A.I. helped me to assemble it. I am not so smart.