├── 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 | 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 | 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 |

Integration with ng-mat-select-infinite-scroll 2 |

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 | 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 |
16 | 17 |
18 | @if(_isToggleAllCheckboxVisible()) 19 | { 20 | 28 | } 29 | 30 | 41 | @if(searching) 42 | { 43 | 45 | } 46 | 47 | @if(!hideClearSearchButton && value && !searching) 48 | { 49 | 68 | } 69 | 70 | 71 | 72 |
73 | 74 | 75 |
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 | [![npm version](https://img.shields.io/npm/v/ngx-mat-select-search.svg?style=flat-square)](https://www.npmjs.com/package/ngx-mat-select-search) 5 | [![npm downloads total](https://img.shields.io/npm/dt/ngx-mat-select-search.svg?style=flat-square)](https://www.npmjs.com/package/ngx-mat-select-search) 6 | [![npm downloads monthly](https://img.shields.io/npm/dm/ngx-mat-select-search.svg?style=flat-square)](https://www.npmjs.com/package/ngx-mat-select-search) 7 | [![CircleCI](https://circleci.com/gh/bithost-gmbh/ngx-mat-select-search.svg?style=svg)](https://circleci.com/gh/bithost-gmbh/ngx-mat-select-search) 8 | [![Donate](https://img.shields.io/badge/Donate-PayPal-yellow.svg?style=flat-square)](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 | Example 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 | [![PayPal](https://www.paypalobjects.com/en_US/CH/i/btn/btn_donateCC_LG.gif)](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 | 44 | 45 | 46 | 53 | 60 | 61 | 62 |
47 | 48 | pschulzk 49 |
50 | Philip Viktor Schulz-Klingauf 51 |
52 |
54 | 55 | salvatoreb98 56 |
57 | Salvatore Butera 58 |
59 |
63 | 64 | 65 | ### Collaborators 66 | 67 | 68 | 69 | 70 | 71 |
72 | 73 | 74 | ### Contributors 75 | 76 | Thank you very much to all our community contributors! 77 | 78 | 79 | 80 | 81 | 82 | 89 | 96 | 103 | 110 | 117 | 124 | 125 | 126 | 133 | 140 | 147 | 154 | 161 | 168 | 169 | 170 | 177 | 184 | 191 | 198 | 205 | 212 | 213 | 214 | 221 | 228 | 235 | 242 | 249 | 256 | 257 | 258 | 265 | 272 | 279 | 286 | 293 | 300 | 301 | 302 | 309 | 316 | 323 | 330 | 337 | 344 | 345 | 346 | 353 | 360 | 367 | 374 | 381 | 388 | 389 | 390 |
83 | 84 | macjohnny 85 |
86 | Esteban Gehring 87 |
88 |
90 | 91 | maechler 92 |
93 | Markus Mächler 94 |
95 |
97 | 98 | swierzbicki 99 |
100 | Sebastian Wierzbicki 101 |
102 |
104 | 105 | angelaki 106 |
107 | Tristan 108 |
109 |
111 | 112 | sirh3e 113 |
114 | Sirh3e 115 |
116 |
118 | 119 | tonyholt 120 |
121 | Tony H 122 |
123 |
127 | 128 | melroy89 129 |
130 | Melroy Van Den Berg 131 |
132 |
134 | 135 | mstawick 136 |
137 | Michał Stawicki 138 |
139 |
141 | 142 | AleixFerreCP 143 |
144 | Aleix Ferré 145 |
146 |
148 | 149 | alexandrupaul7 150 |
151 | Null 152 |
153 |
155 | 156 | blazewalker59 157 |
158 | Blaze Walker 159 |
160 |
162 | 163 | achilehero 164 |
165 | Cristian Raducanu 166 |
167 |
171 | 172 | damianmigo 173 |
174 | Damian Miranda 175 |
176 |
178 | 179 | Danevandy99 180 |
181 | Dane Vanderbilt 182 |
183 |
185 | 186 | davidsansome 187 |
188 | David Sansome 189 |
190 |
192 | 193 | escheiermann 194 |
195 | Edgar Scheiermann 196 |
197 |
199 | 200 | arucar 201 |
202 | Erendis 203 |
204 |
206 | 207 | GipHub123 208 |
209 | Null 210 |
211 |
215 | 216 | gustavovitor 217 |
218 | Gustavo Miranda 219 |
220 |
222 | 223 | meta72 224 |
225 | Henno Lauinger 226 |
227 |
229 | 230 | himanshu-singh1995 231 |
232 | Null 233 |
234 |
236 | 237 | iblislin 238 |
239 | Iblis Lin 240 |
241 |
243 | 244 | jfcere 245 |
246 | Jean-Francois Cere 247 |
248 |
250 | 251 | josephdecock 252 |
253 | Joe DeCock 254 |
255 |
259 | 260 | JomalJohny 261 |
262 | Jomal Johny 263 |
264 |
266 | 267 | bulldog98 268 |
269 | Jonathan Kolberg 270 |
271 |
273 | 274 | KristofGilis 275 |
276 | Kristof Gilis 277 |
278 |
280 | 281 | AhsanAyaz 282 |
283 | Muhammad Ahsan Ayaz 284 |
285 |
287 | 288 | NachmanBerkowitz 289 |
290 | Nachman Aryeh Berkowitz 291 |
292 |
294 | 295 | OvidijusStukas 296 |
297 | Ovidijus Stukas 298 |
299 |
303 | 304 | raysuelzer 305 |
306 | Ray Suelzer 307 |
308 |
310 | 311 | probert94 312 |
313 | Robert Pattis 314 |
315 |
317 | 318 | broekema41 319 |
320 | Roland Broekema 321 |
322 |
324 | 325 | shenay-aydan 326 |
327 | Null 328 |
329 |
331 | 332 | framasev 333 |
334 | Stas Amasev 335 |
336 |
338 | 339 | nischi 340 |
341 | Thierry Nischelwitzer 342 |
343 |
347 | 348 | vlio20 349 |
350 | Vlad Ioffe 351 |
352 |
354 | 355 | WX9yMOXWId 356 |
357 | Null 358 |
359 |
361 | 362 | zpaynter 363 |
364 | Null 365 |
366 |
368 | 369 | evoltafreak 370 |
371 | Joshua 372 |
373 |
375 | 376 | lorenzbaier 377 |
378 | Null 379 |
380 |
382 | 383 | ruekart 384 |
385 | Null 386 |
387 |
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 |
something special
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 |
    88 |
  • {{bank.name}}
  • 89 |
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 | --------------------------------------------------------------------------------