I've got the following 'triple level' nested form:
FormGroup->ArrayOfFormGroups->FormGroup
Top level (myForm):
this.fb.group({
name: '',
description: '',
questions: this.fb.array([])
});
Nested form array element for 'questions':
this.fb.group({
priority: ['1'],
params: this.fb.group({parameter: ['']})
});
Nested form group element for 'params' is a key:value object of random length.
I'm using the following ngFor to go through elements:
<tr *ngFor="let questionConfigForm of myForm.controls.questions.controls; let i=index" [formGroupName]="i">
...
<div *ngFor="let param of objectKeys(questionConfigForm.controls.params.controls)" formGroupName="params">
<input type="text" [formControlName]="param">
I've got the following behavior:
When I'm updating any of the fields on first two form levels I could instantly see changes in corresponding form controls values with {{myForm.value | json}}.
But if I input something in one of 'params' controls I couldn't see any changes in myForm values, but the form data for 'params' controls will be updated if I will make any changes in corresponding 'questions' form.
For me it looks like 'param' form control receives input data, but doesn't trigger some update event, and I don't know how to fix that, except writing my own function to react on (change) and patchValue in form..
So my question is how to make 'params' controls update myForm without that strange behavior?
UPD:
initQuestionConfig() {
return this.fb.group({
priority: ['1'],
params: this.fb.group({parameter: ['']}),
});
}
addQuestionConfig() {
const control = <FormArray>this.myForm.controls['questions'];
const newQuestionCfg = this.initQuestionConfig();
control.push(newQuestionCfg);
}
Finally the problem is solved.
The root of this issue was the way I've cleaned up already existing 'params'.
To remove all parameters from 'questions' I used the following code:
const control = <FormArray>this.myForm.controls['questions'];
control.controls[index]['controls'].params = this.fb.group([]);
And the reason of those glitches was this new 'fb.group' instance.
Now I'm removing params one by one, keeping original formGroup instance and it works as expected:
const control = <FormArray>this.myForm.controls['questions'];
const paramNames = Object.keys(control.controls[index]['controls'].params.controls);
for (let i = 0; i < paramNames.length; i++) {
control.controls[index]['controls'].params.removeControl(paramNames[i]);
}
#MilanRaval thanks for your time again :)
Try this: Give formArrayName and formGroupName like below...
<div formArrayName="testGroup">
<div *ngFor="let test of testGroup.controls; let i=index">
<div [formGroupName]="i">
<div class="well well-sm">
<label>
<input type="checkbox" formControlName="controlName" />
</div>
</div>
</div>
</div>
Related
Situation
I am building a custom filtering component. This allows the user to apply n filters that are displayed with a v-for in the template. The user can update any value in the input fields or remove any of the filters afterwards.
Problem
After removing one of the filters, my array itemRefs got a null value as the last item.
Code (simplified)
<script setup>
const filtersScope = $ref([])
const itemRefs = $ref([])
function addFilter () {
filtersScope.push({ value: '' })
}
function removeFilter (idx) {
filtersScope.splice(idx, 1)
itemRefs.pop() // <- necessary? has no effect
// validate and emit stuff
console.log(itemRefs)
// itemRefs got at least one null item
// itemRefs = [null]
}
// assign the values from the input fields to work with it later on
function updateValue() {
itemRefs.forEach((input, idx) => filtersScope[idx].value = input.value)
}
</script>
<template>
<div v-for="(filter, idx) in filtersScope" :key="filter.id">
<input
type="text"
#keyup="updateValue"
:ref="(input) => { itemRefs[idx] = input }"
:value="filter.value"
>
<button #click="removeFilter(idx)" v-text="'x'" />
</div>
<button #click="addFilter()" v-text="'add filter +'" />
</template>
>>> Working demo
to reproduce:
add two filters
itemRefs got now the template refs as a reference, like: [input, input]
remove one filter, itemRefs now looks: [input, null]
remove the last filter, itemRefs now looks like: [null]
Question
Without the itemRefs.pop() I got the following error, after removing and applying new filters:
Uncaught TypeError: input is null
With the pop() method I prevent a console error, but the null-value in itemRefs still remains.
How do I clean my template refs cleanly?
I don't know what's up with using $refs inside $refs but it's clearly not working as one would expect.
However, you should never need nested $refs. When mutating data, mutate the outer $refs. Use $computed to get a simplified/focused angle/slice of that data.
Here's a working example.
<script setup>
const filtersScope = $ref([])
const values = $computed(() => filtersScope.map(e => e.value))
function addFilter() {
filtersScope.push({ value: '' })
}
function removeFilter(idx) {
filtersScope.splice(idx, 1);
console.log(values)
}
</script>
<template>
<div v-for="(filter, idx) in filtersScope" :key="idx">
<input type="text"
v-model="filtersScope[idx].value">
<button #click="removeFilter(idx)" v-text="'x'" />
</div>
<button #click="addFilter()" v-text="'add filter +'" />
</template>
Here's my stackblitz
When I click on my button (in Stackblitz link) and push a value to the array, I'd like to have the field be valid, but it's remaining invalid. If I hard code in a value, it shows as valid, but not if I push a value from a click event.
QUESTION - Can anyone help me understand why? Do I need to use some sort of formArray type?
Here is my code.
registerForm: FormGroup;
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.registerForm = this.formBuilder.group({
practicedStyles: [[], [Validators.required]]
});
}
get practicedStyles() {
return this.registerForm.get('practicedStyles');
}
add() {
this.practicedStyles.value.push(1);
}
<div class="card m-3">
<h5 class="card-header">Angular 8 Reactive Form Validation</h5>
<div class="card-body">
<form [formGroup]="registerForm">
<p>Is valid: {{practicedStyles.valid}}</p>
<p>Is required: {{practicedStyles.errors?.required}}</p>
<button class="btn btn-primary" (click)="add()">Add</button>
</form>
</div>
</div>
Did you try using FormGroup.setValue?
add() {
this.registerForm.get('practicedStyles').value.push(1);
this.registerForm.setValue({practicedStyles: this.registerForm.get('practicedStyles').value});
}
I think pushing the value directly to the array wouldn't trigger the change because you're not changing the object's reference. And if you try to reassign the FormGroup control to a new object:
this.practicedStyles.value = 1
you'll get the following errro message Cannot assign to 'value' because it is a read-only property.
When manipulating FormGroup controls you should use the FormGroup provided methods to do so:
FormGroup.setValue
FormGroup.patchValue
So even though you are not changing the object's reference, you are notifying the framework that the object did change.
thanks in advance
my Requirement is to make a custom filter with name wise search(done) and checkboxes which filters a Table's Rows(array of objects) by matching the checkbox value with the Row['tags'] (array of strings) and returns row if the tags array consist of value in a checkbox ,
The problem is that the filters(checkbox) is obtained from DB and Dynamically populated thus I cannot use ngmodel
Any implementation ideas are highly appreciated, I've seen a lot of questions with static filters and some filters using pipes but how to handle the dynamic case
so far my implementation,
Template:
<div id="searchByTag" *ngFor="let tag of tagList">
<input
type="checkbox"
(change)="filterByTags(tag, $event)"
/>{{ tag }}
</div>
Ts:
rows=[{},{}] //from db
temp = rows // copied when getting row from db
filterByTags(FilterTag, event) {
if (event.target.checked) {
const filteredRow = this.rows.filter((obj) => {
return tag.includes(FilterTag.toLowerCase());
});
this.rows = filteredRow;
} else {
return (this.rows = this.temp);
}
}
a Row object:
{
"xx":'yyy',
....,
"tags" : [
"org",
"pcb",
]
}
other problem is that the filtering technique currently returns only one row which matches the condition (cleared), but the main thing is the dynamic implementation of tags
you can have ngModel:
if this is your checkboxes = ["org", "pcb"];
then all you need is a record to bind checkboxes values to it:
checkboxes: {[id: string]: {value: any}} = {};
for(let tag of this.tags) {
this.checkboxes[tag] = {value: false}
}
now in your template:
<input type="checkbox" *ngFor="let item of tags"
[(ngModel)]="checkboxes[item].value">
you can see this in this stackblitz:
stackblitz
I have to build a form like so.
This shows upfront. i.e. without user interaction.
When a user pressed the + button it creates the same kind of UI again like so.
You can see that the user can add any number of same UI parts again and again. Can you tell me how to do this?
I went through number of articles. But it has the whole form created once. i.e. not like my use case. Any direction for this?
Example: https://jasonwatmore.com/post/2019/06/25/angular-8-dynamic-reactive-forms-example
and https://alligator.io/angular/reactive-forms-formarray-dynamic-fields/
Based on the form you need to reproduce :
Create a function returning a formGroup to populate your array
createSchoolPath(): FormGroup {
return this.fb.group({
schoolName: '',
level: '',
topics: [],
inProgress: true
})
}
You then need to create your parent form which include your form array (and populate it once for the first form you want to show upfront) :
constructor( private fb: formBuilder) {}
form: FormGroup = this.fb.group({
schoolPaths: this.fb.array([this.createSchoolPaths])
});
Finally as you want to let users add more sections, you need a way to populate your array :
/* component */
addSchoolPath(): void {
const paths = this.form.get('schoolPaths') as FormArray;
// use the first function to push a new formGroup
paths.push(this.createSchoolPath());
}
<!-- html -->
<button (click)="addSchoolPath()">+</button>
To display it :
<form [formGroup]="form">
<div formArrayName="schoolPaths" *ngFor="let path of form.get('schoolPaths').controls; let i = index">
<div [formGroupName]="i">
<!-- place your inputs here -->
</div>
</div>
</form>
<button (click)="addSchoolPath()">+</button>
To complementary Gérôme's answer, you can also use directly a formArray
createGroup()
{
return new FormGroup({
school:new FormControl(),
level:new FormControl(),
topic:new FormControl(),
progress:new FormControl()
})
}
At first you has a formArray
formArray:FormArray=new FormArray([this.createGroup()])
The add button is simple
add()
{
this.formArray.push(this.createGroup())
}
And the .html
<form [formGroup]="formArray">
<div *ngFor="let group of formArray.controls" [formGroup]="group">
<input formControlName="school">
<input formControlName="level">
<input formControlName="topic">
<input formControlName="progress">
</div>
</form>
See that, in general we use a formArray inside a formGroup, But in case you only want a FormArray, you can loop over the formArray.controls directly
Preface:
I am having the hardest time trying to figure out what sounds like an easy process for nested angular forms. I am dealing with a few components here and some of the formGroups and formArrays are being dynamically created and its throwing me off.
Apologies for the large code dump, but its the minimal example I was able to come up with to try and explain my problem.
The parent component is very straight forward as it only has two formControls. I then pass the form to the tasks component to have access to it.
Parent Component
this.intakeForm = this.fb.group({
requestor: ['', Validators.required],
requestJustification: ['', Validators.required]
});
HTML:
<form [formGroup]=“intakeForm”>
<app-tasks
[uiOptions]="uiOptions"
[intakeForm]="intakeForm">
</app-tasks>
</form>
Tasks Component
Some method in here will trigger generateTask which creates the new form group.
ngOnInit() {
this.intakeForm.addControl('tasks', new FormArray([]));
}
// Push a new form group to our tasks array
generateTask(user, tool) {
const control = <FormArray>this.intakeForm.controls['tasks'];
control.push(this.newTaskControl(user, tool))
}
// Return a form group
newTaskControl(user, tool) {
return this.fb.group({
User: user,
Tool: tool,
Roles: this.fb.array([])
})
}
HTML:
<table class="table table-condensed smallText" *ngIf="intakeForm.controls['tasks'].length">
<thead>
<tr>
<th>Role(s)</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let t of intakeForm.get('tasks').controls let i = index; trackBy:trackByIndex" [taskTR]="t" [ui]="uiOptions" [intakeForm]="intakeForm" [taskIndex]="i">
</tr>
</tbody>
</table>
TR Component
Some method in here will trigger the addRole method which will add the form group.
#Input('taskTR') row;
#Input('ui') ui;
#Input('intakeForm') intakeForm: FormGroup;
// Add a new role
addRole($event, task) {
let t = task.get('Roles').controls as FormArray;
t.push(this.newRoleControl($event))
}
// Return a form group
newRoleControl(role) {
return this.fb.group({
Role: [role, Validators.required],
Action: [null, Validators.required]
})
}
HTML
<td class="col-md-9">
<ng-select [items]="ui.adminRoles.options"
bindLabel="RoleName"
bindValue="Role"
placeholder="Select one or more roles"
[multiple]="true"
[clearable]="false"
(add)="addRole($event, row)"
(remove)="removeRole($event, row)">
</td>
The Question
I need to add formControlName to my TR Component, specifically on the ng-select. However, when I try and add a formControlName, it tells me that it needs to be within a formGroup.
From what I can tell, the formGroup is in the tasksComponent and is wrapping the whole table so its technically within a formGroup?
My end goal is to be able to add the formControlName to this input but I am having a hard time trying to figure out the path to get there.
Here is an image of the full form object.
The last expanded section, Role, is what this input should be called via formControlName so that I can perform validation and what not on the control.
Updates
Edit 1 - Changes for #Harry Ninh
Tasks Component HTML
<tbody>
<tr *ngFor="let t of intakeForm.get('tasks').controls let i = index; trackBy:trackByIndex" [taskTR]="t" [ui]="uiOptions" [intakeForm]="intakeForm" [taskIndex]="i" [formGroup]="intakeForm"></tr>
</tbody>
TR Component HTML
<td class="col-md-9">
<ng-select [items]="ui.adminRoles.options"
bindLabel="RoleName"
bindValue="Role"
placeholder="Select one or more roles"
[multiple]="true"
[clearable]="false"
formControlName="Roles"
(add)="addRole($event, row)"
(remove)="removeRole($event, row)">
</td>
Result: ERROR Error: formControlName must be used with a parent formGroup directive.
You are expected to declare [formGroup]="intakeForm" in the root tag of every component that wraps all formControlName, formGroupName and formArrayName properties. Angular won't try to go up the hierarchy to find that when compiling the code.
In TR Component template, the root element (ie. the <td>) should have [formGroup]="intakeForm", in order to tell Angular the formControlName who is related to.