Issue
I'm trying to build a form based on a json. I have a backend using Spring boot that returns the following object to an angular front app:
{
"controls": [
{
"name": "genre",
"type": "radio",
"radioOptions": [
{ "key": "1", "value": "Mr." },
{ "key": "2", "value": "Ms." }
],
"validators": {}
},
{
"name": "firstName",
"label": "First name:",
"value": "",
"type": "text",
"repeat": false,
"validators": {
"required": true,
"minLength": 10
}
},
{
"name": "lastName",
"label": "Last name:",
"value": "",
"type": "text",
"repeat": false,
"validators": {}
},
{
"name": "softwareTopics",
"label": "Softwares:",
"value": "",
"type": "text",
"repeat": true,
"maxRepeat": 5,
"validators": {
"required": true,
"minLength": 10
}
},
{
"name": "hobbies",
"label": "Hobbies",
"value": "",
"type": "select",
"selected": "Select your hobbies",
"multi": false,
"selectOptions": [
{ "key": "1", "value": "Tennis" },
{ "key": "2", "value": "Golf" },
{ "key": "3", "value": "Bike"}
],
"validators": {}
},
{
"name": "time",
"label": "Time",
"value": "",
"type": "time",
"validators": {}
},
{
"name": "date",
"label": "Date",
"value": "",
"type": "date",
"validators": {}
},
{
"name": "comments",
"label": "Comments",
"value": "",
"type": "textarea",
"validators": {}
},
{
"name": "agreeTerms",
"label": "This is a checkbox?",
"value": "false",
"type": "checkbox",
"validators": {}
},
{
"name": "size",
"label": "Size",
"value": "",
"type": "range",
"options": {
"min": "0",
"max": "100",
"step": "1"
},
"validators": {}
},
{
"name": "toggle",
"label": "Do you like toggles?",
"value": "false",
"type": "toggle",
"validators": {}
}
]
}
In this JSON, I try to use as many type of input possibles. Most of the code works actually. In my component, I just have to call my rest API that returns me my JSON object.
ngOnInit(): void {
// loading json response from back
this.apiService.getForm().subscribe(
(response: any) => {
this.jsonResponse = response
this.buildForm((this.jsonResponse.controls))
},
(error: any) => {
console.log(error)
},
() => {
console.log("Done");
}
)
}
From there I try to check my Json in order to generate dynamically my form. For this, I send the response to the buildForm method.
buildForm(controls: JsonFormControls[]): void {
// we will loop all entries of JsonFormControls objects from the controls array
console.log("controls", controls);
let repeatedInputFormGroup = this.fb.group({});
for (const control of controls) {
// some inputs have one or more validators: input can be required, have a min length, max length...
const controlValidators = [];
// a control has a key and a value.
// example: "validators": { "required": true, "minLength": 10 }
// this snippet is reusable: can be optimized if used in many forms
for (const [key, value] of Object.entries(control.validators)) {
switch (key) {
case 'min':
controlValidators.push(Validators.min(value));
break;
case 'max':
controlValidators.push(Validators.max(value));
break;
case 'required':
if (value) {
controlValidators.push(Validators.required);
}
break;
case 'requiredTrue':
if (value) {
controlValidators.push(Validators.requiredTrue);
}
break;
case 'email':
if (value) {
controlValidators.push(Validators.email);
}
break;
case 'minLength':
controlValidators.push(Validators.minLength(value));
break;
case 'maxLength':
controlValidators.push(Validators.maxLength(value));
break;
case 'pattern':
controlValidators.push(Validators.pattern(value));
break;
case 'nullValidator':
if (value) {
controlValidators.push(Validators.nullValidator);
}
break;
default:
break;
}
}
// we must handle repeated inputs
repeatedInputFormGroup = this.fb.group({});
if (control.repeat) {
repeatedInputFormGroup = this.fb.group({
responses: this.fb.array([this.fb.group({response:''})])
})
}
// we add a new control and pass an array of validators
this.form.addControl(
control.name,
this.fb.control(control.value, controlValidators)
);
this.form.controls = { ...this.form.controls, ...repeatedInputFormGroup.controls}
}
}
I also have some inputs that i want to duplicate. For this I tried to add them onto a formGroup named repeatedInputFormGroup. Finally, I use spread operator to finally build my form. At this stage I do not check how many items are added (maxRepeat value).
I instanciated those methods to allow user to add a new input or remove it.
// getter
get items(): FormArray {
return this.form.get('responses') as FormArray;
}
addInputItem(): void {
this.items.push(this.fb.group({response:''}));
}
deleteInputItem(index: number): void {
this.items.removeAt(index);
}
My submit method do nothing special except some console.log
onSubmit(): void {
console.log("Form is valid: " + this.form.valid);
console.log("Form values: " + this.form.value);
}
In my HTML content, I'm chekking the form I receive and build the inputs
<!-- creating the form and loop -->
<span *ngIf="form != null">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div *ngFor="let control of jsonResponse.controls">
<div class="mb-3">
<span *ngIf="control.label != '' && control.type !== 'toggle' && control.type !== 'checkbox'">
<label class="form-label">{{ control.label }}</label>
</span>
<!-- for inputs that are not repeatable -->
<span *ngIf="inputTypes.includes(control.type) && control.repeat === false">
<input
[type]="control.type"
[formControlName]="control.name"
[value]="control.value"
class="form-control"
/>
</span>
<!-- for inputs that are not repeatable -->
<span *ngIf="inputTypes.includes(control.type) && control.repeat === true">
<div formArrayName="{{ control.name }}">
<div
*ngFor="let item of items.controls; let id = index"
class="input-group mb-3"
[formGroupName]="id">
<input
class="form-control"
formControlName="{{ control.name }}"/>
<button type="button" class="btn btn-outline-secondary"
(click)="deleteInputItem(id)">Remove</button>
</div>
<button type="button" class="btn btn-primary" (click)="addInputItem()">Add entry</button>
</div>
</span>
<!-- text area -->
<span *ngIf="control.type === 'textarea'">
<textarea
[formControlName]="control.name"
[value]="control.value"
class="form-control"
></textarea>
</span>
<!-- select -->
<span *ngIf="control.type === 'select'">
<select
[formControlName]="control.name"
class="form-select form-select-lg mb-3">
<option selected>{{ control.selected }}</option>
<option *ngFor="let option of control.selectOptions"
value="{{ option.key }}"> {{ option.value }}</option>
</select>
</span>
<!-- range -->
<span *ngIf="control.type === 'range'">
<label for="{{control.name}}" class="form-label">{{control.label}}</label>
<input
*ngIf="control.type === 'range'"
type="range"
[min]="control.options?.min"
[max]="control.options?.max"
[formControlName]="control.name"
class="form-range"
id="{{control.name}}"
/>
</span>
<!-- handling checkboxes -->
<span *ngIf="control.type === 'checkbox'">
<input class="form-check-input" type="checkbox" [id]="control.name"/>
<label class="form-check-label">{{ control.label }}</label>
</span>
<!-- toggle -->
<span *ngIf="control.type === 'toggle'">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="{{ control.name }}">
<label class="form-check-label" for="{{ control.name }}">Default switch checkbox input</label>
</div>
</span>
<!-- radio buttons -->
<span *ngIf="control.type === 'radio'">
<div class="form-check form-check-inline" *ngFor="let option of control.radioOptions">
<input class="form-check-input" type="radio" name="{{control.name }}"
id="{{option.key}}-{{option.value}}" value="{{option.value}}">
<label class="form-check-label" for="{{option.key}}-{{option.value}}">{{option.value}}</label>
</div>
</span>
</div>
</div>
<div>
<button
class="btn btn-primary"
type="submit">
Submit
</button>
</div>
</form>
</span>
It seems that I have an issue with the inputs I can add with an "Add" button. First on loading the form, I do not retrieve the whole form and get an error message.
core.mjs:6469 ERROR Error: Cannot find control with path: 'softwareTopic -> 0'
at _throwError (forms.mjs:1779)
at setUpFormContainer (forms.mjs:1752)
at FormGroupDirective._setUpFormContainer (forms.mjs:5437)
at FormGroupDirective.addFormGroup (forms.mjs:5327)
at FormGroupName.ngOnInit (forms.mjs:4189)
at callHook (core.mjs:2526)
at callHooks (core.mjs:2495)
at executeInitAndCheckHooks (core.mjs:2446)
at selectIndexInternal (core.mjs:8390)
I also noticed that my radio buttons are not set when I submit form. It's a cool stuff if it works.
Solution
For repeated controls, instead of using another FormControl repeatedInputFormGroup, we can represent them using FormArray. Below is the code that can be added after switch block within buildForm():
// we must handle repeated inputs
const formControl = this.fb.control(control.value, controlValidators);
if (control.repeat) {
this.form.addControl(control.name, this.fb.array([formControl]));
} else {
this.form.addControl(control.name, formControl);
}
We can modify addInputItem and deleteInputItem as:
addInputItem(name: string): void {
// Haven't taken care of setting validators
(<FormArray>this.form.get(name)).push(this.fb.control(''));
}
deleteInputItem(name: string, index: number): void {
(<FormArray>this.form.get(name)).removeAt(index);
}
HTML for repeated control:
<span *ngIf="inputTypes.includes(control.type) && control.repeat === true">
<div formArrayName="{{ control.name }}">
<div *ngFor="let item of form.get(control.name)['controls']; let id = index"
class="input-group mb-3">
<input class="form-control" formControlName="{{ id }}" />
<button
type="button"
class="btn btn-outline-secondary"
(click)="deleteInputItem(control.name, id)">
Remove
</button>
</div>
<button
type="button"
class="btn btn-primary"
(click)="addInputItem(control.name)">
Add entry
</button>
</div>
</span>
Regarding radio buttons not getting set, you would need to add formControlName directive as:
<input class="form-check-input" type="radio" name="{{ control.name }}"
value="{{ option.value }}" id="{{ option.key }}-{{ option.value }}"
[formControlName]="control.name"/>
Answered By - Siddhant
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.