@progress/kendo-angular-filter
Version:
Kendo UI Angular Filter
612 lines (539 loc) • 28.5 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import { Component, Input, Output, EventEmitter, HostBinding, isDevMode, ContentChildren, QueryList, HostListener, ElementRef, ViewChildren, Renderer2 } from '@angular/core';
import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n';
import { FilterService } from './filter.service';
import { nullOperators, isPresent } from './util';
import { ChangeDetectorRef } from '@angular/core';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from './package-metadata';
import { FilterFieldComponent } from './filter-field.component';
import { FilterItem } from './util';
import { NavigationService } from './navigation.service';
import { Keys } from '@progress/kendo-angular-common';
import { FilterErrorMessages } from './error-messages';
import { FilterGroupComponent } from './filter-group.component';
import { LocalizedMessagesDirective } from './localization/localized-messages.directive';
import * as i0 from "@angular/core";
import * as i1 from "./filter.service";
import * as i2 from "@progress/kendo-angular-l10n";
import * as i3 from "./navigation.service";
/**
* Represents the [Kendo UI Filter component for Angular]({% slug overview_filter %}).
* The Filter component enables users to define and apply complex filter criteria.
*
* @example
* ```ts
* @Component({
* selector: 'my-app',
* template: `
* <kendo-filter [filters]="filters" (valueChange)="onValueChange($event)"></kendo-filter>
* `
* })
* export class AppComponent {
* public filters: FilterExpression[] = [
* {
* name: 'country',
* label: 'Country',
* filter: 'string',
* operators: ['eq', 'neq'],
* },
* {
* name: 'budget',
* filter: 'number'
* }
* ];
*
* onValueChange(e: CompositeFilterDescriptor) {
* console.log(e);
* }
* }
* ```
*
* @remarks
* Supported children components are: {@link CustomMessagesComponent}, {@link FilterFieldComponent}.
*/
export class FilterComponent {
filterService;
localization;
cdr;
element;
navigationService;
renderer;
/**
* @hidden
*/
focusout() {
setTimeout(() => {
if (!(document.activeElement.closest('.k-filter'))) {
this.renderer.setAttribute(this.navigationService.currentlyFocusedElement, 'tabindex', '-1');
this.navigationService.currentlyFocusedElement = this.navigationService.flattenFilterItems[this.navigationService.currentToolbarItemIndex].focusableChildren[0];
this.renderer.setAttribute(this.navigationService.currentlyFocusedElement, 'tabindex', '0');
this.navigationService.isFilterNavigationActivated = false;
this.navigationService.isFilterExpressionComponentFocused = false;
}
});
}
/**
* @hidden
*/
focusin() {
if (this.navigationService.isFilterNavigationActivated) {
return;
}
this.navigationService.isFilterNavigationActivated = true;
this.navigationService.currentToolbarItemChildrenIndex = 0;
}
/**
* @hidden
*/
onKeydown(event) {
const keyCode = event.keyCode;
const keys = [Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowLeft, Keys.ArrowRight, Keys.Enter,
Keys.Escape, Keys.Tab];
if (keys.indexOf(keyCode) > -1) {
this.navigationService.processKeyDown(keyCode, event);
}
}
direction;
/**
* Specifies the available user-defined filters. At least one filter should be provided.
*/
set filters(_filters) {
if (_filters.length > 0) {
this.filterService.filters = _filters.map(filterExpression => {
const clonedFilter = Object.assign({}, filterExpression);
if (!clonedFilter.title) {
clonedFilter.title = clonedFilter.field;
}
return clonedFilter;
});
this.setValue(this.value);
}
}
get filters() {
return this.filterService.filters;
}
/**
* Sets the initial `value` of the Filter component.
*/
set value(value) {
const clonedValue = structuredClone(value);
if (this.filtersTypeChanged(this.value, clonedValue)) {
// due to tracking group items by index, the filters should first be set to [] when the value is changed programmatically
this.currentFilter.filters = [];
this.cdr.detectChanges();
}
this._value = clonedValue;
if (this.filters.length > 0) {
this.setValue(this.value);
}
}
get value() {
return this._value;
}
/**
* Fires every time the Filter component value is updated.
* That is each time a Filter Group or Filter Expression is added, removed, or updated.
*/
valueChange = new EventEmitter();
localizationSubscription;
filterFieldsSubscription;
_value = { filters: [], logic: 'and' };
filterFields;
_filterItems;
get filterItems() {
return this._filterItems.toArray();
}
get toolbarElement() {
return this.element.nativeElement.querySelector('.k-toolbar');
}
constructor(filterService, localization, cdr, element, navigationService, renderer) {
this.filterService = filterService;
this.localization = localization;
this.cdr = cdr;
this.element = element;
this.navigationService = navigationService;
this.renderer = renderer;
validatePackage(packageMetadata);
this.direction = localization.rtl ? 'rtl' : 'ltr';
}
ngOnInit() {
this.localizationSubscription = this.localization.changes.subscribe(({ rtl }) => {
this.direction = rtl ? 'rtl' : 'ltr';
this.cdr.detectChanges();
});
}
ngAfterViewInit() {
this.filterFieldsChanged();
this.filterFieldsSubscription = this.filterFields.changes.subscribe(this.filterFieldsChanged.bind(this));
}
ngOnDestroy() {
if (this.localizationSubscription) {
this.localizationSubscription.unsubscribe();
}
if (this.filterFieldsSubscription) {
this.filterFieldsSubscription.unsubscribe();
}
}
filterFieldsChanged() {
if (this.filterFields && this.filterFields.length > 0) {
this.filters = this.filterFields.map((filterField) => ({
...filterField,
title: filterField.title,
editorTemplate: filterField.editorTemplate?.templateRef
}));
}
if (this.filters.length === 0) {
throw new Error(FilterErrorMessages.missingFilters);
}
this.navigationService.reset(this.filterItems);
if (!this.navigationService.currentlyFocusedElement) {
const firstElement = this.navigationService.flattenFilterItems[0].focusableChildren[0];
this.navigationService.currentlyFocusedElement = firstElement;
this.renderer.setAttribute(firstElement, 'tabindex', '0');
}
}
_currentFilter = { logic: 'and', filters: [] };
/**
* @hidden
*/
get currentFilter() {
return this._currentFilter;
}
/**
* @hidden
*/
set currentFilter(value) {
this._currentFilter = value;
}
/**
* @hidden
*/
onValueChange(isRemoveOperation) {
this.cdr.detectChanges();
this.valueChange.emit(this.filterService.normalizedValue);
this.navigationService.reset(this.filterItems);
if (isRemoveOperation) {
if (this.navigationService.currentToolbarItemIndex === this.navigationService.flattenFilterItems.length) {
this.navigationService.currentToolbarItemIndex -= 1;
}
this.navigationService.isFilterExpressionComponentFocused = false;
const itemIndex = this.navigationService.currentToolbarItemIndex;
const toolbarItem = this.navigationService.flattenFilterItems[itemIndex];
const activeChildIndex = toolbarItem.focusableChildren.length - 1;
this.navigationService.currentlyFocusedElement = toolbarItem.focusableChildren[activeChildIndex];
this.renderer.setAttribute(this.navigationService.currentlyFocusedElement, 'tabindex', '0');
this.renderer.addClass(this.navigationService.currentlyFocusedElement, 'k-focus');
this.navigationService.currentlyFocusedElement.focus();
}
}
normalizeFilter(filterDescriptor) {
const foundFilter = this.filterService.filters.find((filter) => filter.field === filterDescriptor.field);
if (isDevMode() && !foundFilter) {
throw new Error(FilterErrorMessages.missingFilterForUsedField(filterDescriptor.field));
}
if (isDevMode() && foundFilter.editor === 'boolean' && (!filterDescriptor.value && filterDescriptor.value !== false)) {
console.warn(FilterErrorMessages.missingValueForBooleanField(filterDescriptor.field));
}
if (isDevMode() && foundFilter.editor === 'boolean' && filterDescriptor.operator !== 'eq') {
console.warn(FilterErrorMessages.operatorBooleanField(filterDescriptor.field));
}
if (filterDescriptor.operator && foundFilter.operators && !foundFilter.operators.some(operator => operator === filterDescriptor.operator)) {
throw new Error(FilterErrorMessages.filterMissingUsedOperator(filterDescriptor.field, filterDescriptor.operator));
}
if (foundFilter.editor === 'boolean') {
filterDescriptor.operator = 'eq';
}
if (foundFilter.editor === 'date' && filterDescriptor.value) {
filterDescriptor.value = new Date(filterDescriptor.value);
}
if (!isPresent(filterDescriptor.value)) {
filterDescriptor.value = null;
}
if (nullOperators.indexOf(filterDescriptor.operator) >= 0) {
filterDescriptor.value = null;
}
}
setValue(items) {
this.normalizeValue(items);
this.filterService.normalizedValue = items;
this.currentFilter = items;
this.cdr.detectChanges();
this.navigationService.reset(this.filterItems);
}
normalizeValue(items) {
if (!this.filterService.filters.length) {
return;
}
items.filters.forEach((item) => {
if (item.filters) {
this.normalizeValue(item);
}
else {
this.normalizeFilter(item);
}
});
}
/**
* @hidden
*/
messageFor(key) {
return this.localization.get(key);
}
/**
* @hidden
*/
filtersTypeChanged(previousValue, newValue) {
if (!(previousValue?.filters) && !(newValue?.filters)) {
return previousValue?.operator != newValue?.operator ||
previousValue?.field != newValue?.field;
}
if (!(previousValue?.filters) || !(newValue?.filters)) {
return true;
}
const previousFilters = previousValue.filters;
const newFilters = newValue.filters;
if (previousFilters.length !== newFilters.length) {
return true;
}
for (let i = 0; i < previousFilters.length; i++) {
if (this.filtersTypeChanged(previousFilters[i], newFilters[i])) {
return true;
}
}
return false;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FilterComponent, deps: [{ token: i1.FilterService }, { token: i2.LocalizationService }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i3.NavigationService }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: FilterComponent, isStandalone: true, selector: "kendo-filter", inputs: { filters: "filters", value: "value" }, outputs: { valueChange: "valueChange" }, host: { listeners: { "focusout": "focusout($event)", "focusin": "focusin($event)", "keydown": "onKeydown($event)" }, properties: { "attr.dir": "this.direction" } }, providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.filter'
},
FilterService,
NavigationService
], queries: [{ propertyName: "filterFields", predicate: FilterFieldComponent }], viewQueries: [{ propertyName: "_filterItems", predicate: FilterItem, descendants: true }], ngImport: i0, template: `
<ng-container kendoFilterLocalizedMessages
i18n-editorDateTodayText="kendo.filter.editorDateTodayText|The text of the Today button of the Date editor"
editorDateTodayText="Today"
i18n-editorDateToggleText="kendo.filter.editorDateToggleText|The title of the Toggle button of the Date editor."
editorDateToggleText="Toggle calendar"
i18n-editorNumericDecrement="kendo.filter.editorNumericDecrement|The title of the Decrement button of the Numeric editor"
editorNumericDecrement="Decrement"
i18n-editorNumericIncrement="kendo.filter.editorNumericIncrement|The title of the Increment button of the Numeric editor"
editorNumericIncrement="Increment"
i18n-filterExpressionOperators="kendo.filter.filterExpressionOperators|The text of the Filter Expression Operators drop down"
filterExpressionOperators="Operators"
i18n-filterExpressionFilters="kendo.filter.filterExpressionFilters|The text of the Filter Expression filters drop down"
filterExpressionFilters="Fields"
i18n-remove="kendo.filter.remove|The text of the Remove button"
remove="Remove"
i18n-addFilter="kendo.filter.addFilter|The text of the Add Filter button"
addFilter="Add Filter"
i18n-addGroup="kendo.filter.addGroup|The text of the Add Group button"
addGroup="Add Group"
i18n-filterAndLogic="kendo.filter.filterAndLogic|The text of the And filter logic"
filterAndLogic="And"
i18n-filterOrLogic="kendo.filter.filterOrLogic|The text of the Or filter logic"
filterOrLogic="Or"
i18n-filterEqOperator="kendo.filter.filterEqOperator|The text of the equal filter operator"
filterEqOperator="Is equal to"
i18n-filterNotEqOperator="kendo.filter.filterNotEqOperator|The text of the not equal filter operator"
filterNotEqOperator="Is not equal to"
i18n-filterIsNullOperator="kendo.filter.filterIsNullOperator|The text of the is null filter operator"
filterIsNullOperator="Is null"
i18n-filterIsNotNullOperator="kendo.filter.filterIsNotNullOperator|The text of the is not null filter operator"
filterIsNotNullOperator="Is not null"
i18n-filterIsEmptyOperator="kendo.filter.filterIsEmptyOperator|The text of the is empty filter operator"
filterIsEmptyOperator="Is empty"
i18n-filterIsNotEmptyOperator="kendo.filter.filterIsNotEmptyOperator|The text of the is not empty filter operator"
filterIsNotEmptyOperator="Is not empty"
i18n-filterStartsWithOperator="kendo.filter.filterStartsWithOperator|The text of the starts with filter operator"
filterStartsWithOperator="Starts with"
i18n-filterContainsOperator="kendo.filter.filterContainsOperator|The text of the contains filter operator"
filterContainsOperator="Contains"
i18n-filterNotContainsOperator="kendo.filter.filterNotContainsOperator|The text of the does not contain filter operator"
filterNotContainsOperator="Does not contain"
i18n-filterEndsWithOperator="kendo.filter.filterEndsWithOperator|The text of the ends with filter operator"
filterEndsWithOperator="Ends with"
i18n-filterGteOperator="kendo.filter.filterGteOperator|The text of the greater than or equal filter operator"
filterGteOperator="Is greater than or equal to"
i18n-filterGtOperator="kendo.filter.filterGtOperator|The text of the greater than filter operator"
filterGtOperator="Is greater than"
i18n-filterLteOperator="kendo.filter.filterLteOperator|The text of the less than or equal filter operator"
filterLteOperator="Is less than or equal to"
i18n-filterLtOperator="kendo.filter.filterLtOperator|The text of the less than filter operator"
filterLtOperator="Is less than"
i18n-filterIsTrue="kendo.filter.filterIsTrue|The text of the IsTrue boolean filter option"
filterIsTrue="Is True"
i18n-filterIsFalse="kendo.filter.filterIsFalse|The text of the IsFalse boolean filter option"
filterIsFalse="Is False"
i18n-filterBooleanAll="kendo.filter.filterBooleanAll|The text of the (All) boolean filter option"
filterBooleanAll="(All)"
i18n-filterAfterOrEqualOperator="kendo.filter.filterAfterOrEqualOperator|The text of the after or equal date filter operator"
filterAfterOrEqualOperator="Is after or equal to"
i18n-filterAfterOperator="kendo.filter.filterAfterOperator|The text of the after date filter operator"
filterAfterOperator="Is after"
i18n-filterBeforeOperator="kendo.filter.filterBeforeOperator|The text of the before date filter operator"
filterBeforeOperator="Is before"
i18n-filterBeforeOrEqualOperator="kendo.filter.filterBeforeOrEqualOperator|The text of the before or equal date filter operator"
filterBeforeOrEqualOperator="Is before or equal to"
i18n-filterFieldAriaLabel="kendo.filter.filterFieldAriaLabel|The text of the filter field aria label"
filterFieldAriaLabel="field"
i18n-filterOperatorAriaLabel="kendo.filter.filterOperatorAriaLabel|The text of the filter operator aria label"
filterOperatorAriaLabel="operator"
i18n-filterValueAriaLabel="kendo.filter.filterValueAriaLabel|The text of the filter value aria label"
filterValueAriaLabel="value"
i18n-filterToolbarAriaLabel="kendo.filter.filterToolbarAriaLabel|The text of the filter row aria label"
filterToolbarAriaLabel="filter row settings"
i18n-filterComponentAriaLabel="kendo.filter.filterComponentAriaLabel|The text of the filter component aria label"
filterComponentAriaLabel="filter component"
>
</ng-container>
<div class="k-filter" [attr.dir]="direction">
<ul class='k-filter-container' role="tree" [attr.aria-label]="messageFor('filterComponentAriaLabel')">
<li class='k-filter-group-main' role="treeitem">
<kendo-filter-group
[currentItem]="currentFilter"
(valueChange)="onValueChange($event)"
>
</kendo-filter-group>
</li>
</ul>
</div>
`, isInline: true, dependencies: [{ kind: "directive", type: LocalizedMessagesDirective, selector: "[kendoFilterLocalizedMessages]" }, { kind: "component", type: FilterGroupComponent, selector: "kendo-filter-group", inputs: ["currentItem"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FilterComponent, decorators: [{
type: Component,
args: [{
providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.filter'
},
FilterService,
NavigationService
],
selector: 'kendo-filter',
template: `
<ng-container kendoFilterLocalizedMessages
i18n-editorDateTodayText="kendo.filter.editorDateTodayText|The text of the Today button of the Date editor"
editorDateTodayText="Today"
i18n-editorDateToggleText="kendo.filter.editorDateToggleText|The title of the Toggle button of the Date editor."
editorDateToggleText="Toggle calendar"
i18n-editorNumericDecrement="kendo.filter.editorNumericDecrement|The title of the Decrement button of the Numeric editor"
editorNumericDecrement="Decrement"
i18n-editorNumericIncrement="kendo.filter.editorNumericIncrement|The title of the Increment button of the Numeric editor"
editorNumericIncrement="Increment"
i18n-filterExpressionOperators="kendo.filter.filterExpressionOperators|The text of the Filter Expression Operators drop down"
filterExpressionOperators="Operators"
i18n-filterExpressionFilters="kendo.filter.filterExpressionFilters|The text of the Filter Expression filters drop down"
filterExpressionFilters="Fields"
i18n-remove="kendo.filter.remove|The text of the Remove button"
remove="Remove"
i18n-addFilter="kendo.filter.addFilter|The text of the Add Filter button"
addFilter="Add Filter"
i18n-addGroup="kendo.filter.addGroup|The text of the Add Group button"
addGroup="Add Group"
i18n-filterAndLogic="kendo.filter.filterAndLogic|The text of the And filter logic"
filterAndLogic="And"
i18n-filterOrLogic="kendo.filter.filterOrLogic|The text of the Or filter logic"
filterOrLogic="Or"
i18n-filterEqOperator="kendo.filter.filterEqOperator|The text of the equal filter operator"
filterEqOperator="Is equal to"
i18n-filterNotEqOperator="kendo.filter.filterNotEqOperator|The text of the not equal filter operator"
filterNotEqOperator="Is not equal to"
i18n-filterIsNullOperator="kendo.filter.filterIsNullOperator|The text of the is null filter operator"
filterIsNullOperator="Is null"
i18n-filterIsNotNullOperator="kendo.filter.filterIsNotNullOperator|The text of the is not null filter operator"
filterIsNotNullOperator="Is not null"
i18n-filterIsEmptyOperator="kendo.filter.filterIsEmptyOperator|The text of the is empty filter operator"
filterIsEmptyOperator="Is empty"
i18n-filterIsNotEmptyOperator="kendo.filter.filterIsNotEmptyOperator|The text of the is not empty filter operator"
filterIsNotEmptyOperator="Is not empty"
i18n-filterStartsWithOperator="kendo.filter.filterStartsWithOperator|The text of the starts with filter operator"
filterStartsWithOperator="Starts with"
i18n-filterContainsOperator="kendo.filter.filterContainsOperator|The text of the contains filter operator"
filterContainsOperator="Contains"
i18n-filterNotContainsOperator="kendo.filter.filterNotContainsOperator|The text of the does not contain filter operator"
filterNotContainsOperator="Does not contain"
i18n-filterEndsWithOperator="kendo.filter.filterEndsWithOperator|The text of the ends with filter operator"
filterEndsWithOperator="Ends with"
i18n-filterGteOperator="kendo.filter.filterGteOperator|The text of the greater than or equal filter operator"
filterGteOperator="Is greater than or equal to"
i18n-filterGtOperator="kendo.filter.filterGtOperator|The text of the greater than filter operator"
filterGtOperator="Is greater than"
i18n-filterLteOperator="kendo.filter.filterLteOperator|The text of the less than or equal filter operator"
filterLteOperator="Is less than or equal to"
i18n-filterLtOperator="kendo.filter.filterLtOperator|The text of the less than filter operator"
filterLtOperator="Is less than"
i18n-filterIsTrue="kendo.filter.filterIsTrue|The text of the IsTrue boolean filter option"
filterIsTrue="Is True"
i18n-filterIsFalse="kendo.filter.filterIsFalse|The text of the IsFalse boolean filter option"
filterIsFalse="Is False"
i18n-filterBooleanAll="kendo.filter.filterBooleanAll|The text of the (All) boolean filter option"
filterBooleanAll="(All)"
i18n-filterAfterOrEqualOperator="kendo.filter.filterAfterOrEqualOperator|The text of the after or equal date filter operator"
filterAfterOrEqualOperator="Is after or equal to"
i18n-filterAfterOperator="kendo.filter.filterAfterOperator|The text of the after date filter operator"
filterAfterOperator="Is after"
i18n-filterBeforeOperator="kendo.filter.filterBeforeOperator|The text of the before date filter operator"
filterBeforeOperator="Is before"
i18n-filterBeforeOrEqualOperator="kendo.filter.filterBeforeOrEqualOperator|The text of the before or equal date filter operator"
filterBeforeOrEqualOperator="Is before or equal to"
i18n-filterFieldAriaLabel="kendo.filter.filterFieldAriaLabel|The text of the filter field aria label"
filterFieldAriaLabel="field"
i18n-filterOperatorAriaLabel="kendo.filter.filterOperatorAriaLabel|The text of the filter operator aria label"
filterOperatorAriaLabel="operator"
i18n-filterValueAriaLabel="kendo.filter.filterValueAriaLabel|The text of the filter value aria label"
filterValueAriaLabel="value"
i18n-filterToolbarAriaLabel="kendo.filter.filterToolbarAriaLabel|The text of the filter row aria label"
filterToolbarAriaLabel="filter row settings"
i18n-filterComponentAriaLabel="kendo.filter.filterComponentAriaLabel|The text of the filter component aria label"
filterComponentAriaLabel="filter component"
>
</ng-container>
<div class="k-filter" [attr.dir]="direction">
<ul class='k-filter-container' role="tree" [attr.aria-label]="messageFor('filterComponentAriaLabel')">
<li class='k-filter-group-main' role="treeitem">
<kendo-filter-group
[currentItem]="currentFilter"
(valueChange)="onValueChange($event)"
>
</kendo-filter-group>
</li>
</ul>
</div>
`,
standalone: true,
imports: [LocalizedMessagesDirective, FilterGroupComponent]
}]
}], ctorParameters: function () { return [{ type: i1.FilterService }, { type: i2.LocalizationService }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i3.NavigationService }, { type: i0.Renderer2 }]; }, propDecorators: { focusout: [{
type: HostListener,
args: ['focusout', ['$event']]
}], focusin: [{
type: HostListener,
args: ['focusin', ['$event']]
}], onKeydown: [{
type: HostListener,
args: ['keydown', ['$event']]
}], direction: [{
type: HostBinding,
args: ['attr.dir']
}], filters: [{
type: Input
}], value: [{
type: Input
}], valueChange: [{
type: Output
}], filterFields: [{
type: ContentChildren,
args: [FilterFieldComponent]
}], _filterItems: [{
type: ViewChildren,
args: [FilterItem]
}] } });