I am requesting data from the server and able to display it in data table successfully .For some reason i had to use "table mat-table" instead of "mat-table" in html after this the pagination functionality stopped working. It was working fine earlier when i was using "mat-table" element .
Also, the reason for using "table" tag was to dynamically hide the columns with no data which i was not able to do earlier. If there's a work around for this ,please let me know .
Thanks in advance!
public array: any = [];//declaring an empty array
import { MatTableDataSource, MatSlideToggleChange } from '#angular/material';
import {MatPaginator, MatSort} from '#angular/material';
constructor(private router: Router, private Auth: AuthService ) { }
dataSource = new MatTableDataSource(this.data);
displayedColumns: string[] = this.array;//passing array to displayedColumns
#ViewChild(MatPaginator) paginator: MatPaginator;
#ViewChild(MatSort) sort: MatSort;
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
getRecords(event){
event.preventDefault();
const target = event.target;
this.someService.getDetails()
.subscribe(
data =>{
this.Response=data.data,
this.dataSource.data = this.Response,
console.log("fetched data :",this.Response);
for (var item of Object.keys(this.Response[0])){
this.array.push(item);// PUSHING THE FETCHED COLUMNS TO ARRAY
}
}
);this.resetArray();
}
<div class="mat-elevation-z8">
<mat-paginator #paginator
[length]="dataSource.data.length"
[pageIndex]="0"
[pageSize]="50"
[pageSizeOptions]="[25, 50, 100, 250]">
</mat-paginator>
<mat-table [dataSource]="dataSource">
<ng-container matColumnDef="userId">
<mat-header-cell *matHeaderCellDef>User Id</mat-header-cell>
<mat-cell *matCellDef= "let element">{{element.userId}}</mat-cell>
</ng-container>
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef>Title</mat-header-cell>
<mat-cell *matCellDef= "let element">{{element.title}}</mat-cell>
</ng-container>
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef>ID</mat-header-cell>
<mat-cell *matCellDef= "let element">{{element.id}}</mat-cell>
</ng-container>
<ng-container matColumnDef="completed">
<mat-header-cell *matHeaderCellDef>Completed</mat-header-cell>
<mat-cell *matCellDef= "let element">{{element.completed}}</mat-cell>
</ng-container>
<ng-container matColumnDef="blank">
<mat-header-cell *matHeaderCellDef>Blank</mat-header-cell>
<mat-cell *matCellDef= "let element"></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns" style="background-color:#bcd8f1;border-color: red;"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
</div>
Data table shows the records with dynamic columns and pagination works fine ,but instead of showing new data only, the data table columns are appended with prevoiously loaded columns and then gets displayed.
From your stackblizt.
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
/*
These variables are not needed
*/
// public data: any = [];
// public array: any = [];
dataSource = new MatTableDataSource([]);
displayedColumns: string[] = [];
// View child require 2 PARAMS
#ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
#ViewChild(MatSort, { static: true }) sort: MatSort;
constructor(private service: Service) { }
ngOnInit() {
this.loadData();
}
showTable() {
// I dont know what you really want to do here.
if(this.dataSource.data.length === 0){
this.loadData();
}
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
loadData() {
this.service.getAuthRecords1().subscribe(
data => {
this.dataSource.data = data as any,
console.log("dummy data :", data);
if (this.displayedColumns.length === 0){
this.displayedColumns = Object.keys(data[0]);
}
});
}
}
Related
Im probably being stupid but as far as i can tell, below code should follow examples from angular material website exactly. however, the sorting result is incorrect.
I can see sorting happened because order changed. row xxx50 became the first row. row xxx01 moved to original position of xxx50. the rest of the data in original order. asc and des result in same order.
Please show me what im doing wrong.
html:
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="agent">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Agent</th>
<td mat-cell *matCellDef="let relationshipInterface"> {{relationshipInterface.agentDisplayName}} </td>
</ng-container>
<ng-container matColumnDef="player">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Player</th>
<td mat-cell *matCellDef="let relationshipInterface"> {{relationshipInterface.playerDisplayName}} </td>
</ng-container>
<ng-container matColumnDef="avatar">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Nickname</th>
<td mat-cell *matCellDef="let relationshipInterface"> {{relationshipInterface.avatarDisplayName}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
<mat-paginator [pageSizeOptions]="[5, 10, 25, 100]"></mat-paginator>
</div>
ts:
import { Component, OnInit, ViewChild } from '#angular/core';
import { MatPaginator, MatSort, MatTableDataSource } from '#angular/material';
export interface RelationshipInterface {
agentName: string;
agentDisplayName: string;
playerName: string;
playerDisplayName: string;
avatarName: string;
avatarDisplayName: string;
}
#Component({
selector: 'app-relationships-table',
templateUrl: './relationships-table.component.html',
styleUrls: ['./relationships-table.component.css']
})
export class RelationshipsTableComponent implements OnInit {
displayedColumns: string[] = ['agent', 'player', 'avatar'];
dataSource: MatTableDataSource<RelationshipInterface>;
#ViewChild(MatPaginator) paginator: MatPaginator;
#ViewChild(MatSort) sort: MatSort;
constructor() {
}
ngOnInit() {
this.dataSource = new MatTableDataSource(
this.getTempData()
);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
applyFilter(filterColumn: string, filterValue: any) {
}
getTempData() {
const newData = [];
for (let i = 0; i < 100; i++) {
let str: string;
if (i < 10) {
str = '0' + i;
} else {
str = '' + i;
}
const relationshipInterface = {
agentName: 'agent name ' + str,
agentDisplayName: 'agent dName ' + str,
playerName: 'player name ' + str,
playerDisplayName: 'player dName ' + str,
avatarName: 'avatar name ' + str,
avatarDisplayName: 'avatar dName ' + str,
};
newData.push(relationshipInterface);
}
return newData;
}
}
after testing, apparently, column names must match data source property names.
i.e.
html:
<ng-container matColumnDef="agent">
ts:
export interface RelationshipInterface {
agent: string; // was agentDisplayName
}
Putting this out there since it is the first hit on Google. If anyone else comes across this issue;
My first problem was that some of the strings I was sorting had white spaces in the beginning of the string. This resulted in weird sorting being done.
The second problem was that MatSort is case sensitive (https://github.com/angular/components/issues/9205). This means it sorts large letters first, then small letters. The list AbCd will be sorted as ACbd.
Solution: Use the sortingDataAccessor to manipulate the data to be sorted before it is passed to the sort functionality. string.trim() removes white space characters in beginning and end of string. It can then be returned as string.toLowerCase() to ensure all data is sorted case insensitively.
Example:
#ViewChild(MatSort, { static: false }) set content(sort: MatSort) {
if (this.dataSource) {
this.dataSource.sortingDataAccessor = (item, property) => {
switch (property) {
case 'description':
return this.cleanData(item.description);
case 'name':
return this.cleanData(item.name);
default:
return item[property] || '';
}
};
this.dataSource.sort = sort;
}
}
private cleanData(sortString: string): string {
let cleanString = '';
if (sortString) {
cleanString = sortString.trim();
}
return cleanString.toLowerCase();
}
I have a service which is publishing objects on an observable, but when I update an array using a subscription, the new object does not display on the list (html):
export class PublicationService {
// ... more code ...
getAllPapersOnState(state: string): Observable<Paper> {
return Observable.create(observer => {
this.WF_SC.deployed().then(instance => {
// State changed watcher to update on-live the observable
const event = instance.AssetStateChanged({});
event.on('data', (data) => {
console.log('StateChanged catched!');
this.getPaper((data['args']['assetAddress'])).then((paper) => observer.next(paper));
});
// TODO Filter by asset type
return instance.findAssetsByState.call(state);
}).then(addresses => {
addresses.forEach((address) => this.getPaper(address).then((paper) => observer.next(paper)));
});
});
}
}
export class HomeComponent implements OnInit, OnDestroy {
papersSubmitted$:Observable<DataCard>;
papersOnReview$:Observable<DataCard>;
papersPublished$:Observable<DataCard>;
constructor(private publicationService: PublicationService) { }
ngOnInit() {
this.papersSubmitted$ = this.publicationService.getAllPapersOnState("Submitted").pipe(
map(paper => HomeComponent.paperToCard(paper,'Read', 'Review'))
);
this.papersOnReview$ = this.publicationService.getAllPapersOnState("OnReview").pipe(
map(paper => HomeComponent.paperToCard(paper,'Read', 'Accept'))
);
this.papersPublished$ = this.publicationService.getAllPapersOnState("Published").pipe(
map(paper => HomeComponent.paperToCard(paper,'Read', ''))
);
}
// ... more code ...
}
<app-cardlist
[title]="'Published'"
[description]="'Last science papers published. Yay!'"
[collection$]="papersPublished$"
(clickActionCard)="clickActionCardHandlePublished($event)"></app-cardlist>
<app-cardlist
[title]="'On Review'"
[description]="'Last papers on review. On publish way!'"
[collection$]="papersOnReview$"
(clickActionCard)="clickActionCardHandleOnReview($event)"></app-cardlist>
<app-cardlist
[title]="'Submitted'"
[description]="'Last papers submitted for reviewing. Be the first one to check them!'"
[collection$]="papersSubmitted$"
(clickActionCard)="clickActionCardHandleSubmitted($event)"></app-cardlist>
export class CardlistComponent implements OnInit {
#Input() title;
#Input() description;
#Input() collection$: Observable<DataCard>;
items:DataCard[] = [];
subscription:Subscription;
#Output() clickActionCard: EventEmitter<any> = new EventEmitter();
constructor() { }
ngOnInit() {
this.subscription = this.collection$.subscribe((data_card:DataCard) => {
console.log(data_card);
this.items.push(data_card);
console.log(this.items);
})
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
<div *ngIf="(items)" class="card-deck">
<div *ngFor="let item of items" class="card" style="width: 18rem;">
<div class="card-body">
<div class="card-body">
<h5 class="card-title">{{item['title']}}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{item['subtitle']}}</h6>
<p class="card-text">{{item['description']}}</p>
{{item['action_1_name']}}
{{item['action_2_name']}}
</div>
</div>
</div>
</div>
The new object published on observable $collection should be displayed on the CardList but nothing is refreshed although console.log(data_card) show the new object.
Repository code: HomeComponent, CardListComponent
I have scenario as below:
I want to achieve is:
When user click on All then all options shall be selected and when user click All again then all options shall be deselcted.
If All option is checked and user click any other checkbox than All then All and clicked checkbox shall be deselected.
When user selects 4 options one by one then All shall be selected.
HTML file
<mat-select placeholder="User Type" formControlName="UserType" multiple>
<mat-option *ngFor="let filters of userTypeFilters" [value]="filters.key">
{{filters.value}}
</mat-option>
<mat-option #allSelected (click)="toggleAllSelection()" [value]="0">All</mat-option>
</mat-select>
TS file
this.searchUserForm = this.fb.group({
userType: new FormControl('')
});
userTypeFilters = [
{
key: 1, value: 'Value 1',
},
{
key: 2, value: 'Value 2',
},
{
key: 3, value: 'Value 3',
},
{
key: 4, value: 'Value 4',
}
]
toggleAllSelection() {
if (this.allSelected.selected) {
this.searchUserForm.controls.userType
.patchValue([...this.userTypeFilters.map(item => item.key), 0]);
} else {
this.searchUserForm.controls.userType.patchValue([]);
}
}
Now, how to achieve 2nd and 3rd point
Stackblitz is: https://stackblitz.com/edit/angular-material-with-angular-v5-znfehg?file=app/app.component.html
Use code as below create function on click each mat-option and select()/deselect() all option:
See stackblitz:https://stackblitz.com/edit/angular-material-with-angular-v5-jsgvx6?file=app/app.component.html
TS:
togglePerOne(all){
if (this.allSelected.selected) {
this.allSelected.deselect();
return false;
}
if(this.searchUserForm.controls.userType.value.length==this.userTypeFilters.length)
this.allSelected.select();
}
toggleAllSelection() {
if (this.allSelected.selected) {
this.searchUserForm.controls.userType
.patchValue([...this.userTypeFilters.map(item => item.key), 0]);
} else {
this.searchUserForm.controls.userType.patchValue([]);
}
}
HTML:
<form [formGroup]="searchUserForm" fxFlex fxLayout="column" autocomplete="off" style="margin: 30px">
<mat-select placeholder="User Type" formControlName="userType" multiple>
<mat-option *ngFor="let filters of userTypeFilters" [value]="filters.key" (click)="togglePerOne(allSelected.viewValue)">
{{filters.value}}
</mat-option>
<mat-option #allSelected (click)="toggleAllSelection()" [value]="0">All</mat-option>
</mat-select>
</form>
Simply you can do it without adding a new option to your data source by adding a checkbox.
See the: Demo
import { Component, VERSION, ViewChild } from '#angular/core';
import { FormControl } from '#angular/forms';
import { MatSelect } from '#angular/material/select';
import { MatOption } from '#angular/material/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
#ViewChild('select') select: MatSelect;
allSelected=false;
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'}
];
toggleAllSelection() {
if (this.allSelected) {
this.select.options.forEach((item: MatOption) => item.select());
} else {
this.select.options.forEach((item: MatOption) => item.deselect());
}
}
optionClick() {
let newStatus = true;
this.select.options.forEach((item: MatOption) => {
if (!item.selected) {
newStatus = false;
}
});
this.allSelected = newStatus;
}
}
.select-all{
margin: 5px 17px;
}
<mat-form-field>
<mat-label>Favorite food</mat-label>
<mat-select #select multiple>
<div class="select-all">
<mat-checkbox [(ngModel)]="allSelected"
[ngModelOptions]="{standalone: true}"
(change)="toggleAllSelection()">Select All</mat-checkbox>
</div>
<mat-option (click)="optionClick()" *ngFor="let food of foods" [value]="food.value">
{{food.viewValue}}
</mat-option>
</mat-select>
</mat-form-field>
Another way to do this is with the #ViewChild selector to get the mat-select component and troggle the mat-options items selected or unselected. We need also a variable to save the selected actual status to select or unselect all the elements on every click. Hope will help.
import {MatOption, MatSelect} from "#angular/material";
export class ExampleAllSelector {
myFormControl = new FormControl();
elements: any[] = [];
allSelected = false;
#ViewChild('mySel') skillSel: MatSelect;
constructor() {}
toggleAllSelection() {
this.allSelected = !this.allSelected; // to control select-unselect
if (this.allSelected) {
this.skillSel.options.forEach( (item : MatOption) => item.select());
} else {
this.skillSel.options.forEach( (item : MatOption) => {item.deselect()});
}
this.skillSel.close();
}
}
<mat-select #mySel placeholder="Example" [formControl]="myFormControl" multiple>
<mat-option [value]="0" (click)="toggleAllSelection()">All items</mat-option>
<mat-option *ngFor="let element of elements" [value]="element">{{skill.name}}</mat-option>
</mat-select>
Here is an example of how to extend a material option component.
See stackblitz Demo
Component:
import { ChangeDetectorRef, Component, ElementRef, HostListener, HostBinding, Inject, Input, OnDestroy, OnInit, Optional } from '#angular/core';
import { MAT_OPTION_PARENT_COMPONENT, MatOptgroup, MatOption, MatOptionParentComponent } from '#angular/material/core';
import { AbstractControl } from '#angular/forms';
import { MatPseudoCheckboxState } from '#angular/material/core/selection/pseudo-checkbox/pseudo-checkbox';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
#Component({
selector: 'app-select-all-option',
templateUrl: './select-all-option.component.html',
styleUrls: ['./select-all-option.component.css']
})
export class SelectAllOptionComponent extends MatOption implements OnInit, OnDestroy {
protected unsubscribe: Subject<any>;
#Input() control: AbstractControl;
#Input() title: string;
#Input() values: any[] = [];
#HostBinding('class') cssClass = 'mat-option';
#HostListener('click') toggleSelection(): void {
this. _selectViaInteraction();
this.control.setValue(this.selected ? this.values : []);
}
constructor(elementRef: ElementRef<HTMLElement>,
changeDetectorRef: ChangeDetectorRef,
#Optional() #Inject(MAT_OPTION_PARENT_COMPONENT) parent: MatOptionParentComponent,
#Optional() group: MatOptgroup) {
super(elementRef, changeDetectorRef, parent, group);
this.title = 'Select All';
}
ngOnInit(): void {
this.unsubscribe = new Subject<any>();
this.refresh();
this.control.valueChanges
.pipe(takeUntil(this.unsubscribe))
.subscribe(() => {
this.refresh();
});
}
ngOnDestroy(): void {
super.ngOnDestroy();
this.unsubscribe.next();
this.unsubscribe.complete();
}
get selectedItemsCount(): number {
return this.control && Array.isArray(this.control.value) ? this.control.value.filter(el => el !== null).length : 0;
}
get selectedAll(): boolean {
return this.selectedItemsCount === this.values.length;
}
get selectedPartially(): boolean {
const selectedItemsCount = this.selectedItemsCount;
return selectedItemsCount > 0 && selectedItemsCount < this.values.length;
}
get checkboxState(): MatPseudoCheckboxState {
let state: MatPseudoCheckboxState = 'unchecked';
if (this.selectedAll) {
state = 'checked';
} else if (this.selectedPartially) {
state = 'indeterminate';
}
return state;
}
refresh(): void {
if (this.selectedItemsCount > 0) {
this.select();
} else {
this.deselect();
}
}
}
HTML:
<mat-pseudo-checkbox class="mat-option-pseudo-checkbox"
[state]="checkboxState"
[disabled]="disabled"
[ngClass]="selected ? 'bg-accent': ''">
</mat-pseudo-checkbox>
<span class="mat-option-text">
{{title}}
</span>
<div class="mat-option-ripple" mat-ripple
[matRippleTrigger]="_getHostElement()"
[matRippleDisabled]="disabled || disableRipple">
</div>
CSS:
.bg-accent {
background-color: #2196f3 !important;
}
Another possible solution:
using <mat-select [(value)]="selectedValues" in the template and set the selectedValues via toggle function in the component.
Working Stackblitz Demo.
Component
export class AppComponent {
selectedValues: any;
allSelected = false;
public displayDashboardValues = [
{key:'0', valuePositionType: 'undefined', viewValue:'Select all'},
{key:'1', valuePositionType: 'profit-loss-area', viewValue:'result'},
{key:'2', valuePositionType: 'cash-area', viewValue:'cash'},
{key:'3', valuePositionType: 'balance-area', viewValue:'balance'},
{key:'4', valuePositionType: 'staff-area' ,viewValue:'staff'},
{key:'5', valuePositionType: 'divisions-area', viewValue:'divisions'},
{key:'6', valuePositionType: 'commisions-area', viewValue:'commisions'},
];
toggleAllSelection() {
this.allSelected = !this.allSelected;
this.selectedValues = this.allSelected ? this.displayDashboardValues : [];
}
}
Template
<mat-select [(value)]="selectedValues" (selectionChange)="selectionChange($event)" formControlName="dashboardValue" multiple>
<mat-option [value]="displayDashboardValues[0]" (click)="toggleAllSelection()">{{ displayDashboardValues[0].viewValue }}</mat-option>
<mat-divider></mat-divider>
<div *ngFor="let dashboardPosition of displayDashboardValues">
<mat-option class="dashboard-select-option" *ngIf="dashboardPosition.key>0" [value]="dashboardPosition">
{{ dashboardPosition.viewValue }}
</mat-option>
</div>
</mat-select>
There are some problems with other answers. The most important one is that they're listening to the click event which is not complete (user can select an option via space key on the keyboard).
I've created a component that solves all the problems:
#Component({
selector: 'app-multi-select',
templateUrl: './multi-select.component.html',
styleUrls: ['./multi-select.component.scss'],
})
export class MultiSelectComponent<V> implements OnInit {
readonly _ALL_SELECTED = '__ALL_SELECTED__' as const;
#Input() options: ReadonlyArray<{ value: V; name: string }> = [];
#Input('selectControl') _selectControl!: FormControl | AbstractControl | null | undefined;
get selectControl(): FormControl {
return this._selectControl as FormControl;
}
#Input() label: string = '';
#Input() hasSelectAllOption = false;
selectedValues: (V | '__ALL_SELECTED__')[] = [];
constructor() {}
ngOnInit(): void {}
onSelectAllOptions({ isUserInput, source: { selected } }: MatOptionSelectionChange) {
if (!isUserInput) return;
this.setValues(selected ? this.options.map(o => o.value) : []);
}
private setValues(values: (V | '__ALL_SELECTED__')[]) {
const hasAllOptions = ArrayUtils.arraysAreSame(
values,
this.options.map(o => o.value),
);
if (!values.includes(this._ALL_SELECTED)) {
if (hasAllOptions) {
values = [...values, this._ALL_SELECTED];
}
} else if (!hasAllOptions) {
values = values.filter(o => o !== this._ALL_SELECTED);
}
setTimeout(() => {
this.selectedValues = values;
});
this.selectControl.setValue(values.filter(o => (o as any) !== this._ALL_SELECTED));
}
onSelectOtherOptions({ isUserInput, source: { selected, value } }: MatOptionSelectionChange) {
if (!isUserInput) return;
this.setValues(
selected ? [...this.selectedValues, value] : this.selectedValues.filter(o => o !== value),
);
}
}
<mat-form-field>
<mat-label>Choose some options</mat-label>
<mat-select multiple [value]="selectedValues">
<mat-option
*ngFor="let d of options"
[value]="d.value"
(onSelectionChange)="onSelectOtherOptions($event)"
>
{{ d.name }}
</mat-option>
<mat-option
*ngIf="hasSelectAllOption"
[value]="_ALL_SELECTED"
(onSelectionChange)="onSelectAllOptions($event)"
>
Select all
</mat-option>
</mat-select>
</mat-form-field>
I created a dropdown in an angular library to be used in our applications. I used angular-material2 for the dropdown (mat-select and mat-autocomplete).
I must be doing something wrong since I don't get the value when I use the dropdown in an app. I already tried pretty much everything I found on the net, with no result.
I commented most of it and I'm trying to solve the simplest version, but even in this case I'm not getting the value. Here is what I have now:
DropdownComponent.html library:
<mat-form-field appearance="outline">
<mat-select disableOptionCentering (selectionChange)="writeValue($event)" [multiple]="multi">
<mat-option *ngFor="let item of list" [value]="item">
{{ item }}
</mat-option>
</mat-select>
</mat-form-field>
DropdownComponent.ts library:
import {Component, OnInit, ViewEncapsulation, Input, forwardRef} from '#angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, FormControl} from '#angular/forms';
import {Observable} from 'rxjs';
#Component({
selector: 'pux-dropdown',
templateUrl: './dropdown.component.html',
styleUrls: ['./dropdown.component.scss'],
encapsulation: ViewEncapsulation.None,
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true },
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => DropdownComponent), multi: true }
]
})
export class DropdownComponent implements OnInit, ControlValueAccessor {
#Input() list: any[] = [];
#Input() selected: any;
#Input() multi = false;
#Input() search = false;
items: any[] = [];
propagateChange = (_: any) => {};
validateFn: any = () => {};
constructor() { }
ngOnInit() {
this.items = this.list;
}
// Form
get value(): any { return this.selected; }
set value(newValue: any) {
if (newValue !== this.selected) {
this.writeValue(newValue);
this.registerOnChange(newValue);
this.selected = newValue;
}
}
registerOnChange(fn: any): void { this.propagateChange = fn; }
registerOnTouched(fn: any): void {}
setDisabledState(isDisabled: boolean): void {}
writeValue(obj: any): void {
if (obj !== null) {
this.selected = obj.value;
this.registerOnChange(this.selected);
console.log(this.selected);
}
}
validate(c: FormControl) { return this.validateFn(c); }
}
DropDownComponent.html application:
<div>
<form [formGroup]="selectForm" (ngSubmit)="saveSelect(selectForm)" #form1="ngForm">
<div>
<pux-dropdown formControlName="selectValue" [list]="list1"> </pux-dropdown>
</div> <br>
<button mat-flat-button="primary" type="submit" class="btn btn-primary">Save</button>
</form> <br>
<div>
Saved Value: {{selectValue | json}}
</div>
</div>
DropdownComponent.ts application:
import {Component, OnInit} from '#angular/core';
import {FormGroup, FormBuilder} from '#angular/forms';
const states = [
'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware',
'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky',
'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi',
'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico',
'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania',
'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont',
'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'
];
#Component({
selector: 'app-dropdown',
templateUrl: './dropdown.component.html',
styleUrls: ['./dropdown.component.scss']
})
export class DropdownComponent implements OnInit {
list1;
multi: boolean;
selected: any;
search: boolean;
// Form
selectForm: FormGroup;
selectValue: string;
constructor(private fb: FormBuilder) { }
ngOnInit() {
this.list1 = states;
// Form
this.selectForm = this.fb.group({
selectValue: this.selected
});
}
saveSelect(formValues) {
console.log(formValues.value.selectValue);
this.selectValue = formValues.value.selectValue;
}
}
The console.log in writeValue in the library gives me the value I select in the dropdown, but the console.log in saveSelect shows me null. So the value isn't sent to the parent. Any idea what I'm doing wrong? Thank you in advance.
Your writeValue implementation needs to call the change function, but instead it is calling the registerOnChange function which is there for the form control to register its change function. Try something like this:
propagateChange: (value: any) => void = () => {};
registerOnChange(fn: (value: any) => void) { this.propagateChange = fn; }
writeValue(obj: any): void {
if (obj !== null && obj !== this.selected) {
this.selected = obj.value;
this.propagateChange(this.selected);
}
}
I'm trying to add validations such that the end date can't be before the start date. Unfortunately I have no idea how to do that, and I didn't find any helpful advice in the internet so far. My form looks like this:
editAndUpdateForm(tageler: Tageler) {
this.tageler = tageler;
this.tagelerForm = this.fb.group({
title: [this.tageler.title, Validators.required],
text: this.tageler.text,
group: [[this.tageler.group], Validators.required],
date_start: new Date(this.tageler.start).toISOString().slice(0, 10),
date_end: new Date(this.tageler.end).toISOString().slice(0, 10),
...
});
this.tagelerForm.valueChanges
.subscribe(data => this.onValueChanged(data));
}
My validations so far:
onValueChanged(data?: any) {
if (!this.tagelerForm) {
return;
}
const form = this.tagelerForm;
for (const field in this.formErrors) {
// clear previous error message (if any)
this.formErrors[field] = '';
const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
for (const key in control.errors) {
this.formErrors[field] += messages[key] + ' ';
}
}
}
}
validationMessages = {
'title': {
'required': 'Geben Sie bitte einen Namen ein.',
},
'group': {
'required': 'Wählen Sie bitte eine Gruppe aus.'
},
'bringAlong': {
'required': 'Bitte Feld ausfüllen.'
},
'uniform': {
'required': 'Bitte Feld ausfüllen.'
},
};
formErrors = {
'title': 'Geben Sie bitte einen Namen ein.',
'group': 'Wählen Sie bitte eine Gruppe aus.',
'bringAlong': 'Bitte Feld ausfüllen',
'uniform': 'Bitte Feld ausfüllen',
};
The the form-controls 'date_start' & 'date_end' contain a date-string of the form 'dd.MM.yyyy', and I want 'date_end' to be bigger or equal 'date_start'.
I'd like to directly display the error message (my html code looks like this:)
<label for="formControlName_date_end" class="col-3 col-form-label">Ende:</label>
<div class="col-5">
<input id="formControlName_date_end" class="form-control" formControlName="date_end" type="date" value="{{tageler.end | date: 'yyyy-MM-dd'}}">
</div>
<div *ngIf="formErrors.date_end" class="alert alert-danger">
{{ formErrors.date_end }}
</div>
Could someone help me?
Thanks!
Based on the answer of santiagomaldonado I have created a generic ValidatorFn that can be used in multiple Reactive Forms with a dynamic return value.
export class DateValidators {
static dateLessThan(dateField1: string, dateField2: string, validatorField: { [key: string]: boolean }): ValidatorFn {
return (c: AbstractControl): { [key: string]: boolean } | null => {
const date1 = c.get(dateField1).value;
const date2 = c.get(dateField2).value;
if ((date1 !== null && date2 !== null) && date1 > date2) {
return validatorField;
}
return null;
};
}
}
Import the validator and use it like this in your formgroup validators.
this.form = this.fb.group({
loadDate: null,
deliveryDate: null,
}, { validator: Validators.compose([
DateValidators.dateLessThan('loadDate', 'deliveryDate', { 'loaddate': true }),
DateValidators.dateLessThan('cargoLoadDate', 'cargoDeliveryDate', { 'cargoloaddate': true })
])});
Now you can use the validation in HTML.
<md-error *ngIf="form.hasError('loaddate')">Load date must be before delivery date</md-error>
You can also do it with Reactive Forms.
The FormBuilder API lets you add custom validators.
Valid keys for the extra parameter map are validator and asyncValidator
Example:
import { Component } from '#angular/core';
import { FormGroup, FormBuilder, Validators } from '#angular/forms';
#Component({
selector: 'reactive-form',
templateUrl: './reactive-form.html'
})
export class ReactiveFormComponent {
form: FormGroup
constructor(private fb: FormBuilder){
this.createForm();
}
createForm() {
this.form = this.fb.group({
dateTo: ['', Validators.required ],
dateFrom: ['', Validators.required ]
}, {validator: this.dateLessThan('dateFrom', 'dateTo')});
}
dateLessThan(from: string, to: string) {
return (group: FormGroup): {[key: string]: any} => {
let f = group.controls[from];
let t = group.controls[to];
if (f.value > t.value) {
return {
dates: "Date from should be less than Date to"
};
}
return {};
}
}
}
Note that I'm comparing the values of the inputs date and from with >, but by default this would be comparing strings.
In the live example I'm using angular-date-value-accessor and importing the directive useValueAsDate.
<input formControlName="dateFrom" type="date" useValueAsDate />
With this directive group.controls[from].value and group.controls[to].value returns Date and then I can compare them with <.
Live example in plunkr
Credits to Dave's answer
create a form group . Let the controls be a part of form group .
new FormGroup({
startDate: this.fb.group({
dateInput: [{value: ""}, startDateValidator()]
}),
endDate: this.fb.group({
dateInput: ["", endDateValidator()]
})
}, startDateCannotBeLessThanEndDateValidator());
startDateCannotBeLessThanEndDateValidator(formGroup: FormGroup) {
let startDate = formGroup.get("startDate");
let endDate = formGroup.get("endDate");
// compare 2 dates
}
we cant do it in validation because we need two control values that is startdate and enddate for comparison. So it is better to compare two dates in your component
error:any={isError:false,errorMessage:''};
compareTwoDates(){
if(new Date(this.form.controls['date_end'].value)<new Date(this.form.controls['date_start'].value)){
this.error={isError:true,errorMessage:'End Date can't before start date'};
}
}
In your html
<label for="formControlName_date_end" class="col-3 col-form-label">Ende:</label>
<div class="col-5">
<input id="formControlName_date_end" class="form-control" formControlName="date_end" type="date" value="{{tageler.end | date: 'yyyy-MM-dd'}}" (blur)="compareTwoDates()">
</div>
<div *ngIf="error.isError" class="alert alert-danger">
{{ error.errorMessage }}
</div>
I am using moment, and in angular 7 to compare and validate dates, i use this function:
datesValidator(date1: any, date2: any): {[key:string]:any} | null {
return (group: FormGroup): { [key: string]: any } | null => {
let start = group.controls[date1].value;
let end = group.controls[date2].value;
let datum1 = _moment(start).startOf('day');
let datum2 = _moment(end).startOf('day');
if (_moment(datum1).isSameOrAfter(datum2)) {
this.alert.red("Error: wrong period!"); //popup message
return { 'error': 'Wrong period!' };
}
return null; //period is ok, return null
};
}
Mine is angular7 + ngprime(for calendar)
(*if you don't want ngprime just replace calendar part to others.)
Refer below code for date validation.
My code has additional validation that
once start date is selected, I block previous days in end data's calendar
so that the end date will be always later than that.
if you don't want to block date, delete [minDate] part.
it is also working.
> Component
export class test implements OnInit {
constructor() { }
defaultDate: Date = new Date();
checkDate = true;
form: FormGroup;
today: Date = new Date(); //StartDate for startDatetime
startDate: Date = new Date(); //StartDate for endDatetime
initFormControls(): void {
this.today.setDate(this.today.getDate());
this.startDate.setDate(this.today.getDate()); //or start date + 1 day
this.form = new FormGroup({
startDatetime: new FormControl('', [Validators.required]),
endDatetime: new FormControl('', [Validators.required])
},
{ validators: this.checkDateValidation });
}
checkDateValidation: ValidatorFn = (control: FormGroup): ValidationErrors | null => {
try {
let startingDatefield = control.get('startDatetime').value;
this.startDate = new Date(startingDatefield); //new Date(startingDatefield).getDate()
let endingDatefield = control.get('endDatetime').value;
if (this.today >= startingDatefield) { //compare to current date
console.log("Please set a Start Date that is on or after Current Date and Time.");
return { 'effectiveStartDatetime': false };
} else
if (startingDatefield > endingDatefield && endingDatefield) {
console.log("Please set an End Date and Time that is after the Start Date and Time.");
return {};
} else {
return {};
}
} catch (err) {
}
};
onSubmit() {
//if form is not valid
if (!this.form.valid) {
console.log(" Please fill in all the mandatory fields");
// do sth
return;
}
//if form is valid
//do sth
}
ngOnInit() {
this.initFormControls();
}
> HTML
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div>
<div>
<p-button type="submit" label="submit"></p-button>
</div>
</div>
<div>
<p>Start Date/Time"</p>
<div>
<p-calendar formControlName="startDatetime" appendTo="body" showTime="true" hourFormat="24" stepMinute="30"
showSeconds="false" dateFormat="yy-mm-dd" [minDate]="today"></p-calendar>
<div
*ngIf="form.get('startDatetime').invalid && (form.get('startDatetime').dirty || form.get('startDatetime').touched)">
<div *ngIf="form.get('startDatetime').hasError('required')">
</div>
</div>
</div>
<p>End Date/Time</p>
<div>
<p-calendar formControlName="endDatetime" appendTo="body" showTime="true" hourFormat="24" stepMinute="30"
showSeconds="false" dateFormat="yy-mm-dd" [minDate]="startDate"></p-calendar>
<div *ngIf="form.get('endDatetime').invalid && (form.get('endDatetime').dirty || form.get('endDatetime').touched)">
<div *ngIf="!checkDate || form.get('endDatetime').hasError('required')">
</div>
</div>
</div>
</div>
</form>