Skip to content

Commit f7f68e7

Browse files
committed
feat(Filtering): add filtering conditions for time and dateTime columns #7678
1 parent 70fbb8c commit f7f68e7

13 files changed

+303
-30
lines changed

projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts

+8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export interface IGridResourceStrings {
2626
igx_grid_filter_notNull?: string;
2727
igx_grid_filter_before?: string;
2828
igx_grid_filter_after?: string;
29+
igx_grid_filter_at?: string;
30+
igx_grid_filter_not_at?: string;
31+
igx_grid_filter_at_before?: string;
32+
igx_grid_filter_at_after?: string;
2933
igx_grid_filter_today?: string;
3034
igx_grid_filter_yesterday?: string;
3135
igx_grid_filter_thisMonth?: string;
@@ -153,6 +157,10 @@ export const GridResourceStringsEN: IGridResourceStrings = {
153157
igx_grid_filter_notNull: 'Not Null',
154158
igx_grid_filter_before: 'Before',
155159
igx_grid_filter_after: 'After',
160+
igx_grid_filter_at: 'At',
161+
igx_grid_filter_not_at: 'Not At',
162+
igx_grid_filter_at_before: 'At or Before',
163+
igx_grid_filter_at_after: 'At or After',
156164
igx_grid_filter_today: 'Today',
157165
igx_grid_filter_yesterday: 'Yesterday',
158166
igx_grid_filter_thisMonth: 'This Month',

projects/igniteui-angular/src/lib/data-operations/filtering-condition.ts

+214-1
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,220 @@ export class IgxDateFilteringOperand extends IgxFilteringOperand {
372372
return searchVal.has(target.toISOString());
373373
}
374374

375-
private validateInputData(target: Date) {
375+
protected validateInputData(target: Date) {
376+
if (!(target instanceof Date)) {
377+
throw new Error('Could not perform filtering on \'date\' column because the datasource object type is not \'Date\'.');
378+
}
379+
}
380+
}
381+
382+
export class IgxDateTimeFilteringOperand extends IgxDateFilteringOperand {
383+
protected constructor() {
384+
super();
385+
let index = this.operations.indexOf(this.condition('equals'));
386+
this.operations.splice(index, 1);
387+
index = this.operations.indexOf(this.condition('doesNotEqual'));
388+
this.operations.splice(index, 1);
389+
this.operations = [{
390+
name: 'equals',
391+
isUnary: false,
392+
iconName: 'equals',
393+
logic: (target: Date, searchVal: Date) => {
394+
if (!target) {
395+
return false;
396+
}
397+
this.validateInputData(target);
398+
const targetp = IgxDateTimeFilteringOperand.getDateParts(target, 'yMdhms');
399+
const searchp = IgxDateTimeFilteringOperand.getDateParts(searchVal, 'yMdhms');
400+
return targetp.year === searchp.year &&
401+
targetp.month === searchp.month &&
402+
targetp.day === searchp.day &&
403+
targetp.hours === searchp.hours &&
404+
targetp.minutes === searchp.minutes &&
405+
targetp.seconds === searchp.seconds;
406+
}
407+
}, {
408+
name: 'doesNotEqual',
409+
isUnary: false,
410+
iconName: 'not-equal',
411+
logic: (target: Date, searchVal: Date) => {
412+
if (!target) {
413+
return true;
414+
}
415+
this.validateInputData(target);
416+
const targetp = IgxDateTimeFilteringOperand.getDateParts(target, 'yMdhms');
417+
const searchp = IgxDateTimeFilteringOperand.getDateParts(searchVal, 'yMdhms');
418+
return targetp.year !== searchp.year ||
419+
targetp.month !== searchp.month ||
420+
targetp.day !== searchp.day ||
421+
targetp.hours !== searchp.hours ||
422+
targetp.minutes !== searchp.minutes ||
423+
targetp.seconds !== searchp.seconds;
424+
}
425+
}].concat(this.operations);
426+
}
427+
}
428+
429+
export class IgxTimeFilteringOperand extends IgxFilteringOperand {
430+
protected constructor() {
431+
super();
432+
this.operations = [{
433+
name: 'at',
434+
isUnary: false,
435+
iconName: 'equals',
436+
logic: (target: Date, searchVal: Date) => {
437+
if (!target) {
438+
return false;
439+
}
440+
this.validateInputData(target);
441+
const targetp = IgxDateFilteringOperand.getDateParts(target, 'hms');
442+
const searchp = IgxDateFilteringOperand.getDateParts(searchVal, 'hms');
443+
return targetp.hours === searchp.hours &&
444+
targetp.minutes === searchp.minutes &&
445+
targetp.seconds === searchp.seconds;
446+
}
447+
}, {
448+
name: 'not_at',
449+
isUnary: false,
450+
iconName: 'not-equal',
451+
logic: (target: Date, searchVal: Date) => {
452+
if (!target) {
453+
return true;
454+
}
455+
this.validateInputData(target);
456+
const targetp = IgxDateFilteringOperand.getDateParts(target, 'hms');
457+
const searchp = IgxDateFilteringOperand.getDateParts(searchVal, 'hms');
458+
return targetp.hours !== searchp.hours ||
459+
targetp.minutes !== searchp.minutes ||
460+
targetp.seconds !== searchp.seconds;
461+
}
462+
}, {
463+
name: 'before',
464+
isUnary: false,
465+
iconName: 'is-before',
466+
logic: (target: Date, searchVal: Date) => {
467+
if (!target) {
468+
return false;
469+
}
470+
471+
this.validateInputData(target);
472+
const targetn = IgxDateFilteringOperand.getDateParts(target, 'hms');
473+
const search = IgxDateFilteringOperand.getDateParts(searchVal, 'hms');
474+
475+
return targetn.hours < search.hours ? true : targetn.hours === search.hours && targetn.minutes < search.minutes ?
476+
true : targetn.hours === search.hours && targetn.minutes === search.minutes && targetn.seconds < search.seconds;
477+
}
478+
}, {
479+
name: 'after',
480+
isUnary: false,
481+
iconName: 'is-after',
482+
logic: (target: Date, searchVal: Date) => {
483+
if (!target) {
484+
return false;
485+
}
486+
487+
this.validateInputData(target);
488+
const targetn = IgxDateFilteringOperand.getDateParts(target, 'hms');
489+
const search = IgxDateFilteringOperand.getDateParts(searchVal, 'hms');
490+
491+
return targetn.hours > search.hours ? true : targetn.hours === search.hours && targetn.minutes > search.minutes ?
492+
true : targetn.hours === search.hours && targetn.minutes === search.minutes && targetn.seconds > search.seconds;
493+
}
494+
}, {
495+
name: 'at_before',
496+
isUnary: false,
497+
iconName: 'is-before',
498+
logic: (target: Date, searchVal: Date) => {
499+
if (!target) {
500+
return false;
501+
}
502+
503+
this.validateInputData(target);
504+
const targetn = IgxDateFilteringOperand.getDateParts(target, 'hms');
505+
const search = IgxDateFilteringOperand.getDateParts(searchVal, 'hms');
506+
return (targetn.hours === search.hours && targetn.minutes === search.minutes && targetn.seconds === search.seconds) ||
507+
targetn.hours < search.hours ? true : targetn.hours === search.hours && targetn.minutes < search.minutes ?
508+
true : targetn.hours === search.hours && targetn.minutes === search.minutes && targetn.seconds < search.seconds;
509+
}
510+
}, {
511+
name: 'at_after',
512+
isUnary: false,
513+
iconName: 'is-after',
514+
logic: (target: Date, searchVal: Date) => {
515+
if (!target) {
516+
return false;
517+
}
518+
519+
this.validateInputData(target);
520+
const targetn = IgxDateFilteringOperand.getDateParts(target, 'hms');
521+
const search = IgxDateFilteringOperand.getDateParts(searchVal, 'hms');
522+
return (targetn.hours === search.hours && targetn.minutes === search.minutes && targetn.seconds === search.seconds) ||
523+
targetn.hours > search.hours ? true : targetn.hours === search.hours && targetn.minutes > search.minutes ?
524+
true : targetn.hours === search.hours && targetn.minutes === search.minutes && targetn.seconds > search.seconds;
525+
}
526+
}, {
527+
name: 'empty',
528+
isUnary: true,
529+
iconName: 'is-empty',
530+
logic: (target: Date) => target === null || target === undefined
531+
}, {
532+
name: 'notEmpty',
533+
isUnary: true,
534+
iconName: 'not-empty',
535+
logic: (target: Date) => target !== null && target !== undefined
536+
}].concat(this.operations);
537+
}
538+
539+
/**
540+
* Splits a Date object into parts
541+
*
542+
* @memberof IgxDateFilteringOperand
543+
*/
544+
public static getDateParts(date: Date, dateFormat?: string): IDateParts {
545+
const res = {
546+
day: null,
547+
hours: null,
548+
milliseconds: null,
549+
minutes: null,
550+
month: null,
551+
seconds: null,
552+
year: null
553+
};
554+
if (!date || !dateFormat) {
555+
return res;
556+
}
557+
if (dateFormat.indexOf('y') >= 0) {
558+
res.year = date.getFullYear();
559+
}
560+
if (dateFormat.indexOf('M') >= 0) {
561+
res.month = date.getMonth();
562+
}
563+
if (dateFormat.indexOf('d') >= 0) {
564+
res.day = date.getDate();
565+
}
566+
if (dateFormat.indexOf('h') >= 0) {
567+
res.hours = date.getHours();
568+
}
569+
if (dateFormat.indexOf('m') >= 0) {
570+
res.minutes = date.getMinutes();
571+
}
572+
if (dateFormat.indexOf('s') >= 0) {
573+
res.seconds = date.getSeconds();
574+
}
575+
if (dateFormat.indexOf('f') >= 0) {
576+
res.milliseconds = date.getMilliseconds();
577+
}
578+
return res;
579+
}
580+
581+
protected findValueInSet(target: any, searchVal: Set<any>) {
582+
if (!target) {
583+
return false;
584+
}
585+
return searchVal.has(target.toISOString());
586+
}
587+
588+
protected validateInputData(target: Date) {
376589
if (!(target instanceof Date)) {
377590
throw new Error('Could not perform filtering on \'date\' column because the datasource object type is not \'Date\'.');
378591
}

projects/igniteui-angular/src/lib/data-operations/filtering-strategy.ts

+12-11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { resolveNestedPath, parseDate } from '../core/utils';
44
import { GridType } from '../grids/common/grid.interface';
55

66
const DateType = 'date';
7+
const TimeType = 'time';
78

89
export interface IFilteringStrategy {
910
filter(data: any[], expressionsTree: IFilteringExpressionsTree, advancedExpressionsTree?: IFilteringExpressionsTree,
@@ -25,9 +26,9 @@ export class NoopFilteringStrategy implements IFilteringStrategy {
2526
}
2627

2728
export abstract class BaseFilteringStrategy implements IFilteringStrategy {
28-
public findMatchByExpression(rec: any, expr: IFilteringExpression, isDate?: boolean, grid?: GridType): boolean {
29+
public findMatchByExpression(rec: any, expr: IFilteringExpression, isDate?: boolean, isTime?: boolean, grid?: GridType): boolean {
2930
const cond = expr.condition;
30-
const val = this.getFieldValue(rec, expr.fieldName, isDate, grid);
31+
const val = this.getFieldValue(rec, expr.fieldName, isDate, isTime, grid);
3132
return cond.logic(val, expr.searchVal, expr.ignoreCase);
3233
}
3334

@@ -59,9 +60,10 @@ export abstract class BaseFilteringStrategy implements IFilteringStrategy {
5960
return true;
6061
} else {
6162
const expression = expressions as IFilteringExpression;
62-
const isDate = grid && grid.getColumnByName(expression.fieldName) ?
63-
grid.getColumnByName(expression.fieldName).dataType === DateType : false;
64-
return this.findMatchByExpression(rec, expression, isDate, grid);
63+
const column = grid && grid.getColumnByName(expression.fieldName);
64+
const isDate = column ? column.dataType === DateType : false;
65+
const isTime = column ? column.dataType === TimeType : false;
66+
return this.findMatchByExpression(rec, expression, isDate, isTime, grid);
6567
}
6668
}
6769

@@ -71,7 +73,7 @@ export abstract class BaseFilteringStrategy implements IFilteringStrategy {
7173
public abstract filter(data: any[], expressionsTree: IFilteringExpressionsTree,
7274
advancedExpressionsTree?: IFilteringExpressionsTree, grid?: GridType): any[];
7375

74-
protected abstract getFieldValue(rec: any, fieldName: string, isDate?: boolean, grid?: GridType): any;
76+
protected abstract getFieldValue(rec: any, fieldName: string, isDate?: boolean, isTime?: boolean, grid?: GridType): any;
7577
}
7678

7779
export class FilteringStrategy extends BaseFilteringStrategy {
@@ -104,9 +106,9 @@ export class FilteringStrategy extends BaseFilteringStrategy {
104106
return res;
105107
}
106108

107-
protected getFieldValue(rec: any, fieldName: string, isDate: boolean = false): any {
109+
protected getFieldValue(rec: any, fieldName: string, isDate: boolean = false, isTime: boolean = false): any {
108110
let value = resolveNestedPath(rec, fieldName);
109-
value = value && isDate ? parseDate(value) : value;
111+
value = value && (isDate || isTime) ? parseDate(value) : value;
110112
return value;
111113
}
112114
}
@@ -126,13 +128,12 @@ export class FormattedValuesFilteringStrategy extends FilteringStrategy {
126128
return !this.fields || this.fields.length === 0 || this.fields.some(f => f === fieldName);
127129
}
128130

129-
protected getFieldValue(rec: any, fieldName: string, isDate: boolean = false, grid?: GridType): any {
131+
protected getFieldValue(rec: any, fieldName: string, isDate: boolean = false, isTime: boolean = false, grid?: GridType): any {
130132
const column = grid.getColumnByName(fieldName);
131133
let value = resolveNestedPath(rec, fieldName);
132134

133135
value = column.formatter && this.shouldApplyFormatter(fieldName) ?
134-
column.formatter(value) :
135-
value && isDate ? parseDate(value) : value;
136+
column.formatter(value) : value && (isDate || isTime) ? parseDate(value) : value;
136137

137138
return value;
138139
}

projects/igniteui-angular/src/lib/grids/cell.component.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
[style.width.%]="100"
118118
[outlet]="grid.outlet"
119119
mode="dropdown"
120-
format="hh:mm:ss tt"
120+
[format]="column.defaultTimeFormat"
121121
[(value)]="editValue"
122122
[igxFocus]="true"
123123
>
@@ -126,7 +126,7 @@
126126
</ng-container>
127127
<ng-container *ngIf="column.dataType === 'dateTime'">
128128
<igx-input-group>
129-
<input type="text" igxInput [igxDateTimeEditor]="'dd/MM/yyyy HH:mm tt'" [(ngModel)]="editValue" [igxFocus]="true"/>
129+
<input type="text" igxInput [igxDateTimeEditor]="column.defaultDateTimeFormat" [(ngModel)]="editValue" [igxFocus]="true"/>
130130
</igx-input-group>
131131
</ng-container>
132132
<ng-container *ngIf="column.dataType === 'currency'">

projects/igniteui-angular/src/lib/grids/columns/column.component.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import {
2121
IgxBooleanFilteringOperand,
2222
IgxNumberFilteringOperand,
2323
IgxDateFilteringOperand,
24-
IgxStringFilteringOperand
24+
IgxStringFilteringOperand,
25+
IgxDateTimeFilteringOperand,
26+
IgxTimeFilteringOperand
2527
} from '../../data-operations/filtering-condition';
2628
import { ISortingStrategy, DefaultSortingStrategy } from '../../data-operations/sorting-strategy';
2729
import { DisplayDensity } from '../../core/displayDensity';
@@ -1416,6 +1418,19 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy {
14161418
*/
14171419
public hasNestedPath: boolean;
14181420

1421+
/**
1422+
* @hidden
1423+
* @internal
1424+
*/
1425+
public defaultTimeFormat = 'hh:mm:ss tt';
1426+
1427+
/**
1428+
* @hidden
1429+
* @internal
1430+
*/
1431+
public defaultDateTimeFormat = 'dd/MM/yyyy HH:mm:ss';
1432+
1433+
14191434
/**
14201435
* Returns the filteringExpressionsTree of the column.
14211436
* ```typescript
@@ -1635,9 +1650,13 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy {
16351650
this.filters = IgxNumberFilteringOperand.instance();
16361651
break;
16371652
case DataType.Date:
1653+
this.filters = IgxDateFilteringOperand.instance();
1654+
break;
16381655
case DataType.Time:
1656+
this.filters = IgxTimeFilteringOperand.instance();
1657+
break;
16391658
case DataType.DateTime:
1640-
this.filters = IgxDateFilteringOperand.instance();
1659+
this.filters = IgxDateTimeFilteringOperand.instance();
16411660
break;
16421661
case DataType.String:
16431662
default:

projects/igniteui-angular/src/lib/grids/common/column.interface.ts

+2
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,7 @@ export interface ColumnType {
4747
hasLastPinnedChildColumn: boolean;
4848
pipeArgs: IColumnPipeArgs;
4949
hasNestedPath: boolean;
50+
defaultTimeFormat: string;
51+
defaultDateTimeFormat: string;
5052
getGridTemplate(isRow: boolean, isIE: boolean): string;
5153
}

0 commit comments

Comments
 (0)