angular dynamic forms add nested form arrays - javascript

I am trying to create a dynamic form where you can add forms dynamically, and sub forms dynamically. For example:
+ user1
--- + color1
--- + color2
--- + color3
+ user2
--- + color1
--- + color2
--- + color3
+ user 3
--- + color1
Where as you can add as many users as you want, and you can add as many colors as you want to each user. I can get the first array to work (users), but not sure about the nested array (colors). I have it set up so it loads a user and a color at the beginning. Here is my code I have so far:
export class FormsComponent implements OnInit {
myForm: FormGroup;
constructor(private fb: FormBuilder) { }
ngOnInit() {
this.myForm = this.fb.group({
email: '',
users: this.fb.array([])
})
}
get userForms() {
return this.myForm.get('users') as FormArray
}
get colorForms() {
return this.userForms.get('colors') as FormArray
}
addUser() {
const userGroup = this.fb.group({
user: [],
colors: this.fb.array([])
})
this.userForms.push(userGroup);
}
deleteUser(i) {
this.userForms.removeAt(i);
}
addColor() {
const colorGroup = this.fb.group({
color: []
})
this.colorForms.push(colorGroup);
}
deleteColor(i) {
this.colorForms.removeAt(i)
}
}
And this is my html code:
<form [formGroup]="myForm">
<hr>
<input formControlName="email">
<div formArrayName="users">
<div *ngFor="let user of userForms.controls; let i=index" [formGroupName]="i">
<input formControlName="user">
<button (click)="deleteUser(i)">Delete User</button>
<div formArrayName="colors">
<div *ngFor="let color of colorForms.controls; let t=index" [formGroupName]="t">
<input formControlName="color">
<button (click)="deleteColor(t)">Delete Color</button>
</div>
</div>
</div>
</div>
<button (click)="addUser()">Add User</button>
</form>
Obviously this doesn't work, so I am trying to comprehend what I need to do.

It doesn't work because you do not take into account the index in which controls are stored.
For example
get colorForms() {
return this.userForms.get('colors') as FormArray
}
won't work since userForms returns FormArray and you have to specify index to get colors control which belongs to specific user.
So it might look like:
getColors(index) {
return this.userForms.get([index, 'colors']) as FormArray;
}
and in html:
<div *ngFor="let color of getColors(i).controls;...>
Also you need to keep it in mind when working with colors array:
addColor(index: number) {
const colorGroup = this.fb.group({
color: []
})
this.getColors(index).push(colorGroup);
^^^^^^^^^^^^
use the them method to get correct FormArray
}
deleteColor(userIndex: number, colorIndex: number) {
this.getColors(userIndex).removeAt(colorIndex);
}
See also Ng-run Example

Related

How to get the values of default checked checkbox

I was working on a project of multiple checkbox. There, I want the checkboxes to be checked from the start and the value to be in the form(I am using reactive form). The user can unselect the boxes according to their wish and the data will be stored accordingly. This is the stackblitz of the project. There I was able to make the checkbox checked from the beginning, but when I hit the submit button there is no data when I console-logged. I think this is some binding issue,but I couldn't figure out what is exactly the problem.
Can someone help?
Thanks in advance.
This is code:
<form [formGroup]="form" (ngSubmit)="submit()">
<div class="form-group">
<label for="website">Website:</label>
<div *ngFor="let web of websiteList">
<label>
<input
type="checkbox"
[value]="web.id"
(change)="onCheckboxChange($event)"
[checked]="web.isSelected"
/>
{{ web.name }}
</label>
</div>
</div>
<button class="btn btn-primary" type="submit">Submit</button>
</form>
form: FormGroup;
websiteList: any = [
{ id: 1, name: 'HDTuto.com', isSelected: true },
{ id: 2, name: 'HDTuto.com', isSelected: true },
{ id: 3, name: 'NiceSnippets.com', isSelected: true },
];
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
website: this.fb.array([], [Validators.required]),
});
}
ngOnInit() {}
onCheckboxChange(e: any) {
const website: FormArray = this.form.get('website') as FormArray;
console.log('checking ->', e);
if (e.target.checked) {
website.push(new FormControl(e.target.value));
console.log('website ->', website);
} else {
//console.log(e);
const index = website.controls.findIndex((x) => {
console.log('x.value ->', x.value);
console.log('target.value ->', e.target.value);
x.value === e.target.value;
});
website.removeAt(index);
}
}
submit() {
console.log(this.form.value);
}
https://stackblitz.com/edit/angular-ivy-qar4ph?file=src/app/app.component.ts
Pay attention to changes in template:
Added formArrayName attribute to checkboxes wrapper and formControlName attribute to input element.
Removed change and checked attributes
In the component ts file:
Added initial form array values
Added mapping to submit method
Removed onCheckboxChange method

How to bind to an Angular form from users selected option

OK it's a bit more complicated than the headline..
This form I am working on is a form group. It has a few fields ( supplement name, description and tags) the supplement name one is what I need help with as I have not worked on a complicated form like this and want to get it right and not just offer a messy patch job.
Here is the expected logical order of what happens
user adds a new supplement by clicking on the field and begins typing "creatine" for example
there is a query sent out that fetches products based on the entry into the input and
returns a JSON that are offered as suggestions
user clicks the suggestion "creatine"
field is populated and binds
we add another entry through the "add option" and repeat for X amount of products we want to
add.
What actually happens
user adds new supplement by clicking the field and types "creatine" suggestion request is
sent off and populates the dropdown
user clicks on the suggestion "creatine" the field takes that value
value is actually blank
user adds another supplement but the previous selection is in the field
user clears it and types again
value is blank
What needs to happen is the user can add X amount of supplements and able to grab whatever option from the dropdown recommendation and it is added to the form group array and does not interfere with the other form group array values.
I know this is not the right way to bind the form and I don't think it's right the way i'm binding the mat input field to trigger the query and this is the reason why I'm asking the question again, to not offer a patch job.
Component code
import { Subscription } from 'rxjs/Subscription';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '#angular/forms';
import { UtilitiesService } from '../../utilities/utilities.service';
import { GetSupplementsService } from './get-supplements.service';
#Component({
selector: 'app-supplements',
templateUrl: './supplements.component.html',
styleUrls: ['./supplements.component.css'],
providers: [GetSupplementsService],
})
export class SupplementsComponent implements OnInit {
supplementForm: FormGroup;
queryField: FormControl = new FormControl();
private supplementInventoryResults: Array<ISupplementInventoryResponse>;
private eventForm: FormGroup;
private searchResults: any;
private searchSubscription: Subscription;
private addSupplementSubscription: Subscription;
subcription: Subscription;
constructor (
private bottomSheet: MatBottomSheet,
private _fb: FormBuilder,
private ref: ChangeDetectorRef,
private _utils: UtilitiesService,
private getSupplements: GetSupplementsService,
private router: Router
) { }
public ngOnInit(): void {
this.browsingStackHistory = false;
this.loading = true;
this.supplementForm = this._fb.group({ // the form in question
entryArray: this._fb.array([
this.getUnit()
])
});
this.searchSubscription =
this.queryField.valueChanges
.debounceTime(600)
.distinctUntilChanged()
.switchMap((query) => this.getSupplements.search_supplement_by_category(query))
.subscribe((result) => {
this.searchResults = result;
});
}
public ngOnDestroy(): void {
this.subcription.unsubscribe();
}
private getUnit(): FormGroup {
return this._fb.group({
supplementName: [''],
description: [''],
tags: ['']
});
}
private addUnit(): void {
const control = <FormArray>this.supplementForm.controls['entryArray'];
control.push(this.getUnit());
}
private removeUnit(i: number): void {
const control = <FormArray>this.supplementForm.controls['entryArray'];
control.removeAt(i);
}
private addSupplement(): void { // this will do the post to the service
const supplementObject = {
start: this._utils.get_upload_time(),
data: this.supplementForm.value,
rating: 0
};
}
}
Template
[![<mat-tab label="Add Stack (Test)">
<div style="padding:8px;">
<div fxLayout="row wrap">
<div fxFlex.gt-sm="50%" fxFlex="100">
<h1>Add New Supplements Stack</h1>
<form \[formGroup\]="supplementForm" class="log-workout">
<!-- Start form units array with first row must and dynamically add more -->
<div fxLayout="column" fxLayoutAlign="center center" class="row-height">
<div formArrayName="entryArray">
<mat-divider></mat-divider>
<!-- loop throught units -->
<div *ngFor="let reps of supplementForm.controls.entryArray.controls; let i=index">
<!-- row divider show for every nex row exclude if first row -->
<mat-divider *ngIf="supplementForm.controls.entryArray.controls.length > 1 && i > 0"></mat-divider>
<br>
<!-- group name in this case row index -->
<div \[formGroupName\]="i">
<!-- unit name input field -->
<div class="row">
<mat-form-field class="example-form">
<input matInput placeholder="Supplement Name" \[formControl\]="addSupplementField"
formControlName="supplementName" \[matAutocomplete\]="auto">
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let product of supplementResults" \[value\]="product?.product_name">
<img class="example-option-img" aria-hidden \[src\]="product?.product_image" height="25">
{{product?.product_name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field class="example-form">
<input matInput placeholder="Description" formControlName="description" required>
</mat-form-field>
<mat-form-field class="example-form">
<input matInput placeholder="Tags" formControlName="tags" required>
</mat-form-field>
</div>
<!-- row delete button, hidden if there is just one row -->
<button mat-mini-fab color="warn" *ngIf="supplementForm.controls.entryArray.controls.length > 1"
(click)="removeUnit(i)">
<mat-icon>delete forever</mat-icon>
</button>
</div>
</div>
<!-- New unit button -->
<mat-divider></mat-divider>
<mat-card-actions>
<button mat-raised-button (click)="addUnit()">
<mat-icon>add box</mat-icon>
Add Other Product
</button>
</mat-card-actions>
<button mat-raised-button (click)="addSupplement()">
<mat-icon>add box</mat-icon>
Add Supplement
</button>
</div>
</div>
<!-- End form units array -->
</form>
</div>
</div>
</div>][1]][1]
Having the below when the getUnit() function is called apparently binds it in the sense it will operate independently and without conflicts.
private getUnit(): FormGroup {
const formGroup = this._fb.group({
supplementName: [''],
review: [''],
rating: [''],
notes: [''],
tags: ['']
});
formGroup.get('supplementName').valueChanges
.debounceTime(300)
.distinctUntilChanged()
.switchMap((search) => this.getSupplements.search_supplement_by_category(search))
.subscribe((products) => {
this.supplementResults = products;
});
return formGroup;
}

Show hide text box when specific option is selected from dropdown inside dynamic form Angular

I have a form where a user could add one/more div of Address on click of add button.
I want if user select options=5 from the dropdown, want to show and hide textbox in that particular address Div.
Component Code
get contactFormGroup() {
return this.form.get('Array') as FormArray;
}
ngOnInit() {
this.form= this.fb.group({
Array: this.fb.array([])
});
}
createContact(): FormGroup {
return this.fb.group({
ABC: [null, Validators.compose([Validators.required])],
Test: [null, Validators.compose([Validators.required])]
});
}
addContact() {
this.Group.push(this.createContact());
}
showValue(event) {
const selectedValue = event;
if (selectedValue === '5') {
this.showValuetxtbox = true;
} else {
this.showValuetxtbox = false;
}
}
As you are looping to add the divs, you could use a template reference variable on the drop down. e.g #select then refer to that in the *ngIf:
<form [formGroup]="addExpressionform">
<div formArrayName="expressionArray">
<div *ngFor="let item of contactFormGroup.controls; let i = index;" [formGroupName]="i">
<carbon-dropdown #select
(optionSelected)="showValue($event)"
[formControlN]="'UOM'"
[options]="listOptions" [formGroup]="item"
name="UOM"
>
</carbon-dropdown>
<carbon-text-input *ngIf="select.value == 5"
[formControlN]="'Value'"
[formGroup]="item"
name="Value"
>
</carbon-text-input>
<carbon-button type="primary" (click)="submit()" id="save-parameter">Save</carbon-button>
</div>
</div>
</form>
Simplified StackBlitz demo.
Take a look at this Stackblitz, it's referred to in the Angular docs and could serve as boilerplate to what you are trying to achieve.
You should isolate every possible type of question by creating a different class for each one, so you can shape the data and then use ngSwitch to dynamically create the HTML accordingly.
Question base class:
export class QuestionBase<T> {
controlType: string;
value: T;
key: string;
label: string;
// etc
constructor(options) {
// constructor logic
}
}
Some special class that inherents from base class
import { QuestionBase } from './question-base';
export class SpecialQuestion extends QuestionBase<string> {
controlType = 'specialQuestion';
type: string;
// special Question
specialValue: string;
constructor(options) {
super(options);
this.type = options['type'] || '';
}
}
Then, a question component:
<div [formGroup]="form">
<label>{{question.label}}</label>
<div [ngSwitch]="question.controlType">
// controls logic
<input *ngSwitchCase="'textbox'" >
<select *ngSwitchCase="'specialQuestion'"></select>
</div>
</div>
Then you throw this into a container component where you loop through the entire questions array.
This way your code will be future proof and reusable as you add/change functionality to your forms down the road. You won't have to create spaghetti to meet edge case requirements like an extra input field.

How to add input fields dynamically in angular 6

I need to add input fields to array of objects and a map which grows dynamically based on user's choice.
For e.g. InputForm has a list and a map which needs to be filled by user.
export class InputForm {
mapA: {};
listA: ListA[] = [];
}
export class ListA {
input1 : String
input2 : number
}
I need to show it on UI in such a way that input1, input2 and key, value for map of criteria is visible as the input field. The user fills all 4 input fields and clicks on the add button.
Then again same input fields should be editable for the user for the second input. This way he can build list and map and when he clicks on submit button array and map should have all the values filled before.
*ngFor doesn't work because the initial list is empty.
Assuming you are using Angular Reactive Form, you can use a combination of *ngFor and FormArray. You can start with an empty FormArray and add dynamically using the .push() method.
Here is a good and detailed example
// order-form.component.ts:
#Component({
selector: '...',
templateUrl: './order-form.component.html'
})
export class OrderFormComponent implements OnInit{
orderForm: FormGroup;
items: FormArray;
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.orderForm = this.formBuilder.group({
customerName: '',
email: '',
items: this.formBuilder.array([ this.createItem() ])
});
}
createItem(): FormGroup {
return this.formBuilder.group({
name: '',
description: '',
price: ''
});
}
addItem(): void {
this.items = this.orderForm.get('items') as FormArray;
this.items.push(this.createItem());
}
}
<!-- order-form.component.html -->
<div formArrayName="items"
*ngFor="let item of orderForm.get('items').controls; let i = index;">
<div [formGroupName]="i">
<input formControlName="name" placeholder="Item name">
<input formControlName="description" placeholder="Item description">
<input formControlName="price" placeholder="Item price">
</div>
Chosen name: {{ orderForm.controls.items.controls[i].controls.name.value }}
</div>

Traversing a nested FormArray in Angular 2

I am trying to traverse through a nested FormArray to keep track of a set of questions, each given their own 'FormControl'. I have created a FormArray called 'groups' as such. The hierarchy goes Groups[Questions[Answers]]
constructor(private surveyService: SurveyService, #Inject(FormBuilder) private fb: FormBuilder) {
this.survey = <Survey>SURVEYS[0];
this.reactiveForm = fb.group({
name: ['', Validators.required],
groups: fb.array(this.getGroups())
});
}
getGroups() : AbstractControl[] {
return Array.apply(null,
Array(this.survey.groups.length)).map((x, index) => new FormArray(this.getQuestions(index)))
}
getQuestions(index) : AbstractControl[] {
return Array.apply(null,
Array(this.survey.groups[index].questions.length)).map(x => new FormControl(x, Validators.required));
}
Here is my HTML code:
<form [formGroup]="reactiveForm">
<div formArrayName="groups">
<div *ngFor="let group of survey.groups; index as groupIndex" [formArrayName]=groupIndex>
<h1>{{group.groupName}}</h1>
<div *ngFor="let question of group.questions; index as questionIndex" [formArrayName]=questionIndex>
<h2>{{question.question}}</h2>
<label *ngFor="let answer of question.responses ; index as answerIndex">
<input type="radio" [value]=answerIndex [formControlName]=questionIndex>{{answer}}
</label>
</div>
</div>
</div>
</form>
I figure it to work as such: the first ngFor pulls the array of Questions, the second ngFor pulls the array of answers, and then the final ngFor uses [formControlName] in order to bind each question to the same Control, giving it a unique answer. However, the error I get is this:
Error: Cannot find control with path: 'groups -> 0 -> 0 -> 0'
If Groups[0] is an array and Groups[0][0] is also an array, why does the [0] of that suddenly not exist anymore? How should one go about traversing a FormArray like this?
Here is what you have created:
Now look at your template:
<form [formGroup]="reactiveForm">
<div formArrayName="groups">
<div ... [formArrayName]=groupIndex>
...
<div ... [formArrayName]=questionIndex>
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Do you see such FormArray in your FormGroup? I don't
<...
<label *ngFor="let answer of question.responses ; index as answerIndex">
<input ...[formControlName]=questionIndex>...
</label>
</div>
</div>
</div>
</form>
As you can see you Angular can't find FormControl by path groups -> 0 -> 0 -> 0 because it should be groups -> 0 -> 0
So if you will remove redundant [formArrayName]=questionIndex directive then it should work.
Ng-run Example
Tip: use <pre>{{reactiveForm.value | json}}</pre> to test FormGroup structure
For demo I used simple structure:
export class Group {
title: string;
questions: Question[] = [];
}
export class Question {
title: string;
answers: Answer[] = [];
}
export class Answer {
id: number;
title: string;
}
StackBlitz Demo

Categories

Resources