Briefly: Is there any way to implement a custom comparator for the number column filters?
Long story:
I use ag-grid in Angular (2). I provide my own components for cells in my ag-grid:
let column = {
headerName: column.header,
field: column.id,
cellRendererFramework: DynamicComponent,
filter: this.convertFormatToFilterType(column.format) // returns "text", "number" or "date"
}
In order to make the filters get values from my cells properly I provide custom comparators (this one is for the text column filters):
if (c.filter === "text")
c['filterParams'] = {
textCustomComparator: (filter, cell, filterText): boolean => {
var filterTextLowerCase = filterText.toLowerCase();
var valueLowerCase = cell.value.toString().toLowerCase();
switch (filter) {
case 'contains':
return valueLowerCase.indexOf(filterTextLowerCase) >= 0;
case 'notContains':
return valueLowerCase.indexOf(filterTextLowerCase) === -1;
case 'equals':
return valueLowerCase === filterTextLowerCase;
case 'notEqual':
return valueLowerCase != filterTextLowerCase;
case 'startsWith':
return valueLowerCase.indexOf(filterTextLowerCase) === 0;
case 'endsWith':
var index = valueLowerCase.lastIndexOf(filterTextLowerCase);
return index >= 0 && index === (valueLowerCase.length - filterTextLowerCase.length);
default:
// should never happen
console.warn('invalid filter type ' + filter);
return false;
}
}
};
You can see I need to access the value of the cell by using "cell.value". The code above works fine.
What I have troubles with is providing similar functionality for the number column filters - they don't seem to use any custom comparator. Therefore, what is happening, the filter tries to access the cell's value directly instead of using "cell.value".
So, is there any way to implement a custom comparator for the number column filters? Or, if not, any other way I can get the value from my cells correctly in this case?
What I ended up doing is implementing a ag-grid custom filter component
import { Component, ViewChild, ViewContainerRef } from '#angular/core';
import { IFilterParams, IDoesFilterPassParams, RowNode, IAfterGuiAttachedParams } from 'ag-grid/main';
import { IFilterAngularComp } from 'ag-grid-angular/main';
// https://www.ag-grid.com/javascript-grid-filter-component/#gsc.tab=0 / Angular Filtering
// create your filter as a Angular component
#Component({
selector: 'filter-cell',
template: `
<select #select (ngModelChange)="onSelectChange($event)" [ngModel]="operator">
<option value="eq">Equals</option>
<option value="neq">Not equal</option>
<option value="lt">Less than</option>
<option value="lte">Less than or equals</option>
<option value="gt">Greater than</option>
<option value="gte">Greater than or equals</option>
<option value="inrange">In range</option>
</select>
<br>
<input #input (ngModelChange)="onChange($event)" [ngModel]="text">
<br>
<div *ngIf='operator === "inrange"'>
<input #input2 (ngModelChange)="onChange2($event)" [ngModel]="text2">
</div>
`,
styles: ['select { margin: 2px 4px; }', 'input { height: 26px; margin: 2px 4px; }']
})
export class GridNumberFilterComponent implements IFilterAngularComp {
private params: IFilterParams;
private valueGetter: (rowNode: RowNode) => any;
public operator: string = 'eq';
public text: string = '';
public text2: string = '';
#ViewChild('select', { read: ViewContainerRef }) public select;
#ViewChild('input', { read: ViewContainerRef }) public input;
#ViewChild('input2', { read: ViewContainerRef }) public input2;
agInit(params: IFilterParams): void {
this.params = params;
this.valueGetter = params.valueGetter;
}
isFilterActive(): boolean {
return this.text !== null && this.text !== undefined && this.text !== '';
}
doesFilterPass(params: IDoesFilterPassParams): boolean {
let cellNumber = Number(this.valueGetter(params.node).value);
let filterNumber = this.text ? Number(this.text) : -Infinity;
let filterNumber2 = this.text2 ? Number(this.text2) : Infinity;
switch (this.operator) {
case 'eq': return cellNumber === filterNumber;
case 'neq': return cellNumber !== filterNumber;
case 'lt': return cellNumber < filterNumber;
case 'lte': return cellNumber <= filterNumber;
case 'gt': return cellNumber > filterNumber;
case 'gte': return cellNumber >= filterNumber;
case 'inrange': return cellNumber >= filterNumber && cellNumber <= filterNumber2;
default: return true;
}
}
getModel(): any {
return { value: this.text };
}
setModel(model: any): void {
this.text = model ? model.value : '';
}
afterGuiAttached(params: IAfterGuiAttachedParams): void {
this.input.element.nativeElement.focus();
}
componentMethod(message: string): void {
alert(`Alert from PartialMatchFilterComponent ${message}`);
}
onSelectChange(newValue): void {
if (this.operator !== newValue) {
this.operator = newValue;
this.params.filterChangedCallback();
}
}
onChange(newValue): void {
if (this.text !== newValue) {
this.text = newValue;
this.params.filterChangedCallback();
}
}
onChange2(newValue): void {
if (this.text2 !== newValue) {
this.text2 = newValue;
this.params.filterChangedCallback();
}
}
}
which I add to my column like this:
let column = {
headerName: column.header,
field: column.id,
cellRendererFramework: DynamicComponent,
filterFramework: GridNumberFilterComponent
}
Related
My Svelte component is set up to find all people in a MongoDB document and list them in a table. When clicking on a column header the collection should sort by that column/field and it should toggle the sort direction with subsequent clicks.
My script section:
$: sortColumn = 'name';
$: sortDirection = 'asc';
$: sortParameters = setSortParams(sortColumn, sortDirection);
$: contactList = [];
function getContactList(sortObj) {
contactList = Contacts.find({
isBlocked: false,
isDeleted: { $ne: true }
},
{
sort: sortObj
}).fetch();
contactList = contactList;
}
onMount(() => {
getContactList(setSortParams(sortColumn, sortDirection));
});
function changeSortDirection() {
if (sortDirection === 'asc') {
sortDirection = 'desc';
} else {
sortDirection = 'asc';
}
}
function sortByColumn(col) {
if (col === sortColumn) {
changeSortDirection();
} else {
sortDirection = 'asc';
}
sortColumn = col;
getContactList(setSortParams(sortColumn, sortDirection));
}
function setSortParams(sortField, sDirection) {
let sortParams = [];
let direction = sDirection || 1;
let field = sortField || 'name';
if (direction === 'asc') {
direction = 1;
} else {
direction = -1;
}
if (field === 'name') {
sortParams.push(['firstName', direction]);
sortParams.push(['lastName', direction]);
} else {
sortParams.push([field, direction]);
}
sortParams = sortParams;
return sortParams;
}
And the relevant part of my svelte file:
{#each columns as column}
<th class="contact-table__column contact-table__column-header"
on:click={() => sortByColumn(column.type)}>
<span class="contact-table__title">{column.display} {sortDirection}</span>
</th>
{/each}
The collection reorders when I click on a different column header, but it doesn't reorder when I click on the same header (it should switch between ASC and DESC sort order).
I'm new to Svelte and Meteor so I'm sure there's a few things I'm doing wrong. I appreciate any help.
I'm using AgGrid with tree data. https://www.ag-grid.com/javascript-grid-tree-data/
The problem is that I need to have 2 leaf node on the tree with the same name, because they share the name but not it's properties.
I thought this was specified by getRowNodeId GridOption. Example of usage in Option 1: https://www.ag-grid.com/javascript-grid-rxjs/.
But it doesn't.
He is my code of that property:
...
getRowNodeId: (data: any) => any = (data) => {
return (data.parent !== undefined ? data.parent * 1000 : 0) + data.properties.id;
},
...
As you can see, I want to add 2 nodes with the same name but only 1 renders. What can I do?
UPDATE: Code added
My AgGrid component:
import { Component, ViewChild, ElementRef, OnDestroy, Input, Output, EventEmitter } from '#angular/core';
import { Events, ToastController, AlertController } from 'ionic-angular';
import * as _ from 'lodash';
import 'ag-grid-enterprise';
import { Http } from '#angular/http';
import { GridApi, TemplateService, GridOptions, ColDef } from 'ag-grid';
import { TemplateServiceProvider } from '../../providers/services';
import { NgModel } from '#angular/forms';
import { Toast } from '../../classes/classes';
import { EventSubscriber } from '../../classes/eventSubscriber/eventSubscriber';
#Component({
selector: 'page-datatableWithGroups',
templateUrl: 'datatableWithGroups.html'
})
export class DatatableWithGroupsPage extends EventSubscriber implements OnDestroy {
#ViewChild('quickFilterInput') quickFilterInput: ElementRef;
#Input() rowData: any[] = [];
#Input() columnDefs = [];
#Input() title: string;
#Output() onLoadRow: EventEmitter<any> = new EventEmitter();
#Output() onAddRow: EventEmitter<any> = new EventEmitter();
#Output() onDeleteRow: EventEmitter<any> = new EventEmitter();
#Output() onDuplicateRow: EventEmitter<any> = new EventEmitter();
#Output() onSelectedRow: EventEmitter<any> = new EventEmitter();
public gridApi: GridApi;
public gridColumnApi;
public printPending: boolean = true;
public showColumnTools: boolean;
public showColumnVisibilityTools: boolean;
public showFilterTools: boolean = true;
private groupDefaultExpanded;
private hoverNode = null;
private isControlPressed: boolean;
private newGroupsCounter: number = 0;
private autoGroupColumnDef: ColDef = {
rowDrag: true,
headerName: "Group",
width: 250,
suppressMovable: true,
cellRendererParams: {
suppressCount: true,
},
valueFormatter: (params) => {
if (!params.data.properties.checkpointList) return params.value;
return params.value + ' (' + (params.data.subGroups ? params.data.subGroups.length : 0) + ')'
+ '(' + (params.data.properties.checkpointList ? params.data.properties.checkpointList.length : 0) + ') ';
},
cellClassRules: {
"hover-over": (params) => {
return params.node === this.hoverNode;
},
"checkpointGroup-title": (params) => {
return this.isCheckpointGroup(params.node.data);
}
}
};
addRowFunction: () => void;
isCheckpointGroup: (nodeData) => boolean = (nodeData) => {
return nodeData && nodeData.properties.checkpointList;
}
getDataPath: (data: any) => any = (data) => {
return data.properties.orgHierarchy;
};
getRowNodeId: (data: any) => any = (data) => {
return (data.properties.parent !== undefined ? data.properties.parent * 1000 : 0) + data.properties.id + (data.properties.component !== undefined ? data.properties.component.id * 100000 : 0);
}
getRowNodeWithUpdatedId: (data: any) => any = (data) => {
return (data.properties.parentId !== undefined ? data.properties.parentId * 1000 : (data.properties.parent !== undefined ? data.properties.parent * 1000 : 0)) + data.properties.id + (data.properties.component !== undefined ? data.properties.component.id * 100000 : 0);
}
public gridOptions: GridOptions = {
enableFilter: this.showFilterTools,
enableColResize: true,
animateRows: true,
cacheQuickFilter: this.showFilterTools,
treeData: true,
quickFilterText: this.quickFilterInput ? this.quickFilterInput.nativeElement.value : '',
colResizeDefault: 'shift',
groupDefaultExpanded: this.groupDefaultExpanded,
rowSelection: 'single',
rowDeselection: true,
defaultColDef: {
filter: "agTextColumnFilter"
},
// components: this.components,
getDataPath: this.getDataPath,
getRowNodeId: this.getRowNodeId,
autoGroupColumnDef: this.autoGroupColumnDef,
deltaRowDataMode: true,
onModelUpdated: () => {
if (this.gridApi && this.columnDefs) {
this.gridColumnApi.autoSizeColumn('ag-Grid-AutoColumn');
this.adjustColumnsToFitWindow();
}
},
onSelectionChanged: (event) => {
let selectedRows = event.api.getSelectedNodes();
let selectedRow = selectedRows && selectedRows.length > 0 ? selectedRows[0] : undefined;
if (selectedRow && this.isCheckpointGroup(selectedRow.data)) {
this.onSelectedRow.emit(selectedRow);
} else {
this.onSelectedRow.emit(undefined);
}
},
};
constructor(
events: Events,
public http: Http,
public templateService: TemplateServiceProvider,
public toastController: ToastController,
public alertCtrl: AlertController,
) {
super(events, [
{ eventName: 'datatable:updateList', callbackFunction: () => this.onLoadRow.emit() },
{ eventName: 'datatable:resizeTable', callbackFunction: () => this.gridApi.sizeColumnsToFit() }
])
this.groupDefaultExpanded = -1;
}
ngOnInit() {
super.subscribeEvents();
this.subscribeEvents();
}
subscribeEvents() {
this.addRowFunction = () => { this.onAddRow.emit() };
window.onresize = () => this.adjustColumnsToFitWindow();
document.addEventListener('keydown', (evt: any) => {
evt = evt || window.event;
if ((evt.keyCode && evt.keyCode === 17) || (evt.which && evt.which === 17)) this.isControlPressed = !this.isControlPressed;
});
}
updateRowData(rowData) {
this.gridApi.setRowData(rowData);
this.gridApi.clearFocusedCell();
this.reAssignSortProperty();
}
onGridReady(params) {
this.gridApi = params.api;
this.gridColumnApi = params.columnApi;
this.adjustColumnsToFitWindow();
if (!this.rowData || this.rowData.length === 0) this.gridApi.hideOverlay()
}
onRowDragMove(event) {
this.setHoverNode(event);
}
onRowDragLeave() {
this.setHoverNode(undefined);
}
setHoverNode(event) {
let overNode = event ? event.overNode : undefined;
if (this.hoverNode === overNode) return;
var rowsToRefresh = [];
if (this.hoverNode) {
rowsToRefresh.push(this.hoverNode);
}
if (overNode) {
rowsToRefresh.push(overNode);
}
this.hoverNode = overNode;
this.refreshRows(rowsToRefresh);
}
refreshRows(rowsToRefresh) {
var params = {
rowNodes: rowsToRefresh,
force: true
};
this.gridApi.refreshCells(params);
}
onRowDragEnd(event) {
let toIndex;
let overNode = event.overNode;
let movingNode = event.node;
let movingData = movingNode.data;
if (overNode === movingNode) return;
if (!overNode) {
if (this.isCheckpointGroup(movingData)) {
overNode = (<any>this.gridApi.getModel()).rootNode;
toIndex = this.rowData.length;
} else {
return;
}
}
let overData = overNode.data;
let fromIndex = this.rowData.indexOf(movingData);
toIndex = toIndex ? toIndex : this.rowData.indexOf(overData) + 1;
let newParentNode = (!overNode.data || this.isCheckpointGroup(overNode.data)) ? overNode : overNode.parent;
if (overData && (overData.properties.parentId === 0 || (!overData.properties.parentId && overData.properties.parent === 0)) && this.isCheckpointGroup(movingData) && this.isControlPressed) {
overNode = (<any>this.gridApi.getModel()).rootNode;
newParentNode = (!overNode.data || this.isCheckpointGroup(overNode.data)) ? overNode : overNode.parent;
} else if (overData && this.isControlPressed) {
newParentNode = overNode.parent;
}
if (toIndex > fromIndex) toIndex--;
let oldParentNode = movingNode.parent;
let newParentPath = (newParentNode.data && this.isCheckpointGroup(newParentNode.data)) ? newParentNode.data.properties.orgHierarchy : [];
let needToChangeParent = !this.arePathsEqual(newParentPath, movingData.properties.orgHierarchy);
if (fromIndex === toIndex && !needToChangeParent) return;
if (this.isInvalidMoveTo(movingNode, newParentNode)) {
new Toast(this.toastController).showToast('Invalid Move');
return;
}
if (needToChangeParent) {
if (this.checkNodeExistsInParent(movingData, newParentNode)) return;
let updatedRows = [];
this.moveToPath(newParentPath, movingNode, updatedRows, oldParentNode.data, newParentNode.data || { properties: { id: 0 } });
this.gridApi.updateRowData({ update: updatedRows });
this.refreshRows(this.rowData);
}
let newStore = this.rowData.slice();
this.moveInArray(newStore, fromIndex, toIndex);
this.gridApi.setRowData(newStore);
this.rowData = newStore;
this.setHoverNode(undefined);
}
checkNodeExistsInParent(newRow, newParentNode) {
let cloneRow = _.cloneDeep(newRow);
cloneRow.properties.parentId = newParentNode && newParentNode.data ? newParentNode.data.properties.id : 0;
if (this.isCheckpointGroup(cloneRow) && this.existsInParent(cloneRow)) {
new Toast(this.toastController).showToast('"' + cloneRow.properties.name + '" already exists on "' + (newParentNode.data ? newParentNode.data.properties.name : 'root') + '"');
return true;
}
return false
}
checkNodeExistsInTemplate(newRow) {
if (!this.isCheckpointGroup(newRow) && this.existsInTemplate(newRow)) {
new Toast(this.toastController).showToast('"' + newRow.properties.name + '" already exists on the template');
return true;
}
return false
}
existsInParent(newRow) {
return this.rowData.find(row => this.getRowNodeWithUpdatedId(row) === this.getRowNodeWithUpdatedId(newRow));
}
existsInTemplate(newRow) {
return this.rowData.find(row => row.properties.id === newRow.properties.id);
}
moveInArray(arr, old_index, new_index) {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) {
arr.push(undefined);
}
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
};
moveToPath(newParentPath, node, allUpdatedNodes, oldParentData?, newParentData?) {
var newChildPath = this.updateNodePath(node.data, newParentPath);
if (newParentData) this.updateParents(node.data, oldParentData, newParentData);
allUpdatedNodes.push(node.data);
if (oldParentData) allUpdatedNodes.push(oldParentData);
if (newParentData) allUpdatedNodes.push(newParentData);
if (node.childrenAfterGroup) this.moveChildrenPaths(node.childrenAfterGroup, newChildPath, allUpdatedNodes);
}
updateNodePath(nodeData, newParentPath) {
var oldPath = nodeData.properties.orgHierarchy;
var fileName = oldPath[oldPath.length - 1];
var newChildPath = newParentPath.slice();
newChildPath.push(fileName);
nodeData.properties.orgHierarchy = newChildPath;
return newChildPath;
}
updateParents(nodeData, oldParentData, newParentData) {
nodeData.properties.parentId = newParentData.properties.id;
if (this.isCheckpointGroup(nodeData)) {
if (oldParentData) oldParentData.subGroups = oldParentData.subGroups.filter(subGroup => subGroup.properties.id !== nodeData.properties.id);
if (newParentData && newParentData.subGroups !== undefined) newParentData.subGroups.push(nodeData);
} else {
if (oldParentData) oldParentData.properties.checkpointList = oldParentData.properties.checkpointList.filter(checkpoint => checkpoint.properties.id !== nodeData.properties.id);
if (newParentData && newParentData.properties.checkpointList) newParentData.properties.checkpointList.push(nodeData);
}
}
moveChildrenPaths(children, newChildPath, allUpdatedNodes) {
children.forEach((child) => {
this.moveToPath(newChildPath, child, allUpdatedNodes);
});
}
isInvalidMoveTo(selectedNode, targetNode) {
let isInvalid = false;
if (targetNode.parent && targetNode.parent.data) {
isInvalid = this.isInvalidMoveTo(selectedNode, targetNode.parent);
if (this.getRowNodeWithUpdatedId(targetNode.parent.data) === this.getRowNodeWithUpdatedId(selectedNode.data)) isInvalid = true;
}
if (targetNode && targetNode.data && this.getRowNodeWithUpdatedId(targetNode.data) === this.getRowNodeWithUpdatedId(selectedNode.data)) isInvalid = true;
return isInvalid;
}
arePathsEqual(path1, path2) {
let newPathLength = path1.length + 1;
let oldPathLength = path2.length;
if (this.isControlPressed && newPathLength === 2 && oldPathLength === 1) return true;
if (newPathLength !== oldPathLength) return false;
var equal = true;
path1.forEach(function (item, index) {
if (path2[index] !== item) {
equal = false;
}
});
return equal;
}
reAssignSortProperty() {
this.gridApi.forEachNode((node) => {
node.data.properties.sort = node.childIndex + 1
})
};
addRows(data) {
let selectedGroupNode = data.selectedGroup;
let groupIndex = selectedGroupNode ? this.rowData.indexOf(selectedGroupNode.data) + 1 : this.rowData.length;
data.selectedRows.forEach((selectedRow, index) => {
let newRowData = _.cloneDeep(selectedRow.data);
newRowData.orgHierarchy = selectedGroupNode ? _.cloneDeep(selectedGroupNode.data.properties.orgHierarchy) : [];
newRowData.orgHierarchy.push(newRowData.name);
newRowData = { properties: newRowData };
newRowData.properties.parent = selectedGroupNode ? selectedGroupNode.data.properties.id : 0;
if (this.isCheckpointGroup(newRowData)) {
if (!this.checkNodeExistsInParent(newRowData, selectedGroupNode)) {
this.newGroupsCounter--;
newRowData.properties.checkpointGroupId = newRowData.properties.id;
newRowData.properties.id = this.newGroupsCounter;
newRowData.subGroups = [];
if (selectedGroupNode && selectedGroupNode.data) selectedGroupNode.data.subGroups.push(newRowData);
this.rowData.splice(groupIndex + index + (selectedGroupNode ? selectedGroupNode.allChildrenCount : 0), 0, newRowData)
}
} else {
if (!this.checkNodeExistsInTemplate(newRowData)) {
newRowData.properties.templateCheckpointGroupCheckpointId = -1;
if (selectedGroupNode && selectedGroupNode.data) selectedGroupNode.data.properties.checkpointList.push(newRowData);
this.rowData.splice(groupIndex + index + (selectedGroupNode ? selectedGroupNode.allChildrenCount : 0), 0, newRowData)
}
}
});
if (selectedGroupNode) data.selectedGroup.expanded = true;
this.updateRowData(this.rowData);
}
deleteRow(row) {
if (row.properties.checkpointList && row.properties.checkpointList.length > 0) {
row.properties.checkpointList.forEach(checkpoint => {
this.deleteRow(checkpoint);
});
}
if (row.subGroups && row.subGroups.length > 0) {
row.subGroups.forEach(subGroup => {
this.deleteRow(subGroup);
});
}
this.deleteByUpdatedParentId(row);
this.updateRowData(this.rowData);
}
deleteByUpdatedParentId(row) {
this.rowData = this.rowData.filter(existingRow => this.getRowNodeWithUpdatedId(existingRow) !== this.getRowNodeWithUpdatedId(row));
}
adjustColumnsToFitWindow() {
if (this.gridApi) this.gridApi.sizeColumnsToFit();
}
onFilterTextBoxChanged() {
this.gridApi.setQuickFilter(this.quickFilterInput.nativeElement.value);
}
exportCsv() {
let params = {
fileName: 'Rows',
sheetName: 'Rows',
columnSeparator: ';'
};
this.gridApi.exportDataAsCsv(params);
}
print() {
this.setPrinterFriendly();
this.printPending = true;
// if (this.gridApi.isAnimationFrameQueueEmpty()) {
this.onAnimationQueueEmpty({ api: this.gridApi });
// }
}
onAnimationQueueEmpty(event) {
if (this.printPending) {
this.printPending = false;
this.printTable();
}
}
printTable() {
// let rest = <any>document.querySelector("ion-content :not(#grid)");
// rest.style.display = 'none';
print();
}
setPrinterFriendly() {
let eGridDiv = <any>document.getElementById("grid");
let preferredWidth = this.gridApi.getPreferredWidth();
preferredWidth += 2;
eGridDiv.style.width = preferredWidth + "px";
eGridDiv.style.height = "";
this.gridApi.setGridAutoHeight(true);
}
}
The html:
<ion-content padding>
<ion-item class="generic-tools-wrapper">
<div>
<input type="text" #quickFilterInput placeholder="Filter..." (input)="onFilterTextBoxChanged()" />
<button ion-button small type="button" icon-only (click)="exportCsv()">
<ion-icon name="list-box"></ion-icon>
</button>
<button ion-button small type="button" icon-only (click)="print()">
<ion-icon name="print"></ion-icon>
</button>
<button ion-button small [color]="isControlPressed ? 'danger': 'primary'" type="button" icon-only (click)="isControlPressed = !isControlPressed">
<ion-icon name="return-right"></ion-icon>
</button>
</div>
</ion-item>
<ion-label> <strong>Attention!</strong> The grouping option is <strong>{{isControlPressed ? 'deactivated':
'activated'}}</strong>,
elements
dragged over a group will be put <strong>{{isControlPressed ? 'next to': 'inside'}} that group</strong>. Press
<strong>Control</strong> to switch this option. </ion-label>
<div style="height: 90%;box-sizing: border-box; ">
<ag-grid-angular id="grid" style="width: 100%; height: 100%;" class="ag-theme-material" [rowData]="rowData"
[columnDefs]="columnDefs" [gridOptions]="gridOptions" (gridReady)="onGridReady($event)" (rowDragMove)="onRowDragMove($event)"
(rowDragEnd)="onRowDragEnd($event)" (rowDragLeave)="onRowDragLeave($event)" (viewportChanged)="adjustColumnsToFitWindow()">
</ag-grid-angular>
</div>
</ion-content>
I'm using angular 2 reactive forms and made a validator for a date of birth field. The validator is working, but it turns out the date of birth field is split into three new field: year, month, day. They all have their own validators. My question is, how can I change my code so my original date of birth validator works on three fields.
my original validator that checks one field.
input(2000/12/12) is valid
export function dobValidator(control) {
const val = control.value;
const dobPattern = /^\d{4}\/\d{2}\/\d{2}$/ ;
const comp = val.split('/');
const y = parseInt(comp[0], 10);
const m = parseInt(comp[1], 10);
const d = parseInt(comp[2], 10);
const jsMonth = m - 1;
const date = new Date(y, jsMonth, d);
const isStringValid = dobPattern.test(control.value);
const isDateValid = (date.getFullYear() === y && date.getMonth() === jsMonth && date.getDate() === d);
return (isStringValid && isDateValid) ? null : { invalidDob: ('Date of birth not valid') };
};
new html with 3 fields
year has a validator that checks the year
day has a validator that checks if the input is between 1 and 31
month has a validator that checks if the input is between 1 and 12.
I want to combine the above input of the three field into a new string and use my original date of birth validator.
<label>Date of birth :</label>
<div>
<div class="col-xs-1">
<input required type="text" formControlName="day" class="form-control" placeholder="dd" id="day"/>
<p *ngIf="form.controls.day.dirty && form.controls.day.errors">{{ form.controls.day.errors.invalidDay }}</p>
</div>
<div class="col-xs-1">
<input required type="text" formControlName="month" class="form-control" placeholder="mm" id="month"/>
<p *ngIf="form.controls.month.dirty && form.controls.month.errors">{{ form.controls.month.errors.invalidMonth }}</p>
</div>
<div class="col-xs-2">
<input required type="text" formControlName="year" class="form-control" placeholder="yyyy" id="year"/>
<p *ngIf="form.controls.year.dirty && form.controls.year.errors">{{ form.controls.year.errors.invalidYear }}</p>
</div>
</div>
<div>
<button type="submit" [disabled]="form.invalid">Submit</button>
</di>
I have created a validator for comparing two dates (their format is NgbDateStruct - as used in ng-bootstrap package's datepickers)
import { Directive, forwardRef, Attribute } from '#angular/core';
import { Validator, AbstractControl, NG_VALIDATORS, ValidatorFn } from '#angular/forms';
import { NgbDateStruct } from "#ng-bootstrap/ng-bootstrap";
import { toDate } from "../helpers/toDate";
export function dateCompareValidator(compareToControl: string, compareToValue: NgbDateStruct, compareType: string, reverse: boolean, errorName: string = 'dateCompare'): ValidatorFn {
return (c: AbstractControl): { [key: string]: any } => {
let compare = function (self: Date, compareTo: Date): any {
console.log('comparing ', compareType.toLowerCase());
console.log(self);
console.log(compareTo);
if (compareType.toLowerCase() === 'ge') {
if (self >= compareTo) {
return true;
} else {
return false;
}
} else if (compareType.toLowerCase() === 'le') {
if (self <= compareTo) {
return true;
} else {
return false;
}
}
return false;
};
// self value
let v = c.value;
// compare vlaue
let compareValue: Date;
let e;
if (compareToValue) {
compareValue = toDate(compareToValue);
} else {
e = c.root.get(compareToControl);
if (e) {
compareValue = toDate(e.value);
}
else {
// OTHER CONTROL NOT FOUND YET
return null;
}
}
let controlToValidate: AbstractControl = reverse ? e : c;
// validate and set result
let error = null;
let result = compare(toDate(c.value), compareValue);
if (result === true) {
console.log('clearing errors', compareToControl);
if (controlToValidate.errors) {
delete controlToValidate.errors[errorName];
if (!Object.keys(controlToValidate.errors).length) {
controlToValidate.setErrors(null);
}
}
else {
console.log('errors property not found in control', controlToValidate);
}
} else {
error = {};
error[errorName] = false;
controlToValidate.setErrors(error);
console.log(controlToValidate.errors);
console.log(controlToValidate.value);
console.log('Error Control', controlToValidate);
console.log('returning errors');
}
return reverse ? null : error;
}
}
Couldn't manage to modify much lot to best describe here as an answer but I believe you would get your query answered in this validator function code.
Note:
Function toDate() used in the code is a small function I created to convert NgbDateStruct into a javascript date object so that comparing dates can get easier. Here goes its implementation:
import { NgbDateStruct } from "#ng-bootstrap/ng-bootstrap"
export function toDate(ngbDate: NgbDateStruct): Date {
return ngbDate != null ? new Date(Date.UTC(ngbDate.year, ngbDate.month, ngbDate.day)) : null;
}
I am using a form validation with javascript.
When submitting, the background-color of those input fields which are not valid changes to red color. When filling up this field and typing into another input field, the red background-color of the former field should go away. This is at the moment not the case. It only disappears when submitting again. How can I make this possible that the bg color changes back to normal when typing into another field?
// Return true if the input value is not empty
function isNotEmpty(inputId, errorMsg) {
var inputElement = document.getElementById(inputId);
var errorElement = document.getElementById(inputId + "Error");
var inputValue = inputElement.value.trim();
var isValid = (inputValue.length !== 0); // boolean
showMessage(isValid, inputElement, errorMsg, errorElement);
return isValid;
}
/* If "isValid" is false, print the errorMsg; else, reset to normal display.
* The errorMsg shall be displayed on errorElement if it exists;
* otherwise via an alert().
*/
function showMessage(isValid, inputElement, errorMsg, errorElement) {
if (!isValid) {
// Put up error message on errorElement or via alert()
if (errorElement !== null) {
errorElement.innerHTML = errorMsg;
} else {
alert(errorMsg);
}
// Change "class" of inputElement, so that CSS displays differently
if (inputElement !== null) {
inputElement.className = "error";
inputElement.focus();
}
} else {
// Reset to normal display
if (errorElement !== null) {
errorElement.innerHTML = "";
}
if (inputElement !== null) {
inputElement.className = "";
}
}
}
The form:
<td>Name<span class="red">*</span></td>
<td><input type="text" id="name" name="firstname"/></td>
<p id="nameError" class="red"> </p>
The submit:
<input type="submit" value="SEND" id="submit"/>
Css:
input.error { /* for the error input text fields */
background-color: #fbc0c0;
}
Update:
I tried this but it seems not to work:
function checkFilled() {
var inputVal = document.querySelectorAll("#offerteFirstname, #offerteLastname, #offertePhone, #offertePoster, #offerteStreet").value;
if (inputVal == "") {
document.querySelectorAll("#offerteFirstname, #offerteLastname, #offertePhone, #offertePoster, #offerteStreet").style.backgroundColor = "red";
}
else{
document.querySelectorAll("#offerteFirstname, #offerteLastname, #offertePhone, #offertePoster, #offerteStreet").style.backgroundColor = "white";
}
}
checkFilled();
here example of it
<input type="text" id="subEmail" onchange="checkFilled();"/>
and now you can JavaScript on input
function checkFilled() {
var inputVal = document.getElementById("subEmail").value;
if (inputVal == "") {
document.getElementById("subEmail").style.backgroundColor = "white";
}
else{
document.getElementById("subEmail").style.backgroundColor = "red";
}
}
checkFilled();
here working demo
Try
$(formElement).on('keyup','input.error', function(){
if(<values changed>){
$(this).removeClass('error');
}
});
In Chosen drop down plugin, a search is started after 2 characters are typed in chosen drop down.
I need the search to not start until after inputing at least two characters in the search box.
Can any one suggest how to do this?
I did a small change to start to search after the third character, is not the best option but works, in the chosen JS in the AbstractChosen.prototype.winnow_results function after the line searchText = this.get_search_text(); add the following code: if (searchText != "" && searchText.length < 3) return;. Remember to change the < 3 by your own size.
Hope this help you
See part of the code below:
AbstractChosen.prototype.winnow_results = function() {
var escapedSearchText, option, regex, regexAnchor, results, results_group, searchText, startpos, text, zregex, _i, _len, _ref;
this.no_results_clear();
results = 0;
searchText = this.get_search_text();
if (searchText != "" && searchText.length < 3) return;
escapedSearchText = searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
I know this post it's old but i had to face this problem just now and wanted to share my result. I wrapped everything in a function extendedSearch and set it as a callback while chosen is emitting the chosen:showing_dropdown event.
My problem was that i need the search to show the results after the 2nd character typed in search and must filter out certain strings from the results.
Bellow you'll find a demo which shows results on the 3rd character typed in, and will keep visible only those results that end with letter "E".
$(function() {
/**
* By default the results are hidden when clicking a dropdown.
* {
* toggleResults: false, // a function(searchValue) that returns a boolean
* filterResults: false, // a function(dropDownValue, selectValue) that returns a boolean
* }
* #param options
*/
const extendedSearch = (options = {}) => {
const defaultOptions = {
toggleResults: false,
filterResults: false,
};
options = { ...{},
...defaultOptions,
...options
};
/**
* Main element
*/
return (evt, params) => {
let originalElement = $(evt.currentTarget);
let searchInput = params.chosen.search_field;
const customSearch = (options = {}) => {
let defaultOptions = {
originalElement: null,
searchInput: null
};
options = { ...{},
...defaultOptions,
...options
};
if (!(options.originalElement instanceof jQuery) || !options.originalElement) {
throw new Error('Custom Search: originalElement is invalid.');
}
if (!(options.searchInput instanceof jQuery) || !options.searchInput) {
throw new Error('Custom Search: searchInput is invalid.');
}
let res = options.searchInput
.parent()
.next('.chosen-results');
res.hide();
if (typeof options.toggleResults !== 'function') {
options.toggleResults = (value) => true;
}
if (options.filterResults && typeof options.filterResults !== 'function') {
options.filterResults = (shownText = '', selectValue = '') => true;
}
/**
* Search Input Element
*/
return (e) => {
let elem = $(e.currentTarget);
let value = elem.val() || '';
if (value.length && options.toggleResults(value) === true) {
res.show();
if (options.filterResults) {
let children = res.children();
let active = 0;
$.each(children, (idx, item) => {
let elItem = $(item);
let elemIdx = elItem.attr('data-option-array-index');
let shownText = elItem.text();
let selectValue = options.originalElement.find('option:eq(' + elemIdx + ')').attr('value') || '';
if (options.filterResults(shownText, selectValue) === true) {
active++;
elItem.show();
} else {
active--;
elItem.hide();
}
});
if (active >= 0) {
res.show();
} else {
res.hide();
}
}
} else {
res.hide();
}
};
};
options = {
...{},
...options,
...{
originalElement,
searchInput
}
};
let searchInstance = customSearch(options);
searchInput
.off('keyup', searchInstance)
.on('keyup', searchInstance)
}
};
/** This is the final code */
const inputValidator = (value) => {
console.log('input', value);
return $.trim(value).length > 2;
};
const textResultsValidator = (dropDownValue, selectValue) => {
if ($.trim(dropDownValue).substr(-1, 1) === 'E') {
console.log('results shown', dropDownValue, '|', selectValue);
return true;
}
return false;
};
$(".chosen-select")
.chosen()
.on('chosen:showing_dropdown', extendedSearch({
toggleResults: inputValidator,
filterResults: textResultsValidator
}));
});
#import url("https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/chosen.min.css")
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/chosen.jquery.min.js"></script>
<select class="chosen-select">
<option value="">Pick something</option>
<option value="APPLE">APPLE</option>
<option value="APPLE JUICE">APPLE JUICE</option>
<option value="BANANA">BANANA</option>
<option value="ANANAS">ANANAS</option>
<option value="ORANGE">ORANGE</option>
<option value="ORANGES">ORANGES</option>
<option value="STRAWBERRY">STRAYBERRY</option>
</select>