UNPKG

@devlukaszmichalak/mat-select-filter

Version:
145 lines (139 loc) 13 kB
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