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 >
<span *ngIf="control.label != '' && control.type !== 'toggle' && control.type !== 'checkbox'">
<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"
/>
</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"
[formGroupName]="id">
<input
formControlName="{{ control.name }}"/>
<button type="button"
(click)="deleteInputItem(id)">Remove</button>
</div>
<button type="button" (click)="addInputItem()">Add entry</button>
</div>
</span>
<!-- text area -->
<span *ngIf="control.type === 'textarea'">
<textarea
[formControlName]="control.name"
[value]="control.value"
></textarea>
</span>
<!-- select -->
<span *ngIf="control.type === 'select'">
<select
[formControlName]="control.name"
>
<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}}" >{{control.label}}</label>
<input
*ngIf="control.type === 'range'"
type="range"
[min]="control.options?.min"
[max]="control.options?.max"
[formControlName]="control.name"
id="{{control.name}}"
/>
</span>
<!-- handling checkboxes -->
<span *ngIf="control.type === 'checkbox'">
<input type="checkbox" [id]="control.name"/>
<label >{{ control.label }}</label>
</span>
<!-- toggle -->
<span *ngIf="control.type === 'toggle'">
<div >
<input type="checkbox" id="{{ control.name }}">
<label for="{{ control.name }}">Default switch checkbox input</label>
</div>
</span>
<!-- radio buttons -->
<span *ngIf="control.type === 'radio'">
<div *ngFor="let option of control.radioOptions">
<input type="radio" name="{{control.name }}"
id="{{option.key}}-{{option.value}}" value="{{option.value}}">
<label for="{{option.key}}-{{option.value}}">{{option.value}}</label>
</div>
</span>
</div>
</div>
<div>
<button
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.
CodePudding user response:
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"
>
<input formControlName="{{ id }}" />
<button
type="button"
(click)="deleteInputItem(control.name, id)">
Remove
</button>
</div>
<button
type="button"
(click)="addInputItem(control.name)">
Add entry
</button>
</div>
</span>
Regarding radio buttons not getting set, you would need to add formControlName
directive as:
<input type="radio" name="{{ control.name }}"
value="{{ option.value }}" id="{{ option.key }}-{{ option.value }}"
[formControlName]="control.name"/>