Issue
I'm creating a dynamic form that uses a json response from a backend to render a Reactive Form.
So an example of the json response is this...
"controls": [
{
"name": "name",
"label": "Name",
"value": "",
"type": "text",
"validators": {
"required": true,
"minLength": 4
}
},
]
}
And then the ts file builds the formGroup using formBuilder. I'm however having issues adding an array of formControl checkboxes... where one of the checkboxes MUST be checked or the form would be invalid. So the json looks like...
{
"controls": [
{
"title": "Which of these items do you want to buy? Check as many as you'd like",
"name": "items",
"checkboxOptions": [
{
"value": "",
"label": "Item 1",
"name": "item1"
},
{
"value": "",
"label": "Item 2",
"name": "item2"
}
],
"type": "checkboxgroup",
"validators": {
"required": true
}
}
]
}
I can successfully add the checkboxgroup as it's own form group that has an array of form controls. I can then add that formGroup to the parent formGroup... but I'm having issues with the html part of the nested formGroup.
Btw... this is what the rendering of the textbox would look like
<form [formGroup]='myForm' (keydown.enter)="$event.preventDefault()" novalidate>
<div *ngFor='let control of formControl.controls'>
<mat-form-field appearance="outline">
<mat-label>{{ control.label }}</mat-label>
<input matInput [formControlName]='control.name' [type]='control.type'>
</mat-form-field>
</div>
</form>
but I'm not sure how I'd render the array of formControl checkboxes with it's own validator. Thanks
Solution
You can create a component that mannage a FormControl that store an array.
Some like
@Component({
selector: 'mat-checkbox-group',
template: `
<span [class.horizontal]="horizontal">
<ul>
<li *ngFor="let item of list">
<mat-checkbox [ngModel]="control.value &&
control.value.indexOf(item.value)>=0"
(ngModelChange)="update($event,item.value)">
{{item.text}}
</mat-checkbox>
</li>
</ul>
</span>
`,
styles: [
`
ul{
padding:0;
list-style: none;
}
.horizontal li{
display:inline-block;
margin-right:.5rem;
}
`,
],
})
export class CheckboxGroup {
list: { value: string | number; text: string }[];
control: FormControl;
keys: any;
log: any;
horizontal:boolean
constructor(@Optional() @Attribute('horizontal') _horizontal:any){
this.horizontal=_horizontal!==undefined && _horizontal!==null?
_horizontal!=="false":false
}
@Input('control') set _control(value: any) {
this.control = value as FormControl;
}
@Input('source') set _source(value: any[]) {
const type = typeof value[0];
if (type == 'string' || type == 'number')
this.list = value.map((x) => ({ value: x, text: '' + x }));
else {
const match = [
...JSON.stringify(value[0])
.replace(/\"/g, '')
.matchAll(/(\w+)\:[^,]*/g),
].map((x) => x[1]);
this.list = value.map((x) => ({ value: x[match[0]], text: x[match[1]] }));
}
}
update(checked: boolean, value: string | number) {
const oldValue = this.control.value || [];
if (!checked)
this.control.setValue(
oldValue.filter((x: string | number) => x != value)
);
else
this.control.setValue(
this.list
.filter(
(x: any) => x.value == value || oldValue.indexOf(x.value) >= 0
)
.map((x) => x.value)
);
}
}
Allow you write
<mat-checkbox-group [source]="control.checkboxOptions"
[control]="myform.get(control.name)" >
</mat-checkbox-group>
Be carefull, the component allow as "source" an array of string, an array of numbers of an array of object. The first property is the value, the second one the label of the checkbox
See stackblitz
Update we can avoid create a new component, but in this case our .html is like
<div *ngFor="let control of controlsJson?.controls">
<div class="form-title">{{ control.title }}</div>
<div class="bottom-space" *ngIf="control.type === 'checkboxgroup'">
<ng-container *ngIf="dynamicForm.get(control.name) as ctrl">
<div *ngFor="let item of control.checkboxOptions">
<mat-checkbox
[ngModel]="ctrl.value && ctrl.value.indexOf(item.value) >= 0"
(ngModelChange)="
update($event, item.value, control.checkboxOptions, ctrl)
"
[ngModelOptions]="{ standalone: true }"
>
{{ item.label }} {{ item.value }}
</mat-checkbox>
</div>
</ng-container>
</div>
</div>
We use
<ng-container *ngIf="dynamicForm.get(control.name) as ctrl">
to avoid repeat dynamicForm.get(control.name)
See that we use as [ngModel] -the're NO bannana sintax
[ngModel]="ctrl.value && ctrl.value.indexOf(item.value) >= 0"
To control, our update need as arguments
- $event:true or false according is cheched or nor
- item.value: the value of the option
- control.checkboxOptions: the list of options
- ctrl: the FormControl
The function update is very similar to when we have a component
update(checked: boolean, value: string | number,options:any[],control:any) {
const oldValue = control.value || [];
if (!checked)
control.setValue(
oldValue.filter((x: string | number) => x != value)
);
else
control.setValue(
options
.filter(
(x: any) => x.value == value || oldValue.indexOf(x.value) >= 0
)
.map((x) => x.value)
);
}
But be carefull!!!
we only has an uniq FormControl
for (const control of this.controlsJson.controls) {
if (control.type == 'checkboxgroup') {
this.dynamicForm.addControl(
control.name,new FormControl())
}
}
And we need take account when we define the "options", the "value" of each option should be diferent
"checkboxOptions": [
{
"value": "false", //<--this value will be store
"label": "Item 1",
"name": "item1"
},
{
"value": "true", //<--this value will be store
"label": "Item 2",
"name": "item2"
}
],
If we want "at least one" only need add the Validators.required when we create the FormControl. Better we create a function that return an array of validators based in an array of string.
Imagine some like
"validators": ["required","max(3)"]
We can make a function
getValidators(validators:string[])
{
return validators.map(x=>{
if (x=="required")
return Validators.required;
if (x.substr(0,3)=="max")
{
const max=x.match(/max\((\d+)\)/)
return Validators.max(+max[1])
}
....
return null //<--if some string not fullfilled any "if"
}).filter(x=>x)
}
And write some like
for (const control of this.controlsJson.controls) {
if (control.type == 'checkboxgroup') {
this.dynamicForm.addControl(
control.name,new FormControl(null,this.getValidators(control.validators)))
}
}
See a forked stackblitz
Answered By - Eliseo
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.