Issue
I have created a component that represents a select box using Angular Material. This component receives a FormGroup as input and works correctly when placed within another component. I need to enable the addition and removal of this component using buttons. I have managed to make it work using ViewContainerRef.createComponent and can pass the parameters needed to dynamically populate the select's content. However, I am encountering the error 'ERROR Error: Cannot find control with name: 'control2'.'
Parent component
import { STEPPER_GLOBAL_OPTIONS } from "@angular/cdk/stepper";
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { FormBuilder, FormControl, FormGroup } from "@angular/forms";
@Component({
selector: "gcr-create",
template: `<gcr-select-form
[formGroup]="form"
formControlName="control1"
otherParam="OK"
></gcr-select-form>
</div>
<ng-template #containerMoreSelect></ng-template>
<button mat-stroked-button (click)="click()">Add More</button>`,
styles: [``],
providers: [
{
provide: STEPPER_GLOBAL_OPTIONS,
useValue: { showError: true },
},
],
})
export class ParentComponent implements OnInit {
@ViewChild("containerMoreSelect", {
read: ViewContainerRef,
})
containerMoreSelect!: ViewContainerRef;
public form!: FormGroup;
constructor() {}
ngOnInit(): void {
this.formInit();
}
formInit() {
this.form = new FormGroup({
control1: new FormControl(),
});
}
click() {
this.form.addControl("control2", new FormControl());
const component = this.containerMoreSelect.createComponent(SelectReteComponent);
component.setInput("formGroup", this.form);
component.setInput("formControlName", "control2");
component.setInput("otherParam", "OK");
}
}
Child component
import {
Component,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
} from "@angular/core";
import {
AbstractControl,
ControlContainer,
FormGroup,
FormGroupDirective,
} from "@angular/forms";
import { MatSelectChange } from "@angular/material/select";
import { ValueText } from "../models/value-text.model";
@Component({
selector: "gcr-select-form",
template: `<mat-form-field>
<mat-label>Select what you prefer</mat-label>
<mat-select
[formControlName]="formControlName"
(selectionChange)="getSelection($event)">
<mat-option></mat-option>
<mat-option *ngFor="let value of values" [value]="value.id">
{{ value.text}}
</mat-option>
</mat-select>
</mat-form-field>`,
styles: ``,
viewProviders: [
{
provide: ControlContainer,
useExisting: FormGroupDirective,
},
],
})
export class SelectReteComponent implements OnChanges {
@Input() formGroup!: FormGroup;
@Input() formControlName!: string;
@Input() otherParam!: string;
@Output() selected= new EventEmitter<ValueText>();
constructor(private ValuesService: ValuesRepositoryService) {}
/* Data are loaded from DB */
ngOnChanges(changes: SimpleChanges) {
/* Other stuff */
console.log("formGroup" + this.formGroup.controls); // I can see all control, control2 included
console.log("formControlName " + this.formControlName); // It shows control2 correctly
console.log("otherParam" + this.otherParam); // It shows "OK"
}
getSelection(event: MatSelectChange) {
console.log("event.value " + event.value);
this.selected.emit({
value: event.value,
text: event.source.triggerValue,
});
}
}
Thank you in advance, Vincenzo
I tried to load the child component by passing the FormGroup
, and I expected the FormControl
to be recognized.
Solution
Please refrain from using the directives for @Input
or @Output
, you are using formControlName
as an input which is causing you bugs.
For this scenario, its best you go for *ngFor
instead of view container ref, since its simpler, but I am providing both methods for you to review.
Using ngFor
<div formArrayName="selectArr">
<gcr-select-form *ngFor="let control of selectArrControls" [control]="control"
></gcr-select-form>
</div>
For this particular scenario, if we pass the control as input we will be able to bind the select properly. Also its better you go for formArray
instead of unique controls, since its more easily maintainable!
Also we need to do useExisting: forwardRef(() => FormGroupDirective),
to prevent angular from throwing that error, it to refer to references which are not yet defined, when we use forwardRef
.
Please find below working example!
main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';
import { STEPPER_GLOBAL_OPTIONS } from '@angular/cdk/stepper';
import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import {
FormArray,
FormBuilder,
FormControl,
FormGroup,
ReactiveFormsModule,
} from '@angular/forms';
import { SelectReteComponent } from './child/child.component';
import { provideAnimations } from '@angular/platform-browser/animations';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-root',
standalone: true,
template: `
<form [formGroup]="form">
<div formArrayName="selectArr">
<gcr-select-form [control]="selectArrControls[0]"
></gcr-select-form>
<ng-template #containerMoreSelect></ng-template>
</div>
</form>
<button mat-stroked-button (click)="click()">Add More</button>`,
styles: [``],
imports: [SelectReteComponent, ReactiveFormsModule, CommonModule],
providers: [
{
provide: STEPPER_GLOBAL_OPTIONS,
useValue: { showError: true },
},
],
})
export class App {
@ViewChild('containerMoreSelect', {
read: ViewContainerRef,
})
containerMoreSelect!: ViewContainerRef;
public form: FormGroup = new FormGroup({
selectArr: new FormArray([new FormControl()]),
});
constructor() {}
ngOnInit(): void {
this.formInit();
}
formInit() {}
get selectArr(): FormArray {
return this.form.get('selectArr') as FormArray;
}
get selectArrControls(): FormControl[] {
return this.selectArr?.controls as FormControl[];
}
click() {
const array = this.selectArr;
array.push(new FormControl());
const component =
this.containerMoreSelect.createComponent(SelectReteComponent);
component.setInput(
'control',
this.selectArrControls[this.selectArrControls.length - 1]
);
component.setInput('otherParam', 'OK');
}
}
bootstrapApplication(App, {
providers: [provideAnimations()],
});
child
import { CommonModule } from '@angular/common';
import {
Component,
EventEmitter,
forwardRef,
Input,
OnChanges,
Output,
SimpleChanges,
} from '@angular/core';
import {
AbstractControl,
ControlContainer,
FormArray,
FormControl,
FormGroup,
FormGroupDirective,
ReactiveFormsModule,
} from '@angular/forms';
import { MatSelectChange, MatSelectModule } from '@angular/material/select';
@Component({
selector: 'gcr-select-form',
imports: [MatSelectModule, ReactiveFormsModule, CommonModule],
standalone: true,
template: `
<mat-form-field>
<mat-label>Select what you prefer</mat-label>
<mat-select
[formControl]="control"
(selectionChange)="getSelection($event)">
<mat-option></mat-option>
<mat-option *ngFor="let value of values" [value]="value.id">
{{ value.text}}
</mat-option>
</mat-select>
</mat-form-field>`,
styles: ``,
viewProviders: [
{
provide: ControlContainer,
useExisting: forwardRef(() => FormGroupDirective),
},
],
})
export class SelectReteComponent implements OnChanges {
@Input() control!: FormControl;
@Input() formControlName!: number;
@Input() otherParam!: string;
@Output() selected = new EventEmitter<any>();
values = [
{ id: 1, text: 'test' },
{ id: 2, text: 'test2' },
{ id: 3, text: 'test3' },
];
form!: FormGroup;
constructor(private controlContainer: ControlContainer) {}
/* Data are loaded from DB */
ngOnChanges(changes: SimpleChanges) {
/* Other stuff */
// console.log('formGroup' + this.formGroup.controls); // I can see all control, control2 included
// console.log('formControlName ' + this.formControlName); // It shows control2 correctly
// console.log('otherParam' + this.otherParam); // It shows "OK"
}
getSelection(event: MatSelectChange) {
console.log('event.value ' + event.value);
this.selected.emit({
value: event.value,
text: event.source.triggerValue,
});
}
}
Answered By - Naren Murali
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.