@devlukaszmichalak/mat-select-filter
Version:
A filter for mat-select
145 lines (139 loc) • 13 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, DestroyRef, viewChild, input, output, signal, Component, NgModule } from '@angular/core';
import * as i2 from '@angular/forms';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { merge, fromEvent, throttleTime, debounceTime, tap, map, finalize, timer, takeUntil } from 'rxjs';
import * as i3 from '@angular/material/progress-spinner';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i4 from '@angular/material/input';
import { MatInputModule } from '@angular/material/input';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
class MatSelectFilterComponent {
constructor() {
this.#fb = inject(FormBuilder);
this.#destroyRef = inject(DestroyRef);
this.input = viewChild('input');
this.array = input.required();
this.placeholder = input('Search...');
this.color = input('white');
this.displayMember = input();
this.showSpinner = input(true);
this.noResultsMessage = input('No results');
this.hasGroup = input();
this.groupArrayName = input();
this.filterDebounceTime = input(0);
this.filteredReturn = output();
this.noResults = signal(false);
this.localSpinner = signal(false);
this.#shouldFocus = signal(true);
this.filteredItems = [];
this.searchForm = this.#fb.group({
filterValue: ''
});
}
#fb;
#destroyRef;
#shouldFocus;
ngOnInit() {
// since the component is not destroyed, we want to reset the filter
// when no options would show and mat-select-filter is not visible
// this also sets item to be focused on the next time it will be opened
merge(fromEvent(document, 'scroll'), fromEvent(document, 'keyup'), fromEvent(document, 'mousemove'), fromEvent(document, 'click')).pipe(throttleTime(100), takeUntilDestroyed(this.#destroyRef)).subscribe(() => {
const filters = document.querySelectorAll('mat-select-filter');
if (filters.length === 0) {
this.#shouldFocus.set(true);
if (this.filteredItems.length === 0) {
this.searchForm.controls.filterValue.setValue('');
this.filteredReturn.emit(this.array());
}
}
if (filters.length > 0 && this.#shouldFocus()) {
this.input()?.nativeElement.focus();
this.#shouldFocus.set(false);
}
});
this.searchForm.valueChanges
.pipe(debounceTime(this.filterDebounceTime()), tap(() => this.localSpinner.set(true)), map(changes => changes.filterValue?.toLowerCase()), takeUntilDestroyed(this.#destroyRef), finalize(() => this.filteredReturn.emit(this.array())))
.subscribe(userInputLowerCase => {
if (userInputLowerCase) {
this.#filterArray(userInputLowerCase);
// NO RESULTS VALIDATION
this.noResults.set(!this.filteredItems == null || this.filteredItems.length === 0);
}
else {
this.filteredItems = this.array().slice();
this.noResults.set(false);
}
this.filteredReturn.emit(this.filteredItems);
timer(2000)
.pipe(takeUntil(this.searchForm.valueChanges))
.subscribe(() => this.localSpinner.set(false));
});
}
#filterArray(userInputLowerCase) {
// IF THE DISPLAY MEMBER INPUT IS SET, WE CHECK THE SPECIFIC PROPERTY
if (!this.displayMember()) {
this.filteredItems = this.array().filter((name) => name.toLowerCase().includes(userInputLowerCase));
return;
}
if (this.hasGroup() && this.groupArrayName() && this.displayMember()) {
this.filteredItems = this.array()
.map((element) => {
const objCopy = { ...element };
objCopy[this.groupArrayName()] = objCopy[this.groupArrayName()]
.filter((groupItem) => groupItem[this.displayMember()].toLowerCase().includes(userInputLowerCase));
return objCopy;
})
.filter((element) => element[this.groupArrayName()].length > 0);
return;
}
// OTHERWISE, WE CHECK THE ENTIRE STRING
this.filteredItems = this.array().filter((item) => item[this.displayMember()].toLowerCase().includes(userInputLowerCase));
}
handleKeydown(event) {
// PREVENT PROPAGATION FOR ALL ALPHANUMERIC CHARACTERS TO AVOID SELECTION ISSUES
if (this.#isAlphanumeric(event)) {
event.stopPropagation();
}
}
#isAlphanumeric(event) {
const key = event.key;
return !!key && key.length === 1 && /^[a-zA-Z0-9 ]$/.test(key);
}
;
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MatSelectFilterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.3", type: MatSelectFilterComponent, isStandalone: true, selector: "mat-select-filter", inputs: { array: { classPropertyName: "array", publicName: "array", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, displayMember: { classPropertyName: "displayMember", publicName: "displayMember", isSignal: true, isRequired: false, transformFunction: null }, showSpinner: { classPropertyName: "showSpinner", publicName: "showSpinner", isSignal: true, isRequired: false, transformFunction: null }, noResultsMessage: { classPropertyName: "noResultsMessage", publicName: "noResultsMessage", isSignal: true, isRequired: false, transformFunction: null }, hasGroup: { classPropertyName: "hasGroup", publicName: "hasGroup", isSignal: true, isRequired: false, transformFunction: null }, groupArrayName: { classPropertyName: "groupArrayName", publicName: "groupArrayName", isSignal: true, isRequired: false, transformFunction: null }, filterDebounceTime: { classPropertyName: "filterDebounceTime", publicName: "filterDebounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filteredReturn: "filteredReturn" }, viewQueries: [{ propertyName: "input", first: true, predicate: ["input"], descendants: true, isSignal: true }], ngImport: i0, template: "<form [formGroup]=\"searchForm\" class=\"mat-filter\"\r\n [ngStyle]=\"{'background-color': color()}\">\r\n <div>\r\n <input #input\r\n class=\"mat-filter-input\"\r\n matInput\r\n placeholder=\"{{placeholder()}}\"\r\n formControlName=\"filterValue\"\r\n (keydown)=\"handleKeydown($event)\">\r\n @if (localSpinner() && showSpinner()) {\r\n <mat-spinner class=\"spinner\" diameter=\"16\"></mat-spinner>\r\n }\r\n </div>\r\n @if (noResults()) {\r\n <div class=\"noResultsMessage no-results-message\">\r\n {{ noResultsMessage() }}\r\n </div>\r\n }\r\n</form>", styles: [".mat-filter{position:sticky;top:-8px;margin-top:-8px;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:gray;z-index:100;font-size:inherit;box-shadow:none;border-radius:0;padding:16px;-webkit-box-sizing:border-box;box-sizing:border-box}.mat-filter:has(.no-results-message){border:0}.mat-filter-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;outline:none;border:0;background-color:unset;color:gray;width:100%}.spinner{position:absolute;right:16px;top:calc(50% - 8px)}.no-results-message{margin-top:16px;font-family:Roboto,Helvetica Neue,sans-serif;font-size:16px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i3.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MatSelectFilterComponent, decorators: [{
type: Component,
args: [{ selector: 'mat-select-filter', imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
MatInputModule
], template: "<form [formGroup]=\"searchForm\" class=\"mat-filter\"\r\n [ngStyle]=\"{'background-color': color()}\">\r\n <div>\r\n <input #input\r\n class=\"mat-filter-input\"\r\n matInput\r\n placeholder=\"{{placeholder()}}\"\r\n formControlName=\"filterValue\"\r\n (keydown)=\"handleKeydown($event)\">\r\n @if (localSpinner() && showSpinner()) {\r\n <mat-spinner class=\"spinner\" diameter=\"16\"></mat-spinner>\r\n }\r\n </div>\r\n @if (noResults()) {\r\n <div class=\"noResultsMessage no-results-message\">\r\n {{ noResultsMessage() }}\r\n </div>\r\n }\r\n</form>", styles: [".mat-filter{position:sticky;top:-8px;margin-top:-8px;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:gray;z-index:100;font-size:inherit;box-shadow:none;border-radius:0;padding:16px;-webkit-box-sizing:border-box;box-sizing:border-box}.mat-filter:has(.no-results-message){border:0}.mat-filter-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;outline:none;border:0;background-color:unset;color:gray;width:100%}.spinner{position:absolute;right:16px;top:calc(50% - 8px)}.no-results-message{margin-top:16px;font-family:Roboto,Helvetica Neue,sans-serif;font-size:16px}\n"] }]
}] });
class MatSelectFilterModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MatSelectFilterModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.3", ngImport: i0, type: MatSelectFilterModule, imports: [MatSelectFilterComponent], exports: [MatSelectFilterComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MatSelectFilterModule, imports: [MatSelectFilterComponent] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: MatSelectFilterModule, decorators: [{
type: NgModule,
args: [{
declarations: [],
imports: [MatSelectFilterComponent],
exports: [MatSelectFilterComponent]
}]
}] });
/*
* Public API Surface of mat-select-filter
*/
/**
* Generated bundle index. Do not edit.
*/
export { MatSelectFilterComponent, MatSelectFilterModule };
//# sourceMappingURL=devlukaszmichalak-mat-select-filter.mjs.map