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;
}
}
}
}
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
This is my method where i try to check from the database which usernames are already taken:
takenUsernames(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>((resolve, reject) => {
this.employeeService.getEmployeeList().subscribe((employees) => {
this.employees = employees;
for (let employee of control.value) {
if (this.employees.indexOf(employee) !== -1) {
resolve({ usernameIsTaken: true });
} else {
resolve(null);
}
}
});
});
return promise;
}
And this is my formGroup object:
this.newProjectForm = new FormGroup({
projectName: new FormControl(null, Validators.required),
description: new FormControl(null),
assignedEmployees: new FormControl(null, [Validators.required]),
employees: new FormArray(
[],
[this.forbidUsernames.bind(this)],
[this.takenUsernames.bind(this)]
),
});
As you can see employees is a FormArray so i loop true control.value to check each username but it doesnt seem to work. The problem is in the takenUsernames function because i dont get the pending status if i inspect the element.
Here's the whole html code:
link
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght#600&family=Roboto+Mono:wght#500&display=swap"
type="text"
rel="stylesheet"
/>
<div class="backdrop" (click)="onFormAlertClose()"></div>
<div class="alert-box">
<form [formGroup]="newProjectForm" (ngSubmit)="onSubmit()">
<h3>New Project Form</h3>
<div class="form-group">
<label for="projectName">Project Name</label>
<input
type="text"
id="projectName"
class="form-control"
[formControlName]="'projectName'"
/>
<!-- if omit [] also omit ''-->
<span
*ngIf="
!newProjectForm.get('projectName').valid &&
newProjectForm.get('projectName').touched
"
>Please enter a project name</span
>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
id="description"
class="form-control"
formControlName="description"
></textarea>
</div>
<div class="form-group">
<label for="assignedEmployees">Assign Employees</label>
<div class="larger-width-divider"></div>
<input type="text" class="search-box" placeholder="Search..." />
<select
name="employees"
class="form-control"
multiple
size="employees.length"
formControlName="assignedEmployees"
>
<ng-container *ngIf="newProjectForm.get('projectName').valid">
<option id="option" *ngFor="let employee of employees">{{
employee.userName
}}</option>
</ng-container>
</select>
<span
*ngIf="
!newProjectForm.get('assignedEmployees').valid &&
newProjectForm.get('assignedEmployees').touched
"
>Please select at least one employee</span
>
</div>
<div>
<button
type="button"
class="add-emp-btn"
*ngIf="!addEmployeeBtnClicked"
(click)="onEmpBtnClicked()"
[disabled]="newProjectForm.invalid"
>
Add Employees
</button>
<ng-container *ngIf="addEmployeeBtnClicked" formArrayName="employees">
<div *ngFor="let employeesControl of getControls(); let i = index">
<label for="userName">Insert one or multiple employee</label>
<input type="text" [formControlName]="i" />
<div class="width-divider"></div>
<button
type="button"
class="goto-add-emp-btn"
(click)="onEmpBtnClicked()"
>
Cancel
</button>
<div>
<span
class="last-span"
*ngIf="
!newProjectForm.get('employees').valid &&
newProjectForm.get('employees').touched
"
>
<span
*ngIf="newProjectForm.get('employees').errors?.nameIsForbidden"
>This name is invalid!</span
>
<span *ngIf="newProjectForm.get('employees').errors?.required"
>Add at least on employee</span
>
<span
*ngIf="newProjectForm.get('employees').errors?.usernameIsTaken"
></span>
</span>
</div>
</div>
</ng-container>
</div>
<div class="height-divider"></div>
<div class="alert-box-actions">
<button type="submit" class="submit" [disabled]="!newProjectForm.valid">
Create
</button>
<div class="width-divider"></div>
<button type="button" (click)="onFormAlertClose()">Cancel</button>
</div>
</form>
</div>
And here's the whole class:
import { Component, Output, OnInit } from "#angular/core";
import { Subject, Observable } from "rxjs";
import { ProjectService } from "src/app/services/project.service";
import { FormGroup, FormControl, Validators, FormArray } from "#angular/forms";
import { IEmployee } from "../entities/employee";
import { EmployeeService } from "src/app/services/employee.service";
#Component({
selector: "app-alert",
templateUrl: "./new-project-form-alert.component.html",
styleUrls: ["./new-project-form-alert.component.css"],
})
export class AlertComponent implements OnInit {
closeNewProjectAlert: Subject<boolean> = new Subject<boolean>();
employees: IEmployee[];
newProjectForm: FormGroup;
addEmployeeBtnClicked = false;
forbiddenUsernames = [
"Admin",
"Manager",
"Developer",
"Guest",
"admin",
"manager",
"developer",
"guest",
];
constructor(
private projectService: ProjectService,
private employeeService: EmployeeService
) {}
ngOnInit() {
this.employeeService.getEmployeeList().subscribe((employees) => {
this.employees = employees;
});
this.newProjectForm = new FormGroup({
// properties are string so when the files is minified they are not destroyed
projectName: new FormControl(null, Validators.required),
description: new FormControl(null),
assignedEmployees: new FormControl(null, [Validators.required]),
employees: new FormArray(
[],
[this.forbidUsernames.bind(this)],
this.takenUsernames
),
});
}
onFormAlertClose() {
this.projectService.closeNewProjectForm$.next(true);
}
onSubmit() {
console.log(this.newProjectForm);
this.newProjectForm.reset();
this.onFormAlertClose();
console.log((<FormArray>this.newProjectForm.get("employees")).controls);
}
onEmpBtnClicked() {
if (!this.addEmployeeBtnClicked) {
this.addEmployeeBtnClicked = true;
const control = new FormControl(null, Validators.required);
(<FormArray>this.newProjectForm.get("employees")).push(control);
} else {
this.addEmployeeBtnClicked = false;
(<FormArray>this.newProjectForm.get("employees")).removeAt(0);
}
}
forbidUsernames(control: FormControl): { [s: string]: boolean } {
for (let employee of control.value) {
if (this.forbiddenUsernames.indexOf(employee) !== -1) {
return { nameIsForbidden: true };
}
}
return null; // this if username is valid, do not return false
}
takenUsernames(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>((resolve, reject) => {
const observable = this.employeeService.getEmployeeList().subscribe((employees) => {
this.employees = employees;
for (let employee of control.value) {
if (this.employees.indexOf(employee) !== -1) {
resolve({ usernameIsTaken: true });
} else {
resolve(null);
}
}
});
});
return promise;
}
getControls() {
return (<FormArray>this.newProjectForm.get("employees")).controls;
}
}
Sorry i know it a big mess, it's my first web app.
Edit: i provided a small video of the form if it helps: https://streamable.com/ua7mcq
Well yep, i have named the formArray as employees because when i submit the form the username or usernames get added to IEmployee objects and saved in the database.
But of the time of typing the usernames they are just string and i was comparing these strings with the employee objects.
So here's it fixed:
takenUsernames(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>((resolve, reject) => {
const observable = this.employeeService.getEmployeeList().subscribe((employees) => {
this.employees = employees;
const usernames = this.employees.map(employee => employee.userName)
console.log(usernames)
for (let employee of control.value) {
if (usernames.indexOf(employee) !== -1) {
resolve({ usernameIsTaken: true });
} else {
resolve(null);
}
}
});
});
return promise;
}
But now i will reduce the input box to be able to add just one employee at time, wasted too much time on these component.
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
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
I have ReviewComponent. In this component I show all reviews and can write a new review. But when I added a new review, all reviews don't refresh and refresh only if I click a link in the component("Write a review" or "Read all reviews"). How to make refresh all reviews after add a new review?
review.component.ts
import { Component, OnInit, ChangeDetectionStrategy, Input } from '#angular/core';
import { FormBuilder } from '#angular/forms';
import { ItemService } from '../../services/item.service';
#Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-review',
templateUrl: './review.component.html',
styleUrls: [ './review.component.css' ]
})
export class ReviewComponent implements OnInit {
#Input() itemId: string;
stars = [];
reviewId: string;
reviews;
commentBox: boolean = false;
reviewsBox: boolean = false;
grades = 0;
commentForm = this.fb.group({
comment: [ '' ],
assessment: [ '1' ]
});
constructor(private fb: FormBuilder, private _itemService: ItemService) {}
ngOnInit() {
this.getReviews();
}
getReviews() {
this._itemService.getReviews(this.itemId).subscribe((data) => {
this.reviews = data.map((review) => {
return {
id: review.payload.doc.id,
...review.payload.doc.data()
};
});
this.reviewId = this.reviews.length + '';
let avgAssess = 0;
this.reviews.forEach((review) => {
avgAssess += parseInt(review.assessment);
});
this.grades = Math.floor(avgAssess / this.reviews.length);
console.log(this.grades);
this.stars = [];
for (let i = 0; i < this.grades; i++) {
this.stars.push('grade');
}
for (let i = this.grades; i < 5; i++) {
this.stars.push('star_border');
}
});
}
showCommentBox() {
this.commentBox = !this.commentBox;
}
showReviewsBox() {
this.reviewsBox = !this.reviewsBox;
}
sendComment() {
let review = {
comment: this.commentForm.get('comment').value,
assessment: this.commentForm.get('assessment').value
};
this._itemService.addReview(this.itemId, this.reviewId, review);
this.showCommentBox();
this.commentForm.patchValue({
comment: [ '' ],
assessment: [ '1' ]
});
}
validPaymentInput(event) {
var char = String.fromCharCode(event.which);
if (!/[1-5]/.test(char)) {
event.preventDefault();
}
}
}
item.service.ts
import { Injectable } from '#angular/core';
import { AngularFirestore } from '#angular/fire/firestore';
#Injectable({
providedIn: 'root'
})
export class ItemService {
constructor(private _firestore: AngularFirestore) {}
addReview(key: string, reviewId, review) {
this._firestore.collection('items').doc(key).collection('reviews').doc(reviewId).set(review);
}
getReviews(key: string) {
return this._firestore.collection('items').doc(key).collection('reviews').snapshotChanges();
}
}
review.component.html
<div class="container">
<div>
<div class="stars" >
<mat-icon *ngFor="let star of stars">{{star}}</mat-icon>
<div>({{reviews.length}})</div>
</div>
<div class="reviews" (click)="showReviewsBox()">
<span>Read all {{reviews.length}} Reviews</span>
</div>
<div class="write" (click)="showCommentBox()">
<span>Write a review</span>
</div>
</div>
<div class="comment-box" *ngIf="commentBox" [formGroup]="commentForm">
<div [ngClass]="{'has-error' : commentForm.get('comment').errors && (commentForm.get('comment').touched || commentForm.get('comment').dirty)}">
<label> Enter a comment</label>
<textarea formControlName="comment" required></textarea>
<div class="error-msg"
*ngIf="commentForm.get('comment').errors && (commentForm.get('comment').touched || commentForm.get('comment').dirty)">
<span *ngIf="commentForm.get('comment').errors.required">
Comment field is required
</span>
</div>
</div>
<div [ngClass]="{'has-error' : commentForm.get('assessment').errors && (commentForm.get('assessment').touched || commentForm.get('assessment').dirty)}">
<label for="">Evaluate from 1 to 5</label>
<input type="text" (keypress)="validPaymentInput($event)" formControlName="assessment" max="5" maxlength="1" required>
<app-button btnText="Submit" [disabled]="!commentForm.valid" (click)="sendComment()"></app-button>
</div>
</div>
<div class="reviews-box" *ngIf="reviewsBox && reviews.length > 0">
<div class="review" *ngFor="let review of reviews">{{ review.comment }}</div>
</div>
</div>
Call getReviews() method inside your sendComment() method. Refer code snippet below:
sendComment() {
let review = {
comment: this.commentForm.get('comment').value,
assessment: this.commentForm.get('assessment').value
};
this._itemService.addReview(this.itemId, this.reviewId, review);
this.getReviews();
this.showCommentBox();
this.commentForm.patchValue({
comment: [ '' ],
assessment: [ '1' ]
});
}
Update:
Have noticed that the change detection strategy is onPush, which means that you have ensure that all your objects are immutable. Please change the method getReviews() to as shown below:
getReviews() {
this._itemService.getReviews(this.itemId).subscribe((data) => {
this.reviews = data.map((review) => {
return {
id: review.payload.doc.id,
...review.payload.doc.data()
};
});
this.reviewId = this.reviews.length + '';
let avgAssess = 0;
this.reviews.forEach((review) => {
avgAssess += parseInt(review.assessment);
});
this.grades = Math.floor(avgAssess / this.reviews.length);
console.log(this.grades);
this.stars = [];
const _stars:any=[];
for (let i = 0; i < this.grades; i++) {
_stars.push('grade');
}
for (let i = this.grades; i < 5; i++) {
_stars.push('star_border');
}
// to ensure that your object is immutable. Do not change the object content once it is assigned
this.stars=_stars;
});
}
Update 2:
Looks like the objects are immutable, which is causing the change detection to not work. Please remove the following line from your component:
changeDetection: ChangeDetectionStrategy.OnPush,