├── src
├── assets
│ ├── .gitkeep
│ └── example-new.png
├── app
│ ├── examples
│ │ ├── 04-option-groups-example
│ │ │ ├── option-groups-example.component.scss
│ │ │ ├── option-groups-example.component.html
│ │ │ └── option-groups-example.component.ts
│ │ ├── 08-infinite-scroll-example
│ │ │ ├── infinite-scroll-example.component.scss
│ │ │ ├── infinite-scroll-example.component.html
│ │ │ └── infinite-scroll-example.component.ts
│ │ ├── 01-single-selection-example
│ │ │ ├── single-selection-example.component.scss
│ │ │ ├── single-selection-example.component.html
│ │ │ └── single-selection-example.component.ts
│ │ ├── 02-multiple-selection-example
│ │ │ ├── multiple-selection-example.component.scss
│ │ │ ├── multiple-selection-example.component.html
│ │ │ └── multiple-selection-example.component.ts
│ │ ├── 03-custom-clear-icon-example
│ │ │ ├── custom-clear-icon-example.component.scss
│ │ │ ├── custom-clear-icon-example.component.html
│ │ │ └── custom-clear-icon-example.component.ts
│ │ ├── 05-server-side-search-example
│ │ │ ├── server-side-search-example.component.scss
│ │ │ ├── server-side-search-example.component.html
│ │ │ └── server-side-search-example.component.ts
│ │ ├── 09-custom-no-entries-found-example
│ │ │ ├── custom-no-entries-found-example.component.scss
│ │ │ ├── custom-no-entries-found-example.component.html
│ │ │ └── custom-no-entries-found-example.component.ts
│ │ ├── 06-multiple-selection-select-all-example
│ │ │ ├── multiple-selection-select-all-example.component.scss
│ │ │ ├── multiple-selection-select-all-example.component.html
│ │ │ └── multiple-selection-select-all-example.component.ts
│ │ ├── 07-tooltip-select-all-example
│ │ │ ├── tooltip-select-all-example.component.scss
│ │ │ ├── tooltip-select-all-example.component.html
│ │ │ └── tooltip-select-all-example.component.ts
│ │ └── demo-data.ts
│ ├── app.component.scss
│ ├── mat-select-search
│ │ ├── public_api.ts
│ │ ├── mat-select-search-clear.directive.ts
│ │ ├── mat-select-no-entries-found.directive.ts
│ │ ├── ngx-mat-select-search.module.ts
│ │ ├── default-options.ts
│ │ ├── mat-select-search.component.html
│ │ ├── mat-select-search.component.scss
│ │ ├── mat-select-search.component.ts
│ │ └── mat-select-search.component.spec.ts
│ ├── app.component.spec.ts
│ ├── app.component.html
│ └── app.component.ts
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── tsconfig.app.json
├── tsconfig.spec.json
├── styles.scss
├── index.html
├── main.ts
├── test.ts
└── polyfills.ts
├── ng-package.json
├── .editorconfig
├── .github
├── workflows
│ └── main.yml
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── .browserslistrc
├── tsconfig.json
├── .gitignore
├── LICENSE
├── eslint.config.js
├── karma.conf.js
├── .circleci
└── config.yml
├── package.json
├── angular.json
├── CHANGELOG.md
└── README.md
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/examples/04-option-groups-example/option-groups-example.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/examples/08-infinite-scroll-example/infinite-scroll-example.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/examples/01-single-selection-example/single-selection-example.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/examples/02-multiple-selection-example/multiple-selection-example.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/examples/03-custom-clear-icon-example/custom-clear-icon-example.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/examples/05-server-side-search-example/server-side-search-example.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | margin-bottom: 200px;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/examples/09-custom-no-entries-found-example/custom-no-entries-found-example.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bithost-gmbh/ngx-mat-select-search/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/src/app/examples/06-multiple-selection-select-all-example/multiple-selection-select-all-example.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/example-new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bithost-gmbh/ngx-mat-select-search/HEAD/src/assets/example-new.png
--------------------------------------------------------------------------------
/src/app/examples/07-tooltip-select-all-example/tooltip-select-all-example.component.scss:
--------------------------------------------------------------------------------
1 | ::ng-deep .ngx-mat-select-search-toggle-all-tooltip {
2 | font-size: 0.8em;
3 | }
--------------------------------------------------------------------------------
/src/app/mat-select-search/public_api.ts:
--------------------------------------------------------------------------------
1 | export * from './mat-select-search.component';
2 | export * from './ngx-mat-select-search.module';
3 | export * from './default-options';
4 |
--------------------------------------------------------------------------------
/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "dist-lib",
4 | "lib": {
5 | "entryFile": "src/app/mat-select-search/public_api.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "baseUrl": "./",
6 | "types": []
7 | },
8 | "files": [
9 | "main.ts",
10 | "polyfills.ts"
11 | ],
12 | "include": [
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "baseUrl": "./",
6 | "types": [
7 | "jasmine",
8 | "node"
9 | ]
10 | },
11 | "files": [
12 | "test.ts",
13 | "polyfills.ts"
14 | ],
15 | "include": [
16 | "**/*.spec.ts",
17 | "**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - master
5 |
6 | jobs:
7 | contrib-readme-job:
8 | runs-on: ubuntu-latest
9 | name: A job to automate contrib in readme
10 | steps:
11 | - name: Contribute List
12 | uses: akhilmhdh/contributors-readme-action@v2.3.10
13 | env:
14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
15 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @import '@angular/material/prebuilt-themes/deeppurple-amber.css';
2 |
3 | body {
4 | font-family: Roboto, Arial, sans-serif;
5 | margin: 0;
6 | }
7 |
8 | .basic-container {
9 | padding: 5px;
10 | padding-left: 10px;
11 | padding-right: 10px;
12 | }
13 |
14 | .version-info {
15 | font-size: 8pt;
16 | float: right;
17 | }
18 | [dir="rtl"] .version-info{
19 | float: left;
20 | }
21 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false
8 | };
9 |
--------------------------------------------------------------------------------
/src/app/mat-select-search/mat-select-search-clear.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive } from '@angular/core';
2 |
3 | /**
4 | * Directive for providing a custom clear-icon.
5 | * e.g.
6 | *
7 | * delete
8 | *
9 | */
10 | @Directive({
11 | selector: '[ngxMatSelectSearchClear]',
12 | })
13 | export class MatSelectSearchClearDirective {}
14 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NgxMatSelectSearch
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # You can see what browsers were selected by your queries by running:
6 | # npx browserslist
7 |
8 | > 0.5%
9 | last 2 versions
10 | Firefox ESR
11 | not dead
12 | not IE 9-11 # For IE 9-11 support, remove 'not'.
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=NAX558HVGAX8Q
9 |
--------------------------------------------------------------------------------
/src/app/mat-select-search/mat-select-no-entries-found.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive } from '@angular/core';
2 |
3 | /**
4 | * Directive for providing a custom no entries found element.
5 | * e.g.
6 | *
7 | *
8 | * No entries found
9 | *
10 | *
11 | */
12 | @Directive({
13 | selector: '[ngxMatSelectNoEntriesFound]',
14 | })
15 | export class MatSelectNoEntriesFoundDirective {}
16 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { bootstrapApplication } from '@angular/platform-browser';
3 | import { provideAnimations } from '@angular/platform-browser/animations';
4 |
5 | import { environment } from './environments/environment';
6 | import { AppComponent } from './app/app.component';
7 |
8 | if (environment.production) {
9 | enableProdMode();
10 | }
11 |
12 | bootstrapApplication(AppComponent, {
13 | providers: [
14 | provideAnimations()
15 | ]
16 | })
17 | .catch(err => console.log(err));
18 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | // First, initialize the Angular testing environment.
11 | getTestBed().initTestEnvironment(
12 | BrowserDynamicTestingModule,
13 | platformBrowserDynamicTesting(), {
14 | teardown: { destroyAfterEach: false }
15 | }
16 | );
17 |
--------------------------------------------------------------------------------
/src/app/examples/01-single-selection-example/single-selection-example.component.html:
--------------------------------------------------------------------------------
1 | Single selection
2 |
3 |
4 |
5 |
6 |
7 |
8 | @for (bank of $filteredBanks(); track $index)
9 | {
10 |
11 | {{bank.name}}
12 |
13 | }
14 |
15 |
16 |
17 |
18 | Selected Bank: {{bankCtrl.value?.name}}
19 |
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "module": "es2020",
5 | "esModuleInterop": true,
6 | "outDir": "./dist/out-tsc",
7 | "sourceMap": true,
8 | "declaration": false,
9 | "moduleResolution": "node",
10 | "experimentalDecorators": true,
11 | "strictNullChecks": true,
12 | "target": "ES2022",
13 | "typeRoots": [
14 | "node_modules/@types"
15 | ],
16 | "lib": [
17 | "es2017",
18 | "dom"
19 | ],
20 | "useDefineForClassFields": false
21 | },
22 | "angularCompilerOptions": {
23 | "strictInjectionParameters": true,
24 | "strictInputAccessModifiers": true,
25 | "strictTemplates": true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, waitForAsync } from '@angular/core/testing';
2 |
3 | import { AppComponent } from './app.component';
4 | import { NoopAnimationsModule } from '@angular/platform-browser/animations';
5 |
6 | describe('AppComponent', () => {
7 | beforeEach(waitForAsync(() => {
8 | TestBed.configureTestingModule({
9 | imports: [
10 | NoopAnimationsModule
11 | ],
12 | }).compileComponents();
13 | }));
14 | it('should create the app', waitForAsync(() => {
15 | const fixture = TestBed.createComponent(AppComponent);
16 | fixture.detectChanges();
17 | const app = fixture.debugElement.componentInstance;
18 | expect(app).toBeTruthy();
19 | }));
20 |
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/examples/02-multiple-selection-example/multiple-selection-example.component.html:
--------------------------------------------------------------------------------
1 |
2 | Multiple selection
3 |
4 |
5 |
6 |
7 |
8 |
9 | @for(bank of $filteredBanks(); track $index)
10 | {
11 |
12 | {{bank.name}}
13 |
14 | }
15 |
16 |
17 |
18 |
19 | Selected Banks:
20 |
21 |
22 | @for(bank of bankMultiCtrl.value; track $index)
23 | {
24 | - {{bank.name}}
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[FEATURE]"
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 |
12 |
13 | **Describe the solution you'd like**
14 |
15 |
16 | **Describe alternatives you've considered**
17 |
18 |
19 | **Additional context**
20 |
21 |
--------------------------------------------------------------------------------
/src/app/examples/03-custom-clear-icon-example/custom-clear-icon-example.component.html:
--------------------------------------------------------------------------------
1 | Single selection with custom clear icon
2 |
3 |
4 |
5 |
6 |
7 | delete
8 |
9 |
10 | @for (bank of $filteredBanks(); track $index)
11 | {
12 |
13 | {{bank.name}}
14 |
15 | }
16 |
17 |
18 |
19 |
20 | Selected Bank: {{bankCtrl.value?.name}}
21 |
22 |
--------------------------------------------------------------------------------
/src/app/examples/05-server-side-search-example/server-side-search-example.component.html:
--------------------------------------------------------------------------------
1 | Server Side Search
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | @for (bank of $filteredServerSideBanks(); track $index)
10 | {
11 |
12 | {{bank.name}}
13 |
14 | }
15 |
16 |
17 |
18 |
19 | Selected Bank: {{bankServerSideCtrl.value?.name}}
20 |
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /dist-server
6 | /tmp
7 | /out-tsc
8 |
9 | # dependencies
10 | /node_modules
11 |
12 | # IDEs and editors
13 | /.idea
14 | .project
15 | .classpath
16 | .c9/
17 | *.launch
18 | .settings/
19 | *.sublime-workspace
20 |
21 | # IDE - VSCode
22 | .vscode/*
23 | !.vscode/settings.json
24 | !.vscode/tasks.json
25 | !.vscode/launch.json
26 | !.vscode/extensions.json
27 |
28 | # misc
29 | /.angular/cache
30 | /.sass-cache
31 | /connect.lock
32 | /coverage
33 | /libpeerconnection.log
34 | npm-debug.log
35 | yarn-error.log
36 | testem.log
37 | /typings
38 | .history
39 |
40 | # e2e
41 | /e2e/*.js
42 | /e2e/*.map
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 |
48 | /.ng_build
49 | /dist-lib
50 |
--------------------------------------------------------------------------------
/src/app/examples/04-option-groups-example/option-groups-example.component.html:
--------------------------------------------------------------------------------
1 | Single selection with option groups
2 |
3 |
4 |
5 |
6 |
7 |
8 | @for(group of $filteredBankGroups(); track $index)
9 | {
10 |
11 | @for(bank of group.banks; track $index)
12 | {
13 |
14 | {{bank.name}}
15 |
16 | }
17 |
18 | }
19 |
20 |
21 |
22 |
23 | Selected Bank: {{bankGroupsCtrl.value?.name}}
24 |
25 |
--------------------------------------------------------------------------------
/src/app/examples/09-custom-no-entries-found-example/custom-no-entries-found-example.component.html:
--------------------------------------------------------------------------------
1 | Single selection with custom no entries found element
2 |
3 |
4 |
5 |
6 |
7 |
8 | No entries found
9 |
12 |
13 |
14 |
15 | @for(bank of $filteredBanks(); track $index)
16 | {
17 |
18 | {{bank.name}}
19 |
20 | }
21 |
22 |
23 |
24 |
25 | Selected Bank: {{bankCtrl.value?.name}}
26 |
27 |
--------------------------------------------------------------------------------
/src/app/mat-select-search/ngx-mat-select-search.module.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018 Bithost GmbH All Rights Reserved.
3 | *
4 | * Use of this source code is governed by an MIT-style license that can be
5 | * found in the LICENSE file at https://angular.io/license
6 | */
7 |
8 | import { NgModule } from '@angular/core';
9 | import { MatSelectSearchComponent } from './mat-select-search.component';
10 |
11 | import { MatSelectSearchClearDirective } from './mat-select-search-clear.directive';
12 | import { MatSelectNoEntriesFoundDirective } from './mat-select-no-entries-found.directive';
13 |
14 | export const MatSelectSearchVersion = '8.0.4';
15 | export { MatSelectSearchClearDirective };
16 | export { MatSelectNoEntriesFoundDirective };
17 |
18 | @NgModule({
19 | imports: [
20 | MatSelectSearchComponent,
21 | MatSelectSearchClearDirective,
22 | MatSelectNoEntriesFoundDirective
23 | ],
24 | exports: [
25 | MatSelectSearchComponent,
26 | MatSelectSearchClearDirective,
27 | MatSelectNoEntriesFoundDirective
28 | ]
29 | })
30 | export class NgxMatSelectSearchModule {}
31 |
--------------------------------------------------------------------------------
/src/app/examples/06-multiple-selection-select-all-example/multiple-selection-select-all-example.component.html:
--------------------------------------------------------------------------------
1 | Multiple selection with Select All Checkbox
2 |
3 |
4 |
5 |
6 |
11 |
12 | @for(bank of $filteredBanks(); track $index)
13 | {
14 |
15 | {{bank.name}}
16 |
17 | }
18 |
19 |
20 |
21 |
22 | Selected Banks:
23 |
24 |
25 | @for(bank of bankMultiCtrl.value; track $index)
26 | {
27 | - {{bank.name}}
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/src/app/examples/03-custom-clear-icon-example/custom-clear-icon-example.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { MatFormFieldModule } from '@angular/material/form-field';
3 | import { MatSelectModule } from '@angular/material/select';
4 | import { ReactiveFormsModule } from '@angular/forms';
5 | import { MatIcon } from '@angular/material/icon';
6 |
7 | import { SingleSelectionExampleComponent } from '../01-single-selection-example/single-selection-example.component';
8 | import { MatSelectSearchComponent } from '../../mat-select-search/mat-select-search.component';
9 | import { MatSelectSearchClearDirective } from '../../mat-select-search/mat-select-search-clear.directive';
10 |
11 |
12 | @Component({
13 | selector: 'app-custom-clear-icon-example',
14 | templateUrl: './custom-clear-icon-example.component.html',
15 | styleUrl: './custom-clear-icon-example.component.scss',
16 | imports: [MatFormFieldModule, MatSelectModule, ReactiveFormsModule, MatSelectSearchComponent, MatIcon, MatSelectSearchClearDirective]
17 | })
18 | export class CustomClearIconExampleComponent extends SingleSelectionExampleComponent {}
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Bithost GmbH
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | const eslint = require("@eslint/js");
3 | const tseslint = require("typescript-eslint");
4 | const angular = require("angular-eslint");
5 |
6 | module.exports = tseslint.config(
7 | {
8 | files: ["**/*.ts"],
9 | extends: [
10 | eslint.configs.recommended,
11 | ...tseslint.configs.recommended,
12 | ...tseslint.configs.stylistic,
13 | ...angular.configs.tsRecommended,
14 | ],
15 | processor: angular.processInlineTemplates,
16 | rules: {
17 | "@angular-eslint/prefer-standalone": "off",
18 | "@angular-eslint/directive-selector": [
19 | "error",
20 | {
21 | type: "attribute",
22 | style: "camelCase",
23 | },
24 | ],
25 | "@angular-eslint/component-selector": [
26 | "error",
27 | {
28 | type: "element",
29 | style: "kebab-case",
30 | },
31 | ],
32 | },
33 | },
34 | {
35 | files: ["**/*.html"],
36 | extends: [
37 | ...angular.configs.templateRecommended,
38 | ...angular.configs.templateAccessibility,
39 | ],
40 | rules: {},
41 | }
42 | );
43 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client:{
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly', 'text-summary' ],
20 | fixWebpackSourcePaths: true
21 | },
22 | angularCli: {
23 | environment: 'dev'
24 | },
25 | reporters: ['progress', 'kjhtml'],
26 | port: 9876,
27 | colors: true,
28 | logLevel: config.LOG_INFO,
29 | autoWatch: true,
30 | browsers: ['Chrome'],
31 | singleRun: false
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/src/app/examples/08-infinite-scroll-example/infinite-scroll-example.component.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | Selected Bank: {{bankCtrl?.value}}
6 |
7 |
8 |
9 |
32 |
--------------------------------------------------------------------------------
/src/app/examples/07-tooltip-select-all-example/tooltip-select-all-example.component.html:
--------------------------------------------------------------------------------
1 | Tooltip on the Select All Checkbox
2 |
3 |
4 |
5 |
6 |
13 |
14 | @for(bank of $filteredBanks(); track $index)
15 | {
16 |
17 | {{bank.name}}
18 |
19 | }
20 |
21 |
22 |
23 |
24 | Selected Banks:
25 |
26 |
27 | @for(bank of bankMultiCtrl.value; track $index)
28 | {
29 | - {{bank.name}}
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG]"
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 |
12 |
13 | **To Reproduce**
14 |
19 | *Important* Please provide a reproduction example. You can use https://stackblitz.com/github/bithost-gmbh/ngx-mat-select-search-example as a starting point
20 |
21 | **Expected behavior**
22 |
23 |
24 | **Screenshots**
25 |
26 |
27 | **Desktop (please complete the following information):**
28 |
33 |
34 | **Smartphone (please complete the following information):**
35 |
41 |
42 | **Additional context**
43 | Add any other context about the problem here.
44 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 | Angular Material 2 App
3 |
4 |
5 |
ngx-mat-select-search
6 |
Angular component providing an input field for searching / filtering
7 | MatSelect
8 | options of the Angular Material library.
9 |
10 |
https://github.com/bithost-gmbh/ngx-mat-select-search
11 |
12 |
Examples
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Right-to-left
32 |
33 |
34 |
35 |
36 |
37 | ngx-mat-select-search Version: {{matSelectSearchVersion}}
38 | Material Version: {{version.full}}
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/app/mat-select-search/default-options.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken } from '@angular/core';
2 | import { MatSelectSearchComponent } from './mat-select-search.component';
3 |
4 | /** List of inputs of NgxMatSelectSearchComponent that can be configured with a global default. */
5 | export const configurableDefaultOptions = [
6 | 'ariaLabel',
7 | 'clearSearchInput',
8 | 'closeIcon',
9 | 'closeSvgIcon',
10 | 'disableInitialFocus',
11 | 'disableScrollToActiveOnOptionsChanged',
12 | 'enableClearOnEscapePressed',
13 | 'hideClearSearchButton',
14 | 'noEntriesFoundLabel',
15 | 'placeholderLabel',
16 | 'preventHomeEndKeyPropagation',
17 | 'searching',
18 | ] as const;
19 |
20 | export type ConfigurableDefaultOptions = typeof configurableDefaultOptions[number];
21 |
22 | /**
23 | * InjectionToken that can be used to specify global options. e.g.
24 | *
25 | * ```typescript
26 | * providers: [
27 | * {
28 | * provide: MAT_SELECTSEARCH_DEFAULT_OPTIONS,
29 | * useValue: {
30 | * closeIcon: 'delete',
31 | * noEntriesFoundLabel: 'No options found'
32 | * }
33 | * }
34 | * ]
35 | * ```
36 | *
37 | * See the corresponding inputs of `MatSelectSearchComponent` for documentation.
38 | */
39 | export const MAT_SELECTSEARCH_DEFAULT_OPTIONS = new InjectionToken('mat-selectsearch-default-options');
40 |
41 | /** Global configurable options for MatSelectSearch. */
42 | export type MatSelectSearchOptions = Readonly>>;
43 |
--------------------------------------------------------------------------------
/src/app/examples/01-single-selection-example/single-selection-example.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, computed } from '@angular/core';
2 | import { toSignal } from '@angular/core/rxjs-interop';
3 | import { FormControl, ReactiveFormsModule } from '@angular/forms';
4 | import { MatFormFieldModule } from '@angular/material/form-field';
5 | import { MatSelectModule } from '@angular/material/select';
6 | import { MatSelectSearchComponent } from '../../mat-select-search/mat-select-search.component';
7 | import { startWith } from 'rxjs/operators';
8 | import { Bank, BANKS } from '../demo-data';
9 |
10 |
11 | @Component({
12 | selector: 'app-single-selection-example',
13 | templateUrl: './single-selection-example.component.html',
14 | styleUrl: './single-selection-example.component.scss',
15 | imports: [MatFormFieldModule, MatSelectModule, ReactiveFormsModule, MatSelectSearchComponent]
16 | })
17 | export class SingleSelectionExampleComponent {
18 |
19 | /** List of banks */
20 | protected banks: Bank[] = BANKS;
21 |
22 | /** Control for the selected bank */
23 | public bankCtrl: FormControl = new FormControl(this.banks[10]);
24 |
25 | /** Control for the MatSelect filter keyword */
26 | public bankFilterCtrl: FormControl = new FormControl('', {nonNullable: true});
27 |
28 | /** List of banks filtered by search keyword */
29 | public $filteredBanks = computed(() => {
30 | const search = (this.$bankControlsChanges() || '').toLowerCase();
31 | if (!search) return [...this.banks];
32 | return this.banks.filter(bank => bank.name.toLowerCase().includes(search));
33 | });
34 | $bankControlsChanges = toSignal(this.bankFilterCtrl.valueChanges.pipe(startWith('')));
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/examples/02-multiple-selection-example/multiple-selection-example.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, computed } from '@angular/core';
2 | import { toSignal } from '@angular/core/rxjs-interop';
3 | import { FormControl, ReactiveFormsModule } from '@angular/forms';
4 | import { startWith } from 'rxjs/operators';
5 | import { MatSelectModule } from '@angular/material/select';
6 | import { MatFormFieldModule } from '@angular/material/form-field';
7 | import { MatSelectSearchComponent } from '../../mat-select-search/mat-select-search.component';
8 |
9 | import { Bank, BANKS } from '../demo-data';
10 |
11 | @Component({
12 | selector: 'app-multiple-selection-example',
13 | templateUrl: './multiple-selection-example.component.html',
14 | styleUrl: './multiple-selection-example.component.scss',
15 | imports: [MatFormFieldModule, MatSelectModule, ReactiveFormsModule, MatSelectSearchComponent]
16 | })
17 | export class MultipleSelectionExampleComponent {
18 |
19 | /** List of banks */
20 | protected banks: Bank[] = BANKS;
21 |
22 | /** Control for the selected bank for multi-selection */
23 | public bankMultiCtrl: FormControl = new FormControl([this.banks[10], this.banks[11], this.banks[12]], {nonNullable: true});
24 |
25 | /** Control for the MatSelect filter keyword multi-selection */
26 | public bankMultiFilterCtrl: FormControl = new FormControl('', {nonNullable: true});
27 |
28 | /** List of banks filtered by search keyword */
29 | public $filteredBanks = computed(() => {
30 | const search = (this.$bankControlsChanges() || '').toLowerCase();
31 | if (!search) return [...this.banks];
32 | return this.banks.filter(bank => bank.name.toLowerCase().includes(search));
33 | });
34 | $bankControlsChanges = toSignal(this.bankMultiFilterCtrl.valueChanges.pipe(startWith('')));
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/examples/09-custom-no-entries-found-example/custom-no-entries-found-example.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, computed } from '@angular/core';
2 | import { toSignal } from '@angular/core/rxjs-interop';
3 | import { FormControl, ReactiveFormsModule } from '@angular/forms';
4 | import { startWith } from 'rxjs/operators';
5 | import { MatSelectModule } from '@angular/material/select';
6 | import { MatFormFieldModule } from '@angular/material/form-field';
7 | import { MatButtonModule } from '@angular/material/button';
8 | import { MatIcon } from '@angular/material/icon';
9 |
10 | import { Bank, BANKS } from '../demo-data';
11 | import { MatSelectSearchComponent } from '../../mat-select-search/mat-select-search.component';
12 | import { MatSelectNoEntriesFoundDirective } from '../../mat-select-search/mat-select-no-entries-found.directive';
13 |
14 | @Component({
15 | selector: 'app-custom-no-entries-found-example',
16 | templateUrl: './custom-no-entries-found-example.component.html',
17 | styleUrl: './custom-no-entries-found-example.component.scss',
18 | imports: [MatFormFieldModule, MatSelectModule, ReactiveFormsModule, MatSelectSearchComponent, MatIcon, MatSelectNoEntriesFoundDirective, MatButtonModule]
19 | })
20 | export class CustomNoEntriesFoundExampleComponent {
21 |
22 | /** List of banks */
23 | protected banks: Bank[] = BANKS;
24 |
25 | /** Control for the selected bank */
26 | public bankCtrl: FormControl = new FormControl(this.banks[10]);
27 |
28 | /** Control for the MatSelect filter keyword */
29 | public bankFilterCtrl: FormControl = new FormControl('', {nonNullable: true});
30 |
31 | /** List of banks filtered by search keyword */
32 | public $filteredBanks = computed(() => {
33 | const search = (this.$bankControlsChanges() || '').toLowerCase();
34 | if (!search) return [...this.banks];
35 | return this.banks.filter(bank => bank.name.toLowerCase().includes(search));
36 | });
37 | $bankControlsChanges = toSignal(this.bankFilterCtrl.valueChanges.pipe(startWith('')));
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/examples/demo-data.ts:
--------------------------------------------------------------------------------
1 |
2 | export interface Bank {
3 | id: string;
4 | name: string;
5 | }
6 |
7 | export interface BankGroup {
8 | name: string;
9 | banks: Bank[];
10 | }
11 |
12 |
13 | /** list of banks */
14 | export const BANKS: Bank[] = [
15 | {name: 'Bank A (Switzerland)', id: 'A'},
16 | {name: 'Bank B (Switzerland)', id: 'B'},
17 | {name: 'Bank C (France)', id: 'C'},
18 | {name: 'Bank D (France)', id: 'D'},
19 | {name: 'Bank E (France)', id: 'E'},
20 | {name: 'Bank F (Italy)', id: 'F'},
21 | {name: 'Bank G (Italy)', id: 'G'},
22 | {name: 'Bank H (Italy)', id: 'H'},
23 | {name: 'Bank I (Italy)', id: 'I'},
24 | {name: 'Bank J (Italy)', id: 'J'},
25 | {name: 'Bank Kolombia (United States of America)', id: 'K'},
26 | {name: 'Bank L (Germany)', id: 'L'},
27 | {name: 'Bank M (Germany)', id: 'M'},
28 | {name: 'Bank N (Germany)', id: 'N'},
29 | {name: 'Bank O (Germany)', id: 'O'},
30 | {name: 'Bank P (Germany)', id: 'P'},
31 | {name: 'Bank Q (Germany)', id: 'Q'},
32 | {name: 'Bank R (Germany)', id: 'R'}
33 | ];
34 |
35 | /** list of bank groups */
36 | export const BANKGROUPS: BankGroup[] = [
37 | {
38 | name: 'Switzerland',
39 | banks: [
40 | {name: 'Bank A', id: 'A'},
41 | {name: 'Bank B', id: 'B'}
42 | ]
43 | },
44 | {
45 | name: 'France',
46 | banks: [
47 | {name: 'Bank C', id: 'C'},
48 | {name: 'Bank D', id: 'D'},
49 | {name: 'Bank E', id: 'E'},
50 | ]
51 | },
52 | {
53 | name: 'Italy',
54 | banks: [
55 | {name: 'Bank F', id: 'F'},
56 | {name: 'Bank G', id: 'G'},
57 | {name: 'Bank H', id: 'H'},
58 | {name: 'Bank I', id: 'I'},
59 | {name: 'Bank J', id: 'J'},
60 | ]
61 | },
62 | {
63 | name: 'United States of America',
64 | banks: [
65 | {name: 'Bank Kolombia', id: 'K'},
66 | ]
67 | },
68 | {
69 | name: 'Germany',
70 | banks: [
71 | {name: 'Bank L', id: 'L'},
72 | {name: 'Bank M', id: 'M'},
73 | {name: 'Bank N', id: 'N'},
74 | {name: 'Bank O', id: 'O'},
75 | {name: 'Bank P', id: 'P'},
76 | {name: 'Bank Q', id: 'Q'},
77 | {name: 'Bank R', id: 'R'}
78 | ]
79 | }
80 | ];
81 |
--------------------------------------------------------------------------------
/src/app/examples/05-server-side-search-example/server-side-search-example.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, signal } from '@angular/core';
2 | import { toSignal } from '@angular/core/rxjs-interop';
3 | import { FormControl, ReactiveFormsModule } from '@angular/forms';
4 | import { debounceTime, delay, tap, map, startWith } from 'rxjs/operators';
5 | import { MatFormFieldModule } from '@angular/material/form-field';
6 | import { MatSelectModule } from '@angular/material/select';
7 |
8 | import { Bank, BANKS } from '../demo-data';
9 | import { MatSelectSearchComponent } from '../../mat-select-search/mat-select-search.component';
10 |
11 |
12 | @Component({
13 | selector: 'app-server-side-search-example',
14 | templateUrl: './server-side-search-example.component.html',
15 | styleUrl: './server-side-search-example.component.scss',
16 | imports: [MatFormFieldModule, MatSelectModule, ReactiveFormsModule, MatSelectSearchComponent]
17 | })
18 | export class ServerSideSearchExampleComponent {
19 |
20 | /** List of banks */
21 | protected banks: Bank[] = BANKS;
22 |
23 | /** Control for the selected bank for server side filtering */
24 | public bankServerSideCtrl: FormControl = new FormControl(null);
25 |
26 | /** Control for filter for server side. */
27 | public bankServerSideFilteringCtrl: FormControl = new FormControl('', {nonNullable: true});
28 |
29 | bankControlsChanges$ = this.bankServerSideFilteringCtrl.valueChanges.pipe(
30 | startWith(''),
31 | tap(() => this.searching.set(true)),
32 | debounceTime(200));
33 | filteredBanks$ = this.bankControlsChanges$.pipe(
34 | map(search => {
35 | if (!this.banks) {
36 | return [];
37 | }
38 |
39 | // simulate server fetching and filtering data
40 | return this.banks.filter(bank => bank.name.toLowerCase().indexOf(search) > -1);
41 | }),
42 | delay(500),
43 | tap(() => this.searching.set(false)));
44 |
45 | /** Indicate search operation is in progress */
46 | public searching = signal(false);
47 |
48 | /** List of banks filtered after simulating server side search */
49 | public $filteredServerSideBanks = toSignal(this.filteredBanks$);
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { VERSION } from '@angular/material/core';
3 | import { MatSlideToggleModule } from '@angular/material/slide-toggle';
4 | import { MatToolbar } from '@angular/material/toolbar';
5 |
6 | import { MatSelectSearchVersion } from './mat-select-search/ngx-mat-select-search.module';
7 | import { SingleSelectionExampleComponent } from './examples/01-single-selection-example/single-selection-example.component';
8 | import { MultipleSelectionExampleComponent } from './examples/02-multiple-selection-example/multiple-selection-example.component';
9 | import { CustomClearIconExampleComponent } from './examples/03-custom-clear-icon-example/custom-clear-icon-example.component';
10 | import { OptionGroupsExampleComponent } from './examples/04-option-groups-example/option-groups-example.component';
11 | import { ServerSideSearchExampleComponent } from './examples/05-server-side-search-example/server-side-search-example.component';
12 | import { MultipleSelectionSelectAllExampleComponent } from './examples/06-multiple-selection-select-all-example/multiple-selection-select-all-example.component';
13 | import { TooltipSelectAllExampleComponent } from './examples/07-tooltip-select-all-example/tooltip-select-all-example.component';
14 | import { CustomNoEntriesFoundExampleComponent } from './examples/09-custom-no-entries-found-example/custom-no-entries-found-example.component';
15 |
16 | @Component({
17 | selector: 'app-root',
18 | templateUrl: 'app.component.html',
19 | styleUrl: 'app.component.scss',
20 | imports: [
21 | MatToolbar,
22 | MatSlideToggleModule,
23 |
24 | SingleSelectionExampleComponent,
25 | CustomClearIconExampleComponent,
26 | MultipleSelectionExampleComponent,
27 | CustomNoEntriesFoundExampleComponent,
28 | OptionGroupsExampleComponent,
29 | ServerSideSearchExampleComponent,
30 | MultipleSelectionSelectAllExampleComponent,
31 | TooltipSelectAllExampleComponent
32 | ]
33 | })
34 | export class AppComponent {
35 |
36 | version = VERSION;
37 |
38 | matSelectSearchVersion = MatSelectSearchVersion;
39 |
40 | private rightToLeft = false;
41 |
42 | toggleRightToLeft() {
43 | this.rightToLeft = !this.rightToLeft;
44 | document.body.dir = this.rightToLeft ? 'rtl' : '';
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/examples/04-option-groups-example/option-groups-example.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, computed } from '@angular/core';
2 | import { toSignal } from '@angular/core/rxjs-interop';
3 | import { FormControl, ReactiveFormsModule } from '@angular/forms';
4 | import { startWith } from 'rxjs/operators';
5 | import { MatFormFieldModule } from '@angular/material/form-field';
6 | import { MatSelectModule } from '@angular/material/select';
7 |
8 | import { MatSelectSearchComponent } from '../../mat-select-search/mat-select-search.component';
9 | import { Bank, BankGroup, BANKGROUPS } from '../demo-data';
10 |
11 |
12 | @Component({
13 | selector: 'app-option-groups-example',
14 | templateUrl: './option-groups-example.component.html',
15 | styleUrl: './option-groups-example.component.scss',
16 | imports: [MatFormFieldModule, MatSelectModule, ReactiveFormsModule, MatSelectSearchComponent]
17 | })
18 | export class OptionGroupsExampleComponent {
19 |
20 | /** List of bank groups */
21 | protected bankGroups: BankGroup[] = this.copyBankGroups(BANKGROUPS);
22 |
23 | /** Control for the selected bank for option groups */
24 | public bankGroupsCtrl: FormControl = new FormControl(null);
25 |
26 | /** Control for the MatSelect filter keyword for option groups */
27 | public bankGroupsFilterCtrl: FormControl = new FormControl('', {nonNullable: true});
28 |
29 |
30 | public $filteredBankGroups = computed(() => {
31 | const search = (this.$bankControlsChanges() || '').toLowerCase();
32 | if (!search) return this.bankGroups;
33 | return this.copyBankGroups(BANKGROUPS).filter(bankGroup => {
34 | if(bankGroup.name.toLowerCase().includes(search)) return true;
35 | bankGroup.banks = bankGroup.banks.filter(bank => bank.name.toLowerCase().includes(search));
36 | return bankGroup.banks.length > 0;
37 | });
38 | });
39 | $bankControlsChanges = toSignal(this.bankGroupsFilterCtrl.valueChanges.pipe(startWith('')));
40 |
41 | protected copyBankGroups(bankGroups: BankGroup[]) {
42 | const bankGroupsCopy: BankGroup[] = [];
43 | bankGroups.forEach(bankGroup => {
44 | bankGroupsCopy.push({
45 | name: bankGroup.name,
46 | banks: bankGroup.banks.slice()
47 | });
48 | });
49 | return bankGroupsCopy;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE10 and IE11 requires the following for the Reflect API. */
22 | import 'core-js/es6/reflect';
23 |
24 |
25 | /** Evergreen browsers require these. **/
26 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
27 | import 'core-js/es7/reflect';
28 |
29 | /**
30 | * By default, zone.js will patch all possible macroTask and DomEvents
31 | * user can disable parts of macroTask/DomEvents patch by setting following flags
32 | */
33 |
34 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
35 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
36 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
37 |
38 | /*
39 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
40 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
41 | */
42 | // (window as any).__Zone_enable_cross_context_check = true;
43 |
44 | /***************************************************************************************************
45 | * Zone JS is required by default for Angular itself.
46 | */
47 | import 'zone.js'; // Included with Angular CLI.
48 |
49 |
50 |
51 | /***************************************************************************************************
52 | * APPLICATION IMPORTS
53 | */
54 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Javascript Node CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
4 | #
5 | version: 2.1
6 |
7 | orbs:
8 | browser-tools: circleci/browser-tools@1.5.2
9 |
10 | jobs:
11 | build:
12 | docker:
13 | # specify the version you desire here
14 | - image: cimg/node:20.18.3-browsers
15 |
16 | # Specify service dependencies here if necessary
17 | # CircleCI maintains a library of pre-built images
18 | # documented at https://circleci.com/docs/2.0/circleci-images/
19 | # - image: circleci/mongo:3.4.4
20 |
21 | working_directory: ~/repo
22 |
23 | steps:
24 | - checkout
25 | - browser-tools/install-browser-tools
26 |
27 | # Download and cache dependencies
28 | - restore_cache:
29 | keys:
30 | - v1-dependencies-{{ checksum "package-lock.json" }}
31 | # fallback to using the latest cache if no exact match is found
32 | - v1-dependencies-
33 |
34 | - run: DISABLE_OPENCOLLECTIVE=true
35 | - run: npm ci
36 |
37 | - save_cache:
38 | paths:
39 | - node_modules
40 | key: v1-dependencies-{{ checksum "package-lock.json" }}
41 |
42 | # run tests!
43 | - run: npm run test.ci
44 | - run: npm run lint
45 | - run: npm run build.ci --prod
46 | - run: npm run build-lib
47 | - persist_to_workspace:
48 | root: ./
49 | paths:
50 | - dist
51 | deploy:
52 | # see https://circleci.com/blog/deploying-documentation-to-github-pages-with-continuous-integration
53 | docker:
54 | - image: cimg/node:20.18.3
55 | steps:
56 | - checkout
57 | - attach_workspace:
58 | at: artifacts
59 | - run:
60 | name: Install and configure dependencies
61 | command: |
62 | npm install --silent gh-pages@6.3.0
63 | git config user.email "ci-build@bithost.ch"
64 | git config user.name "ci-build"
65 | - add_ssh_keys:
66 | fingerprints:
67 | - "9a:12:bb:95:c2:8b:7a:be:29:42:99:34:06:4f:68:cd"
68 | - run: node_modules/.bin/gh-pages --message "[ci skip] Updates" --dist artifacts/dist/browser
69 | workflows:
70 | version: 2
71 | build-and-deploy:
72 | jobs:
73 | - build
74 | - deploy:
75 | requires:
76 | - build
77 | filters:
78 | branches:
79 | only: master
80 |
--------------------------------------------------------------------------------
/src/app/examples/06-multiple-selection-select-all-example/multiple-selection-select-all-example.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, computed } from '@angular/core';
2 | import { toSignal } from '@angular/core/rxjs-interop';
3 | import { FormControl, ReactiveFormsModule } from '@angular/forms';
4 | import { startWith } from 'rxjs/operators';
5 | import { MatFormFieldModule } from '@angular/material/form-field';
6 | import { MatSelectModule } from '@angular/material/select';
7 |
8 | import { MatSelectSearchComponent } from '../../mat-select-search/mat-select-search.component';
9 | import { Bank, BANKS } from '../demo-data';
10 |
11 | @Component({
12 | selector: 'app-multiple-selection-select-all-example',
13 | templateUrl: './multiple-selection-select-all-example.component.html',
14 | styleUrl: './multiple-selection-select-all-example.component.scss',
15 | imports: [MatFormFieldModule, MatSelectModule, ReactiveFormsModule, MatSelectSearchComponent]
16 | })
17 | export class MultipleSelectionSelectAllExampleComponent {
18 |
19 | /** List of banks */
20 | protected banks: Bank[] = BANKS;
21 |
22 | /** Control for the selected bank for multi-selection */
23 | public bankMultiCtrl: FormControl = new FormControl([this.banks[10], this.banks[11], this.banks[12]], {nonNullable: true});
24 |
25 | /** Control for the MatSelect filter keyword multi-selection */
26 | public bankMultiFilterCtrl: FormControl = new FormControl('', {nonNullable: true});
27 |
28 | /** List of banks filtered by search keyword */
29 | public $filteredBanks = computed(() => {
30 | const search = (this.$bankControlsChanges() || '').toLowerCase();
31 | if (!search) return [...this.banks];
32 | return this.banks.filter(bank => bank.name.toLowerCase().includes(search));
33 | });
34 | $bankControlsChanges = toSignal(this.bankMultiFilterCtrl.valueChanges.pipe(startWith('')));
35 |
36 | $selectedBanks = toSignal(this.bankMultiCtrl.valueChanges.pipe(startWith(this.bankMultiCtrl.value)));
37 |
38 | $isIndeterminate = computed(() => {
39 | const selectedBanks = this.$selectedBanks();
40 | if (!selectedBanks) return false;
41 | return selectedBanks.length > 0 && selectedBanks.length < this.banks.length;
42 | });
43 | $isChecked = computed(() => {
44 | const selectedBanks = this.$selectedBanks();
45 | if (!selectedBanks) return false;
46 | return selectedBanks.length === this.banks.length;
47 | });
48 |
49 | toggleSelectAll(selectAllValue: boolean) {
50 | if(selectAllValue) {
51 | this.bankMultiCtrl.patchValue([...this.$filteredBanks()]);
52 | } else {
53 | this.bankMultiCtrl.patchValue([]);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/examples/07-tooltip-select-all-example/tooltip-select-all-example.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, computed } from '@angular/core';
2 | import { toSignal } from '@angular/core/rxjs-interop';
3 | import { FormControl, ReactiveFormsModule } from '@angular/forms';
4 | import { startWith } from 'rxjs/operators';
5 |
6 | import { MatFormFieldModule } from '@angular/material/form-field';
7 | import { MatSelectModule } from '@angular/material/select';
8 |
9 | import { Bank, BANKS } from '../demo-data';
10 | import { MatSelectSearchComponent } from '../../mat-select-search/mat-select-search.component';
11 |
12 |
13 | @Component({
14 | selector: 'app-tooltip-select-all-example',
15 | templateUrl: './tooltip-select-all-example.component.html',
16 | styleUrl: './tooltip-select-all-example.component.scss',
17 | imports: [MatFormFieldModule, MatSelectModule, ReactiveFormsModule, MatSelectSearchComponent]
18 | })
19 | export class TooltipSelectAllExampleComponent {
20 |
21 | /** List of banks */
22 | protected banks: Bank[] = BANKS;
23 |
24 | /** Control for the selected bank for multi-selection */
25 | public bankMultiCtrl: FormControl = new FormControl([this.banks[10], this.banks[11], this.banks[12]], {nonNullable: true});
26 |
27 | /** Control for the MatSelect filter keyword multi-selection */
28 | public bankMultiFilterCtrl: FormControl = new FormControl('', {nonNullable: true});
29 |
30 | /** List of banks filtered by search keyword */
31 | public $filteredBanks = computed(() => {
32 | const search = (this.$bankControlsChanges() || '').toLowerCase();
33 | if (!search) return [...this.banks];
34 | return this.banks.filter(bank => bank.name.toLowerCase().includes(search));
35 | });
36 | $bankControlsChanges = toSignal(this.bankMultiFilterCtrl.valueChanges.pipe(startWith('')));
37 |
38 | $selectedBanks = toSignal(this.bankMultiCtrl.valueChanges.pipe(startWith(this.bankMultiCtrl.value)));
39 |
40 | $isIndeterminate = computed(() => {
41 | const selectedBanks = this.$selectedBanks();
42 | if (!selectedBanks) return false;
43 | return selectedBanks.length > 0 && selectedBanks.length < this.banks.length;
44 | });
45 | $isChecked = computed(() => {
46 | const selectedBanks = this.$selectedBanks();
47 | if (!selectedBanks) return false;
48 | return selectedBanks.length === this.banks.length;
49 | });
50 |
51 | toggleSelectAll(selectAllValue: boolean) {
52 | if(selectAllValue) {
53 | this.bankMultiCtrl.patchValue([...this.$filteredBanks()]);
54 | } else {
55 | this.bankMultiCtrl.patchValue([]);
56 | }
57 | }
58 | public tooltipMessage = 'Select All / Unselect All';
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ngx-mat-select-search",
3 | "description": "Angular component providing an input field for searching / filtering MatSelect options of the Angular Material library.",
4 | "version": "8.0.4",
5 | "license": "MIT",
6 | "scripts": {
7 | "ng": "ng",
8 | "start": "ng serve",
9 | "build": "ng build --configuration production",
10 | "build.ci": "npm run build -- --progress=false --base-href=/ngx-mat-select-search/",
11 | "test": "ng test --watch=false",
12 | "test.ci": "npm run test -- --progress=false --watch=false",
13 | "lint": "ng lint",
14 | "build-lib": "ng-packagr -p ng-package.json"
15 | },
16 | "author": "Esteban Gehring, Bithost GmbH",
17 | "bugs": {
18 | "url": "https://github.com/bithost-gmbh/ngx-mat-select-search/issues"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/bithost-gmbh/ngx-mat-select-search.git"
23 | },
24 | "keywords": [
25 | "angular",
26 | "angular 2",
27 | "material",
28 | "MatSelect",
29 | "select",
30 | "search",
31 | "filter"
32 | ],
33 | "homepage": "https://github.com/bithost-gmbh/ngx-mat-select-search",
34 | "peerDependencies": {
35 | "@angular/material": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0"
36 | },
37 | "devDependencies": {
38 | "@angular-devkit/build-angular": "^19.1.6",
39 | "@angular/animations": "^19.1.5",
40 | "@angular/cdk": "^19.1.3",
41 | "@angular/cli": "^19.1.6",
42 | "@angular/common": "^19.1.5",
43 | "@angular/compiler": "^19.1.5",
44 | "@angular/compiler-cli": "^19.1.5",
45 | "@angular/core": "^19.1.5",
46 | "@angular/forms": "^19.1.5",
47 | "@angular/language-service": "^19.1.5",
48 | "@angular/material": "^19.1.3",
49 | "@angular/platform-browser": "^19.1.5",
50 | "@angular/platform-browser-dynamic": "^19.1.5",
51 | "@angular/router": "^19.1.5",
52 | "@types/jasmine": "~3.6.0",
53 | "@types/jasminewd2": "^2.0.10",
54 | "@types/node": "^12.20.52",
55 | "angular-eslint": "19.1.0",
56 | "codelyzer": "^6.0.2",
57 | "core-js": "^2.6.12",
58 | "eslint": "^9.20.0",
59 | "jasmine-core": "~3.8.0",
60 | "jasmine-spec-reporter": "~5.0.0",
61 | "karma": "^6.3.20",
62 | "karma-chrome-launcher": "^3.1.1",
63 | "karma-coverage-istanbul-reporter": "~3.0.2",
64 | "karma-jasmine": "^4.0.2",
65 | "karma-jasmine-html-reporter": "^1.5.0",
66 | "ng-packagr": "^19.1.2",
67 | "rxjs": "^6.6.7",
68 | "ts-node": "~7.0.1",
69 | "typescript": "5.7.3",
70 | "typescript-eslint": "8.23.0",
71 | "zone.js": "^0.15.0"
72 | },
73 | "dependencies": {
74 | "tslib": "^2.4.0"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/app/mat-select-search/mat-select-search.component.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
76 |
77 | @if(_showNoEntriesFound$ | async)
78 | {
79 |
80 | @if(noEntriesFound)
81 | {
82 |
83 | }
84 | @else
85 | {
86 | {{noEntriesFoundLabel}}
87 | }
88 |
89 | }
90 |
91 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "ngx-mat-select-search": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "architect": {
11 | "build": {
12 | "builder": "@angular-devkit/build-angular:application",
13 | "options": {
14 | "outputPath": {
15 | "base": "dist"
16 | },
17 | "index": "src/index.html",
18 | "tsConfig": "src/tsconfig.app.json",
19 | "polyfills": [
20 | "src/polyfills.ts"
21 | ],
22 | "assets": [
23 | "src/assets",
24 | "src/favicon.ico"
25 | ],
26 | "styles": [
27 | "src/styles.scss"
28 | ],
29 | "scripts": [],
30 | "extractLicenses": false,
31 | "sourceMap": true,
32 | "optimization": false,
33 | "namedChunks": true,
34 | "browser": "src/main.ts"
35 | },
36 | "configurations": {
37 | "production": {
38 | "budgets": [
39 | {
40 | "type": "anyComponentStyle",
41 | "maximumWarning": "6kb"
42 | }
43 | ],
44 | "optimization": true,
45 | "outputHashing": "all",
46 | "sourceMap": false,
47 | "namedChunks": false,
48 | "extractLicenses": true,
49 | "fileReplacements": [
50 | {
51 | "replace": "src/environments/environment.ts",
52 | "with": "src/environments/environment.prod.ts"
53 | }
54 | ]
55 | }
56 | },
57 | "defaultConfiguration": ""
58 | },
59 | "serve": {
60 | "builder": "@angular-devkit/build-angular:dev-server",
61 | "options": {
62 | "buildTarget": "ngx-mat-select-search:build"
63 | },
64 | "configurations": {
65 | "production": {
66 | "buildTarget": "ngx-mat-select-search:build:production"
67 | }
68 | }
69 | },
70 | "extract-i18n": {
71 | "builder": "@angular-devkit/build-angular:extract-i18n",
72 | "options": {
73 | "buildTarget": "ngx-mat-select-search:build"
74 | }
75 | },
76 | "test": {
77 | "builder": "@angular-devkit/build-angular:karma",
78 | "options": {
79 | "main": "src/test.ts",
80 | "karmaConfig": "./karma.conf.js",
81 | "polyfills": "src/polyfills.ts",
82 | "tsConfig": "src/tsconfig.spec.json",
83 | "scripts": [],
84 | "styles": [
85 | "src/styles.scss"
86 | ],
87 | "assets": [
88 | "src/assets",
89 | "src/favicon.ico"
90 | ]
91 | }
92 | },
93 | "lint": {
94 | "builder": "@angular-eslint/builder:lint",
95 | "options": {
96 | "lintFilePatterns": [
97 | "src/**/*.ts",
98 | "src/**/*.html"
99 | ]
100 | }
101 | }
102 | }
103 | }
104 | },
105 | "schematics": {
106 | "@schematics/angular:component": {
107 | "prefix": "app",
108 | "style": "scss"
109 | },
110 | "@schematics/angular:directive": {
111 | "prefix": "app"
112 | }
113 | },
114 | "cli": {
115 | "analytics": false,
116 | "schematicCollections": [
117 | "angular-eslint"
118 | ]
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/app/mat-select-search/mat-select-search.component.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018 Bithost GmbH All Rights Reserved.
3 | *
4 | * Use of this source code is governed by an MIT-style license that can be
5 | * found in the LICENSE file at https://angular.io/license
6 | */
7 |
8 | $clear-button-width: 40px;
9 | $multiple-check-width: 33px;
10 | //set min-height according to `.mat-mdc-option` min-height (https://github.com/angular/components/blob/f699d2e2a4f2648abe68ccde0a30b70fdd313f37/src/material/core/option/option.scss#L19C3-L19C20)
11 | $mat-option-min-height: 48px;
12 | $mat-select-search-clear-x: 4px;
13 | $mat-select-search-spinner-x: 16px;
14 | $mat-select-search-toggle-all-checkbox-x: 5px;
15 | $mat-select-panel-padding: 8px;
16 |
17 | .mat-select-search-hidden {
18 | visibility: hidden;
19 | }
20 |
21 | .mat-select-search-inner {
22 | position: absolute;
23 | top: 0;
24 | left: 0;
25 | width: 100%;
26 | z-index: 100;
27 | font-size: inherit;
28 | /*
29 | compensate effects of .mat-datepicker-content
30 | (see https://github.com/angular/material2/blob/master/src/lib/datepicker/datepicker-content.scss#L27)
31 | TODO: implement proper theming (https://github.com/bithost-gmbh/ngx-mat-select-search/issues/34)
32 | */
33 | box-shadow: none;
34 |
35 | background-color: var(--mat-sys-surface-container, var(--mat-select-panel-background-color, white));
36 |
37 | &.mat-select-search-inner-multiple.mat-select-search-inner-toggle-all {
38 | .mat-select-search-inner-row {
39 | display: flex;
40 | align-items: center;
41 | }
42 | }
43 | }
44 |
45 | .mat-select-search-input {
46 | box-sizing: border-box;
47 | width: 100%;
48 | border: none;
49 | font-family: inherit;
50 | font-size: inherit;
51 | color: currentColor;
52 | outline: none;
53 | background-color: var(--mat-sys-surface-container, var(--mat-select-panel-background-color, white));
54 | padding: 0 $clear-button-width + $mat-select-search-clear-x 0 16px;
55 | height: calc($mat-option-min-height - 1px);
56 | line-height: calc($mat-option-min-height - 1px);
57 |
58 | :host-context([dir="rtl"]) & {
59 | padding-right: 16px;
60 | padding-left: $clear-button-width + $mat-select-search-clear-x;
61 | }
62 | }
63 |
64 | .mat-select-search-input::placeholder {
65 | color: var(--mdc-filled-text-field-input-text-placeholder-color);
66 | }
67 |
68 | .mat-select-search-inner-toggle-all {
69 | .mat-select-search-input {
70 | padding-left: 5px;
71 | }
72 | }
73 |
74 | .mat-select-search-no-entries-found {
75 | padding-top: 8px;
76 | }
77 |
78 | .mat-select-search-clear {
79 | position: absolute;
80 | right: $mat-select-search-clear-x;
81 | top: 0;
82 |
83 | :host-context([dir="rtl"]) & {
84 | right: auto;
85 | left: $mat-select-search-clear-x;
86 | }
87 | }
88 |
89 | .mat-select-search-spinner {
90 | position: absolute;
91 | right: $mat-select-search-spinner-x;
92 | top: calc(50% - 8px);
93 |
94 | :host-context([dir="rtl"]) & {
95 | right: auto;
96 | left: $mat-select-search-spinner-x;
97 | }
98 | }
99 |
100 | ::ng-deep .mat-mdc-option[aria-disabled=true].contains-mat-select-search {
101 | /* let move mat-select-search at the top of the dropdown. As option is disabled, there will be no-ripple hence safe. */
102 | position: sticky;
103 | top: -$mat-select-panel-padding;
104 | z-index: 1;
105 | opacity: 1;
106 | margin-top: -$mat-select-panel-padding;
107 | pointer-events: all;
108 |
109 | .mat-icon {
110 | margin-right: 0;
111 | margin-left: 0;
112 | }
113 | mat-pseudo-checkbox {
114 | display: none;
115 | }
116 | .mdc-list-item__primary-text {
117 | opacity: 1;
118 | }
119 | }
120 |
121 | .mat-select-search-toggle-all-checkbox {
122 | padding-left: $mat-select-search-toggle-all-checkbox-x;
123 |
124 | :host-context([dir="rtl"]) & {
125 | padding-left: 0;
126 | padding-right: $mat-select-search-toggle-all-checkbox-x;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/app/examples/08-infinite-scroll-example/infinite-scroll-example.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, ViewChild } from '@angular/core';
2 | import { FormControl } from '@angular/forms';
3 | import { combineLatest, merge, Observable, Subject } from 'rxjs';
4 | import { map, mapTo, scan, startWith } from 'rxjs/operators';
5 | import { MatSelect } from '@angular/material/select';
6 | import { Bank } from '../demo-data';
7 |
8 | /**
9 | * Based upon: https://stackblitz.com/edit/mat-select-search-with-infinity-scroll
10 | */
11 | @Component({
12 | selector: 'app-infinite-scroll-example',
13 | standalone: false,
14 | templateUrl: './infinite-scroll-example.component.html',
15 | styleUrls: ['./infinite-scroll-example.component.scss']
16 | })
17 | export class InfiniteScrollExampleComponent implements OnDestroy {
18 |
19 | @ViewChild('matSelectInfiniteScroll', { static: true } )
20 | infiniteScrollSelect: MatSelect;
21 |
22 | /** List with all available data, mocks some sort of backend data source */
23 | private mockBankList: Bank[] = Array.from({ length: 1000 }).map((_, i) => ({
24 | id: String(i),
25 | name: `Bank ${i}`
26 | }));
27 |
28 | /** Control for the selected bank id */
29 | public bankCtrl: FormControl = new FormControl(null);
30 |
31 | /** Control for the MatSelect filter keyword */
32 | public bankFilterCtrl: FormControl = new FormControl('', {nonNullable: true});
33 |
34 | /** List of data corresponding to the search input */
35 | private filteredData$: Observable = this.bankFilterCtrl.valueChanges.pipe(
36 | startWith(''),
37 | map(searchKeyword => {
38 | if (!searchKeyword) {
39 | return this.mockBankList;
40 | }
41 | return this.mockBankList.filter((bank) =>
42 | bank.name.toLowerCase().indexOf(searchKeyword.toLowerCase()) > -1
43 | );
44 | })
45 | );
46 |
47 | /** Number of items added per batch */
48 | batchSize = 20;
49 |
50 | private incrementBatchOffset$: Subject = new Subject();
51 | private resetBatchOffset$: Subject = new Subject();
52 |
53 | /** Minimum offset needed for the batch to ensure the selected option is displayed */
54 | private minimumBatchOffset$: Observable = combineLatest([
55 | this.filteredData$,
56 | this.bankFilterCtrl.valueChanges
57 | ]).pipe(
58 | map(([filteredData]) => {
59 | if (!this.bankFilterCtrl.value && this.bankCtrl.value) {
60 | const index = filteredData.findIndex(bank => bank.id === this.bankCtrl.value);
61 | return index + this.batchSize;
62 | } else {
63 | return 0;
64 | }
65 | }),
66 | startWith(0)
67 | );
68 |
69 | /** Length of the visible data / start of the next batch */
70 | private batchOffset$ = combineLatest([
71 | merge(
72 | this.incrementBatchOffset$.pipe(mapTo(true)),
73 | this.resetBatchOffset$.pipe(mapTo(false))
74 | ),
75 | this.minimumBatchOffset$
76 | ]).pipe(
77 | scan((batchOffset, [doIncrement, minimumOffset]) => {
78 | if (doIncrement) {
79 | return Math.max(batchOffset + this.batchSize, minimumOffset + this.batchSize);
80 | } else {
81 | return Math.max(minimumOffset, this.batchSize);
82 | }
83 | }, this.batchSize),
84 | );
85 |
86 |
87 | /** List of data, filtered by the search keyword, limited to the length accumulated by infinity scrolling */
88 | filteredBatchedData$: Observable = combineLatest([
89 | this.filteredData$,
90 | this.batchOffset$
91 | ]).pipe(
92 | map(([filteredData, batchOffset]) => filteredData.slice(0, batchOffset))
93 | );
94 |
95 | private destroy$: Subject = new Subject();
96 |
97 |
98 |
99 | ngOnDestroy() {
100 | this.destroy$.next();
101 | }
102 |
103 | /**
104 | * Load the next batch
105 | */
106 | getNextBatch(): void {
107 | this.incrementBatchOffset$.next();
108 | }
109 |
110 | }
111 |
--------------------------------------------------------------------------------
/src/app/mat-select-search/mat-select-search.component.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018 Bithost GmbH All Rights Reserved.
3 | *
4 | * Use of this source code is governed by an MIT-style license that can be
5 | * found in the LICENSE file at https://angular.io/license
6 | */
7 |
8 | import { ViewportRuler } from '@angular/cdk/scrolling';
9 | import {
10 | ChangeDetectionStrategy,
11 | ChangeDetectorRef,
12 | Component,
13 | ContentChild,
14 | ElementRef,
15 | EventEmitter,
16 | forwardRef,
17 | Inject,
18 | Input,
19 | OnDestroy,
20 | OnInit,
21 | Optional,
22 | Output,
23 | QueryList,
24 | ViewChild
25 | } from '@angular/core';
26 | import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
27 | import { MatOption } from '@angular/material/core';
28 | import { MatFormField } from '@angular/material/form-field';
29 | import { MatSelect } from '@angular/material/select';
30 | import { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs';
31 | import { delay, filter, map, startWith, switchMap, take, takeUntil, tap } from 'rxjs/operators';
32 | import { MatSelectSearchClearDirective } from './mat-select-search-clear.directive';
33 | import { configurableDefaultOptions, MAT_SELECTSEARCH_DEFAULT_OPTIONS, MatSelectSearchOptions } from './default-options';
34 | import { MatSelectNoEntriesFoundDirective } from './mat-select-no-entries-found.directive';
35 | import { MatCheckbox } from '@angular/material/checkbox';
36 | import { MatDivider } from '@angular/material/divider';
37 | import { AsyncPipe } from '@angular/common';
38 | import { MatTooltip } from '@angular/material/tooltip';
39 | import { MatProgressSpinner } from '@angular/material/progress-spinner';
40 | import { MatIcon } from '@angular/material/icon';
41 | import { MatButtonModule } from '@angular/material/button';
42 |
43 | /**
44 | * Component providing an input field for searching MatSelect options.
45 | *
46 | * Example usage:
47 | *
48 | * interface Bank {
49 | * id: string;
50 | * name: string;
51 | * }
52 | *
53 | * @Component({
54 | * selector: 'my-app-data-selection',
55 | * template: `
56 | *
57 | *
58 | *
59 | *
60 | *
61 | *
62 | * {{bank.name}}
63 | *
64 | *
65 | *
66 | * `
67 | * })
68 | * export class DataSelectionComponent implements OnInit, OnDestroy {
69 | *
70 | * // control for the selected bank
71 | * public bankCtrl: FormControl = new FormControl();
72 | * // control for the MatSelect filter keyword
73 | * public bankFilterCtrl: FormControl = new FormControl();
74 | *
75 | * // list of banks
76 | * private banks: Bank[] = [{name: 'Bank A', id: 'A'}, {name: 'Bank B', id: 'B'}, {name: 'Bank C', id: 'C'}];
77 | * // list of banks filtered by search keyword
78 | * public filteredBanks: ReplaySubject = new ReplaySubject(1);
79 | *
80 | * // Subject that emits when the component has been destroyed.
81 | * private _onDestroy = new Subject();
82 | *
83 | *
84 | * ngOnInit() {
85 | * // load the initial bank list
86 | * this.filteredBanks.next(this.banks.slice());
87 | * // listen for search field value changes
88 | * this.bankFilterCtrl.valueChanges
89 | * .pipe(takeUntil(this._onDestroy))
90 | * .subscribe(() => {
91 | * this.filterBanks();
92 | * });
93 | * }
94 | *
95 | * ngOnDestroy() {
96 | * this._onDestroy.next();
97 | * this._onDestroy.complete();
98 | * }
99 | *
100 | * private filterBanks() {
101 | * if (!this.banks) {
102 | * return;
103 | * }
104 | *
105 | * // get the search keyword
106 | * let search = this.bankFilterCtrl.value;
107 | * if (!search) {
108 | * this.filteredBanks.next(this.banks.slice());
109 | * return;
110 | * } else {
111 | * search = search.toLowerCase();
112 | * }
113 | *
114 | * // filter the banks
115 | * this.filteredBanks.next(
116 | * this.banks.filter(bank => bank.name.toLowerCase().indexOf(search) > -1)
117 | * );
118 | * }
119 | * }
120 | */
121 | @Component({
122 | selector: 'ngx-mat-select-search',
123 | templateUrl: './mat-select-search.component.html',
124 | styleUrls: ['./mat-select-search.component.scss'],
125 | providers: [
126 | {
127 | provide: NG_VALUE_ACCESSOR,
128 | useExisting: forwardRef(() => MatSelectSearchComponent),
129 | multi: true
130 | }
131 | ],
132 | imports: [
133 | AsyncPipe,
134 | ReactiveFormsModule,
135 | MatCheckbox,
136 | MatDivider,
137 | MatTooltip,
138 | MatProgressSpinner,
139 | MatIcon,
140 | MatButtonModule
141 | ],
142 | changeDetection: ChangeDetectionStrategy.OnPush
143 | })
144 | export class MatSelectSearchComponent implements OnInit, OnDestroy, ControlValueAccessor {
145 |
146 | /** Label of the search placeholder */
147 | @Input() placeholderLabel = 'Suche';
148 |
149 | /** Type of the search input field */
150 | @Input() type = 'text';
151 |
152 | /** Font-based icon used for displaying Close-Icon */
153 | @Input() closeIcon = 'close';
154 |
155 | /** SVG-based icon used for displaying Close-Icon. If set, closeIcon is overridden */
156 | @Input() closeSvgIcon?: string;
157 |
158 | /** Label to be shown when no entries are found. Set to null if no message should be shown. */
159 | @Input() noEntriesFoundLabel = 'Keine Optionen gefunden';
160 |
161 | /**
162 | * Whether the search field should be cleared after the dropdown menu is closed.
163 | * Useful for server-side filtering. See [#3](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/3)
164 | */
165 | @Input() clearSearchInput = true;
166 |
167 | /** Whether to show the search-in-progress indicator */
168 | @Input() searching = false;
169 |
170 | /** Disables initial focusing of the input field */
171 | @Input() disableInitialFocus = false;
172 |
173 | /** Enable clear input on escape pressed */
174 | @Input() enableClearOnEscapePressed = false;
175 |
176 | /**
177 | * Prevents home / end key being propagated to mat-select,
178 | * allowing to move the cursor within the search input instead of navigating the options
179 | */
180 | @Input() preventHomeEndKeyPropagation = false;
181 |
182 | /** Disables scrolling to active options when option list changes. Useful for server-side search */
183 | @Input() disableScrollToActiveOnOptionsChanged = false;
184 |
185 | /** Adds 508 screen reader support for search box */
186 | @Input() ariaLabel = 'dropdown search';
187 |
188 | /** Whether to show Select All Checkbox (for mat-select[multi=true]) */
189 | @Input() showToggleAllCheckbox = false;
190 |
191 | /** Select all checkbox checked state */
192 | @Input() toggleAllCheckboxChecked = false;
193 |
194 | /** select all checkbox indeterminate state */
195 | @Input() toggleAllCheckboxIndeterminate = false;
196 |
197 | /** Display a message in a tooltip on the toggle-all checkbox */
198 | @Input() toggleAllCheckboxTooltipMessage = '';
199 |
200 | /** Define the position of the tooltip on the toggle-all checkbox. */
201 | @Input() toggleAllCheckboxTooltipPosition: 'left' | 'right' | 'above' | 'below' | 'before' | 'after' = 'below';
202 |
203 | /** Show/Hide the search clear button of the search input */
204 | @Input() hideClearSearchButton = false;
205 |
206 | /**
207 | * Always restore selected options on selectionChange for mode multi (e.g. for lazy loading/infinity scrolling).
208 | * Defaults to false, so selected options are only restored while filtering is active.
209 | */
210 | @Input() alwaysRestoreSelectedOptionsMulti = false;
211 |
212 | /**
213 | * Recreate array of selected values for multi-selects.
214 | *
215 | * This is useful if the selected values are stored in an immutable data structure.
216 | */
217 | @Input() recreateValuesArray = false;
218 |
219 | /** Output emitter to send to parent component with the toggle all boolean */
220 | @Output() toggleAll = new EventEmitter();
221 |
222 | /** Reference to the search input field */
223 | @ViewChild('searchSelectInput', { read: ElementRef, static: true }) searchSelectInput: ElementRef;
224 |
225 | /** Reference to the search input field */
226 | @ViewChild('innerSelectSearch', { read: ElementRef, static: true }) innerSelectSearch: ElementRef;
227 |
228 | /** Reference to custom search input clear icon */
229 | @ContentChild(MatSelectSearchClearDirective) clearIcon: MatSelectSearchClearDirective;
230 |
231 | /** Reference to custom no entries found element */
232 | @ContentChild(MatSelectNoEntriesFoundDirective) noEntriesFound: MatSelectNoEntriesFoundDirective;
233 |
234 | /** Current search value */
235 | get value(): string {
236 | return this._formControl.value;
237 | }
238 | private _lastExternalInputValue?: string;
239 |
240 | onTouched: () => void = () => {
241 | // do nothing.
242 | };
243 |
244 | /** Reference to the MatSelect options */
245 | public set _options(_options: QueryList) {
246 | this._options$.next(_options);
247 | }
248 | public get _options(): QueryList {
249 | return this._options$.getValue();
250 | }
251 |
252 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
253 | public _options$: BehaviorSubject> = new BehaviorSubject>(null as any);
254 |
255 | private optionsList$: Observable = this._options$.pipe(
256 | switchMap(_options => _options ?
257 | _options.changes.pipe(
258 | map(options => options.toArray()),
259 | startWith(_options.toArray()),
260 | ) : of(null)
261 | )
262 | );
263 |
264 | private optionsLength$: Observable = this.optionsList$.pipe(
265 | map(options => options ? options.length : 0)
266 | );
267 |
268 | /** Previously selected values when using */
269 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
270 | private previousSelectedValues: any[];
271 |
272 | public _formControl: FormControl = new FormControl('', {nonNullable: true});
273 |
274 | /** Whether to show the no entries found message */
275 | public _showNoEntriesFound$: Observable = combineLatest([
276 | this._formControl.valueChanges,
277 | this.optionsLength$
278 | ]).pipe(
279 | map(([value, optionsLength]) => !!(this.noEntriesFoundLabel && value
280 | && optionsLength === this.getOptionsLengthOffset()))
281 | );
282 |
283 | /** Subject that emits when the component has been destroyed. */
284 | private _onDestroy = new Subject();
285 |
286 | /** Reference to active descendant for ARIA Support. */
287 | private activeDescendant: HTMLElement;
288 |
289 | constructor(@Inject(MatSelect) public matSelect: MatSelect,
290 | public changeDetectorRef: ChangeDetectorRef,
291 | private _viewportRuler: ViewportRuler,
292 | @Optional() @Inject(MatOption) public matOption?: MatOption,
293 | @Optional() @Inject(MatFormField) public matFormField?: MatFormField,
294 | @Optional() @Inject(MAT_SELECTSEARCH_DEFAULT_OPTIONS) defaultOptions?: MatSelectSearchOptions
295 | ) {
296 | this.applyDefaultOptions(defaultOptions);
297 | }
298 |
299 | private applyDefaultOptions(defaultOptions?: MatSelectSearchOptions) {
300 | if (!defaultOptions) {
301 | return;
302 | }
303 | for (const key of configurableDefaultOptions) {
304 | if (Object.prototype.hasOwnProperty.call(defaultOptions, key)) {
305 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
306 | (this[key] as any) = defaultOptions[key];
307 | }
308 | }
309 | }
310 |
311 | ngOnInit() {
312 | // set custom mat-option class if the component was placed inside a mat-option
313 | if (this.matOption) {
314 | this.matOption.disabled = true;
315 | this.matOption._getHostElement().classList.add('contains-mat-select-search');
316 | this.matOption._getHostElement().setAttribute('role', 'presentation');
317 | } else {
318 | console.error(' must be placed inside a element');
319 | }
320 |
321 | // when the select dropdown panel is opened or closed
322 | this.matSelect.openedChange
323 | .pipe(
324 | delay(1),
325 | takeUntil(this._onDestroy)
326 | )
327 | .subscribe((opened) => {
328 | if (opened) {
329 | this.updateInputWidth();
330 | // focus the search field when opening
331 | if (!this.disableInitialFocus) {
332 | this._focus();
333 | }
334 | } else {
335 | // clear it when closing
336 | if (this.clearSearchInput) {
337 | this._reset();
338 | }
339 | }
340 | });
341 |
342 |
343 |
344 | // set the first item active after the options changed
345 | this.matSelect.openedChange
346 | .pipe(
347 | take(1),
348 | switchMap(() => {
349 | this._options = this.matSelect.options;
350 |
351 | // Closure variable for tracking the most recent first option.
352 | // In order to avoid causing the list to
353 | // scroll to the top when options are added to the bottom of
354 | // the list (eg: infinite scroll), we compare only
355 | // the changes to the first options to determine if we
356 | // should set the first item as active.
357 | // This prevents unnecessary scrolling to the top of the list
358 | // when options are appended, but allows the first item
359 | // in the list to be set as active by default when there
360 | // is no active selection
361 | let previousFirstOption = this._options.toArray()[this.getOptionsLengthOffset()];
362 |
363 | return this._options.changes
364 | .pipe(tap(() => {
365 | // avoid "expression has been changed" error
366 | setTimeout(() => {
367 | // Convert the QueryList to an array
368 | const options = this._options.toArray();
369 |
370 | // The true first item is offset by 1
371 | const currentFirstOption = options[this.getOptionsLengthOffset()];
372 |
373 | const keyManager = this.matSelect._keyManager;
374 | if (keyManager && this.matSelect.panelOpen && currentFirstOption) {
375 |
376 | // set first item active and input width
377 |
378 | // Check to see if the first option in these changes is different from the previous.
379 | const firstOptionIsChanged = !previousFirstOption
380 | || !this.matSelect.compareWith(previousFirstOption.value, currentFirstOption.value);
381 |
382 | // CASE: The first option is different now.
383 | // Indicates we should set it as active and scroll to the top.
384 | if (firstOptionIsChanged
385 | || !keyManager.activeItem
386 | || !options.find(option => this.matSelect.compareWith(option.value, keyManager.activeItem?.value))) {
387 | keyManager.setActiveItem(this.getOptionsLengthOffset());
388 | }
389 |
390 | // wait for panel width changes
391 | setTimeout(() => {
392 | this.updateInputWidth();
393 | });
394 | }
395 |
396 | // Update our reference
397 | previousFirstOption = currentFirstOption;
398 | });
399 | }));
400 | })
401 | )
402 | .pipe(takeUntil(this._onDestroy))
403 | .subscribe();
404 |
405 | // add or remove css class depending on whether to show the no entries found message
406 | // note: this is hacky
407 | this._showNoEntriesFound$.pipe(
408 | takeUntil(this._onDestroy)
409 | ).subscribe(showNoEntriesFound => {
410 | // set no entries found class on mat option
411 | if (this.matOption) {
412 | if (showNoEntriesFound) {
413 | this.matOption._getHostElement().classList.add('mat-select-search-no-entries-found');
414 | } else {
415 | this.matOption._getHostElement().classList.remove('mat-select-search-no-entries-found');
416 | }
417 | }
418 | });
419 |
420 | // resize the input width when the viewport is resized, i.e. the trigger width could potentially be resized
421 | this._viewportRuler.change()
422 | .pipe(takeUntil(this._onDestroy))
423 | .subscribe(() => {
424 | if (this.matSelect.panelOpen) {
425 | this.updateInputWidth();
426 | }
427 | });
428 |
429 | this.initMultipleHandling();
430 |
431 | this.optionsList$.pipe(
432 | takeUntil(this._onDestroy)
433 | ).subscribe(() => {
434 | // update view when available options change
435 | this.changeDetectorRef.markForCheck();
436 | });
437 | }
438 |
439 | _emitSelectAllBooleanToParent(state: boolean) {
440 | this.toggleAll.emit(state);
441 | }
442 |
443 | ngOnDestroy() {
444 | this._onDestroy.next();
445 | this._onDestroy.complete();
446 | }
447 |
448 | _isToggleAllCheckboxVisible(): boolean {
449 | return this.matSelect.multiple && this.showToggleAllCheckbox;
450 | }
451 |
452 | /**
453 | * Handles the key down event with MatSelect.
454 | * Allows e.g. selecting with enter key, navigation with arrow keys, etc.
455 | * @param event
456 | */
457 | _handleKeydown(event: KeyboardEvent) {
458 | // Prevent propagation for all alphanumeric characters in order to avoid selection issues
459 |
460 | // Needed to avoid handling in https://github.com/angular/components/blob/5439460d1fe166f8ec34ab7d48f05e0dd7f6a946/src/material/select/select.ts#L965
461 | if ((event.key && event.key.length === 1)
462 | || (this.preventHomeEndKeyPropagation && (event.key === 'Home' || event.key === 'End'))
463 | ) {
464 | event.stopPropagation();
465 | }
466 |
467 | if (this.matSelect.multiple && event.key && event.key === 'Enter') {
468 | // Regain focus after multiselect, so we can further type
469 | setTimeout(() => this._focus());
470 | }
471 |
472 | // Special case if click Escape, if input is empty, close the dropdown, if not, empty out the search field
473 | if (this.enableClearOnEscapePressed && event.key === 'Escape' && this.value) {
474 | this._reset(true);
475 | event.stopPropagation();
476 | }
477 | }
478 |
479 | /**
480 | * Handles the key up event with MatSelect.
481 | * Allows e.g. the announcing of the currently activeDescendant by screen readers.
482 | */
483 | _handleKeyup(event: KeyboardEvent) {
484 | if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
485 | const ariaActiveDescendantId = this.matSelect._getAriaActiveDescendant();
486 | const index = this._options.toArray().findIndex(item => item.id === ariaActiveDescendantId);
487 | if (index !== -1) {
488 | this.unselectActiveDescendant();
489 | this.activeDescendant = this._options.toArray()[index]._getHostElement();
490 | this.activeDescendant.setAttribute('aria-selected', 'true');
491 | this.searchSelectInput.nativeElement.setAttribute('aria-activedescendant', ariaActiveDescendantId);
492 | }
493 | }
494 | }
495 |
496 | writeValue(value: string) {
497 | this._lastExternalInputValue = value;
498 | this._formControl.setValue(value);
499 | this.changeDetectorRef.markForCheck();
500 | }
501 |
502 | onBlur() {
503 | this.unselectActiveDescendant();
504 | this.onTouched();
505 | }
506 |
507 | registerOnChange(fn: (value: string) => void) {
508 | this._formControl.valueChanges.pipe(
509 | filter(value => value !== this._lastExternalInputValue),
510 | tap(() => this._lastExternalInputValue = undefined),
511 | takeUntil(this._onDestroy)
512 | ).subscribe(fn);
513 | }
514 |
515 | registerOnTouched(fn: () => void) {
516 | this.onTouched = fn;
517 | }
518 |
519 | /**
520 | * Focuses the search input field
521 | */
522 | public _focus() {
523 | if (!this.searchSelectInput || !this.matSelect.panel) {
524 | return;
525 | }
526 | // save and restore scrollTop of panel, since it will be reset by focus()
527 | // note: this is hacky
528 | const panel = this.matSelect.panel.nativeElement;
529 | const scrollTop = panel.scrollTop;
530 |
531 | // focus
532 | this.searchSelectInput.nativeElement.focus();
533 |
534 | panel.scrollTop = scrollTop;
535 | }
536 |
537 | /**
538 | * Resets the current search value
539 | * @param focus whether to focus after resetting
540 | */
541 | public _reset(focus?: boolean) {
542 | this._formControl.setValue('');
543 | if (focus) {
544 | this._focus();
545 | }
546 | }
547 |
548 |
549 | /**
550 | * Initializes handling
551 | * Note: to improve this code, mat-select should be extended to allow disabling resetting the selection while filtering.
552 | */
553 | private initMultipleHandling() {
554 | if (!this.matSelect.ngControl) {
555 | if (this.matSelect.multiple) {
556 | // note: the access to matSelect.ngControl (instead of matSelect.value / matSelect.valueChanges)
557 | // is necessary to properly work in multi-selection mode.
558 | console.error('the mat-select containing ngx-mat-select-search must have a ngModel or formControl directive when multiple=true');
559 | }
560 | return;
561 | }
562 | // if
563 | // store previously selected values and restore them when they are deselected
564 | // because the option is not available while we are currently filtering
565 | this.previousSelectedValues = this.matSelect.ngControl.value;
566 |
567 | if (!this.matSelect.ngControl.valueChanges) {
568 | return;
569 | }
570 |
571 | this.matSelect.ngControl.valueChanges
572 | .pipe(takeUntil(this._onDestroy))
573 | .subscribe((values) => {
574 | let restoreSelectedValues = false;
575 | if (this.matSelect.multiple) {
576 | if ((this.alwaysRestoreSelectedOptionsMulti || (this._formControl.value && this._formControl.value.length))
577 | && this.previousSelectedValues && Array.isArray(this.previousSelectedValues)) {
578 | if (!values || !Array.isArray(values)) {
579 | values = [];
580 | }
581 | const optionValues = this.matSelect.options.map(option => option.value);
582 | this.previousSelectedValues.forEach(previousValue => {
583 | if (!values.some(v => this.matSelect.compareWith(v, previousValue))
584 | && !optionValues.some(v => this.matSelect.compareWith(v, previousValue))) {
585 | // if a value that was selected before is deselected and not found in the options, it was deselected
586 | // due to the filtering, so we restore it.
587 | if (this.recreateValuesArray) {
588 | values = [...values, previousValue];
589 | } else {
590 | values.push(previousValue);
591 | }
592 | restoreSelectedValues = true;
593 | }
594 | });
595 | }
596 | }
597 | this.previousSelectedValues = values;
598 |
599 | if (restoreSelectedValues) {
600 | this.matSelect._onChange(values);
601 | }
602 | });
603 | }
604 |
605 | /**
606 | * Set the width of the innerSelectSearch to fit even custom scrollbars
607 | * And support all Operating Systems
608 | */
609 | public updateInputWidth() {
610 | if (!this.innerSelectSearch || !this.innerSelectSearch.nativeElement) {
611 | return;
612 | }
613 | let element: HTMLElement | null = this.innerSelectSearch.nativeElement;
614 | let panelElement: HTMLElement | null = null;
615 | while (element && element.parentElement) {
616 | element = element.parentElement;
617 | if (element.classList.contains('mat-select-panel')) {
618 | panelElement = element;
619 | break;
620 | }
621 | }
622 | if (panelElement) {
623 | this.innerSelectSearch.nativeElement.style.width = panelElement.clientWidth + 'px';
624 | }
625 | }
626 |
627 | /**
628 | * Determine the offset to length that can be caused by the optional matOption used as a search input.
629 | */
630 | private getOptionsLengthOffset(): number {
631 | if (this.matOption) {
632 | return 1;
633 | } else {
634 | return 0;
635 | }
636 | }
637 |
638 | private unselectActiveDescendant() {
639 | this.activeDescendant?.removeAttribute('aria-selected');
640 | this.searchSelectInput.nativeElement.removeAttribute('aria-activedescendant');
641 | }
642 |
643 | }
644 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 8.0.4
4 | * Fix: Add `@angular/material`: `^21.0.0` to `peerDependencies`
5 |
6 | ## 8.0.3
7 | * Enhancement: Add compatibility with `@angular/material`: `^21.0.0`
8 |
9 | ## 8.0.2
10 | * Enhancement: Convert to standalone component
11 | [#547](https://github.com/bithost-gmbh/ngx-mat-select-search/pull/547)
12 |
13 | Thanks to @NachmanBerkowitz
14 |
15 | ## 8.0.1
16 | * Enhancement: Add compatibility with `@angular/material`: `^20.0.0`
17 | * Fix: Missing brackground color on search input field
18 | [#542](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/542)
19 |
20 | Thanks to @melroy89
21 | * Fix: When changing the font-size, the select input had a wrong height
22 | [#541](https://github.com/bithost-gmbh/ngx-mat-select-search/pull/541)
23 |
24 | Thanks to @angelaki
25 |
26 | ## 8.0.0
27 | * Upgrade: Upgrade project to Angular 19
28 | [#520](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/520)
29 |
30 | Thanks to @swierzbicki
31 |
32 | > [!CAUTION]
33 | > **Breaking Change**: `@angular/material`: `<= 15` is not supported anymore, please use version `16.0.0`.
34 | See [README.md#compatibility](README.md#compatibility)
35 |
36 | ## 7.0.10
37 | * Fix: Explicitly set `standalone: false` for Angular 19 compatibility
38 | [#512](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/512)
39 |
40 | Thanks to @GipHub123
41 |
42 | ## 7.0.9
43 | * Enhancement: Make Placeholder color tunable.
44 | [#506](https://github.com/bithost-gmbh/ngx-mat-select-search/pull/506)
45 |
46 | Thanks to @iblislin
47 |
48 | ## 7.0.8
49 | * Enhancement: Add option `recreateValuesArray` to support immutable data structures in multi-selects.
50 | [#376](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/376)
51 |
52 | Thanks to @KristofGilis
53 |
54 | * Enhancement: Add compatibility with `@angular/material`: `^19.0.0`
55 | * Enhancement: Remove use of deprecated `event.keyCode`
56 | [#485](https://github.com/bithost-gmbh/ngx-mat-select-search/pull/485)
57 |
58 | Thanks to @sirh3e
59 | * Documentation: Improved example doc strings
60 | [#489](https://github.com/bithost-gmbh/ngx-mat-select-search/pull/489)
61 |
62 | Thanks to @sirh3e
63 |
64 | ## 7.0.7
65 | * Fix blocked aria-hidden when opening the dropdown for the first time
66 | [#474](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/474)
67 |
68 | Thanks to @AleixFerreCP
69 | * Enhancement: Enable `strictNullChecks`
70 | [#476](https://github.com/bithost-gmbh/ngx-mat-select-search/pull/476)
71 |
72 | Thanks to @davidsansome
73 |
74 | ## 7.0.6
75 | * Enhancement: Add compatibility with `@angular/material`: `^18.0.0`
76 |
77 | ## 7.0.5
78 | * Fix select search should compare values of options for first item
79 | [#445](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/445)
80 |
81 | Thanks to @lorenzbaier
82 | * Enhancement: Add compatibility with `@angular/material`: `^17.0.0`
83 |
84 | ## 7.0.4
85 | * Fix background issue with `@angular/material`: `^16.2.0` with toggle-all button
86 | [#435](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/435)
87 |
88 | Thanks to @ioanbin
89 | * Fix issue with setting the first active item
90 | [#436](https://github.com/bithost-gmbh/ngx-mat-select-search/pull/436)
91 |
92 | Thanks to @Danevandy99
93 |
94 | ## 7.0.3
95 | * Fix background issue with `@angular/material`: `^16.2.0`
96 | [#431](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/431)
97 |
98 | Thanks to @chutzemischt and @akaNightmare
99 |
100 | ## 7.0.2
101 | * Fix compatibility with `@angular/material`: `^16.0.0`
102 | [#425](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/425)
103 |
104 | Thanks to @GipHub123 for reporting
105 |
106 | ## 7.0.1
107 | * Enhancement: Add compatibility with `@angular/material`: `^16.0.0`
108 |
109 | ## 7.0.0
110 | * Update compatibility to `@angular/material@15` with the MDC-based `MatSelectModule` (`@angular/material/select`).
111 | [#412](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/412)
112 |
113 | Thanks to @pureyoy and @swierzbicki
114 |
115 | **Breaking Change**: The `MatLegacySelectModule` (`@angular/material/legacy-select`) of `@angular/material@15` is not supported anymore, please use version `6.0.0`.
116 | See [README.md#compatibility](README.md#compatibility)
117 |
118 | ## 6.0.0
119 | * Add Angular Material 15 as a peer dependency \
120 | [#408](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/408)
121 |
122 | Thanks to @swierzbicki
123 | * Fix select all example [#336](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/336)
124 |
125 | Thanks to @AhsanAyaz
126 | * Update compatibility to `@angular/material@15` with `MatLegacySelectModule` (`@angular/material/legacy-select`).
127 | [#395](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/395)
128 |
129 | **Breaking Change**: `@angular/material`: `<= 14` is not supported anymore, please use version `5.0.0`.
130 | See [README.md#compatibility](README.md#compatibility)
131 |
132 | ## 5.0.0
133 | * Enhancement: Accessibility Issue: screenreaders reading blank before each option.
134 | [#349](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/349)
135 |
136 | **Breaking Change**: The `@Input() indexAndLengthScreenReaderText` became obsolete and was removed.
137 |
138 | Thanks to @escheiermann
139 | * Enhancement: **Breaking Change** Renamed misspelled `@Input() toogleAllCheckboxTooltipPosition`
140 | to `@Input() toggleAllCheckboxTooltipPosition`.
141 | [#266](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/266)
142 |
143 | Thanks to @bulldog98 for reporting
144 |
145 | ## 4.2.1
146 | * Bugfix: Revert copying in mode multi so `(selectionChange)` works properly.
147 | [#387](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/387)
148 |
149 | Thanks to @angelaki for reporting
150 |
151 | ## 4.2.0
152 | * Enhancement: Enable the use of a custom template for notFoundEntries
153 | [#381](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/381)
154 |
155 | Thanks to @ruekart
156 |
157 | ## 4.1.2
158 | * Bugfix: Restoring selected values in mode multi throws error
159 | [#376](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/376)
160 |
161 | Thanks to @qstiegler
162 | * Chore: Update npm packages
163 |
164 | ## 4.1.1
165 | * Enhancement: Rename `MATSELECTSEARCH_GLOBAL_OPTIONS` to `MAT_SELECTSEARCH_DEFAULT_OPTIONS`
166 | [#369](https://github.com/bithost-gmbh/ngx-mat-select-search/pull/369)
167 |
168 | Thanks to @angelaki
169 |
170 | ## 4.1.0
171 | * Feature: Default `@Input()` values can be configured globally using `MATSELECTSEARCH_GLOBAL_OPTIONS`
172 | [#364](https://github.com/bithost-gmbh/ngx-mat-select-search/pull/364)
173 |
174 | Thanks to @angelaki
175 | * Feature: Close icon can be configured with `closeIcon` and `closeSvgIcon`
176 | [#364](https://github.com/bithost-gmbh/ngx-mat-select-search/pull/364)
177 |
178 | Thanks to @angelaki
179 |
180 | ## 4.0.2
181 | * Enhancement: Add compatibility with `@angular/core`: `^14.0.0`, `@angular/material`: `^14.0.0`
182 |
183 | ## 4.0.1
184 | * Fix: Remove incompatible angular versions from `Readme.md` and `package.json`
185 |
186 | ## 4.0.0
187 | * Enhancement: Build and package using `@angular/core@13` in Ivy format.
188 | [#347](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/347)
189 |
190 | **Breaking Change**: `@angular/core`: `<= 11.0.0` is not supported anymore, please use version `3.3.3`.
191 |
192 | Thanks to @nseni for reporting
193 |
194 | ## 3.3.3
195 | * Enhancement: Document compatibility with `@angular/core`: `^13.0.0`, `@angular/material`: `^13.0.0` in `README.md`
196 |
197 | Thanks to @meta72
198 |
199 | ## 3.3.2
200 | * Fix: Add compatibility with `@angular/core`: `^13.0.0`, `@angular/material`: `^13.0.0` in `package.json`
201 |
202 | ## 3.3.1
203 | * Enhancement: Add compatibility with `@angular/core`: `^13.0.0`, `@angular/material`: `^13.0.0`
204 |
205 | ## 3.3.0
206 | * Enhancement: Add option to restore selected items using infinity-scroll
207 | [#320](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/320)
208 |
209 | Thanks to @angelaki
210 |
211 | ## 3.2.2
212 | * Enhancement: Update `peerDependencies` in `package.json` for compatibility with `@angular/core`: `^12.0.0`, `@angular/material`: `^12.0.0`
213 |
214 | ## 3.2.1
215 | * Enhancement: Add compatibility with `@angular/core`: `^12.0.0`, `@angular/material`: `^12.0.0`
216 | * Enhancement: Improve select all example
217 |
218 | Thanks to @achilehero
219 |
220 | ## 3.2.0
221 | * Feature: allow disabling the clear search button with `[hideClearSearchButton]="true"`
222 | [#290](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/290)
223 |
224 | Thanks to @vlio20
225 |
226 | ## 3.1.4
227 | * Fix: null-pointer exception if no form control directive on `mat-select`
228 | [#281](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/281)
229 |
230 | Thanks to @Daishy for reporting
231 |
232 | ## 3.1.3
233 | * Fix: incorrect restoration of selection when using `[multiple]="true"`
234 | [#279](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/279)
235 |
236 | Thanks to @broekema41
237 |
238 | ## 3.1.2
239 | * Fix: incorrect restoration of selection when using `[multiple]="true"` and no initial selection prior to filtering
240 | [#270](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/270)
241 |
242 | Thanks to @pranavneeraj for reporting
243 |
244 | ## 3.1.1
245 | * Enhancement: Add compatibility with `@angular/core`: `^11.0.0`, `@angular/material`: `^11.0.0`
246 | * Fix: set first item active if no item is active after filtering
247 | [#263](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/263)
248 |
249 | Thanks to @Ayoubane for reporting
250 | * Fix: in multi select mode after enter the search is not focused
251 | [#265](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/265)
252 |
253 | Thanks to @bulldog98
254 |
255 | ## 3.1.0
256 | * Feature: Support Right-To-Left
257 | [#258](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/258)
258 |
259 | Thanks to @framasev
260 | * Fix: ensure the placeholder is displayed
261 | [#256](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/256)
262 |
263 | Thanks to @aminsmartsenese for reporting
264 | * Fix: Refactor infinity scrolling example
265 | [#253](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/253)
266 |
267 | Thanks to @NitinMagdum for reporting
268 | * Fix: properly restore selection when using `[multiple]="true"`
269 | [#260](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/260)
270 |
271 | Thanks to @begandroide for reporting
272 |
273 |
274 | ## 3.0.3
275 | * Fix: properly restore selection when using `[multiple]="true"`
276 | [#249](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/249)
277 |
278 | Thanks to @croy618 for reporting
279 |
280 | ## 3.0.2
281 | * Fix: hide no entries found message when options don't change initially
282 | [#245](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/245)
283 |
284 | Thanks to @PabloPerezAguilo for reporting
285 |
286 | ## 3.0.1
287 | * Fix: correctly show no entries found message when no options are available initially (e.g. in server-side search)
288 | [#239](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/239)
289 |
290 | Thanks to @Arjun-1r for reporting
291 |
292 | ## 3.0.0
293 | * Fix: prevent scrolling to first option if option does not change (e.g. with infinity scrolling) [#200](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/200)
294 |
295 | Thanks to @raysuelzer
296 | * Enhancement: Add option to clear input when pressing escape with `[enableClearOnEscapePressed]="true"`
297 | [#231](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/231)
298 |
299 | Thanks to @nischi
300 | * Enhancement: Add compatibility with `@angular/core`: `^10.0.0`, `@angular/material`: `^10.0.0`
301 | * Chore: (**Breaking Change**) The possibility to place the `` element directly inside ``
302 | without wrapping it in an `` element was removed due to changes of the public API of `mat-select`. [#208](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/208)
303 |
304 | To fix this, simply place the `` inside a `` element.
305 | Thanks to @evoltafreak
306 |
307 | ## 2.2.0
308 | * Enhancement: add tooltip message to select-all checkbox [#227](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/227)
309 |
310 | Thanks to @arucar
311 |
312 | ## 2.1.2
313 | * Fix: fix selection of different instances of same object when using `multiple` [#215](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/215)
314 |
315 | Thanks to @Springrbua
316 | * Chore: enable linting and use [@angular-extensions/lint-rules](https://github.com/angular-extensions/lint-rules)
317 |
318 | ## 2.1.1
319 | * Fix: Prevent unhandled exceptions when pressing arrow keys with no available options [#201](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/201)
320 |
321 | Thanks to @josephdecock
322 |
323 | ## 2.1.0
324 | * Enhancement: Improve accessibility by reading the selected option
325 | [#186](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/186)
326 |
327 | Thanks to @ZacaryPaynter
328 |
329 | * Bugfix: Fix corners not rounded
330 | [#176](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/176)
331 |
332 | Thanks to @jfcere
333 |
334 | * Bugfix: Fix input width not updated correctly
335 | [#175](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/175)
336 | * Bugfix: ToggleAllCheckbox doesn't use correct theme color
337 | [183](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/183)
338 |
339 | Thanks to @LoganDupont for reporting
340 |
341 | * Enhancement: Improve readme on how to use i18n translation for labels
342 | [#180](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/180)
343 |
344 | Thanks to @JomalJohny
345 |
346 | ## 2.0.0
347 | * Enhancement: Update to Angular `8.2.10`
348 | * Enhancement: Add compatibility with `@angular/core`: `^9.0.0`, `@angular/material`: `^9.0.0`
349 | [#173](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/173)
350 |
351 | **Breaking Change**: `@angular/core`: `< 8.0.0` is not supported anymore, please use version `1.8.0`.
352 | * Enhancement: Replace `@angular/material` root imports
353 | [#161](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/161)
354 |
355 | ## 1.8.0
356 | * Enhancement: Add option to show a toggle all checkbox with `[showToggleAllCheckbox]="true"`
357 | [#145](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/145)
358 |
359 | Thanks to @blazewalker59
360 |
361 | * Enhancement: Allow custom content transclusion with `.mat-select-search-custom-header-content`
362 |
363 | ## 1.7.6
364 | * Bugfix: spinner not visible after reopening select panel
365 | [#153](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/153)
366 |
367 | Thanks to @saithis for reporting
368 |
369 | ## 1.7.5
370 | * Bugfix: Avoid `Cannot read property 'attach' of undefined` when ``
371 | is not inside a `` element with Angular 8
372 | [#146](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/146)
373 |
374 | ## 1.7.4
375 | * Enhancement: Allow setting the initial value of the search input
376 | [#147](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/147)
377 |
378 | Thanks to @sehgalneha for reporting
379 |
380 | ## 1.7.3
381 | * Enhancement: Allow setting accessibility label of the input with ` @Input() ariaLabel`
382 | [#137](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/137)
383 |
384 | Thanks to @tonyholt
385 |
386 | ## 1.7.2
387 | * Enhancement: Allow setting type of the search input field with `@Input() type`
388 | [#138](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/138)
389 |
390 | Thanks to @botoxparty
391 |
392 | * Enhancement: Tested and update peer dependency compatibility to allow
393 | `@angular/core`: `^8.0.0`, `@angular/material`: `^8.0.0`
394 |
395 | ## 1.7.1
396 | * Enhancement: allow disabling scrolling active element into view when option list changes
397 | with `@Input() disableScrollToActiveOnOptionsChanged` [#130](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/130)
398 |
399 | Thanks to @drakeBear for reporting
400 |
401 | * Bugfix: input field not focused in IE [#131](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/131)
402 | * Bugfix: clear button not visible after reopening select panel in combination with `[clearSearchInput]="false"`
403 | [#133](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/133)
404 |
405 | Thanks to @cappster for reporting
406 |
407 | ## 1.7.0
408 | * Enhancement: ensure the active option is not covered by the search input when navigating
409 | with the arrow keys or when the option list changes when searching.
410 | [#119](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/119)
411 |
412 | Thanks to @Mabiro
413 |
414 | * Enhancement: allow disabling propagation of home / end key via `@Input() preventHomeEndKeyPropagation`
415 | to enable moving the cursor inside the search field instead of navigating the options when pressing
416 | Home / End [#43](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/43)
417 |
418 | Thanks to @geraldhe for reporting
419 |
420 | * Bugfix / Enhancement: Update the input width when the viewport is resized
421 | [#81](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/81)
422 |
423 | Thanks to @mhosman for reporting
424 |
425 | * Enhancement: add opacity transition animation for search clear icon
426 |
427 | ## 1.6.0
428 | * Enhancement: Allow showing a loading / searching indicator with `@Input() searching`
429 | [#114](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/114)
430 |
431 | Thanks to @mstawick
432 |
433 |
434 | ## 1.5.3
435 | * Bugfix: Avoid space when opening select panel after previously showing "no entries found" message when placing `` inside a `` element
436 | [#107](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/107)
437 |
438 | Thanks to @aroblu94 for reporting
439 |
440 | ## 1.5.2
441 | * Bugfix: Show "no entries found" message when placing `` inside a `` element
442 | [#101](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/101)
443 |
444 | Thanks to @mstawick for reporting
445 |
446 | ## 1.5.1
447 | * Bugfix: Hide checkbox when placing `` inside a `` element
448 | and with `` [#98](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/98)
449 |
450 | Thanks to @jkhadivi for reporting
451 |
452 | ## 1.5.0
453 | * Bugfix: Fix scroll issue in iOS [#70](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/70)
454 |
455 | Thanks to @JiaHongL
456 |
457 | * Enhancement / Bugfix: Allow placing `` inside a `` element.
458 | This prevents the search field being placed outside of the visible viewport ([#1](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/1)).
459 |
460 | Note: it is still possible to place the `` element directly inside ``
461 | without wrapping it in an `` element.
462 |
463 | Thanks to @hanuhimanshu
464 |
465 | * Examples: Add example for server-side filtering [#26](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/26)
466 |
467 | Thanks to @hanuhimanshu
468 |
469 | * Examples: Add example for option groups [#15](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/15)
470 |
471 | Thanks to @maechler
472 |
473 | * Examples: refactor examples into separate components [#86](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/86)
474 |
475 | ## 1.4.2
476 | * Bugfix: Error when quickly selecting an option [#69](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/69)
477 |
478 | Thanks to @olaf89 for reporting
479 |
480 | * Bugfix: Filter selection jumps to next entry [#73](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/73)
481 |
482 | Thanks to @Kimmova
483 |
484 | ## 1.4.1
485 | * Bugfix: Wrong panel positioning when select is at bottom edge of viewport
486 | due to overridden panel height (`350px`). The default panel height (`256px`) will be used
487 | [#63](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/63)
488 |
489 | Note: the panel height can be changed via css (not recommended, as it leads to issues):
490 | ```css
491 | .mat-select-search-panel {
492 | ...
493 | max-height: 350px;
494 | }
495 | ```
496 |
497 | Thanks to @hadsy for reporting.
498 |
499 | ## 1.4.0
500 | * Enhancement: Tested and update peer dependency compatibility to allow
501 | `@angular/core`: `^7.0.0`, `@angular/cdk`: `^7.0.0`, `@angular/material`: `^7.0.0`
502 | * Enhancement: Allow disabling initial focusing of the input field with `@Input() disableInitialFocus`
503 | [#47](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/47)
504 | * Bugfix: Clearing the search input by clicking the clear icon did not work with `[clearSearchInput]="false"`
505 | [#55](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/55)
506 |
507 | Thanks to @ofriedrich for reporting
508 |
509 | ## 1.3.1
510 | * Bugfix: Error thrown when used together with `*ngIf` [#53](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/53)
511 |
512 | Thanks to @rhyre for reporting
513 |
514 | ## 1.3.0
515 | * Enhancement: allow customization of the clear icon [#41](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/41)
516 |
517 | Thanks to @OvidijusStukas
518 |
519 | * Enhancement: Add note about possible workaround for search input being displayed
520 | outside of the visible viewport in some cases [#1](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/1)
521 |
522 | Thanks to @maxencefrenette
523 |
524 | ## 1.2.4
525 | * Enhancement: ensure forward compatibility independent of markup changes [#38](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/38)
526 | * Enhancement: fix warnings in tests, improve example
527 |
528 | ## 1.2.3
529 | * Bugfix: input shows rounded corners when used together with MatDatepicker [#33](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/33)
530 |
531 | ## 1.2.2
532 | * Bugfix: input shows drop shadow when used together with MatDatepicker [#33](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/33)
533 |
534 | Thanks @irowbin for reporting
535 |
536 | ## 1.2.1
537 |
538 | * Bugfix: Width of the input field is wrong in IE11 when using large option texts with angular material 6+. [#29](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/29)
539 |
540 | Thanks to @Sabartius
541 |
542 | ## 1.2.0
543 |
544 | * Enhancement: allow preventing clearing the search input when closing the select, needed for server-side filtering. [#3](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/3)
545 |
546 | Thanks to @damianmigo
547 |
548 | ## 1.1.0
549 |
550 | * Enhancement: Use material theming and typography [#21](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/21)
551 |
552 | Thanks to @Avejack
553 |
554 | * Enhancement: Adjust input field width to actual scroll bar width [#21](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/21)
555 |
556 | Thanks to @Avejack
557 |
558 | * Enhancement: Add Angular 6 compatibility, update dependencies [#23](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/23)
559 |
560 | Note: this reverts the RxJS operator path improvements ([#17](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/17)) in order to be compatible with both, Angular 5.x.x and 6.x.x
561 |
562 | ## 1.0.5
563 |
564 | * Enhancement: Really improve tree-shaking by importing RxJS operators from specific path [#17](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/17)
565 |
566 | Thanks to @mtraynham
567 |
568 | ## 1.0.4
569 |
570 | * Enhancement: improve tree-shaking by importing RxJS operators from specific path [#17](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/17)
571 |
572 | Thanks to @mtraynham
573 |
574 | ## 1.0.3
575 |
576 | * Enhancement: prevent scrollbar flashing when opening / closing [#2](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/2)
577 |
578 | Thanks to @alexandrupaul7
579 |
580 | ## 1.0.2
581 |
582 | * Enhancement: disable autocomplete for search input field [#5](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/5)
583 |
584 | ## 1.0.1
585 |
586 | * Bugfix: don't clear initial selection with `multi="true"` [#6](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/6)
587 |
588 | Thanks to @joqkey
589 |
590 | * Bugfix: show "no options found" message as soon as no options are found [#4](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/4), [#10](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/10)
591 |
592 | ## 1.0.0
593 |
594 | * Initial Release
595 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NgxMatSelectSearch
2 | [https://github.com/bithost-gmbh/ngx-mat-select-search](https://github.com/bithost-gmbh/ngx-mat-select-search)
3 |
4 | [](https://www.npmjs.com/package/ngx-mat-select-search)
5 | [](https://www.npmjs.com/package/ngx-mat-select-search)
6 | [](https://www.npmjs.com/package/ngx-mat-select-search)
7 | [](https://circleci.com/gh/bithost-gmbh/ngx-mat-select-search)
8 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=NAX558HVGAX8Q)
9 |
10 | ## What does it do?
11 | Angular component providing an input field for searching / filtering [MatSelect](https://material.angular.io/components/select/overview) options of the [Angular Material](https://material.angular.io) library.
12 |
13 |
14 |
15 | ## Try it
16 | See it in action at
17 |
18 | * [https://stackblitz.com/github/bithost-gmbh/ngx-mat-select-search-example](https://stackblitz.com/github/bithost-gmbh/ngx-mat-select-search-example?file=src%2Fapp%2Fapp.component.html)
19 |
20 | see example code, builds in browser, latest version, latest material version
21 | * [https://bithost-gmbh.github.io/ngx-mat-select-search/](https://bithost-gmbh.github.io/ngx-mat-select-search/)
22 |
23 | pre-built, latest version, material 19, works on mobile
24 |
25 | **Important Note**: This project is meant as a temporary implementation of [https://github.com/angular/material2/issues/5697](https://github.com/angular/material2/issues/5697).
26 | The goal is to have an implementation in the official Angular Material repository, a new PR will be created.
27 |
28 | ## Contributions
29 | Contributions are welcome, please open an issue and preferably file a pull request.
30 |
31 | ### Support Development
32 |
33 | We aim at providing the best service possible by constantly improving `NgxMatSelectSearch` and responding fast to bug reports. We do this fully free of cost.
34 | If you feel like this library was useful to you and saved you and your business some precious time, please consider making a donation to support its maintenance and further development.
35 |
36 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=NAX558HVGAX8Q)
37 |
38 | ### Financial Supporters
39 |
40 | Thank you very much to our financial supporters!
41 |
42 |
43 |
63 |
64 |
65 | ### Collaborators
66 |
67 |
68 |
72 |
73 |
74 | ### Contributors
75 |
76 | Thank you very much to all our community contributors!
77 |
78 |
79 |
391 |
392 |
393 | ## How to use it?
394 | Install `ngx-mat-select-search` in your project:
395 | ```
396 | npm install ngx-mat-select-search
397 | ```
398 |
399 | Import the `NgxMatSelectSearchModule` e.g. in your `app.module.ts`:
400 | ```typescript
401 | import { MatSelectModule } from '@angular/material';
402 | import { NgxMatSelectSearchModule } from 'ngx-mat-select-search';
403 |
404 | @NgModule({
405 | imports: [
406 | ...
407 | MatSelectModule,
408 | NgxMatSelectSearchModule
409 | ],
410 | })
411 | export class AppModule {}
412 | ```
413 |
414 | Use the `ngx-mat-select-search` component inside a `mat-select` element by placing it inside a `` element:
415 | ```html
416 |
417 |
418 |
419 |
420 |
421 |
422 | {{bank.name}}
423 |
424 |
425 |
426 | ```
427 | See the examples in [https://github.com/bithost-gmbh/ngx-mat-select-search/tree/master/src/app/examples](https://github.com/bithost-gmbh/ngx-mat-select-search/tree/master/src/app/examples)
428 | how to wire the `ngx-mat-select-search` and filter the options available.
429 | Or have a look at [https://github.com/bithost-gmbh/ngx-mat-select-search-example](https://github.com/bithost-gmbh/ngx-mat-select-search-example) to see it in a standalone app.
430 |
431 | ### Template driven forms
432 | You can alternatively use it with template driven forms as follows:
433 | ```html
434 |
435 | ```
436 |
437 | ### Labels
438 | In order to change the labels, use the inputs specified in the [API](#api) section as follows:
439 | ```html
440 |
443 | ```
444 | To use the [i18n](https://angular.io/guide/i18n) API for translation of the labels, add the corresponding `i18n-...` attributes:
445 | ```html
446 |
451 | ```
452 |
453 | ### Compatibility
454 |
455 | #### Current release
456 |
457 | * `@angular/core`: `^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0`
458 | * `@angular/material`: `^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0` with `MatSelectModule` (`@angular/material/select`)
459 |
460 |
461 | #### Version [`7.0.10`](https://github.com/bithost-gmbh/ngx-mat-select-search/tree/7.0.10)
462 |
463 | * `@angular/core`: `^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0`
464 | * `@angular/material`: `^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0` with `MatSelectModule` (`@angular/material/select`)
465 |
466 | #### Version [`6.0.0`](https://github.com/bithost-gmbh/ngx-mat-select-search/tree/6.0.0)
467 |
468 | * `@angular/core`: `^15.0.0`
469 | * `@angular/material`: `^15.0.0` with `MatLegacySelectModule` (`@angular/material/legacy-select`)
470 |
471 | #### Version [`5.0.0`](https://github.com/bithost-gmbh/ngx-mat-select-search/tree/5.0.0)
472 |
473 | * `@angular/core`: `^12.0.0 || ^13.0.0 || ^14.0.0`
474 | * `@angular/material`: `^12.0.0 || ^13.0.0 || ^14.0.0`
475 |
476 | #### Version [`3.3.3`](https://github.com/bithost-gmbh/ngx-mat-select-search/tree/3.3.3)
477 |
478 | * `@angular/core`: `^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0`
479 | * `@angular/material`: `^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0`
480 |
481 | #### Version [`1.8.0`](https://github.com/bithost-gmbh/ngx-mat-select-search/tree/1.8.0)
482 |
483 | * `@angular/core`: `^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0`
484 | * `@angular/material`: `^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0`
485 |
486 | ### API
487 | The `MatSelectSearchComponent` implements the [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor) interface.
488 | Furthermore, it provides the following inputs:
489 |
490 | #### Inputs
491 | ```typescript
492 | /** Label of the search placeholder */
493 | @Input() placeholderLabel = 'Suche';
494 |
495 | /** Type of the search input field */
496 | @Input() type = 'text';
497 |
498 | /** Font-based icon used for displaying Close-Icon */
499 | @Input() closeIcon = 'close';
500 |
501 | /** SVG-based icon used for displaying Close-Icon. If set, closeIcon is overridden */
502 | @Input() closeSvgIcon?: string;
503 |
504 | /** Label to be shown when no entries are found. Set to null if no message should be shown. */
505 | @Input() noEntriesFoundLabel = 'Keine Optionen gefunden';
506 |
507 | /**
508 | * Whether the search field should be cleared after the dropdown menu is closed.
509 | * Useful for server-side filtering. See [#3](https://github.com/bithost-gmbh/ngx-mat-select-search/issues/3)
510 | */
511 | @Input() clearSearchInput = true;
512 |
513 | /** Whether to show the search-in-progress indicator */
514 | @Input() searching = false;
515 |
516 | /** Disables initial focusing of the input field */
517 | @Input() disableInitialFocus = false;
518 |
519 | /** Enable clear input on escape pressed */
520 | @Input() enableClearOnEscapePressed = false;
521 |
522 | /**
523 | * Prevents home / end key being propagated to mat-select,
524 | * allowing to move the cursor within the search input instead of navigating the options
525 | */
526 | @Input() preventHomeEndKeyPropagation = false;
527 |
528 | /** Disables scrolling to active options when option list changes. Useful for server-side search */
529 | @Input() disableScrollToActiveOnOptionsChanged = false;
530 |
531 | /** Adds 508 screen reader support for search box */
532 | @Input() ariaLabel = 'dropdown search';
533 |
534 | /** Whether to show Select All Checkbox (for mat-select[multi=true]) */
535 | @Input() showToggleAllCheckbox = false;
536 |
537 | /** Select all checkbox checked state */
538 | @Input() toggleAllCheckboxChecked = false;
539 |
540 | /** select all checkbox indeterminate state */
541 | @Input() toggleAllCheckboxIndeterminate = false;
542 |
543 | /** Display a message in a tooltip on the toggle-all checkbox */
544 | @Input() toggleAllCheckboxTooltipMessage = '';
545 |
546 | /** Define the position of the tooltip on the toggle-all checkbox. */
547 | @Input() toggleAllCheckboxTooltipPosition: 'left' | 'right' | 'above' | 'below' | 'before' | 'after' = 'below';
548 |
549 | /** Show/Hide the search clear button of the search input */
550 | @Input() hideClearSearchButton = false;
551 |
552 | /**
553 | * Always restore selected options on selectionChange for mode multi (e.g. for lazy loading/infinity scrolling).
554 | * Defaults to false, so selected options are only restored while filtering is active.
555 | */
556 | @Input() alwaysRestoreSelectedOptionsMulti = false;
557 |
558 | /**
559 | * Recreate array of selected values for multi-selects.
560 | *
561 | * This is useful if the selected values are stored in an immutable data structure.
562 | */
563 | @Input() recreateValuesArray = false;
564 |
565 | /** Output emitter to send to parent component with the toggle all boolean */
566 | @Output() toggleAll = new EventEmitter();
567 |
568 |
569 | ```
570 |
571 | #### Customize clear icon
572 | In order to customize the search icon, add the `ngxMatSelectSearchClear` to your custom clear item (a `mat-icon` or any other element) and place it inside the `ngx-mat-select-search` component:
573 | ```html
574 |
575 | delete
576 |
577 | ```
578 | If just the icon should be changed the inputs `closeIcon` and `closeSvgIcon` can be used.
579 |
580 | #### Customize no entries found element
581 | In order to customize the no entries found element, add the `ngxMatSelectNoEntriesFound` to your custom item (a `mat-icon, span, button` or any other element) and place it inside the `ngx-mat-select-search` component:
582 | ```html
583 |
584 |
585 | No entries found
586 |
589 |
590 |
591 | ```
592 |
593 | #### Custom content
594 | Custom content with the CSS class `mat-select-search-custom-header-content` can be transcluded as follows:
595 | ```html
596 |
597 |
598 |
599 | ```
600 |
601 | #### Global default options
602 | Providing the [`MAT_SELECTSEARCH_DEFAULT_OPTIONS`](src/app/mat-select-search/default-options.ts)
603 | InjectionToken, the default values of several `@Input()` properties can be set globally.
604 | See the documentation of the corresponding `@Input()` properties of `MatSelectSearchComponent`.
605 |
606 | Example:
607 | ```typescript
608 | import { MAT_SELECTSEARCH_DEFAULT_OPTIONS, MatSelectSearchOptions } from 'ngx-mat-select-search';
609 |
610 | @NgModule({
611 | ...
612 | providers: [
613 | {
614 | provide: MAT_SELECTSEARCH_DEFAULT_OPTIONS,
615 | useValue: {
616 | closeIcon: 'delete',
617 | noEntriesFoundLabel: 'No options found',
618 | }
619 | }
620 | ]
621 | })
622 | class AppModule {}
623 | ```
624 |
625 | ## Known Problems
626 | * The currently selected option might be hidden under the search input field when opening the options panel
627 | and the panel is at the screen border.
628 |
629 | ## Development
630 |
631 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.7.1.
632 |
633 | ### Development server
634 |
635 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
636 |
637 | ### Build
638 |
639 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
640 |
641 | ### Library Build / NPM Package
642 | Run `npm run build-lib` to build the library and generate an NPM package.
643 | The build artifacts will be stored in the `dist-lib/` folder.
644 |
645 | To release, run `cd dist-lib/ && npm publish`.
646 |
647 | ### Running unit tests
648 |
649 | Run `npm run test` to execute the unit tests via [Karma](https://karma-runner.github.io).
650 |
--------------------------------------------------------------------------------
/src/app/mat-select-search/mat-select-search.component.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018 Bithost GmbH All Rights Reserved.
3 | *
4 | * Use of this source code is governed by an MIT-style license that can be
5 | * found in the LICENSE file at https://angular.io/license
6 | */
7 |
8 | import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
9 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
10 | import { UntypedFormControl, ReactiveFormsModule } from '@angular/forms';
11 | import { AsyncPipe, CommonModule, NgFor } from '@angular/common';
12 | import { NoopAnimationsModule } from '@angular/platform-browser/animations';
13 | import { MatFormFieldModule } from '@angular/material/form-field';
14 | import { MatSelect, MatSelectModule } from '@angular/material/select';
15 | import { ReplaySubject } from 'rxjs';
16 | import { Subject } from 'rxjs';
17 | import { delay, take } from 'rxjs/operators';
18 | import { takeUntil } from 'rxjs/operators';
19 |
20 | import { MatSelectSearchComponent } from './mat-select-search.component';
21 | import { NgxMatSelectSearchModule } from './ngx-mat-select-search.module';
22 | import { LiveAnnouncer } from '@angular/cdk/a11y';
23 | import { DOWN_ARROW } from '@angular/cdk/keycodes';
24 | import { MAT_SELECTSEARCH_DEFAULT_OPTIONS, MatSelectSearchOptions } from './default-options';
25 |
26 | interface Bank {
27 | id: string;
28 | name: string;
29 | bic: { value: string };
30 | }
31 |
32 | @Component({
33 | selector: 'mat-select-search-test',
34 | imports: [MatSelectSearchComponent, MatFormFieldModule, MatSelectModule, ReactiveFormsModule, NgFor, AsyncPipe],
35 | template: `
36 | Single selection
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | {{bank.name}}
45 |
46 |
47 |
48 |
49 |
50 | Selected Bank: {{bankCtrl.value?.name}}
51 |
52 |
53 | Single selection inside mat-option
54 |
55 |
56 |
57 |
58 |
60 |
61 |
62 | {{bank.name}}
63 |
64 |
65 |
66 |
67 |
68 | Selected Bank: {{bankCtrlMatOption.value?.name}}
69 |
70 |
71 | Multiple selection
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | {{bank.name}}
80 |
81 |
82 |
83 |
84 |
85 | Selected Banks:
86 |
87 |
90 | `,
91 | })
92 | export class MatSelectSearchTestComponent implements OnInit, OnDestroy, AfterViewInit {
93 |
94 | @ViewChild('selectSingle') matSelect: MatSelect;
95 | @ViewChild('selectSingleMatOption') matSelectMatOption: MatSelect;
96 | @ViewChild('selectMulti') matSelectMulti: MatSelect;
97 | @ViewChild('selectSearchSingle') matSelectSearch: MatSelectSearchComponent;
98 | @ViewChild('selectSearchSingleMatOption') matSelectSearchMatOption: MatSelectSearchComponent;
99 | @ViewChild('selectSearchMulti') matSelectSearchMulti: MatSelectSearchComponent;
100 |
101 | // control for the selected bank
102 | public bankCtrl: UntypedFormControl = new UntypedFormControl();
103 | // control for the selected bank
104 | public bankCtrlMatOption: UntypedFormControl = new UntypedFormControl();
105 | // control for the MatSelect filter keyword
106 | public bankFilterCtrl: UntypedFormControl = new UntypedFormControl();
107 | // control for the MatSelect filter keyword
108 | public bankFilterCtrlMatOption: UntypedFormControl = new UntypedFormControl();
109 |
110 | /** control for the selected bank for multi-selection */
111 | public bankMultiCtrl: UntypedFormControl = new UntypedFormControl();
112 |
113 | /** control for the MatSelect filter keyword multi-selection */
114 | public bankMultiFilterCtrl: UntypedFormControl = new UntypedFormControl();
115 |
116 |
117 | // list of banks
118 | public banks: Bank[] = [
119 | { name: 'Bank A', id: 'A', bic: { value: '102' } },
120 | { name: 'Bank B', id: 'B', bic: { value: '203' } },
121 | { name: 'Bank C', id: 'C', bic: { value: '304' } },
122 | { name: 'Bank DC', id: 'DC', bic: { value: '405' } }
123 | ];
124 |
125 | public filteredBanks: ReplaySubject = new ReplaySubject(1);
126 | public filteredBanksMatOption: ReplaySubject = new ReplaySubject(1);
127 |
128 | /** list of banks filtered by search keyword for multi-selection */
129 | public filteredBanksMulti: ReplaySubject = new ReplaySubject(1);
130 |
131 | public initialSingleSelection: Bank;
132 | public initialSingleSelectionMatOption: Bank;
133 | public initialMultiSelection: Bank[] = [];
134 |
135 |
136 | // Subject that emits when the component has been destroyed.
137 | private _onDestroy = new Subject();
138 |
139 | ngOnInit() {
140 | // set initial selection
141 | if (this.initialSingleSelection) {
142 | this.bankCtrl.setValue(this.initialSingleSelection);
143 | }
144 | if (this.initialSingleSelectionMatOption) {
145 | this.bankCtrlMatOption.setValue(this.initialSingleSelectionMatOption);
146 | }
147 | if (this.initialMultiSelection) {
148 | this.bankMultiCtrl.setValue(this.initialMultiSelection);
149 | }
150 |
151 |
152 | // load the initial bank list
153 | this.filteredBanks.next(this.banks.slice());
154 | this.filteredBanksMatOption.next(this.banks.slice());
155 | this.filteredBanksMulti.next(this.banks.slice());
156 |
157 | // listen for search field value changes
158 | this.bankFilterCtrl.valueChanges
159 | .pipe(takeUntil(this._onDestroy))
160 | .subscribe(() => {
161 | this.filterBanks();
162 | });
163 | this.bankFilterCtrlMatOption.valueChanges
164 | .pipe(takeUntil(this._onDestroy))
165 | .subscribe(() => {
166 | this.filterBanksMatOption();
167 | });
168 | this.bankMultiFilterCtrl.valueChanges
169 | .pipe(takeUntil(this._onDestroy))
170 | .subscribe(() => {
171 | this.filterBanksMulti();
172 | });
173 | }
174 |
175 | ngAfterViewInit() {
176 | this.setInitialValue();
177 | }
178 |
179 | ngOnDestroy() {
180 | this._onDestroy.next();
181 | this._onDestroy.complete();
182 | }
183 |
184 | /**
185 | * Sets the initial value after the filteredBanks are loaded initially
186 | */
187 | private setInitialValue() {
188 | this.filteredBanks
189 | .pipe(take(1), takeUntil(this._onDestroy))
190 | .subscribe(() => {
191 | // setting the compareWith property to a comparison function
192 | // triggers initializing the selection according to the initial value of
193 | // the form control (i.e. _initializeSelection())
194 | // this needs to be done after the filteredBanks are loaded initially
195 | // and after the mat-option elements are available
196 | this.matSelect.compareWith = (a: Bank, b: Bank) => a && b && a.id === b.id;
197 | this.matSelectMatOption.compareWith = (a: Bank, b: Bank) => a && b && a.id === b.id;
198 | this.matSelectMulti.compareWith = (a: Bank, b: Bank) => a && b && a.id === b.id;
199 | });
200 | }
201 |
202 | private filterBanks() {
203 | if (!this.banks) {
204 | return;
205 | }
206 |
207 | // get the search keyword
208 | let search = this.bankFilterCtrl.value;
209 | if (!search) {
210 | this.filteredBanks.next(this.banks.slice());
211 | return;
212 | } else {
213 | search = search.toLowerCase();
214 | }
215 |
216 | // filter the banks
217 | this.filteredBanks.next(
218 | this.banks.filter(bank => bank.name.toLowerCase().indexOf(search) > -1)
219 | );
220 | }
221 |
222 | private filterBanksMatOption() {
223 | if (!this.banks) {
224 | return;
225 | }
226 |
227 | // get the search keyword
228 | let search = this.bankFilterCtrlMatOption.value;
229 | if (!search) {
230 | this.filteredBanksMatOption.next(this.banks.slice());
231 | return;
232 | } else {
233 | search = search.toLowerCase();
234 | }
235 |
236 | // filter the banks
237 | this.filteredBanksMatOption.next(
238 | this.banks.filter(bank => bank.name.toLowerCase().indexOf(search) > -1)
239 | );
240 | }
241 |
242 |
243 | private filterBanksMulti() {
244 | if (!this.banks) {
245 | return;
246 | }
247 | // get the search keyword
248 | let search = this.bankMultiFilterCtrl.value;
249 | if (!search) {
250 | this.filteredBanksMulti.next(this.banks.slice());
251 | return;
252 | } else {
253 | search = search.toLowerCase();
254 | }
255 | // filter the banks
256 | this.filteredBanksMulti.next(
257 | this.banks.filter(bank => bank.name.toLowerCase().indexOf(search) > -1)
258 | );
259 | }
260 | }
261 |
262 | describe('MatSelectSearchComponent', () => {
263 | let component: MatSelectSearchTestComponent;
264 | let fixture: ComponentFixture;
265 |
266 | beforeEach(waitForAsync(() => {
267 | TestBed.configureTestingModule({
268 | imports: [
269 | CommonModule,
270 | NoopAnimationsModule,
271 | ReactiveFormsModule,
272 | MatFormFieldModule,
273 | MatSelectModule,
274 | NgxMatSelectSearchModule,
275 | MatSelectSearchTestComponent
276 | ],
277 | declarations: [],
278 | providers: [{
279 | provide: LiveAnnouncer,
280 | useValue: {
281 | announce: jasmine.createSpy('announce')
282 | }
283 | }
284 | ]
285 | })
286 | .compileComponents();
287 | }));
288 |
289 |
290 | beforeEach(() => {
291 | fixture = TestBed.createComponent(MatSelectSearchTestComponent);
292 | component = fixture.componentInstance;
293 | });
294 |
295 | describe('without initial selection', () => {
296 |
297 | beforeEach(() => {
298 | fixture.detectChanges();
299 | });
300 |
301 | it('should create', () => {
302 | expect(component).toBeTruthy();
303 | });
304 |
305 | it('should show a search field and focus it when opening the select', (done) => {
306 |
307 | component.filteredBanks
308 | .pipe(
309 | take(1),
310 | delay(1)
311 | )
312 | .subscribe(() => {
313 | // when the filtered banks are initialized
314 | fixture.detectChanges();
315 |
316 | component.matSelect.open();
317 | fixture.detectChanges();
318 |
319 | component.matSelect.openedChange
320 | .pipe(
321 | take(1),
322 | delay(1)
323 | )
324 | .subscribe((opened) => {
325 | expect(opened).toBe(true);
326 | const searchField = document.querySelector('.mat-select-search-inner .mat-select-search-input');
327 | const searchInner = document.querySelector('.mat-select-search-inner');
328 | expect(searchInner).toBeTruthy();
329 | expect(searchField).toBeTruthy();
330 | // check focus
331 | expect(searchField).toBe(document.activeElement);
332 |
333 | const optionElements = document.querySelectorAll('mat-option');
334 | expect(component.matSelect.options.length).toBe(5);
335 | expect(optionElements.length).toBe(5);
336 |
337 | done();
338 | });
339 |
340 | });
341 |
342 | });
343 |
344 |
345 | it('should filter the options available and highlight the first option in the list, filter the options by input "c" and reset the list', (done) => {
346 |
347 | component.filteredBanks
348 | .pipe(
349 | take(1),
350 | delay(1)
351 | )
352 | .subscribe(() => {
353 | // when the filtered banks are initialized
354 | fixture.detectChanges();
355 |
356 | component.matSelect.open();
357 | fixture.detectChanges();
358 |
359 | component.matSelect.openedChange
360 | .pipe(take(1))
361 | .subscribe((opened) => {
362 | expect(opened).toBe(true);
363 | const searchField = document.querySelector('.mat-select-search-inner .mat-select-search-input');
364 | expect(searchField).toBeTruthy();
365 |
366 | expect(component.matSelect.options.length).toBe(5);
367 |
368 | // search for "c"
369 | component.matSelectSearch._formControl.setValue('c');
370 | fixture.detectChanges();
371 |
372 | expect(component.bankFilterCtrl.value).toBe('c');
373 | expect(component.matSelect.panelOpen).toBe(true);
374 |
375 | component.filteredBanks
376 | .pipe(take(1))
377 | .subscribe(() => {
378 | fixture.detectChanges();
379 |
380 | setTimeout(() => {
381 | expect(component.matSelect.options.length).toBe(3);
382 | const firstSelectableOption = component.matSelect.options.get(1);
383 | expect(firstSelectableOption?.value.id).toBe('C');
384 | expect(firstSelectableOption?.active).toBe(true, 'first active');
385 |
386 | component.matSelectSearch._reset(true);
387 | fixture.detectChanges();
388 |
389 | // check focus
390 | expect(searchField).toBe(document.activeElement);
391 | expect(component.matSelect.panelOpen).toBe(true);
392 |
393 | component.filteredBanks
394 | .pipe(take(1))
395 | .subscribe(() => {
396 | fixture.detectChanges();
397 | if (component.matSelectSearch.clearSearchInput) {
398 | expect(component.matSelect.options.length).toBe(5);
399 | } else {
400 | expect(component.matSelect.options.length).toBe(3);
401 | }
402 |
403 | done();
404 | });
405 | });
406 |
407 | });
408 |
409 | });
410 |
411 | });
412 |
413 | });
414 |
415 | it('should not announce active option if there are no options', (done) => {
416 | const announcer = TestBed.get(LiveAnnouncer);
417 | component.filteredBanks
418 | .pipe(
419 | take(1),
420 | delay(1)
421 | )
422 | .subscribe(() => {
423 | // when the filtered banks are initialized
424 | fixture.detectChanges();
425 |
426 | component.matSelect.open();
427 | fixture.detectChanges();
428 |
429 | component.matSelect.openedChange
430 | .pipe(take(1))
431 | .subscribe(() => {
432 |
433 | // search for "something definitely not in the list"
434 | component.matSelectSearch._formControl.setValue('something definitely not in the list');
435 | fixture.detectChanges();
436 |
437 | component.filteredBanks
438 | .pipe(take(1))
439 | .subscribe(() => {
440 | fixture.detectChanges();
441 |
442 | setTimeout(() => {
443 | expect(component.matSelect.options.length).toBe(1);
444 |
445 | component.matSelectSearch._handleKeyup({keyCode: DOWN_ARROW} as KeyboardEvent);
446 | expect(announcer.announce).not.toHaveBeenCalled();
447 | done();
448 | });
449 | });
450 | });
451 | });
452 | });
453 |
454 | describe('inside mat-option', () => {
455 |
456 | it('should show a search field and focus it when opening the select', (done) => {
457 |
458 | component.filteredBanksMatOption
459 | .pipe(
460 | take(1),
461 | delay(1)
462 | )
463 | .subscribe(() => {
464 | // when the filtered banks are initialized
465 | fixture.detectChanges();
466 |
467 | component.matSelectMatOption.open();
468 | fixture.detectChanges();
469 |
470 | component.matSelectMatOption.openedChange
471 | .pipe(
472 | take(1),
473 | delay(1)
474 | )
475 | .subscribe((opened) => {
476 | expect(opened).toBe(true);
477 | const searchField = document.querySelector('.mat-select-search-inner .mat-select-search-input');
478 | const searchInner = document.querySelector('.mat-select-search-inner');
479 | expect(searchInner).toBeTruthy();
480 | expect(searchField).toBeTruthy();
481 | // check focus
482 | expect(searchField).toBe(document.activeElement);
483 |
484 | const optionElements = document.querySelectorAll('mat-option');
485 | expect(component.matSelectMatOption.options.length).toBe(5);
486 | expect(optionElements.length).toBe(5);
487 |
488 | done();
489 | });
490 |
491 | });
492 |
493 | });
494 |
495 |
496 | it('should filter the options available and highlight the first option in the list, filter the options by input "c" and reset the list', (done) => {
497 |
498 | component.filteredBanksMatOption
499 | .pipe(
500 | take(1),
501 | delay(1)
502 | )
503 | .subscribe(() => {
504 | // when the filtered banks are initialized
505 | fixture.detectChanges();
506 |
507 | component.matSelectMatOption.open();
508 | fixture.detectChanges();
509 |
510 | component.matSelectMatOption.openedChange
511 | .pipe(take(1))
512 | .subscribe((opened) => {
513 | expect(opened).toBe(true);
514 | const searchField = document.querySelector('.mat-select-search-inner .mat-select-search-input');
515 | expect(searchField).toBeTruthy();
516 |
517 | expect(component.matSelectMatOption.options.length).toBe(5);
518 |
519 | // search for "c"
520 | component.matSelectSearchMatOption._formControl.setValue('c');
521 | fixture.detectChanges();
522 |
523 | expect(component.bankFilterCtrlMatOption.value).toBe('c');
524 | expect(component.matSelectMatOption.panelOpen).toBe(true);
525 |
526 | component.filteredBanks
527 | .pipe(take(1))
528 | .subscribe(() => {
529 | fixture.detectChanges();
530 |
531 | setTimeout(() => {
532 | expect(component.matSelectMatOption.options.length).toBe(3);
533 | expect(component.matSelectMatOption.options.toArray()[1].value.id).toBe('C');
534 | expect(component.matSelectMatOption.options.toArray()[1].active).toBe(true, 'first active');
535 |
536 | component.matSelectSearchMatOption._reset(true);
537 | fixture.detectChanges();
538 |
539 | // check focus
540 | expect(searchField).toBe(document.activeElement);
541 | expect(component.matSelectMatOption.panelOpen).toBe(true);
542 |
543 | component.filteredBanks
544 | .pipe(take(1))
545 | .subscribe(() => {
546 | fixture.detectChanges();
547 | expect(component.matSelectMatOption.options.length).toBe(5);
548 |
549 | done();
550 | });
551 | });
552 |
553 | });
554 |
555 | });
556 |
557 | });
558 |
559 | });
560 |
561 | it('should compare first option changed by value of "bic"', (done) => {
562 | component.matSelectMatOption.compareWith = (b1: Bank, b2: Bank) => b1?.bic.value === b2?.bic.value;
563 |
564 | component.filteredBanksMatOption
565 | .pipe(
566 | take(1),
567 | delay(1)
568 | )
569 | .subscribe(() => {
570 | // when the filtered banks are initialized
571 | fixture.detectChanges();
572 |
573 | component.matSelectMatOption.open();
574 | fixture.detectChanges();
575 |
576 | component.matSelectMatOption.openedChange
577 | .pipe(take(1))
578 | .subscribe((opened) => {
579 | expect(opened).toBe(true);
580 | const searchField = document.querySelector('.mat-select-search-inner .mat-select-search-input');
581 | expect(searchField).toBeTruthy();
582 |
583 | expect(component.matSelectMatOption.options.length).toBe(5);
584 |
585 | // search for "c"
586 | component.matSelectSearchMatOption._formControl.setValue('c');
587 | fixture.detectChanges();
588 |
589 | expect(component.bankFilterCtrlMatOption.value).toBe('c');
590 | expect(component.matSelectMatOption.panelOpen).toBe(true);
591 |
592 | component.filteredBanks
593 | .pipe(take(1))
594 | .subscribe(() => {
595 | fixture.detectChanges();
596 |
597 | setTimeout(() => {
598 | expect(component.matSelectMatOption.options.length).toBe(3);
599 | expect(component.matSelectMatOption.options.toArray()[1].value.id).toBe('C');
600 | expect(component.matSelectMatOption.options.toArray()[1].active).toBe(true, 'first active');
601 |
602 | // search for DC
603 | component.matSelectSearchMatOption._formControl.setValue('DC');
604 | fixture.detectChanges();
605 |
606 | component.filteredBanks
607 | .pipe(take(1))
608 | .subscribe(() => {
609 | fixture.detectChanges();
610 |
611 | setTimeout(() => {
612 | expect(component.matSelectMatOption.options.length).toBe(2);
613 | expect(component.matSelectMatOption.options.toArray()[1].value.id).toBe('DC');
614 | expect(component.matSelectMatOption.options.toArray()[1].active).toBe(true, 'first active');
615 |
616 | done();
617 | });
618 | });
619 | });
620 |
621 | });
622 |
623 | });
624 |
625 | });
626 |
627 | });
628 | })
629 |
630 | });
631 |
632 | describe('with initial selection', () => {
633 |
634 | it('should set the initial selection of MatSelect', waitForAsync((done) => {
635 | component.initialSingleSelection = component.banks[3];
636 | fixture.detectChanges();
637 |
638 | component.filteredBanks
639 | .pipe(
640 | take(1),
641 | delay(1)
642 | )
643 | .subscribe(() => {
644 |
645 | // when the filtered banks are initialized
646 | fixture.detectChanges();
647 | fixture.whenStable().then(() => {
648 | fixture.detectChanges();
649 | component.matSelect.options.changes
650 | .pipe(take(1))
651 | .subscribe(() => {
652 |
653 | expect(component.matSelect.value).toEqual(component.banks[3]);
654 |
655 | component.matSelect.open();
656 | fixture.detectChanges();
657 |
658 | component.matSelect.openedChange
659 | .pipe(take(1))
660 | .subscribe((opened) => {
661 | expect(opened).toBe(true);
662 | expect(component.matSelect.value).toEqual(component.banks[3]);
663 | expect(component.bankCtrl.value).toEqual(component.banks[3]);
664 |
665 | done();
666 | });
667 | });
668 |
669 | });
670 |
671 | });
672 |
673 | }));
674 |
675 | it('set the initial selection with multi=true and filter the options available, filter the options by input "c" and select an option', waitForAsync((done) => {
676 | component.initialMultiSelection = [component.banks[1]];
677 | fixture.detectChanges();
678 |
679 | component.filteredBanksMulti
680 | .pipe(
681 | take(1),
682 | delay(1)
683 | )
684 | .subscribe(() => {
685 | // when the filtered banks are initialized
686 | fixture.detectChanges();
687 | fixture.whenStable().then(() => {
688 | fixture.detectChanges();
689 | component.matSelect.options.changes
690 | .pipe(take(1))
691 | .subscribe(() => {
692 |
693 | component.matSelectMulti.open();
694 | fixture.detectChanges();
695 |
696 | component.matSelectMulti.openedChange
697 | .pipe(take(1))
698 | .subscribe((opened) => {
699 | expect(opened).toBe(true);
700 | expect(component.matSelectMulti.value).toEqual([component.banks[1]]);
701 | expect(component.bankMultiCtrl.value).toEqual([component.banks[1]]);
702 |
703 | const searchField = document.querySelector('.mat-select-search-inner .mat-select-search-input');
704 | expect(searchField).toBeTruthy();
705 |
706 | expect(component.matSelectMulti.options.length).toBe(4);
707 |
708 | // search for "c"
709 | component.matSelectSearchMulti._formControl.setValue('c');
710 | fixture.detectChanges();
711 |
712 | expect(component.bankFilterCtrl.value).toBe('c');
713 | expect(component.matSelectMulti.panelOpen).toBe(true);
714 |
715 | component.filteredBanks
716 | .pipe(take(1))
717 | .subscribe(() => {
718 | fixture.detectChanges();
719 |
720 | setTimeout(() => {
721 | expect(component.matSelectMulti.options.length).toBe(2);
722 | expect(component.matSelectMulti.options.first.value.id).toBe('C');
723 | expect(component.matSelectMulti.options.first.active).toBe(true, 'first active');
724 |
725 | component.matSelectMulti.options.first._selectViaInteraction();
726 |
727 | fixture.detectChanges();
728 |
729 | // check focus
730 | expect(component.matSelectMulti.panelOpen).toBe(true);
731 |
732 | setTimeout(() => {
733 | fixture.detectChanges();
734 | expect(component.matSelectMulti.value).toEqual([component.banks[1], component.banks[2]]);
735 | expect(component.bankMultiCtrl.value).toEqual([component.banks[1], component.banks[2]]);
736 |
737 | // search for "d"
738 | component.matSelectSearchMulti._formControl.setValue('d');
739 | fixture.detectChanges();
740 |
741 | expect(component.bankFilterCtrl.value).toBe('d');
742 | expect(component.matSelectMulti.panelOpen).toBe(true);
743 |
744 | component.filteredBanks
745 | .pipe(take(1))
746 | .subscribe(() => {
747 | fixture.detectChanges();
748 |
749 | setTimeout(() => {
750 | expect(component.matSelectMulti.options.length).toBe(1);
751 | expect(component.matSelectMulti.options.first.value.id).toBe('DC');
752 | expect(component.matSelectMulti.options.first.active).toBe(true, 'first active');
753 |
754 | component.matSelectMulti.options.first._selectViaInteraction();
755 |
756 | fixture.detectChanges();
757 |
758 | // check focus
759 | expect(component.matSelectMulti.panelOpen).toBe(true);
760 |
761 | setTimeout(() => {
762 | fixture.detectChanges();
763 | expect(component.matSelectMulti.value).toEqual([component.banks[1], component.banks[2], component.banks[3]]);
764 | expect(component.bankMultiCtrl.value).toEqual([component.banks[1], component.banks[2], component.banks[3]]);
765 | done();
766 |
767 | });
768 | });
769 |
770 | });
771 |
772 | });
773 | });
774 |
775 | });
776 |
777 | });
778 | });
779 | });
780 |
781 |
782 | });
783 | }));
784 |
785 | });
786 |
787 | });
788 |
789 |
790 | describe('MatSelectSearchComponent with default options', () => {
791 | let component: MatSelectSearchTestComponent;
792 | let fixture: ComponentFixture;
793 |
794 | beforeEach(waitForAsync(() => {
795 | TestBed.configureTestingModule({
796 | imports: [
797 | CommonModule,
798 | NoopAnimationsModule,
799 | ReactiveFormsModule,
800 | MatFormFieldModule,
801 | MatSelectModule,
802 | NgxMatSelectSearchModule,
803 | MatSelectSearchTestComponent
804 | ],
805 | declarations: [],
806 | providers: [
807 | {
808 | provide: LiveAnnouncer,
809 | useValue: {
810 | announce: jasmine.createSpy('announce')
811 | }
812 | },
813 | {
814 | provide: MAT_SELECTSEARCH_DEFAULT_OPTIONS,
815 | useValue: {
816 | placeholderLabel: 'Mega bla',
817 | } as MatSelectSearchOptions,
818 | },
819 | ]
820 | })
821 | .compileComponents();
822 | }));
823 |
824 |
825 | beforeEach(() => {
826 | fixture = TestBed.createComponent(MatSelectSearchTestComponent);
827 | component = fixture.componentInstance;
828 | fixture.detectChanges();
829 | });
830 |
831 |
832 | it('should create', () => {
833 | expect(component).toBeTruthy();
834 | });
835 |
836 | it('should show a search field and focus it when opening the select', (done) => {
837 |
838 | component.filteredBanks
839 | .pipe(
840 | take(1),
841 | delay(1)
842 | )
843 | .subscribe(() => {
844 | // when the filtered banks are initialized
845 | fixture.detectChanges();
846 |
847 | component.matSelect.open();
848 | fixture.detectChanges();
849 |
850 | component.matSelect.openedChange
851 | .pipe(
852 | take(1),
853 | delay(1)
854 | )
855 | .subscribe(() => {
856 | const searchField = document.querySelector('.mat-select-search-inner .mat-select-search-input') as HTMLInputElement;
857 |
858 | expect(searchField.placeholder).toBe('Mega bla');
859 | done();
860 | });
861 |
862 | });
863 |
864 | });
865 |
866 | });
867 |
--------------------------------------------------------------------------------