Getting input from a reactive form inside a ngFor ,I have to display "correct" or "incorrect" based on the comparison of the user answer with the exercise.question.answer value.
My idea is to create a boolean reactively but I'm struggling with the execution. I'm not being able to compare index[x] of array a with index [x] of array b every time these arrays are created.
This is the template:
<form
fxLayout="column"
fxLayoutGap="2px"
[formGroup]="exerciseForm"
(ngSubmit)="onSubmit(exerciseForm.value)"
>
<ul *ngFor="let exercise of exercises">
<li>{{ exercise.instruction }}</li>
<ul *ngFor="let question of exercise.questions; let i = index">
<li>
{{ question.prefix }}
<mat-form-field>
<input
name="answer"
type="text"
id="answer"
matInput
[formControlName]="question.id"
/>
</mat-form-field>
{{ question.sufix }} -
{{ question.translation }}
<div *ngIf="isAnswered">
<div *ngIf="isCorrect"><p>Correct</p></div>
<div *ngIf="!isCorrect"><p>Incorrect</p></div>
</div>
</li>
</ul>
</ul>
<button type="submit" mat-raised-button color="primary">Submit</button>
</form>
this is the ts (it contains some of the methods I've been attempting)
export class ExerciseTestsComponent implements OnInit {
exerciseForm: FormGroup;
isAnswered: boolean;
isCorrect: boolean;
exercises: Exercise[] = [
new Exercise("Answer this question", [
new Question(1, "Eu", "maluco", "I am crazy", "sou"),
new Question(2, "Eu", "doidinho", "I am cuckoo", "estou")
])
];
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.createGroup();
}
getAnswersArray() {}
createGroup() {
this.exerciseForm = this.fb.group({});
this.exercises[0].questions.forEach(control =>
this.exerciseForm.addControl(control.id.toString(), this.fb.control(""))
);
}
onSubmit(answer: TestAnswer) {
this.isAnswered=true;
//** 1
let answers = [];
let answersInput = [];
this.exercises[0].questions.forEach(pergunta => {
//** 2
answers.push(pergunta.answer);
console.log(answers);
});
//** 3
var bosta = Object;
bosta = this.exerciseForm.value;
console.log(bosta[1]);
if ((answers[1] = bosta[1])) {
console.log("pqp");
}
let incoming = this.exerciseForm.value;
for (var o in incoming) {
answersInput.push(incoming[o]);
console.log(answersInput);
}
answersInput.forEach(a1 =>
answers.forEach(a2 => {
if (a1 === a2) {
console.log("yay");
} else {
console.log("boo");
}
})
);
}
}
//** for every object created, I have to check if answer = */
stackblitz:
https://stackblitz.com/edit/angular-dzzzql
Then when you submit, you can compare both answers
onSubmit(answer: Answer) {
let answers = [];
console.log(this.exercises)
let answersInput = []
this.exercises[0].questions.forEach((pergunta, index) => {
answers.push(pergunta.answer)
console.log(answers)
return answers
})
let i = 0;
for (const field in this.exerciseForm.controls) { // 'field' is a string
console.log(this.exerciseForm.controls[field].value == answers[i]);
i++;
}
}
Working demo in Stackblitz
Related
I'm getting the well known error in my Angular app, but not sure why it happens and how to fix it. I was trying a couple of ways including adding setTimeout, delay(0), switching to different hook but any of them seems to work in my case.
Problem description:
I have a list of products and on click single product can be added to the cart with selected products
//product.list.component.ts
addToProductCart(product: IProduct) {
this.productsService.addProductToSelectedProducts(product);
}
The service looks like below:
//product.service.ts
#Injectable({
providedIn: 'root'
})
export class ProductsService {
selectedProducts: BehaviorSubject<IProduct[]> = new BehaviorSubject<IProduct[]>([]);
product = this.selectedProducts.asObservable();
constructor(private http: HttpClient) { }
getProductsList(): Observable<IProduct[]> {
return this.http.get<IProduct[]>(`${environments.environment.baseUrl}/products`);
}
patchProductLikes(id: number, payload: Partial<IProduct>): Observable<number> {
return this.http.patch<number>(`${environments.environment.baseUrl}/products/${id}`, payload);
}
addProductToSelectedProducts(product: IProduct) {
this.selectedProducts.next([...this.selectedProducts.value, product]);
}
clearSelectedProducts(): void {
this.selectedProducts.next([]);
}
removeSelectedProduct(products: IProduct[]): void {
this.selectedProducts.next(products);
}
}
When product is selected on my header the product count is increased and displayed on cart icon:
//header.component.html
<span (click)="openDialog()" #openCartButton>
<mat-icon matBadge="{{selectedProductsCount}}"matBadgePosition="above after">
shopping_cart
</mat-icon>
</span>
//header.component.ts
openDialog() {
this.dialog.open(CartDetailsComponent, {
width: '450px',
height: '650px',
data: {
positionRelativeToElement: this.openCartButton
}
});
}
getSelectedProductsCount(): void {
this.productsService.product.subscribe((products) => {
this.selectedProductsCount = products.length;
});
}
If header cart icon is clicked the dialog with selected product is opened, and if there are no selected products then empty cart placeholder should be displayed:
//cart-details.component.html
<div *ngIf="products.length > 0 else emptyCart">
<h5 mat-dialog-title>Total order</h5>
<div mat-dialog-content class="product" [#loadProducts]="'in'">
<ul>
<li *ngFor="let groupedProducts of selectedProducts | keyvalue" class="product__product-item">
<div *ngFor="let prod of groupedProducts.value | productPipe; let i = index" class="product-details-container">
<div>
<img [src]="prod.image" alt="Product photo" class="product-details-container__product-img">
</div>
<div class="product-info">
<p>{{prod.name}}
<span class="product-info__price">${{prod.price}}</span>
</p>
<p>
{{prod.productMaterial}}
</p>
<p>
{{prod.color}}
</p>
<p #deleteProduct>Amount: {{groupedProducts.value.length}} </p>
<p>Total: ${{prod.price * groupedProducts.value.length}}</p>
<div class="product-actions-container">
<a (click)="deleteProduct(prod)" class="delete-product">Delete</a>
<a (click)="viewProductDetails(prod)" class="view-details">View details</a>
</div>
</div>
</div>
</li>
<span>SUM: ${{totalSum}}</span>
</ul>
</div>
</div>
<ng-template #emptyCart>
<div class="empty-bag-container">
<mat-icon svgIcon="empty-bag" class="empty-bag-container__empty-bag-icon"></mat-icon>
<h4 class="empty-bag-container__empty-bag-heading">
YOUR BAG IS EMPTY
</h4>
<span class="empty-bag-container__empty-bag-details"> Looks like you haven’t made your choice yet.
Check out 100+ styles for everyone!</span>
</div>
</ng-template>
//cart-details.component.ts
export class CartDetailsComponent implements OnInit, OnDestroy {
private positionRelativeToElement: ElementRef;
isOpen = false;
totalSum = 0;
totalPrices: number[] = [];
private destroySubject: Subject<boolean> = new Subject<boolean>();
selectedProductsCount: number;
selectedProducts: Record<string, IProduct[]>;
productSumPrice: number;
products: IProduct[] = [];
constructor(public dialogRef: MatDialogRef<CartDetailsComponent>,
private productsService: ProductsService,
#Inject(MAT_DIALOG_DATA) public data: { positionRelativeToElement: ElementRef }) {
this.positionRelativeToElement = data.positionRelativeToElement;
}
ngOnInit() {
const matDialogConfig = new MatDialogConfig();
const rect: DOMRect = this.positionRelativeToElement.nativeElement.getBoundingClientRect();
matDialogConfig.position = { right: `10px`, top: `${rect.bottom + 2}px` };
this.dialogRef.updatePosition(matDialogConfig.position);
this.getSelectedProducts();
this.calculatePrices();
}
ngOnDestroy() {
this.destroySubject.next(true);
}
close() {
this.dialogRef.close();
}
deleteProduct(product: IProduct) {
const prodId: number = product.id;
this.selectedProducts[prodId] = this.selectedProducts[prodId].slice(0, this.selectedProducts[prodId].length - 1);
const index: number = this.products.map(x => {
return x.id;
}).indexOf(product.id);
this.products.splice(index, 1);
this.productsService.removeSelectedProduct(this.products);
this.calculatePrices();
}
viewProductDetails(product: IProduct): void {
console.log(product);
}
animateCurrentItem(product: IProduct) {
console.log(product, 'animation');
}
calculatePrices() {
if (this.products.length > 0) {
this.totalPrices = [];
Object.values((this.selectedProducts))
.map((prod) => {
if (prod.length > 0) {
(prod as IProduct[]).map((p) => {
this.totalPrices.push(Number(p.price));
});
}
});
if (this.totalPrices.length > 0) {
this.totalSum = this.totalPrices.reduce((prev, cur) => {
return prev + cur;
});
} else {
this.totalSum = 0;
this.productsService.clearSelectedProducts();
}
}
}
getSelectedProducts() {
this.productsService.product
.pipe(
delay(0),
startWith([]),
takeUntil(this.destroySubject),
)
.subscribe((products) => {
if (products.length > 0) {
this.products = products;
this.productSumPrice = _.sumBy(products, (prod) => parseFloat(prod.price));
this.selectedProductsCount = _.sum(Object.values(_.countBy(products, product => product.id)));
this.selectedProducts = _.groupBy(products, 'id');
}
});
}
}
And here the error occurs. If cart is empty (meaning products.length === 0) the <ng-template #emptyCart> is displayed but with the error:
ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'loading-background: false'. Current value: 'loading-background: true'.
The error is about loading-background in ngx-ui-loader lib witch I use in app.module:
//app.module
(...)
import { NgxUiLoaderModule, NgxUiLoaderHttpModule, NgxUiLoaderConfig, SPINNER, POSITION, PB_DIRECTION } from 'ngx-ui-loader';
imports: [
...
NgxUiLoaderModule.forRoot(ngxUiLoaderConfig),
NgxUiLoaderHttpModule,
]
Any idea what cause the issue and how to fix it and avoid in the future?
I was traying to reproduce it on stackblitz but with no luck :). Although maybe it will help understand my issue ;P
https://stackblitz.com/edit/angular-h3xyop?file=src%2Fapp%2Fproduct-list%2Fproduct-list.component.ts
This is because of your view changed after rendering. You need to use changeDetectorRef to detechChanges. add in constructor
construct(private ref: changeDetectorRef)
{}
and after change you add
this.ref.detectChanges();
https://angular.io/api/core/ChangeDetectorRef
I have from group "additionalForm" in that i have formArray called "validations"
here the formArray is dynamic which is binding the values to validtionsField array. In the validtionsField array i have three objects in which i have two values which needs to be compared and they are Min-length and Max-Length.
ex. If i enter min length greater then the max length it should give error.
here is code for the above functionality
import {
Component,
OnInit,
Inject
} from "#angular/core";
import {
FormControl,
FormArray,
FormGroup,
FormBuilder,
Validators,
AbstractControl,
ValidationErrors,
NgControlStatus,
ValidatorFn
} from "#angular/forms";
import {
MatDialogRef,
MAT_DIALOG_DATA,
MatSnackBar
} from "#angular/material";
#Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"]
})
export class AppComponent implements OnInit {
validtionsField = [{
validField: "Min Length",
type: false,
fieldType: "input",
value: 1,
keyval: "minLength"
},
{
validField: "Max Length",
type: false,
fieldType: "input",
value: 50,
keyval: "maxLength"
},
{
validField: "DataType",
type: false,
fieldType: "dropDown",
dataTypeList: [],
dataTypeId: "minLength",
keyval: "dataTypeId",
value: 874
}
];
dataType = [{
id: 3701,
localeId: 1,
tenantId: 1,
parentCategoryId: null,
parentContentId: 873,
name: "Alphabets",
description: null
},
{
id: 3702,
localeId: 1,
tenantId: 1,
parentCategoryId: null,
parentContentId: 874,
name: "Alphanumeric",
description: null
}
];
additionalForm: FormGroup = this.fb.group({
fieldName: ["", [Validators.required]],
validations: this.fb.array([])
});
constructor(public fb: FormBuilder) {}
ngOnInit() {
let frmArray = this.additionalForm.get("validations") as FormArray;
for (let data of this.validtionsField) {
frmArray.push(this.initSection(data));
}
}
initSection(data) {
return this.fb.group({
validField: [data.validField, [Validators.required]],
type: [data.type, [Validators.required]],
value: [data.value, [Validators.required]],
dataTypeList: [this.dataType, [Validators.required]],
fieldType: [data.fieldType, [Validators.required]],
validArray: []
}, {
validator: this.customValidator
});
}
checkFieldType(data): any {
return data === "dropDown";
}
// trying to access using below functioa to compare values min and max length
public customValidator(control: AbstractControl): ValidationErrors | null {
const newValue = control.get("value") ? control.get("value").value : null;
const values = control.get("value") ? control.get("value").value : [];
console.log("1 " + newValue);
console.log(values);
for (let i = 0, j = values.length; i < j; i++) {
if (newValue === values[i]) {
return {
duplicate2: true
};
}
}
return null;
}
}
<form [formGroup]="additionalForm">
<mat-form-field>
<input formControlName='fieldName' placeholder="Field Name" required matInput>
</mat-form-field>
<div class="row">
<div class="col-md-12 col-sm-12">
\
<div formArrayName="validations">
<ng-container *ngFor="let validationForm of additionalForm.controls.validations.controls; let i = index">
<div class="valid-data" [formGroupName]="i">
<span>
<label>{{validationForm.value.validField }}</label>
</span>
<span>
<ng-container *ngIf="checkFieldType(validationForm.value.fieldType ); else input">
<mat-form-field class="select-dataType">
<mat-select required formControlName='value' placeholder="Datatype">
<mat-option *ngFor="let fieldTypeData of validationForm.value.dataTypeList"
[value]='fieldTypeData.parentContentId'>
{{fieldTypeData.name}}</mat-option>
</mat-select>
</mat-form-field>
</ng-container>
<ng-template #input>
<mat-form-field>
<input required formControlName='value' pattern= "[0-9]+" matInput>
</mat-form-field>
</ng-template>
</span>
<div *ngIf="validationForm.get('value')?.touched ">
<div class="error" *ngIf="validationForm.get('value').hasError('required')">
{{validationForm.value.validField}} is required
</div>
</div>
</div>
</ng-container>
</div>
</div>
</div>
</form>
above is the TS and HTML code and below is the function which i am trying to get old and new value from control but its failing, its giving me value from same input field from same min-length
/// trying this below functio to compare the min and max length.
public customValidator(control: AbstractControl): ValidationErrors | null {
const newValue = control.get('value') ? control.get('value').value : null;
const values = control.get('value') ? control.get('value').value : [];
console.log("1 " + newValue);
console.log(values);
for (let i = 0, j = values.length; i < j; i++) {
if (newValue === values[i]) {
return {
'duplicate2': true
};
}
}
return null;
}
Please help me to compare values from dynamic form array, and here what ever values enters to from-array object are all bind to formcontrolName "value"
here is the link for code :
https://stackblitz.com/edit/angular6-material-components-demo-wgtafn
Since you have two fields minLength & maxLength which validation depends on each other, you can add a validator to the parent group and use a custom ErrorStateMatcher to translate the parent group errors to the children. I also used a FormGroup instead of FormArray, in this case it's more convenient.
#Component({...})
export class AppComponent {
...
readonly invalidLengthMatcher: ErrorStateMatcher = {
isErrorState: () => {
const control = this.additionalForm.get('validations');
return control.hasError('invalidLength');
}
};
readonly controlFields = this.validtionsField.map(field => ({
field,
control: new FormControl(field.value, Validators.required),
errorMatcher: this.errorMatcherByFieldId(field.keyval)
}));
private readonly controlMap = this.controlFields.reduce((controls, controlField) => {
controls[controlField.field.keyval] = controlField.control;
return controls;
}, {});
readonly additionalForm = new FormGroup({
fieldName: new FormControl("", [Validators.required]),
validations: new FormGroup(this.controlMap, {
validators: (group: FormGroup) => {
const [minLength, maxLength] = ['minLength', 'maxLength'].map(fieldId => {
const control = group.get(fieldId);
return Number(control.value);
});
if (minLength > maxLength) {
return {
'invalidLength': true
};
} else {
return null;
}
}
})
});
private errorMatcherByFieldId(fieldId: string): ErrorStateMatcher | undefined {
switch (fieldId) {
case 'minLength':
case 'maxLength':
return this.invalidLengthMatcher;
}
}
}
<form [formGroup]="additionalForm">
<mat-form-field>
<input formControlName='fieldName' placeholder="Field Name" required matInput>
</mat-form-field>
<div class="row">
<div class="col-md-12 col-sm-12">
<div formGroupName="validations" >
<div *ngFor="let controlField of controlFields" class="valid-data">
<span>
<label>{{controlField.field.validField}}</label>
</span>
<span [ngSwitch]="controlField.field.fieldType">
<mat-form-field *ngSwitchCase="'dropDown'" class="select-dataType">
<mat-select required placeholder="Datatype" [formControlName]="controlField.field.keyval">
<mat-option *ngFor="let fieldTypeData of dataType"
[value]='fieldTypeData.parentContentId'>{{fieldTypeData.name}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngSwitchCase="'input'">
<input matInput
required
type="number"
pattern= "[0-9]+"
[formControlName]="controlField.field.keyval"
[errorStateMatcher]="controlField.errorMatcher">
</mat-form-field>
</span>
...
</div>
</div>
</div>
</div>
</form>
StackBlitz
You had to attach this validator to form array so that you can have access to all controls.
My Custom Validator :-
export function customValidateArray(): ValidatorFn {
return (formArray:FormArray):{[key: string]: any} | null=>{
console.log('calling');
let valid:boolean=true;
setTimeout(()=>console.log(formArray),200);
let minIndex = Object.keys(formArray.controls).findIndex((key) => formArray.controls[key].value.validField.toLowerCase()==="min length");
let minLengthValue = formArray.controls[minIndex].value.value;
let maxLengthValue = formArray.controls[Object.keys(formArray.controls).find((key) => formArray.controls[key].value.validField.toLowerCase()==="max length")].value.value;
return minLengthValue < maxLengthValue ? null : {error: 'Min Length Should be less than max length', controlName : formArray.controls[minIndex].value.validField};
}
};
I added it to form array ngOnInit of your code :-
ngOnInit() {
let frmArray = this.additionalForm.get("validations") as FormArray;
for (let data of this.validtionsField) {
frmArray.push(this.initSection(data));
}
frmArray.setValidators(customValidateArray());
}
Use it in template like :-
<div class="error" *ngIf="additionalForm.controls.validations.errors && validationForm.value.validField === additionalForm.controls.validations.errors.controlName">
{{additionalForm.controls.validations.errors.error}}
</div>
Working stackblitz :-
https://stackblitz.com/edit/angular6-material-components-demo-p1tpql
Individual filtering does not come out the box for Swimlanes ngx-datatable. There are not too many solutions out there. I have been working on filtering the column via checkboxes. If the checkbox is clicked the matching property stays visable and all other properties hide from view.
The issue I am getting is when I select multiple checkboxes (more than one) no results appear, my filters only find an individual match I think and having two checked does not satisfy the filter.
e.g. if I select "female" a female gets filtered out. Then if I select "male" the desired result is that both "female" and "male" should be shown.
DEMO - https://codesandbox.io/s/swimlane-filter-table-kjiyl
import { Component, ViewChild, TemplateRef } from "#angular/core";
#Component({
selector: "filter-demo",
template: `
<div>
<h3>
Client-side Search and Filtering
</h3>
<ngx-datatable
#table
class="material"
[columns]="columns"
[columnMode]="'force'"
[headerHeight]="100"
[footerHeight]="50"
[rowHeight]="'auto'"
[limit]="10"
[rows]="rows"
>
</ngx-datatable>
<ng-template #hdrTpl let-column="column" let-sort="sortFn">
<div (click)="sort()">{{ column.name }}</div>
<div>
<label class="small m-0 datatable-checkbox">
<input
type="checkbox"
[(ngModel)]="isFilterFemale"
(change)="doFilter()"
[ngModelOptions]="{ standalone: true }"
/>
Female
</label>
</div>
<div>
<label class="small m-0 datatable-checkbox">
<input
type="checkbox"
[(ngModel)]="isFilterMale"
(change)="doFilter()"
[ngModelOptions]="{ standalone: true }"
/>
Male
</label>
</div>
</ng-template>
</div>
`
})
export class FilterBarComponent {
// #ViewChild(DatatableComponent, { static: false }) table: DatatableComponent;
#ViewChild("hdrTpl", { static: true }) hdrTpl: TemplateRef<any>;
rows = [];
temp = [];
columns = [];
isFilterFemale = false;
isFilterMale = false;
constructor() {
this.fetch(data => {
// cache our list
this.temp = [...data];
// push our inital complete list
this.rows = data;
});
}
ngOnInit() {
this.columns = [
{ name: "name", prop: "name", headerTemplate: this.hdrTpl }
];
this.doFilter();
}
fetch(cb) {
const req = new XMLHttpRequest();
req.open("GET", "assets/data.json");
req.onload = () => {
cb(JSON.parse(req.response));
};
req.send();
}
private doFilter() {
this.rows = this.temp
.filter(x => this.femaleFilter(x.classification.code))
.filter(x => this.maleFilter(x.classification.code));
}
private femaleFilter(classificationCode) {
if (!this.isFilterFemale) {
return true;
} else {
if (classificationCode === "FMALE") {
return true;
} else {
return false;
}
}
}
private maleFilter(classificationCode) {
if (!this.isFilterMale) {
return true;
} else {
if (classificationCode === "MALE") {
return true;
} else {
return false;
}
}
}
}
First of all I want to show countries with this pipe and filter them with inserted characters and I need the ISO codes to show the flags of the countries. The problem is that I want to use a library which has all countries with ISO codes and stuff. This has the key value form.
First of all I export this data to var to be able to use the data.
export var indexedArray: { [key: string]: string }
countryStuff: Country; //currently not used
countries = [] as Array<string>
filteredCountries: Observable<string[]>;
export interface Country { //currently not used
[key: string]: string
}
ngOnInit() {
this.startDate.setFullYear(this.startDate.getFullYear() - 18);
this.buildForm();
this.filteredCountries = this.personalForm.controls['country'].valueChanges
.pipe(
startWith(''),
map(value => this._filter(value))
);
i18nIsoCountries.registerLocale(require("i18n-iso-countries/langs/en.json"));
i18nIsoCountries.registerLocale(require("i18n-iso-countries/langs/de.json"));
this.currentLanguage = this.translateService.currentLang;
indexedArray = i18nIsoCountries.getNames(this.currentLanguage);
for (let key in indexedArray) {
let value = indexedArray[key];
this.countries.push(value);
}
}
In the html I can use this like this:
<mat-option *ngFor="let item of countryStuff | keyvalue:keepOriginalOrder" [value]="item.key">
Key: <b>{{item.key}}</b> and Value: <b>{{item.value}}</b>
</mat-option>
The normal way I can also use but completly without the key value way and just like the Angular examples say (without the TS logic):
<mat-option *ngFor="let option of filteredCountries | async" [value]="option">
<span class="flag-icon flag-icon-de flag-icon-squared"></span>
{{option}}
</mat-option>
That just gives me the full country name like Algeria or something.
I have found an idea here https://long2know.com/2017/04/angular-pipes-filtering-on-multiple-keys/ but I couldn't change it for my purposes. That would be even more perfect if I could filter for key and value so maybe "DE" for key and "Ger" for value of Germany. It does not seem to be possible with existing pipes.
Edit on request (filtering):
private _filter(value: string): string[] {
var filterValue;
if (value) {
filterValue = value.toLowerCase();
} else {
filterValue = "";
}
return this.countries.filter(option => option.toLowerCase().startsWith(filterValue));
}
Also updated the ngOnInit()
I got it working with the i18n library and even flag-icon-css
TypeScript (filter class):
#Pipe({
name: 'filterLanguages'
})
export class FilterLanguages implements PipeTransform {
transform(items: any, filter: any, isAnd: boolean): any {
if (filter && Array.isArray(items)) {
let filterKeys = Object.keys(filter);
if (isAnd) {
return items.filter(item =>
filterKeys.reduce((memo, keyName) =>
(memo && new RegExp(filter[keyName], 'gi').test(item[keyName])) || filter[keyName] === "", true));
} else {
return items.filter(item => {
return filterKeys.some((keyName) => {
return new RegExp(filter[keyName], 'gi').test(item[keyName]) || filter[keyName] === "";
});
});
}
} else {
return items;
}
}
}
HTML:
<mat-form-field class="field-sizing">
<input matInput required placeholder="{{ 'REGISTRATION.COUNTRY' | translate }}" name="country"
id="country" [matAutocomplete]="auto" formControlName="country" [value]="filterText" />
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
<mat-option *ngFor="let item of countries | filterLanguages:{ name: filterText, iso: filterText, flagId: filterText } : false" [value]="item.name">
<span class="flag-icon flag-icon-{{item.flagId}} flag-icon-squared"></span>
{{ item.name }} - {{ item.iso }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
TypeScript (component):
export var countryList: {
[key: string]: string
}
declare const require;
export class YourComponent implements OnInit {
countries = [] as Array<any>
currentLanguage: string;
filterText: string;
ngOnInit() {
i18nIsoCountries.registerLocale(require("i18n-iso-countries/langs/en.json"));
i18nIsoCountries.registerLocale(require("i18n-iso-countries/langs/de.json"));
this.currentLanguage = this.translateService.currentLang;
countryList = i18nIsoCountries.getNames(this.currentLanguage);
this.buildForm();
this.createCountries();
this.personalForm.controls['country']
.valueChanges
.pipe(debounceTime(100))
.pipe(distinctUntilChanged())
.subscribe(term => {
this.filterText = term;
});
}
ngAfterContentChecked() {
this.cdRef.detectChanges();
if (this.currentLanguage != this.translateService.currentLang) {
countryList = i18nIsoCountries.getNames(this.translateService.currentLang);
this.createCountries();
this.currentLanguage = this.translateService.currentLang;
this.personalForm.get('country').updateValueAndValidity();
}
}
createCountries() {
this.countries = [];
for (let key in countryList) {
var countryName: string = countryList[key];
var isoValue: string = key;
var isoLowerValue: string = key.toLowerCase();
this.countries.push({ name: countryName, iso: isoValue, flagId: isoLowerValue })
}
}
You need the i18n library and like in my example a formControl.
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