Traversing a nested FormArray in Angular 2 - javascript

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

Related

Angular pass checkbox value to custom pipe

I'm looping through the values and displaying checkboxes which I'm showing, now in addition there is an search box filter on the top of list of checkboxes basically to filter out checkbox list.
HTML
<input [(ngModel)]="searchText" class="lmn-input lmn-select-input-inner mt-input" placeholder="Search by key word" id="mngd-seg"/>
<div formArrayName="checkBoxValueList"
*ngFor="let dataConceptList of scopeSetDetails.controls['checkBoxValueList'].controls | searchFilter : searchText; let i = index">
<label class="lmn-selection-control">
<span class="lmn-checkbox">
<input class="lmn-control-input" type="checkbox" [formControlName]="i">
<i class="lmn-control-icon lmnicon lmnicon-check"></i>
</span>
<span>Data Concept </span>
<span style="padding-left: 150px">{{ checkBoxValueList[i].scopeSets }}</span>
</label>
</div>
Therefore I've created an custom pipe 'searchFilter' and using this pipe above in *ngFor
PIPE
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'searchFilter',
})
export class FilterPipe implements PipeTransform {
transform(items: any, searchText: string): any {
if (!items) return [];
if (!searchText) return items;
searchText = searchText.toLowerCase();
return items.filter((it) => {
return it.name.toLowerCase().includes(searchText);
});
}
}
In TS I'm initializing the checkboxes to false (with checkboxes, the Angular form control value is bound to the "checked" state (not the "value" attribute), so it should be either true or false) since it needs to be unchecked initially.
TS
const formControls = this.checkBoxValueList.map(
(control) => new FormControl(false)
);
const selectAllControl = new FormControl(false);
this.scopeSetDetails = this.fb.group({
search_text: this.fb.control(''),
checkBoxValueList: new FormArray(formControls, this.minSelectedScopeSet(1)),
selectAll: selectAllControl,
});
Now issue is in filter pipe I'm getting items as an array of FormControl whose value property is 'false'. I need the checkbox text somehow and initially all checkboxes should be unchecked. how can I achieve this?

How to access the properties of a formArray in HTML?

I'm trying to implement a reactive Angular form, but, I can't access the properties of the array on HTML, I never worked with reactive form, if anyone could guide me I would be grateful! I'm using Angular 10 and I have the following code:
TS
operationModel: IScriptOperationsModel;
formOperation: FormGroup;
constructor(
private fb: FormBuilder,
...
) {}
ngOnInit() {
this.operationModel = new IScriptOperationsModel();
this.operationModel.scriptOperationOrders = [];
this.buildForm(new IScriptOperationsModel());
this.scriptOperationsService.findScriptOperation(this.operationId).subscribe((operation) => {
this.operationModel = operation.data as IScriptOperationsModel; // api return
this.buildForm(this.operationModel); // I pass the return of the api to the form
});
}
buildForm(operation: IScriptOperationsModel) {
this.formOperation = this.fb.group({
descriptionCode: [operation.descriptionCode],
description: [operation.description],
workStations: this.fb.array([])
});
this.formOperation.setControl('workStations', this.fb.array(this.operationModel.scriptOperationOrders));
}
get workStations(): FormArray {
return this.formOperation.get('workStations') as FormArray;
}
HTML
<div
class="card"
[ngClass]="{'bg-principal': idx === 0, 'bg-alternative': idx !== 0}"
formArrayName="workStations"
*ngFor="let workstation of workStations.controls; index as idx"
>
<div class="card-body" [formGroupName]="idx">
<div class="form-row">
<div class="form-group col-md-1">
<label>Id Oper.</label>
<input
type="text"
name="idOperation"
class="form-control"
disabled
formControlName="rank" <!-- whatever with or without binding gives error -->
/>
</div>
<div class="form-group col-md-2">
<label>Time</label>
<input
type="time" class="form-control" name="defaultTime"
[formControlName]="defaultTime" <!-- whatever with or without binding gives error -->
/>
</div>
</div>
</div>
</div>
Models
export class IScriptOperationsModel extends Serializable {
public description: string;
public descriptionCode: string;
public scriptOperationOrders: IScriptOperationOrdersModel[]; // array which I need
}
export class IScriptOperationOrdersModel extends Serializable {
public rank: number;
public defaultTime: string;
public asset?: IAssetModel;
public provider?: IProviderModel;
}
error-handler.service.ts:87 Error: Cannot find control with path: 'workStations -> 0 -> rank' # undefined:undefined
NOTE: I already looked at some answers here on the site such as this, this and this , but none of them solved this problem!
your problem is here :
this.formOperation.setControl(
'workStations',
this.fb.array(this.operationModel.scriptOperationOrders) <== here the problem
);
you are passing an array of IScriptOperationOrdersModel instead of array of form group.
To make your code working, you have to loop on every element of this.operationModel.scriptOperationOrders array , and instanciate a new FormControl object then push it in the workStations form array.
To access its elements, you can use controls[indexOfGroup].rate
You can take a look at this simple example you will understand everything.

angular dynamic forms add nested form arrays

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

Angular 2 ngFor Object array object not working

I have this data:
component.ts:
demo = [{
'ob1': [{'ob1_first':'value11'}, {'ob1_second':'value12'}],
'ob2': [{'ob2_first':'value21'}, {'ob2_second':'value22'}],
}]
I want show it in view ngFor, but I still get [Object object]
My html:
<div *ngFor="let key of demo">
{{demo[key]}}
</div>
My question is:
How to refer to Object array object in ngFor ?
Your json format is not correct. Still solution is as below,
<div *ngFor="let key of demo">
{{getValue(key) |json}} <br/>
</div>
getValue(key){
console.log(key)
return Object.values(key);
}
LIVE DEMO
demo is an array with 1 object.
*ngFor="let key of demo" -> iterates through the object which is only one in your case.
{
'ob1': [{'ob1_first':'value11'}, {'ob1_second':'value12'}],
'ob2': [{'ob2_first':'value21'}, {'ob2_second':'value22'}],
}
this gets you the first key ->
demo[key].ob1
which has
[{'ob1_first':'value11'}, {'ob1_second':'value12'}]
as value.
To access this.
You have to do
*ngFor="let innerkey of key.ob1"
then you can access value1 with innerkey.ob1_first
Objects are not iterable directly with *ngFor. To iterate objects, you can use a pipe like this one:
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'keys'
})
export class KeysPipe implements PipeTransform {
transform(value: any, args?: any): any {
let keys = [];
for (let key in value) {
keys.push(key);
}
return keys;
}
}
Then, you use it like this to iterate the object:
<div *ngFor="let item of demo | keys">
{{demo[item]}}
</div>
With the format given by you . you can print your demo object using following code
<div *ngFor="#itemArr of demo">
<span >{{itemArr |json}}</span>
</div>
But if you want to traverse your json data then you may have to correct it as
this.demo =
[
[{'ob1_first':'value11'}, {'ob1_second':'value12'}],
[{'ob2_first':'value21'}, {'ob2_second':'value22'}]
]
and then render it as
<div *ngFor="#itemArr of demo">
<div *ngFor="#item of itemArr">
<span >{{item |json}}</span>
</div>
<hr>
</div>
If still the answer dont solve your problem, Could you state your actual JSON object that you want to render.
<div *ngFor="let key of demo">
{{key}}
</div>
Everything in the demo is represented as key that's the reference to each individual object.
But it seems your object is a bit complicated for *ngFor to handle. So consider using Class for mapping.
Demo.ts
export class Demo {
ob1_first: string;
ob1_second: string;
ob2_first: string;
ob2_second: string;
}
component.ts
#Component({
selector: 'my-component',
......
})
export class Component {
demoArray: Demo[];
ngOnInit() {
// If this throw error you may need to replace with `demo: Demo = new Demo;
demo: Demo;
demo.ob1_first = "value11";
demo.ob1_second = "value12";
demo.ob2_first = "value21";
demo.obj2_secon = "value22";
demo2: Demo;
demo2.ob1_first = "value11";
demo2.ob1_second = "value12";
demo2.ob2_first = "value21";
demo2.obj2_secon = "value22";
this.demoArray.push(demo);
this.demoArray.push(demo2);
}
}
HTML
<div *ngFor="let key of demoArray">
<div>START</div>
<div>{{key.ob1_first}}</div>
<div>{{key.ob1_second}}</div>
<div>{{key.ob2_first}}</div>
<div>{{key.ob2_second}}</div>
<div>END</div>
</div>

Unraveling Angular 2 book, Chapter 1, Example 5

The page shows a list of dives, it has an "add new dive", "clear dives" and a search box, which filters the displayed list as you type into it.
This is the template:
<div class="container-fluid">
<h1>My Latest Dives (Angular/TypeScript)</h1>
<div class="row">
<div class="col-sm-5">
<button class="btn btn-primary btn-lg"
[disabled]="!enableAdd()"
(click)="addDive()">
Add new dive
</button>
<button class="btn btn-danger btn-lg"
(click)="clearDives()">
Clear dives
</button>
</div>
<div class="col-sm-4 col-sm-offset-3">
<input #searchBox class="form-control input-lg"
placeholder="Search"
(keyup)="0" />
</div>
</div>
<div class="row">
<div class="col-sm-4"
*ngFor="let dive of dives | contentFilter:searchBox.value">
<h3>{{dive.site}}</h3>
<h4>{{dive.location}}</h4>
<h2>{{dive.depth}} feet | {{dive.time}} min</h2>
</div>
</div>
</div>
This is the component code:
import {Component} from '#angular/core';
#Component({
selector: 'divelog',
templateUrl: 'app/dive-log.template.html'
})
export class DiveLogComponent {
public dives = [];
private _index = 0;
private _stockDives = [
{
site: 'Abu Gotta Ramada',
location: 'Hurghada, Egypt',
depth: 72,
time: 54
},
{
site: 'Ponte Mahoon',
location: 'Maehbourg, Mauritius',
depth: 54,
time: 38
},
{
site: 'Molnar Cave',
location: 'Budapest, Hungary',
depth: 98,
time: 62
}];
public enableAdd() {
return this._index < this._stockDives.length;
}
public addDive() {
if (this.enableAdd()) {
this.dives.push(this._stockDives[this._index++]);
}
}
public clearDives() {
this.dives = [];
this._index = 0;
}
}
This is the filter code:
import {Pipe, PipeTransform} from '#angular/core';
#Pipe({name: 'contentFilter'})
export class ContentFilterPipe implements PipeTransform {
transform(value: any[], searchFor: string) : any[] {
if (!searchFor) return value;
searchFor = searchFor.toLowerCase();
return value.filter(dive =>
dive.site.toLowerCase().indexOf(searchFor) >= 0 ||
dive.location.toLowerCase().indexOf(searchFor) >= 0 ||
dive.depth.toString().indexOf(searchFor) >= 0 ||
dive.time.toString().indexOf(searchFor) >= 0);
}
}
The filter is getting invoked and the list is getting rerendered whenever I type into the search box, but not when I click "add" button. If I have something in the search box, "add" button does not result in the change of the dive list even though the content of the search box allows the new items to be displayed. How do I change the code so that clicking the "add" button would cause rerendering of the displayed list of dives?
You have a pure pipe so
its method transform will be executed only when it detects a pure
change to the input value.
For your case
*ngFor="let dive of dives | contentFilter:searchBox.value"
the input value will be dives and searchBox.value
According to the angular2 guide on pipes:
A pure change is either a change to a primitive input value
(String, Number, Boolean, Symbol) or a changed object reference (Date,
Array, Function, Object).
When a dive is added, the array reference (dives) isn't changed – hence transform method is not executed.
When something is typed into the filter input, searchBox.value does change - hence transform is executed.
So one of possibles solutions is to have always a new reference array each time a div is added:
Just replace:
this.dives.push(this._stockDives[this._index++]);
with:
this.dives = this.dives.concat(this._stockDives[this._index++]);
or:
this.dives = [...this.dives, this._stockDives[this._index++]];
Second way to do it working is use impure pipe:
#Pipe({name: 'contentFilter', pure: false })

Categories

Resources