I am adding a material expansion panel on click of a button and trying to bind the values of the content into each index of the model array. My array has another array defined in it which gives me a headache when assigning the value of the nested array. The 1st index binds properly but the 2nd and 3rd index of the array repeats. I am also doing 2 way binding, which I will remove and bind it to the service.
The 1st index binds properly but the 2nd and 3rd index of the array repeats. I am also doing 2 way binding, which I will remove and bind it to the service.
<mat-card>
<mat-card-content>
<div class="pesticide-info">
<h2 class="h3 text-center">Pesticide Information</h2>
<mat-accordion>
<mat-expansion-panel *ngFor="let item of recordPesticideInfos; let i = index" [attr.data-index]="i">
<mat-expansion-panel-header>
<mat-panel-title>
Pesticide Product
</mat-panel-title>
<mat-panel-description>
Enter the Pesticide Information
</mat-panel-description>
</mat-expansion-panel-header>
<div *ngFor="let pest of item.pesticideInfo; let j = index" [attr.data-index]="j">
<mat-form-field [floatLabel]="auto">
<mat-label>Product</mat-label>
<!-- <input name=Product{{index}} [(ngModel)]="recordPesticideInfos[index].pesticideInfo[index].Product" placeholder="item">-->
<input matInput placeholder="Product Name" [(ngModel)]="pest.Product" name=Product{{countOfProduct}}
#Product="ngModel">
</mat-form-field>
</div>
</mat-expansion-panel>
<div class="add-product text-center">
<a class="btn btn-success text-white" (click)="addItem()" style="text-align: center"><i
class="fa fa-plus"></i> Add Product</a>
</div>
</mat-accordion>
</div>
</mat-card-content>
export class RecordPesticideInfo {
RecordPesticideInfosId: number;
RecordId: number;
PesticideInfoId: number;
CountOfPestcideProduct: number;
title: string;
description: string;
pesticideInfo: PesticideInfo[];
}
addItem() {
// push object that you need to be added into array
this.recordPesticideInfos.push({
RecordPesticideInfosId: null, RecordId: null,
PesticideInfoId: null, CountOfPestcideProduct: this.countOfProduct++,
title: "Pesticide Product", description: "Enter the Pesticide Information",
pesticideInfo: this.pesticides as PesticideInfo[]
// pesticideInfo: Object.keys(this.pesticides).map(i => this.pesticides[this.countOfProduct++])
});
}
I want to bind a new value of array on each button click
Here is the stackblitz url : https://stackblitz.com/edit/angular-ylqkum
The problem was in this line:
pesticideInfo: this.pesticides as PesticideInfo[]
Every time you added a new product it got the exact same object reference from your this.pesticide member variable.
The reason why it worked for the first item is that there you created the new object inline:
pesticideInfo: [{
PesticideInfoId: null,
Product: null
}]
I added this new function for new default product item creation:
private createNewPesticide() {
return {
PesticideInfoId: null,
Product: null
}
}
Check the working stackblitz.
Related
In My UI, Each Row has level1,level2 and level3 dropdown along with "Add Level" Button.
I want on click on "Add Level" button, it will add a new set of three dropdown level1,level2 and level3 in the same row. As much as user click the "Add level" Button, every time it will dynamically add new set of dropdown in same row.
Other row dropdown will not impact with this.
Can anyone please help to modify my old code to work as my expected output.
StackBlitz Code URL
Please refer the attached image for my expected output for level1 level2 and level3 dropdowns
It's always the same. To control any object using ReactiveForms, you create a FormGroup for Object and FormArray for array. If the Array is an object, you create a FormArray of FormGroup, else you create a FormArray of FormGroup
The first is decided the structure of your data, as "levels" is an array, your data can be, e.g.
{
"plannedProduction": 210468,
"factORLossTree": [
{
"skuid": "000000000034087070",
"skuDescription": "SUNLIGHT DESTINY HW PWD YELLOW 8X900G",
"productionOrderNo": "X49033251",
"category": "Fabric Cleaning",
"levels": [
{
"level1": "",
"level2": "",
"level3": ""
}
....
]
},
{
"skuid": "000000000067141695",
"skuDescription": "SUNLIGHT DESTINY YELLOW 24X170G",
"productionOrderNo": "X49039793",
"category": "Fabric Cleaning",
"levels": [
{
"level1": "",
"level2": "",
"level3": ""
}
....
]
},
....
]
}
As always you have a FormArray you use a getter (*) you still have in your code
get lossFormArray() {
return this.orLosstreeForm.get('factORLossTree') as FormArray;
}
As we have a FormArray inside a FormArray to get the inner FormArray we need another "getter". Well, can not be a getter because we need pass as argument the "index" of the formArray
getLevels(index:number)
{
return this.lossFormArray.at(index).get('levels') as FormArray;
}
we can use also an auxiliar function that return the value of the formArray at index to simplyfied the .html
getItemValue(index:number)
{
return this.lossFormArray.at(index).value;
}
Finally we are add a function to create the formGroup "level"
createLevel() {
return this.fb.group({
level1: [''],
level2: [''],
level3: [''],
});
We are going now to create the formGroup change a bit your function setData
setData() {
const resp = {...}
if (resp.factORLossTree.length > 0) {
this.orLosstreeForm.patchValue({
plannedProduction: resp.plannedVolume,
});
this.lossFormArray.clear();
for (const item of resp.factORLossTree) {
const data = {
skuid: item.skuid,
skuDescription: item.skuDescription,
productionOrderNo: item.productionOrderNo,
category: item.category,
//see how transform the data to creaate the array "levels"
levels: [
{
level1: item.level1,
level2: item.level2,
level3: item.level3,
},
],
};
this.addItem(this.createLevelItem(data));
}
}
}
And your function cteateLevelItem:
createLevelItem(data) {
const formgrp = this.fb.group({
skuid: ['', Validators.required],
...
//see how levels is a formArray, at first with an unique element
//we use the before function createLevel()
levels: this.fb.array([this.createLevel()]),
});
if (data !== null) {
formgrp.patchValue(data);
}
return formgrp;
}
A complex part know is the .html, but I want to show how create a series of dropdown mutually exclusive. It's like this SO, but in this case we are going to have an array of arrays of arrays. Don't worry, make it slow is not so difficult as looks like
First we have a lsit of possibles levels and declare an empty array
allLevels = ['Level 1', 'Level 2', 'Level 3', 'Level 4', 'Level 5'];
levelList: any[] = [];
Then we are going to fill the array levelList. The adecuate place is in the function "createLevel". In this function we are subscribe to the valueChanges of the group. but we need know the index if the level. So the function becomes like
createLevel(index: number) {
//we are going to create some like
//levelList[i][j][0], levelList[i][j][1], levelList[i][j][2]
const j = this.orLosstreeForm &&
this.lossFormArray &&
this.lossFormArray.at(index)?
this.getLevels(index).controls.length:0;
const group = this.fb.group({
level1: [''],
level2: [''],
level3: [''],
});
group.valueChanges
.pipe(startWith(null), takeUntil(this.active))
.subscribe((res: any) => {
res = res || { level1: null, level2: null };
if (!this.levelList[index]) this.levelList[index] = [];
this.levelList[index][j] = [];
this.levelList[index][j][0] = this.allLevels;
this.levelList[index][j][1] = this.allLevels.filter(
(x) => x != res.level1
);
this.levelList[index][j][2] = this.allLevels.filter(
(x) => x != res.level1 && x != res.level2
);
//And we check if there any repeat
if (this.levelList[index][j][1].indexOf(res.level2) < 0)
group.get('level2').setValue(null, { emitEvent: false });
if (this.levelList[index][j][2].indexOf(res.level3) < 0)
group.get('level3').setValue(null, { emitEvent: false });
});
return group;
}
Puff, I know is too large, but be patient, the last part is the .html
<div style="background-color: white;">
<form [formGroup]="orLosstreeForm" *ngIf="orLosstreeForm">
...
<!-- Start of OR loss tree table -->
<div class="selection-area" formArrayName="factORLossTree">
<div
*ngFor="let levelItem of lossFormArray.controls;let i=index" [formGroupName]="i"
>
<ng-container>
...here we can use getItemValue(i).skuDescription
or getItemValue(i).skuid or ...
<div class="deviations" formArrayName="levels">
<div class="row" *ngFor="let group of getLevels(i).controls;
let j=index" [formGroupName]="j">
<div class="col-md-3">
<mat-form-field>
<mat-label>Level 1</mat-label>
<mat-select
name="level1"
formControlName="level1"
>
<mat-option value="">--Select--</mat-option>
<mat-option
*ngFor="let item of levelList[i][j][0]"
[value]="item"
>
{{ item }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-md-3">
<mat-form-field>
<mat-label>Level 2</mat-label>
<mat-select
name="level2"
formControlName="level2"
>
<mat-option value="">--Select--</mat-option>
<mat-option
*ngFor="let item of levelList[i][j][1]"
[value]="item"
>
{{ item }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-md-3">
<mat-form-field>
<mat-label>Level 3</mat-label>
<mat-select name="level3"
formControlName="level3"
>
<mat-option value="">--Select--</mat-option>
<mat-option
*ngFor="let item of levelList[i][j][2]"
[value]="item"
>
{{ item }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-md-3">
<button
class="resetButton"
aria-describedby="Reset SKU"
(click)="getLevels(i).push(createLevel(i))"
>
Add Level
</button>
</div>
</div>
....
</div>
</form>
</div>
Se how to add a new group of levels we only need the
<button (click)="getLevels(i).push(createLevel(i))">
Add Level
</button>
The final stackblitz
Scenario:
Displaying the expansion panel name and content Dynamically using Angular material expansion panel using JSON.
Issue:
I able to place the Panel name Dynamically but for the content, I need to check the JSON in that I have a type key so based on type I need to push that particular functionality that particular div.
JSON:
[{
module: "Person Details",
type: 'table',
moduleData: [
{
"firstName":"MaxRunner",
"ID":"12345",
}
]
},
{
module: "Current Location",
type: 'horizontal-stepper',
CurrentData: [
{
"step":1,
"description":"Philips",
"status":true
},
{
"step":2,
"description":"Philips",
"status":true
},
{
"step":3,
"description":"Philips",
"status":false
}
]
}
];
here based on type key and I am pushing the moduledata key to the div present what I did was like
I manually gave the key names but suppose in future in future if I have objects then I manually cannot set each name in div so is there any dynamically way to do this?
what I did
<div *ngIf="module.type==='table'">
<div *ngFor="let moduleKey of getKeys(module.moduleData[0])">
<div > {{ moduleKey }} </div>
<div> {{module.moduleData[0][moduleKey]}} </div>
</div>
</div>
<div style="border-bottom:0;" *ngIf="module.type==='horizontal-stepper'">
<div [ngClass]="'col-xs-3' + getStatusClass(step, index, module.CurrentData)" *ngFor="let step of module.CurrentData; let index = index">
<div><div class="progress-bar"></div></div>
<div >{{step.step}}</div>
<div class="bs-wizard-info text-center" *ngIf="isActive(step.status, index, module.CurrentData)">{{step.description}}</div>
</div>
</div>
IN the code currently, I am implementing a manual way like giving the key name "module.CurrentData"
here I don't want to give name manually like "getStatusClass(step, index, module.CurrentData)"
Here is my stackblitz.
Here is the updated (generic) version: StackBlitz
I have Updated the data to be more generic by adding data property instead of different property name for different types.
Here is the data look like now:
this.data = [{
module: "Person Details",
type: 'table',
data: [
{
"firstName":"MaxRunner",
"ID":"12345",
}
]
},
{
module: "Current Location",
type: 'horizontal-stepper',
data: [
{
"step":1,
"description":"Philips",
"status":true
},
{
"step":2,
"description":"Philips",
"status":true
},
{
"step":3,
"description":"Philips",
"status":false
}
]
}
];
I have updated the template code and now you only need to pass data instead of different names for different types.
<mat-accordion>
<mat-expansion-panel *ngFor="let module of data">
<mat-expansion-panel-header>
{{module.module}}
</mat-expansion-panel-header>
<div *ngIf="module.type==='table'">
<div *ngFor="let moduleKey of getKeys(module.data[0])">
<div > {{ moduleKey }} </div>
<div> {{module.data[0][moduleKey]}} </div>
</div>
</div>
<div style="border-bottom:0;" *ngIf="module.type==='horizontal-stepper'">
<div [ngClass]="'col-xs-3' + getStatusClass(step, index, module.data)" *ngFor="let step of module.data; let index = index">
<div><div class="progress-bar"></div></div>
<div >{{step.step}}</div>
<div class="bs-wizard-info text-center" *ngIf="isActive(step.status, index, module.data)">{{step.description}}</div>
</div>
</div>
</mat-expansion-panel>
I am not sure this will help you. But in angular, there is a way to iterate over the key-value pairs using keyValuePipe. With this, you will get the key without hardcoding. For example,
<ng-container *ngFor="let module of modules">
<div *ngFor="let moduleKey of module|keyvalue">
<!--you can have dynamic key such as moduledata|currentData etc-->
</div>
</ng-container>
get list with Input in my components :
#Input() usersInput: Section[];
export interface Section {
displayName: string;
userId: number;
title: number;
}
and this is the value list :
0:
displayName: "بدون نام"
firstName: null
lastName: null
title: 0
userId: 1
1:
displayName: "محمدامین چهاردولی"
firstName: "محمدامین"
lastName: "چهاردولی"
title: 0
userId: 2
in ngAfterViewInit i set the input Value to users List :
ngAfterViewInit(): void {
this.users = this.usersInput;
if (this.users.length === 0) {
this.show = false;
} else {
this.show = true;
}
}
this is Users :
users: Section[] = [];
and i use it in html List :
<div *ngFor="let item of users" class="d-flex selected-list-items mt-3">
<div class="col-md-5 col-lg-5 col-xs-5 col-sm-5 col-xl-5">
<label>{{item.displayName}}</label>
</div>
<div class="col-md-5 col-lg-5 col-xs-5 col-sm-5 col-xl-5">
<label> {{ getEnumTranslate(item.title)}}</label>
</div>
<div class="justify-content-center col-md-2 col-lg-2 col-xs-2 col-sm-2 col-xl-2">
<button (click)="deleteUser(item.userId)" mat-button>
<mat-icon aria-label="Delete" color="accent">delete</mat-icon>
</button>
</div>
</div>
now when i need to use delete button :
<button (click)="deleteUser(item.userId)" mat-button>
<mat-icon aria-label="Delete" color="accent">delete</mat-icon>
</button>
ts :
deleteUser(id: number): void {
let userModel = {} as Section;
userModel = this.users.find(x => x.userId === id);
const index = this.users.indexOf(userModel);
this.users.splice(index, 1);
this.emitValueModel.push(
{
title: this.user.title,
userId: this.user.userId
}
);
this.selectedUserId.emit(this.emitValueModel);
if (this.users.length === 0) {
this.show = false;
}
this.cdref.detectChanges();
}
it show me this error :
ERROR TypeError: Cannot delete property '1' of [object Array]
whats the problem??? how can i solve that ?
I've faced the same issue and according to this article the problem is that user array has non configurable properties. I suppose angular Inputs are set as non configurable. When you do:
this.users = this.usersInput
you simply pass the reference of input to this.users.
The solution is to simply copy input array before splicing. In your case:
this.users = [...this.usersInput];
Btw. do it in deleteUser method instead of afterViewInit with local variable. You do not need two class props referring to the same object.
I ran across this issue in my React app, but assume the same issue is occurring here, in Angular. The reason for the error was that I was making a shallow copy via the JS spread operator.
const newArr = [...oldArr];
newArr.splice(1) // Remove all items except for the first one.
// This would throw the error `Cannot delete property '1' of [object Array]`
The solution was to install lodash.clonedeep and perform a deep clone.
import cloneDeep from "lodash.clonedeep";
const newArr = cloneDeep(oldArr);
newArr.splice(1) // Remove all items except for the first one.
// Success!
Try:
deleteUser(id) {
const index = this.users.findIndex(x => x.userId === id);
this.users.splice(index, 1);
}
Working Demo
This article states what you have to do to solve this. Basically, you have to specify that the element of the index in the array you want to delete is configurable. So you just have to state
Object.defineProperty( array , index you want to delete, { configurable : true } );
Article: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Non_configurable_array_element
StackBlitz
Here is my FormArray (variants):
this.productGroup = this.fb.group({
name: '',
variants: this.fb.array([
this.fb.group({
type: '',
options: ''
})
])
})
I'm using MatChips to store a string array. This array needs to be passed to options:
<div formArrayName="variants" *ngFor="let item of productGroup.controls['variants'].controls; let i = index;">
<div [formGroupName]="i">
<div class="row">
<mat-form-field class="col-12">
<input formControlName="type">
</mat-form-field>
</div>
<div class="row">
<mat-form-field class="col-12">
<mat-chip-list #chipList>
<mat-chip *ngFor="let opt of typesOptions" [selectable]="true"
[removable]="true" (removed)="removeOpt(opt)">
{{opt}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="Conjunto de opções deste Tipo"
formControlName="options"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addOpt($event)">
</mat-chip-list>
</mat-form-field>
</div>
</div>
<div class="row">
Add Variants
Remove Variants
</div>
</div>
Here is the methods:
// Dynamic Methods
addItem(): void {
this.variantsArray = this.productGroup.get('variants') as FormArray;
this.variantsArray.push(this.fb.group({
type: '',
options: ''
}));
}
removeItem(index: number) {
this.variantsArray.removeAt(index);
}
// MatChip Methods
addOpt(item: number, event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
// Add our fruit
if ((value || '').trim()) {
this.typesOptions.push(value.trim());
}
// Reset the input value
if (input) {
input.value = '';
}
}
removeOpt(opt: string): void {
const index = this.typesOptions.indexOf(opt);
if (index >= 0) {
this.typesOptions.splice(index, 1);
}
I'm successfully adding dynamic fields to my variants formArray. However MatChipList is the same for every dynamic field. I need to make MatChipList dynamic as well. Is there a way to achieve this? Like changing <mat-chip-list #chipList+i> or something like that.
EDIT: StackBlitz
I'm not sure the dom reference variable #chiplist is the issue. It looks like the matChipList is backed by the typesOptions array, but you only have one array. So every time you add a matChipList component it is still being backed by the same array as all the others. You need to have an array of typesOptions, an array of arrays. Then when you addItem, you also push a new sub array to typesOptions (and similarly remove one for removeItem).
I haven't coded this up, just a suggestion from looking at the code.
Edit - coded up a solution based on James's stackblitz.
https://stackblitz.com/edit/angular-3od6rd-jkidxf
Note I haven't looked in detail at how the delete variant holds together, ideally I'd probably like to use a key/value pair to track the variant options using the dom input element id as the key (which is in the MatChipInputEvent), instead of relying on the outer loop index.
Some code from the stackblitz:
export class ChipsOverviewExample {
productGroup: FormGroup;
variantsArray: FormArray;
typesOptionsArray: string[][] = [];
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.productGroup = this.fb.group({
name: '',
variants: this.fb.array([
this.fb.group({
type: '',
options: ''
})
]),
});
this.typesOptionsArray.push([]);
}
saveProduct(form: FormGroup) {
console.log(form);
}
// Add new item to FormArray
addItem(): void {
this.variantsArray = this.productGroup.get('variants') as FormArray;
this.variantsArray.push(this.fb.group({
type: '',
options: ''
}));
this.typesOptionsArray.push([]);
}
removeItem(index: number) {
this.variantsArray.removeAt(index);
}
addOpt(event: MatChipInputEvent, index: number): void {
const input = event.input;
const value = event.value;
// Add our fruit
if ((value || '').trim()) {
this.typesOptionsArray[index].push(value.trim());
}
// Reset the input value
if (input) {
input.value = '';
}
}
removeOpt(opt: string, index: number): void {
const optIndex = this.typesOptionsArray[index].indexOf(opt);
if (optIndex >= 0) {
this.typesOptionsArray[index].splice(optIndex, 1);
}
}
}
try to make the formGroup as a new component and input formGroup to it(not formGroupName).
<div formArrayName="variants" *ngFor="let item of productGroup.controls['variants'].controls; let i = index;">
<variant [varientGroup]="item"><varient>
<div class="row">
Add Variants
Remove Variants
</div>
</div>
varient component.html
<div [formGroup]="varientGroup">
<div class="row">
<mat-form-field class="col-12">
<input formControlName="type">
</mat-form-field>
</div>
<div class="row">
<mat-form-field class="col-12">
<mat-chip-list #chipList>
<mat-chip *ngFor="let opt of typesOptions" [selectable]="true"
[removable]="true" (removed)="removeOpt(opt)">
{{opt}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="Conjunto de opções deste Tipo"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addOpt($event)">
</mat-chip-list>
</mat-form-field>
</div>
</div>
in varient.component.ts
#Input()varientGroup: FormGroup
This is my object:
const ELEMENT_DATA: Element[] = [
{id: 1, name: 'Hydrogen', symbol: 'H'},
{id: 2, name: 'Hydrogen', symbol: 'H1'},
{id: 3, name: 'Helium', symbol: 'He'}
];
which is displayed in datatable with Edit buttons, like so:
When I click Edit (for example I clicked Hydrogen) it should populate with
name: 'Hydrogen', symbol: 'H'.
But now I am getting the Symbol List dropdown empty.
Demo
When I click the Add button, a pop up will come with two dropdowns: Element List and Symbol List. Based on the Element name Symbol List will come.
Now when I click the Edit button in datatable, that should populate that particular row in the popup. How can I do this?
html
<form [formGroup]="addTaskForm" (ngSubmit)="save()" >
<mat-form-field>
<mat-select formControlName="name" placeholder="Element List" (selectionChange)="elementSelectionChange($event)">
<mat-option *ngFor="let element of Elements" [value]="element.name">
{{ element.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-select formControlName="symbol" placeholder="Symbol List">
<mat-option *ngFor="let element of selectedElementSymbol" [value]="element.symbol">
{{ element.symbol }}
</mat-option>
</mat-select>
</mat-form-field>
<div mat-dialog-actions>
<button mat-button (click)="onNoClick()">Cancel</button>
<button type="submit" mat-button cdkFocusInitial>Add</button>
</div>
</form>
You shoule update datasource after each value added from the dialog, change your save() function like this:
save() {
const data = this.data.originalform.dataSource.data;
this.addTaskForm.value["id"]=data.length+1
data.push(this.addTaskForm.value);
this.data.originalform.dataSource=new MatTableDataSource<Element>(data);
console.log('working');
console.log(this.addTaskForm.value);
}
And to not forget to add a referer to the parent object originalform from this:
let dialogRef = this.dialog.open(DialogOverviewExampleDialog, {
width: '250px',
data: { element: element,originalform:this }
});
Hope this helps:)
Edit:
By call of this comment, i updated the fiddle in the following to fit the paginating in.
Adding paginator figures as following:
this.data.originalform.dataSource.paginator=this.data.originalform.paginator;
The symbol list is initialized in the method elementSelectionChange, but
you are calling elementSelectionChange($event) only when the Element list selection changes.
<mat-select formControlName="name" placeholder="Element List" (selectionChange)="elementSelectionChange($event)">
One way you can do it is,
elementSelectionChange(event) {
this.loadSymbols(event.value);
}
loadSymbols(name) {
let value = this.Elements.find(e => e.name === name);
this.selectedElementSymbol = this.Symbols.filter(e => e.id === value.id);
console.log(this.selectedElementSymbol);
}
and then call loadSymbols in the constructor
constructor() {
if (data.element) {
this.name = data.element.name;
this.symbol = data.element.symbol;
this.loadSymbols(this.name);
}
}
StackBlitz