├── projects ├── demo │ ├── src │ │ ├── assets │ │ │ ├── .gitkeep │ │ │ └── back.jpg │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── app │ │ │ ├── app.component.scss │ │ │ ├── app.module.ts │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ └── app.component.html │ │ ├── main.ts │ │ ├── 404.html │ │ ├── test.ts │ │ ├── index.html │ │ ├── styles.scss │ │ └── polyfills.ts │ ├── e2e │ │ ├── tsconfig.json │ │ ├── src │ │ │ ├── app.po.ts │ │ │ └── app.e2e-spec.ts │ │ └── protractor.conf.js │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ ├── .browserslistrc │ ├── karma.conf.js │ └── tslint.json └── ngx-select-dropdown │ ├── ng-package.json │ ├── tslint.json │ ├── tsconfig.spec.json │ ├── src │ ├── public-api.ts │ ├── lib │ │ ├── pipes │ │ │ ├── limit-to.pipe.ts │ │ │ ├── filter-by.pipe.ts │ │ │ ├── limit-to.pipe.spec.ts │ │ │ └── filter-by.pipe.spec.ts │ │ ├── types │ │ │ └── ngx-select-dropdown.types.ts │ │ ├── ngx-select-dropdown.module.ts │ │ ├── ngx-select-dropdown.service.ts │ │ ├── ngx-select-dropdown.service.spec.ts │ │ ├── ngx-select-dropdown.component.html │ │ ├── ngx-select-dropdown.component.scss │ │ ├── ngx-select-dropdown.component.spec.ts │ │ └── ngx-select-dropdown.component.ts │ └── test.ts │ ├── .npmignore │ ├── tsconfig.lib.json │ ├── License │ ├── karma.conf.js │ ├── package.json │ └── README.md ├── .editorconfig ├── .travis.yml ├── .npmignore ├── .eslintrc.json ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── pull_request_template.md ├── .gitignore ├── tsconfig.json ├── License ├── tslint.json ├── package.json ├── angular.json ├── CONTRIBUTING.md └── README.md /projects/demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishjanky/ngx-select-dropdown/HEAD/projects/demo/src/favicon.ico -------------------------------------------------------------------------------- /projects/demo/src/assets/back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishjanky/ngx-select-dropdown/HEAD/projects/demo/src/assets/back.jpg -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-select-dropdown", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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 | -------------------------------------------------------------------------------- /projects/demo/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/demo/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .credits{ 2 | padding-top:1rem; 3 | } 4 | .nav-tabs{ 5 | margin-bottom:0; 6 | } 7 | .content{ 8 | padding:25px; 9 | } 10 | .white{ 11 | background-color: rgba(255,255,255,0.7) !important; 12 | } 13 | .tabs{ 14 | background-color: transparent; 15 | } 16 | 17 | .header{ 18 | padding:10px; 19 | } -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "ngx", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "ngx", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | addons: 4 | chrome: stable 5 | language: node_js 6 | node_js: 7 | - 12.14.0 8 | script: 9 | - npm run ci 10 | before_script: 11 | - export DISPLAY=:99.0 12 | - sh -e /etc/init.d/xvfb start 13 | - sleep 3 14 | cache: 15 | yarn: false 16 | notifications: 17 | email: false 18 | after_success: 19 | - npm run codecov 20 | -------------------------------------------------------------------------------- /projects/demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-select-dropdown 3 | */ 4 | 5 | export * from './lib/ngx-select-dropdown.service'; 6 | export * from './lib/ngx-select-dropdown.component'; 7 | export * from './lib/types/ngx-select-dropdown.types'; 8 | export * from './lib/ngx-select-dropdown.module'; 9 | export * from './lib/pipes/filter-by.pipe'; 10 | export * from './lib/pipes/limit-to.pipe'; -------------------------------------------------------------------------------- /projects/demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/lib/pipes/limit-to.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'limitTo' 5 | }) 6 | export class LimitToPipe implements PipeTransform { 7 | public transform(array: any[], itemsCount: number, startIndex = 0) { 8 | if (!Array.isArray(array) || itemsCount === 0) { 9 | return array; 10 | } 11 | return array.slice(startIndex, startIndex + itemsCount); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node 2 | /node_modules 3 | npm-debug.log 4 | 5 | # Yarn 6 | yarn-error.log 7 | 8 | # JetBrains 9 | .idea/ 10 | 11 | # VS Code 12 | .vscode/ 13 | 14 | # Windows 15 | Thumbs.db 16 | Desktop.ini 17 | 18 | # Mac 19 | .DS_Store 20 | 21 | # Temporary files 22 | coverage/ 23 | docs 24 | tmp 25 | demo 26 | documentation 27 | 28 | # Library files 29 | src/ 30 | 31 | webpack* 32 | ts* 33 | karma* 34 | .travis* 35 | gulpfile* 36 | yarn* 37 | .editorconfig 38 | CONTRIBUTING* -------------------------------------------------------------------------------- /projects/demo/.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'. -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/.npmignore: -------------------------------------------------------------------------------- 1 | # Node 2 | /node_modules 3 | npm-debug.log 4 | 5 | # Yarn 6 | yarn-error.log 7 | 8 | # JetBrains 9 | .idea/ 10 | 11 | # VS Code 12 | .vscode/ 13 | 14 | # Windows 15 | Thumbs.db 16 | Desktop.ini 17 | 18 | # Mac 19 | .DS_Store 20 | 21 | # Temporary files 22 | coverage/ 23 | docs 24 | tmp 25 | demo 26 | documentation 27 | 28 | # Library files 29 | src/ 30 | 31 | webpack* 32 | ts* 33 | karma* 34 | .travis* 35 | gulpfile* 36 | yarn* 37 | .editorconfig 38 | CONTRIBUTING* -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/no-explicit-any": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/no-explicit-any": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/lib/types/ngx-select-dropdown.types.ts: -------------------------------------------------------------------------------- 1 | export interface NgxDropdownConfig { 2 | displayFn?: any; 3 | displayKey?: string; 4 | search?: boolean; 5 | height?: string; 6 | placeholder?: string; 7 | customComparator?: (a: any, b: any) => number; 8 | limitTo?: number; 9 | moreText?: string; 10 | noResultsFound?: string; 11 | searchPlaceholder?: string; 12 | searchOnKey?: string; 13 | clearOnSelection?: boolean; 14 | inputDirection?: string; 15 | selectAllLabel?: string; 16 | enableSelectAll?: boolean; 17 | } 18 | -------------------------------------------------------------------------------- /projects/demo/src/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 2 | import { SelectDropDownModule } from 'ngx-select-dropdown'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { NgModule } from '@angular/core'; 5 | 6 | import { AppComponent } from './app.component'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | AppComponent 11 | ], 12 | imports: [ 13 | BrowserModule, FormsModule, ReactiveFormsModule, SelectDropDownModule 14 | ], 15 | providers: [], 16 | bootstrap: [AppComponent] 17 | }) 18 | export class AppModule { } 19 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/lib/ngx-select-dropdown.module.ts: -------------------------------------------------------------------------------- 1 | import { FormsModule } from "@angular/forms"; 2 | import { CommonModule } from "@angular/common"; 3 | import { NgModule } from "@angular/core"; 4 | import { NgxSelectDropdownComponent } from "./ngx-select-dropdown.component"; 5 | import { FilterByPipe } from "./pipes/filter-by.pipe"; 6 | import { LimitToPipe } from "./pipes/limit-to.pipe"; 7 | @NgModule({ 8 | declarations: [NgxSelectDropdownComponent, FilterByPipe, LimitToPipe], 9 | imports: [CommonModule, FormsModule], 10 | exports: [NgxSelectDropdownComponent, FilterByPipe, LimitToPipe], 11 | providers: [], 12 | }) 13 | export class SelectDropDownModule { 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": [ 10 | "dom", 11 | "es2018" 12 | ] 13 | }, 14 | "angularCompilerOptions": { 15 | "annotateForClosureCompiler": true, 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "fullTemplateTypeCheck": true, 19 | "strictInjectionParameters": true, 20 | "enableResourceInlining": true 21 | }, 22 | "exclude": [ 23 | "src/test.ts", 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: Manish Kumar 7 | 8 | --- 9 | 10 | #### Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | #### Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | #### Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | #### Additional context 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /projects/demo/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('demo app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/demo/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/demo/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 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting(), { 16 | teardown: { destroyAfterEach: false } 17 | } 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/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'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: any; 12 | 13 | // First, initialize the Angular testing environment. 14 | getTestBed().initTestEnvironment( 15 | BrowserDynamicTestingModule, 16 | platformBrowserDynamicTesting(), { 17 | teardown: { destroyAfterEach: false } 18 | } 19 | ); 20 | // Then we find all the tests. 21 | const context = require.context('./', true, /\.spec\.ts$/); 22 | // And load the modules. 23 | context.keys().map(context); 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.angular/cache 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "skipLibCheck": true, 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "module": "es2020", 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "target": "es2020", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2018", 20 | "dom" 21 | ], 22 | "paths": { 23 | "ngx-select-dropdown": [ 24 | "dist/ngx-select-dropdown" 25 | ], 26 | "ngx-select-dropdown/*": [ 27 | "dist/ngx-select-dropdown/*" 28 | ] 29 | } 30 | }, 31 | "angularCompilerOptions": { 32 | "enableIvy": true, 33 | "compilationMode": "partial", 34 | "fullTemplateTypeCheck": true, 35 | "strictInjectionParameters": true 36 | } 37 | } -------------------------------------------------------------------------------- /projects/demo/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /projects/demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ngx-select-dropdown 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | #### Bug description 12 | 13 | 14 | #### Expected result 15 | 16 | 17 | #### Actual result 18 | 19 | 20 | #### Steps to reproduce 21 | 22 | 23 | 1. 24 | 2. 25 | 3. 26 | #### Context 27 | 28 | 29 | 30 | #### Your Environment 31 | 32 | 33 | * Version used: 34 | * Browser Name and version: 35 | * Operating System and version: 36 | * Link to your project: 37 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/lib/ngx-select-dropdown.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from "@angular/core"; 2 | 3 | @Injectable({ 4 | providedIn: "root", 5 | }) 6 | export class SelectDropDownService { 7 | openDropdownInstance = new EventEmitter(); 8 | closeDropdownInstance = new EventEmitter(); 9 | openInstances: string[] = []; 10 | constructor() { 11 | // constructor 12 | } 13 | public isOpen(instanceId): boolean { 14 | return this.openInstances.indexOf(instanceId) > -1; 15 | } 16 | /** 17 | * @summary: Open a specific dropdown instance based on the instance ID. 18 | * @param instanceId: Instance id of the dropdown that must be opened. 19 | */ 20 | openDropdown(instanceId: string) { 21 | this.openDropdownInstance.emit(instanceId); 22 | } 23 | /** 24 | * @summary: Close a specific dropdown instance based on the instance ID. 25 | * @param instanceId: Instance id of the dropdown that must be closed. 26 | */ 27 | closeDropdown(instanceId: string) { 28 | this.closeDropdownInstance.emit(instanceId); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | #### Changes 2 | 3 | 4 | 5 | 6 | Fixes # (issue) 7 | 8 | #### Type of change 9 | 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | #### Checklist 17 | 18 | - [ ] My code follows the style guidelines of this project 19 | - [ ] I have performed a self-review of my own code 20 | - [ ] I have commented my code, particularly in hard-to-understand areas 21 | - [ ] I have made corresponding changes to the documentation 22 | - [ ] My changes generate no new warnings 23 | - [ ] My changes do not reduce the code coverage percentage. Approproiate test cases have been added for the new code. 24 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | xdescribe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'demo'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('demo'); 23 | }); 24 | 25 | it('should render title', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('.content span').textContent).toContain('demo app is running!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Manish Kumar 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 | -------------------------------------------------------------------------------- /projects/demo/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/demo'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/demo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended"], 3 | "rulesDirectory": ["./../../node_modules/codelyzer"], 4 | "rules": { 5 | "component-class-suffix": false, 6 | "directive-class-suffix": false, 7 | "interface-name": false, 8 | "invoke-injectable": false, 9 | "max-line-length": [false, 100], 10 | "no-access-missing-member": false, 11 | "no-attribute-parameter-decorator": false, 12 | "no-console": [false, "time", "timeEnd", "trace"], 13 | "no-forward-ref": false, 14 | "no-input-rename": false, 15 | "no-output-rename": false, 16 | "no-string-literal": false, 17 | "object-literal-sort-keys": false, 18 | "ordered-imports": false, 19 | "pipe-naming": [false, "camelCase", "my"], 20 | "quotemark": [false, "single", "avoid-escape"], 21 | "trailing-comma": [false, {"multiline": "always", "singleline": "never"}], 22 | "use-host-property-decorator": false, 23 | "use-input-property-decorator": false, 24 | "use-output-property-decorator": false, 25 | "use-pipe-transform-interface": false, 26 | "variable-name": [false, "allow-leading-underscore", "ban-keywords", "check-format"] 27 | } 28 | } -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Manish Kumar 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 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/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: process.cwd(), 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-coverage'), 11 | require('karma-chrome-launcher'), 12 | require('karma-jasmine-html-reporter'), 13 | require('karma-coverage-istanbul-reporter'), 14 | require('@angular-devkit/build-angular/plugins/karma') 15 | ], 16 | client: { 17 | clearContext: false // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | coverageIstanbulReporter: { 20 | dir: '/coverage', 21 | reports: ['html', 'lcovonly', 'text-summary'], 22 | fixWebpackSourcePaths: true 23 | }, 24 | reporters: ['progress', 'kjhtml', 'coverage'], 25 | port: 9876, 26 | colors: true, 27 | logLevel: config.LOG_INFO, 28 | autoWatch: false, 29 | browsers: ['Chrome'], 30 | singleRun: true, 31 | restartOnFileChange: true 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /projects/demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | html,body{ 3 | height: 100%; 4 | width: 100%; 5 | margin: 0; 6 | } 7 | body{ 8 | background-image: url(assets/back.jpg); 9 | background-size: 100% 100%; 10 | background-repeat: no-repeat; 11 | background-attachment: fixed; 12 | } 13 | .white{ 14 | background-color: #fff !important; 15 | } 16 | .container{ 17 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2); 18 | } 19 | h3{ 20 | font-family: 'Roboto', sans-serif; 21 | font-weight: 500; 22 | } 23 | 24 | .codeBlock{ 25 | box-sizing: border-box; 26 | -moz-box-sizing: border-box; 27 | padding: 2em; 28 | margin:15px; 29 | background: #e6eff1; 30 | box-shadow: 0px 0px 20px rgba(0,0,0,0.0); 31 | white-space: pre-wrap; 32 | } 33 | 34 | .codeBlock:hover{ 35 | box-shadow: 5px 5px 20px #e6eff1; 36 | background:rgb(158, 226, 235); 37 | -webkit-transition: all .5s ease-out; 38 | -moz-transition: all .5s ease-out ; 39 | -ms-transition: all .5s ease-out ; 40 | -o-transition: all .5s ease-out ; 41 | transition: all .5s ease-out ; 42 | 43 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended"], 3 | "rulesDirectory": ["node_modules/codelyzer"], 4 | "rules": { 5 | "component-class-suffix": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "directive-class-suffix": false, 10 | "interface-name": false, 11 | "invoke-injectable": false, 12 | "max-line-length": [false, 200], 13 | "no-access-missing-member": false, 14 | "no-attribute-parameter-decorator": false, 15 | "no-console": [false, "time", "timeEnd", "trace"], 16 | "no-forward-ref": false, 17 | "no-input-rename": false, 18 | "no-output-rename": false, 19 | "no-string-literal": false, 20 | "object-literal-sort-keys": false, 21 | "ordered-imports": false, 22 | "pipe-naming": [false, "camelCase", "my"], 23 | "quotemark": [false, "single", "avoid-escape"], 24 | "trailing-comma": [false, {"multiline": "always", "singleline": "never"}], 25 | "use-host-property-decorator": false, 26 | "use-input-property-decorator": false, 27 | "use-output-property-decorator": false, 28 | "use-pipe-transform-interface": false, 29 | "variable-name": [false, "allow-leading-underscore", "ban-keywords", "check-format"], 30 | "member-access": false 31 | } 32 | } -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-select-dropdown", 3 | "version": "3.3.2", 4 | "description": "A angular(4+) select dropdown for single select or multiselct module.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "angular", 8 | "angular-2", 9 | "angular-4", 10 | "angular2", 11 | "angular4", 12 | "ng2", 13 | "ng4", 14 | "ngx", 15 | "select", 16 | "dropdown", 17 | "multiselect", 18 | "multi-select", 19 | "dropdown", 20 | "custom", 21 | "combobox", 22 | "ng-select", 23 | "ngx-select", 24 | "ng2-select", 25 | "ng-dropdown", 26 | "ngx-dropdown", 27 | "ng2-dropdown" 28 | ], 29 | "scripts": { 30 | "postversion": "git push && git push --tags" 31 | }, 32 | "author": { 33 | "name": "Manish Kumar", 34 | "email": "manishjanky@gmail.com" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/manishjanky/ngx-select-dropdown.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/manishjanky/ngx-select-dropdown/issues" 42 | }, 43 | "homepage": "https://github.com/manishjanky/ngx-select-dropdown#readme", 44 | "dependencies": {}, 45 | "peerDependencies": { 46 | "@angular/common": ">=4.0.0", 47 | "@angular/core": ">=4.0.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/lib/ngx-select-dropdown.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { SelectDropDownService } from './ngx-select-dropdown.service'; 4 | 5 | describe('SelectDropDownService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: SelectDropDownService = TestBed.inject(SelectDropDownService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | it('should emit open instance', () => { 13 | const service: SelectDropDownService = TestBed.inject(SelectDropDownService); 14 | spyOn(service.openDropdownInstance, 'emit'); 15 | service.openDropdown('123'); 16 | expect(service.openDropdownInstance.emit).toHaveBeenCalledWith('123'); 17 | }); 18 | 19 | it('should emit close instance', () => { 20 | const service: SelectDropDownService = TestBed.inject(SelectDropDownService); 21 | spyOn(service.closeDropdownInstance, 'emit'); 22 | service.closeDropdown('123'); 23 | expect(service.closeDropdownInstance.emit).toHaveBeenCalledWith('123'); 24 | }); 25 | 26 | it('should result is open', () => { 27 | const service: SelectDropDownService = TestBed.inject(SelectDropDownService); 28 | service.openInstances = ['123']; 29 | service.isOpen('123'); 30 | expect(service.isOpen('123')).toBeTruthy(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/lib/pipes/filter-by.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | /** 4 | * filters an array based on searctext 5 | */ 6 | @Pipe({ 7 | name: 'filterBy' 8 | }) 9 | export class FilterByPipe implements PipeTransform { 10 | public transform(array: any[], searchText?: string, keyName?: string) { 11 | if (!array || !searchText || !Array.isArray(array)) { 12 | return array; 13 | } 14 | if (typeof array[0] === 'string') { 15 | return array.filter((item) => item.toLowerCase().indexOf(searchText.trim().toLowerCase()) > -1); 16 | } 17 | // filter array, items which match and return true will be 18 | // kept, false will be filtered out 19 | if (!keyName) { 20 | return array.filter((item: any) => { 21 | for (const key in item) { 22 | if (typeof item[key] !== 'object' && item[key].toString().toLowerCase().indexOf(searchText.trim().toLowerCase()) > -1) { 23 | return true; 24 | } 25 | } 26 | return false; 27 | }); 28 | } else { 29 | return array.filter((item: any) => { 30 | if (typeof item[keyName] !== 'object' && item[keyName].toString().toLowerCase().indexOf(searchText.trim().toLowerCase()) > -1) { 31 | return true; 32 | } 33 | return false; 34 | }); 35 | } 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /projects/demo/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 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags.ts'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 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__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-select-dropdown", 3 | "version": "3.3.2", 4 | "description": "A angular(4+) select dropdown for single select or multiselct module.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "angular", 8 | "angular-2", 9 | "angular-4", 10 | "angular2", 11 | "angular4", 12 | "ng2", 13 | "ng4", 14 | "ngx", 15 | "select", 16 | "dropdown", 17 | "multiselect", 18 | "multi-select", 19 | "dropdown", 20 | "custom", 21 | "combobox", 22 | "ng-select", 23 | "ngx-select", 24 | "ng2-select", 25 | "ng-dropdown", 26 | "ngx-dropdown", 27 | "ng2-dropdown" 28 | ], 29 | "author": { 30 | "name": "Manish Kumar", 31 | "email": "manishjanky@gmail.com" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/manishjanky/ngx-select-dropdown.git" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/manishjanky/ngx-select-dropdown/issues" 39 | }, 40 | "homepage": "https://github.com/manishjanky/ngx-select-dropdown#readme", 41 | "scripts": { 42 | "ng": "ng", 43 | "start": "ng serve", 44 | "watch": "ng build ngx-select-dropdown --watch", 45 | "build": "ng build ngx-select-dropdown --configuration production", 46 | "test": "ng test ngx-select-dropdown", 47 | "lint": "ng lint ngx-select-dropdown", 48 | "e2e": "ng e2e", 49 | "publish": "ng build ngx-select-dropdown --configuration production && cd dist/ngx-select-dropdown && npm publish", 50 | "ci": "npm run lint && npm run test && npm run build", 51 | "postversion": "git push && git push --tags" 52 | }, 53 | "private": false, 54 | "dependencies": { 55 | "@angular/animations": "^14.0.5", 56 | "@angular/common": "^14.0.5", 57 | "@angular/compiler": "^14.0.5", 58 | "@angular/core": "^14.0.5", 59 | "@angular/forms": "^14.0.5", 60 | "@angular/platform-browser": "^14.0.5", 61 | "@angular/platform-browser-dynamic": "^14.0.5", 62 | "@angular/router": "^14.0.5", 63 | "jquery": "^3.6.0", 64 | "karma-coverage": "^2.2.0", 65 | "materialize-css": "^1.0.0", 66 | "rxjs": "~6.6.7", 67 | "tslib": "^2.0.0", 68 | "zone.js": "~0.11.4" 69 | }, 70 | "devDependencies": { 71 | "@angular-devkit/build-angular": "^14.0.5", 72 | "@angular-devkit/build-ng-packagr": "^0.1002.4", 73 | "@angular-eslint/builder": "^14.1.2", 74 | "@angular-eslint/eslint-plugin": "^14.1.2", 75 | "@angular-eslint/eslint-plugin-template": "^14.1.2", 76 | "@angular-eslint/schematics": "^14.1.2", 77 | "@angular-eslint/template-parser": "^14.1.2", 78 | "@angular/cli": "^14.0.5", 79 | "@angular/compiler-cli": "^14.0.5", 80 | "@angular/language-service": "^14.0.5", 81 | "@types/jasmine": "~3.3.8", 82 | "@types/jasminewd2": "~2.0.3", 83 | "@types/node": "^8.9.5", 84 | "@typescript-eslint/eslint-plugin": "^5.30.5", 85 | "@typescript-eslint/parser": "^5.30.5", 86 | "codelyzer": "^5.0.0", 87 | "eslint": "^8.18.0", 88 | "jasmine-core": "~3.5.0", 89 | "jasmine-spec-reporter": "~5.0.0", 90 | "karma": "~6.4.0", 91 | "karma-chrome-launcher": "~3.1.0", 92 | "karma-coverage-istanbul-reporter": "~3.0.2", 93 | "karma-jasmine": "~4.0.0", 94 | "karma-jasmine-html-reporter": "^1.5.0", 95 | "ng-packagr": "^14.0.3", 96 | "protractor": "~7.0.0", 97 | "ts-node": "^7.0.0", 98 | "tsickle": "~0.39.1", 99 | "tslint": "^6.1.0", 100 | "typescript": "^4.6.4" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { SelectDropDownService } from "ngx-select-dropdown"; 2 | import { OnInit } from "@angular/core"; 3 | import { Component } from "@angular/core"; 4 | import { 5 | UntypedFormBuilder, 6 | UntypedFormGroup, 7 | Validators, 8 | } from "@angular/forms"; 9 | 10 | @Component({ 11 | selector: "app-root", 12 | templateUrl: "./app.component.html", 13 | styleUrls: ["./app.component.scss"], 14 | }) 15 | export class AppComponent implements OnInit { 16 | title = "app"; 17 | tab = 1; 18 | singleSelect: any = []; 19 | multiSelect: any = []; 20 | stringArray: any = []; 21 | objectsArray: any = []; 22 | optTemplate: any = []; 23 | stringOptions = [ 24 | "Burns Dalton", 25 | "Mcintyre Lawson", 26 | "Amie Franklin", 27 | "Jocelyn Horton", 28 | "Fischer Erickson", 29 | "Medina Underwood", 30 | "Goldie Barber", 31 | ]; 32 | config = { 33 | displayKey: "name", // if objects array passed which key to be displayed defaults to description 34 | search: true, 35 | limitTo: 0, 36 | height: "250px", 37 | enableSelectAll: true, 38 | }; 39 | selectedOptions = [ 40 | { 41 | _id: "5a66d6c31d5e4e36c7711b7a", 42 | index: 0, 43 | balance: "$2,806.37", 44 | picture: "http://placehold.it/32x32", 45 | name: "Burns Dalton", 46 | }, 47 | { 48 | _id: "5a66d6c3657e60c6073a2d22", 49 | index: 1, 50 | balance: "$2,984.98", 51 | picture: "http://placehold.it/32x32", 52 | name: "Mcintyre Lawson", 53 | }, 54 | ]; 55 | options = [ 56 | { 57 | _id: "5a66d6c31d5e4e36c7711b7a", 58 | index: 0, 59 | balance: "$2,806.37", 60 | picture: "http://placehold.it/32x32", 61 | name: "Burns Dalton", 62 | }, 63 | { 64 | _id: "5a66d6c3657e60c6073a2d22", 65 | index: 1, 66 | balance: "$2,984.98", 67 | picture: "http://placehold.it/32x32", 68 | name: "Mcintyre Lawson", 69 | }, 70 | { 71 | _id: "5a66d6c376be165a5a7fae33", 72 | index: 2, 73 | balance: "$2,794.16", 74 | picture: "http://placehold.it/32x32", 75 | name: "Amie Franklin", 76 | }, 77 | { 78 | _id: "5a66d6c3f7854b6b4d96333b", 79 | index: 3, 80 | balance: "$2,537.14", 81 | picture: "http://placehold.it/32x32", 82 | name: "Jocelyn Horton", 83 | }, 84 | { 85 | _id: "5a66d6c31f967d4f3e9d84e9", 86 | index: 4, 87 | balance: "$2,141.42", 88 | picture: "http://placehold.it/32x32", 89 | name: "Fischer Erickson", 90 | }, 91 | { 92 | _id: "5a66d6c34cfa8cddefb31602", 93 | index: 5, 94 | balance: "$1,398.60", 95 | picture: "http://placehold.it/32x32", 96 | name: "Medina Underwood", 97 | }, 98 | { 99 | _id: "5a66d6c3d727c450794226de", 100 | index: 6, 101 | balance: "$3,915.65", 102 | picture: "http://placehold.it/32x32", 103 | name: "Goldie Barber", 104 | }, 105 | ]; 106 | resetOption: any; 107 | selectForm: UntypedFormGroup; 108 | constructor( 109 | private fromBuilder: UntypedFormBuilder, 110 | private drodownService: SelectDropDownService 111 | ) { 112 | // this.selectForm = this.fromBuilder.group({ 113 | // selectDrop: [null, Validators.required] 114 | // }); 115 | } 116 | ngOnInit() { 117 | this.resetOption = [this.options[0]]; 118 | } 119 | changeValue($event: any) { 120 | // console.log(this.selectForm.getRawValue()); 121 | } 122 | 123 | searchChange($event) { 124 | console.log($event); 125 | } 126 | 127 | reset() { 128 | this.resetOption = []; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/lib/ngx-select-dropdown.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 |
10 |
11 | 13 | 16 |
17 |
18 |
19 | 23 |
24 |
25 |
26 |
28 | 30 | 31 |
32 |
33 |
34 |
35 |
44 | 46 |
47 | 48 |
49 |
50 |
51 | 52 | 53 |
{{ config.noResultsFound }}
54 |
55 | 56 | 57 | 58 | {{ 59 | config.displayFn 60 | ? config.displayFn(item) 61 | : item[config.displayKey] || item 62 | }} 63 | 64 | 65 | 66 | x 67 | 68 | {{ 69 | config.displayFn 70 | ? config.displayFn(item) 71 | : item[config.displayKey] || item 72 | }} 73 | 74 | 75 | 76 | 77 | 82 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/lib/ngx-select-dropdown.component.scss: -------------------------------------------------------------------------------- 1 | .ngx-dropdown-container { 2 | width: 100%; 3 | position: relative; 4 | 5 | .ngx-dropdown-button { 6 | display: inline-block; 7 | margin-bottom: 0; 8 | font-weight: 400; 9 | line-height: 1.42857143; 10 | vertical-align: middle; 11 | touch-action: manipulation; 12 | cursor: pointer; 13 | user-select: none; 14 | border: 1px solid #ccc; 15 | border-radius: 4px; 16 | color: #333; 17 | background-color: #fff; 18 | white-space: nowrap; 19 | overflow-x: hidden; 20 | text-overflow: ellipsis; 21 | text-align: left; 22 | 23 | span { 24 | display: inline; 25 | vertical-align: middle; 26 | } 27 | 28 | .nsdicon-angle-down { 29 | right: 5px; 30 | 31 | &::before { 32 | border-style: solid; 33 | border-width: 0.1em 0.1em 0 0; 34 | content: ""; 35 | display: inline-block; 36 | height: 10px; 37 | position: relative; 38 | vertical-align: text-top; 39 | width: 10px; 40 | top: 0; 41 | transform: rotate(135deg); 42 | } 43 | 44 | position: relative; 45 | float: right; 46 | transition: transform 0.2s ease; 47 | 48 | &.up { 49 | transform: rotate(180deg); 50 | transition: transform 0.2s ease; 51 | } 52 | } 53 | } 54 | 55 | .ngx-dropdown-button { 56 | width: 100%; 57 | min-height: 30px; 58 | padding: 5px 10px 5px 10px; 59 | background-color: white; 60 | .display-text { 61 | display: inline-block; 62 | width: calc(100% - 20px); 63 | } 64 | } 65 | 66 | .ngx-dropdown-list-container { 67 | box-sizing: border-box; 68 | border: 1px solid rgba(0, 0, 0, 0.15); 69 | border-radius: 4px; 70 | padding-left: 10px; 71 | padding-right: 10px; 72 | z-index: 999999999; 73 | width: 100%; 74 | background-clip: padding-box; 75 | background: white; 76 | position: absolute; 77 | -webkit-box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.21); 78 | -moz-box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.21); 79 | box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.21); 80 | overflow-y: auto; 81 | 82 | .select-options { 83 | padding-top: 10px; 84 | .filled-in:checked + span:not(.lever):after, 85 | .filled-in:not(:checked) + span:not(.lever):after { 86 | height: 15px; 87 | width: 15px; 88 | } 89 | .filled-in:checked + span:not(.lever):after { 90 | border-color: #337ab7; 91 | background-color: #337ab7; 92 | } 93 | .filled-in:checked + span:not(.lever):before { 94 | top: -2px; 95 | left: 1px; 96 | width: 5px; 97 | height: 11px; 98 | } 99 | [type="checkbox"] + span:not(.lever) { 100 | line-height: 15px; 101 | height: 15px; 102 | padding-left: 25px; 103 | } 104 | } 105 | 106 | .search-container { 107 | position: relative; 108 | padding-top: 10px; 109 | margin-top: 5px; 110 | 111 | input { 112 | background-color: transparent; 113 | border: none; 114 | border-bottom: 1px solid #9e9e9e; 115 | border-radius: 0; 116 | outline: none; 117 | height: 2rem; 118 | width: 100%; 119 | font-size: 13px; 120 | margin: 0; 121 | padding: 0; 122 | box-shadow: none; 123 | box-sizing: content-box; 124 | transition: all 0.3s; 125 | 126 | &:focus { 127 | border-bottom: 1px solid #26a69a; 128 | } 129 | 130 | &:focus + label { 131 | transform: translateY(-2px) scale(0.8); 132 | transform-origin: 0 0; 133 | } 134 | } 135 | 136 | label { 137 | color: #9e9e9e; 138 | position: absolute; 139 | top: 0; 140 | left: 0; 141 | height: 100%; 142 | font-size: 1rem; 143 | cursor: text; 144 | -webkit-transition: -webkit-transform 0.2s ease-out; 145 | transition: -webkit-transform 0.2s ease-out; 146 | transition: transform 0.2s ease-out; 147 | transition: transform 0.2s ease-out, -webkit-transform 0.2s ease-out; 148 | -webkit-transform-origin: 0% 100%; 149 | transform-origin: 0% 100%; 150 | text-align: initial; 151 | transform: translateY(12px); 152 | pointer-events: none; 153 | 154 | &.active { 155 | transform: translateY(-2px) scale(0.8); 156 | transform-origin: 0 0; 157 | } 158 | } 159 | } 160 | 161 | .available-items, 162 | .selected-items { 163 | margin-top: 1rem; 164 | margin-bottom: 1rem; 165 | list-style-type: none; 166 | padding-left: 0px; 167 | 168 | &.selected-items { 169 | .selected-item { 170 | background-color: #337ab7; 171 | color: white; 172 | margin-bottom: 2px; 173 | 174 | .nsdicon-close { 175 | font-weight: bold; 176 | font-size: large; 177 | } 178 | } 179 | } 180 | 181 | &.available-items { 182 | .available-item { 183 | &.active { 184 | background-color: #337ab7; 185 | color: #ffff; 186 | } 187 | } 188 | } 189 | 190 | .available-item, 191 | .selected-item { 192 | font-size: inherit; 193 | cursor: pointer; 194 | display: block; 195 | padding: 3px 20px; 196 | clear: both; 197 | font-weight: 400; 198 | line-height: 1.42857143; 199 | color: #333; 200 | white-space: normal; 201 | } 202 | } 203 | } 204 | 205 | .ngx-disabled { 206 | pointer-events: none; 207 | background-color: #e9ecef; 208 | opacity: 1; 209 | cursor: no-drop; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-select-dropdown": { 7 | "projectType": "library", 8 | "root": "projects/ngx-select-dropdown", 9 | "sourceRoot": "projects/ngx-select-dropdown/src", 10 | "prefix": "ngx", 11 | "architect": { 12 | "lint":{ 13 | "builder": "@angular-eslint/builder:lint", 14 | "options": { 15 | "eslintConfig":".eslintrc.js", 16 | "lintFilePatterns": [ 17 | "projects/ngx-select-dropdown/src/**/*.ts" 18 | ] 19 | } 20 | }, 21 | "build": { 22 | "builder": "@angular-devkit/build-ng-packagr:build", 23 | "options": { 24 | "tsConfig": "projects/ngx-select-dropdown/tsconfig.lib.json", 25 | "project": "projects/ngx-select-dropdown/ng-package.json" 26 | }, 27 | "configurations": { 28 | "production": { 29 | } 30 | } 31 | }, 32 | "test": { 33 | "builder": "@angular-devkit/build-angular:karma", 34 | "options": { 35 | "main": "projects/ngx-select-dropdown/src/test.ts", 36 | "tsConfig": "projects/ngx-select-dropdown/tsconfig.spec.json", 37 | "karmaConfig": "projects/ngx-select-dropdown/karma.conf.js", 38 | "codeCoverage": true 39 | } 40 | } 41 | } 42 | }, 43 | "demo": { 44 | "projectType": "application", 45 | "schematics": { 46 | "@schematics/angular:component": { 47 | "style": "scss" 48 | } 49 | }, 50 | "root": "projects/demo", 51 | "sourceRoot": "projects/demo/src", 52 | "prefix": "app", 53 | "architect": { 54 | "build": { 55 | "builder": "@angular-devkit/build-angular:browser", 56 | "options": { 57 | "outputPath": "dist/demo", 58 | "index": "projects/demo/src/index.html", 59 | "main": "projects/demo/src/main.ts", 60 | "polyfills": "projects/demo/src/polyfills.ts", 61 | "tsConfig": "projects/demo/tsconfig.app.json", 62 | "aot": false, 63 | "assets": [ 64 | "projects/demo/src/favicon.ico", 65 | "projects/demo/src/assets" 66 | ], 67 | "styles": [ 68 | "projects/demo/src/styles.scss", 69 | "./node_modules/materialize-css/dist/css/materialize.css" 70 | ], 71 | "scripts": [ 72 | "./node_modules/jquery/dist/jquery.min.js", 73 | "./node_modules/materialize-css/dist/js/materialize.js" 74 | ], 75 | "vendorChunk": true, 76 | "extractLicenses": false, 77 | "buildOptimizer": false, 78 | "sourceMap": true, 79 | "optimization": false, 80 | "namedChunks": true 81 | }, 82 | "configurations": { 83 | "production": { 84 | "fileReplacements": [ 85 | { 86 | "replace": "projects/demo/src/environments/environment.ts", 87 | "with": "projects/demo/src/environments/environment.prod.ts" 88 | } 89 | ], 90 | "optimization": true, 91 | "outputHashing": "all", 92 | "sourceMap": false, 93 | "namedChunks": false, 94 | "aot": true, 95 | "extractLicenses": true, 96 | "vendorChunk": false, 97 | "buildOptimizer": true, 98 | "budgets": [ 99 | { 100 | "type": "initial", 101 | "maximumWarning": "2mb", 102 | "maximumError": "5mb" 103 | }, 104 | { 105 | "type": "anyComponentStyle", 106 | "maximumWarning": "6kb", 107 | "maximumError": "10kb" 108 | } 109 | ] 110 | } 111 | }, 112 | "defaultConfiguration": "" 113 | }, 114 | "serve": { 115 | "builder": "@angular-devkit/build-angular:dev-server", 116 | "options": { 117 | "browserTarget": "demo:build" 118 | }, 119 | "configurations": { 120 | "production": { 121 | "browserTarget": "demo:build:production" 122 | } 123 | } 124 | }, 125 | "extract-i18n": { 126 | "builder": "@angular-devkit/build-angular:extract-i18n", 127 | "options": { 128 | "browserTarget": "demo:build" 129 | } 130 | }, 131 | "test": { 132 | "builder": "@angular-devkit/build-angular:karma", 133 | "options": { 134 | "main": "projects/demo/src/test.ts", 135 | "polyfills": "projects/demo/src/polyfills.ts", 136 | "tsConfig": "projects/demo/tsconfig.spec.json", 137 | "karmaConfig": "projects/demo/karma.conf.js", 138 | "assets": [ 139 | "projects/demo/src/favicon.ico", 140 | "projects/demo/src/assets" 141 | ], 142 | "styles": [ 143 | "projects/demo/src/styles.scss" 144 | ], 145 | "scripts": [] 146 | } 147 | }, 148 | "e2e": { 149 | "builder": "@angular-devkit/build-angular:protractor", 150 | "options": { 151 | "protractorConfig": "projects/demo/e2e/protractor.conf.js", 152 | "devServerTarget": "demo:serve" 153 | }, 154 | "configurations": { 155 | "production": { 156 | "devServerTarget": "demo:serve:production" 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/lib/pipes/limit-to.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { LimitToPipe } from './limit-to.pipe'; 2 | export const testData = [ 3 | { 4 | _id: '5ab9c81febf774900a0ff849', 5 | index: 0, 6 | guid: '3ed3d5d4-7b27-4091-a207-427c196a76fc', 7 | balance: '$1,367.97', 8 | age: 28, 9 | eyeColor: 'brown', 10 | firstName: 'Bertie', 11 | lastName: 'Marks', 12 | company: 'ISOTRONIC', 13 | email: 'bertie.marks@isotronic.info', 14 | phone: '+1 (817) 557-2705', 15 | address: '853 Cook Street, Deercroft, Northern Mariana Islands, 1714', 16 | about: 'Quis aliqua et mollit mollit ad velit ut deserunt tempor sint sunt. Enim occaecat consectetur cillum ipsum cupidatat. Labore occaecat qui tempor veniam laborum deserunt. Elit magna commodo laborum voluptate incididunt ad. Sit elit enim culpa pariatur proident fugiat quis eu magna.' 17 | }, 18 | { 19 | _id: '5ab9c820de24c4ea5ff96665', 20 | index: 1, 21 | guid: 'c40f62a2-7294-4a75-a805-cb5957b55727', 22 | balance: '$2,063.42', 23 | age: 24, 24 | eyeColor: 'brown', 25 | firstName: 'Ramona', 26 | lastName: 'Walter', 27 | company: 'SOPRANO', 28 | email: 'ramona.walter@soprano.com', 29 | phone: '+1 (903) 477-2993', 30 | address: '676 Hendrix Street, Groveville, Marshall Islands, 9712', 31 | about: 'Sint quis cillum ad Lorem in nulla ipsum sunt ut ad quis ad ex. Anim do cillum culpa commodo duis ad nulla fugiat labore fugiat irure aute est elit. Mollit nisi voluptate anim consequat deserunt ullamco. Qui ex laboris sit dolor officia fugiat elit eu commodo nulla aute. Voluptate laborum labore ut tempor consectetur ipsum commodo do irure culpa officia. Nostrud cillum eu ex dolore adipisicing occaecat id nulla dolore.' 32 | }, 33 | { 34 | _id: '5ab9c820d668abc6eb572961', 35 | index: 2, 36 | guid: '6c858720-d0c2-4fba-8316-3a974a2c5ccc', 37 | balance: '$3,890.59', 38 | age: 31, 39 | eyeColor: 'brown', 40 | firstName: 'Holmes', 41 | lastName: 'Ratliff', 42 | company: 'ACLIMA', 43 | email: 'holmes.ratliff@aclima.co.uk', 44 | phone: '+1 (977) 541-2880', 45 | address: '736 Dikeman Street, Vallonia, Wyoming, 1370', 46 | about: 'Reprehenderit et sint eu sunt occaecat sint dolore minim aliqua aute enim incididunt. Labore officia qui proident esse cupidatat sint deserunt. Velit qui incididunt ullamco ullamco qui. Nostrud in sit laboris sit pariatur esse ea dolore elit enim.' 47 | }, 48 | { 49 | _id: '5ab9c820ad13b4f8707133e7', 50 | index: 3, 51 | guid: '29042a8d-f992-4ff7-8609-7cf1cb44ea0c', 52 | balance: '$1,743.95', 53 | age: 28, 54 | eyeColor: 'blue', 55 | firstName: 'Lara', 56 | lastName: 'Cox', 57 | company: 'CENTREE', 58 | email: 'lara.cox@centree.biz', 59 | phone: '+1 (894) 505-3882', 60 | address: '482 Sackman Street, Goodville, Kentucky, 3683', 61 | about: 'Mollit anim nostrud duis anim mollit reprehenderit ad velit mollit anim. Velit veniam reprehenderit minim fugiat. Aliqua est anim esse exercitation deserunt magna ad dolor minim labore. Occaecat deserunt elit occaecat irure eiusmod sit nisi ad in mollit in cupidatat. Ex consequat dolore amet laborum qui.' 62 | }, 63 | { 64 | _id: '5ab9c8203b395f39aa34997d', 65 | index: 4, 66 | guid: '72e39252-f7e7-49d0-9e3b-03ae8261bfeb', 67 | balance: '$2,630.52', 68 | age: 33, 69 | eyeColor: 'blue', 70 | firstName: 'Toni', 71 | lastName: 'Simpson', 72 | company: 'GOKO', 73 | email: 'toni.simpson@goko.io', 74 | phone: '+1 (976) 583-3167', 75 | address: '574 Prescott Place, Lowgap, Rhode Island, 3696', 76 | about: 'Nisi et magna voluptate aute dolor minim commodo laboris nisi est. Reprehenderit sunt occaecat deserunt ea fugiat dolor dolor exercitation ut. Laboris laboris amet quis consectetur eiusmod. Excepteur qui ut et magna tempor magna. Nulla adipisicing commodo mollit velit velit nisi consequat laborum voluptate.' 77 | }, 78 | { 79 | _id: '5ab9c820c181d1a8468a64ae', 80 | index: 5, 81 | guid: 'be7b9b92-d3c8-4175-a57d-d918304e410f', 82 | balance: '$1,601.57', 83 | age: 26, 84 | eyeColor: 'brown', 85 | firstName: 'Megan', 86 | lastName: 'Barr', 87 | company: 'GRAINSPOT', 88 | email: 'megan.barr@grainspot.tv', 89 | phone: '+1 (831) 576-3439', 90 | address: '707 Dewey Place, Gerber, West Virginia, 794', 91 | about: 'Officia mollit dolore occaecat occaecat. Voluptate adipisicing ad sit aliquip laborum proident aliquip nulla incididunt. Aute laboris elit nisi aute exercitation esse sit velit enim duis. Sunt fugiat ad non qui dolor Lorem.' 92 | } 93 | ]; 94 | describe('ArrayLimitToPipe', () => { 95 | it('create an instance', () => { 96 | const pipe = new LimitToPipe(); 97 | expect(pipe).toBeTruthy(); 98 | }); 99 | 100 | it('should return the array with specified limit', () => { 101 | const data = JSON.parse(JSON.stringify(testData)); 102 | const pipe = new LimitToPipe(); 103 | expect(pipe.transform(data, 5)).toEqual(data.slice(0, 5)); 104 | }); 105 | 106 | it('should return the array with specified limit', () => { 107 | const data = JSON.parse(JSON.stringify(testData)); 108 | const pipe = new LimitToPipe(); 109 | expect(pipe.transform(data, 5, 5)).toEqual(data.slice(5, 10)); 110 | }); 111 | 112 | it('should return the array invalid input', () => { 113 | const pipe = new LimitToPipe(); 114 | expect(pipe.transform('test' as any, 5)).toEqual('test' as any); 115 | }); 116 | 117 | it('should return the full array when 0 limit', () => { 118 | const data = JSON.parse(JSON.stringify(testData)); 119 | const pipe = new LimitToPipe(); 120 | expect(pipe.transform(data, 0)).toEqual(data); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing to this project 3 | 4 | Please take a moment to review this document in order to make the contribution 5 | process easy and effective for everyone involved. 6 | 7 | Following these guidelines helps to communicate that you respect the time of 8 | the developers managing and developing this open source project. In return, 9 | they should reciprocate that respect in addressing your issue or assessing 10 | patches and features. 11 | 12 | 13 | ## Using the issue tracker 14 | 15 | The issue tracker is the preferred channel for [bug reports](#bugs), 16 | [features requests](#features) and [submitting pull 17 | requests](#pull-requests), but please respect the following restrictions: 18 | 19 | * Please **do not** use the issue tracker for personal support requests (use 20 | [Stack Overflow](http://stackoverflow.com) or IRC). 21 | 22 | * Please **do not** derail or troll issues. Keep the discussion on topic and 23 | respect the opinions of others. 24 | 25 | 26 | 27 | ## Bug reports 28 | 29 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 30 | Good bug reports are extremely helpful - thank you! 31 | 32 | Guidelines for bug reports: 33 | 34 | 1. **Use the GitHub issue search** — check if the issue has already been 35 | reported. 36 | 37 | 2. **Check if the issue has been fixed** — try to reproduce it using the 38 | latest `master` or development branch in the repository. 39 | 40 | 3. **Isolate the problem** — create a [reduced test 41 | case](http://css-tricks.com/reduced-test-cases/) and a live example. 42 | 43 | A good bug report shouldn't leave others needing to chase you up for more 44 | information. Please try to be as detailed as possible in your report. What is 45 | your environment? What steps will reproduce the issue? What browser(s) and OS 46 | experience the problem? What would you expect to be the outcome? All these 47 | details will help people to fix any potential bugs. 48 | 49 | Example: 50 | 51 | > Short and descriptive example bug report title 52 | > 53 | > A summary of the issue and the browser/OS environment in which it occurs. If 54 | > suitable, include the steps required to reproduce the bug. 55 | > 56 | > 1. This is the first step 57 | > 2. This is the second step 58 | > 3. Further steps, etc. 59 | > 60 | > `` - a link to the reduced test case 61 | > 62 | > Any other information you want to share that is relevant to the issue being 63 | > reported. This might include the lines of code that you have identified as 64 | > causing the bug, and potential solutions (and your opinions on their 65 | > merits). 66 | 67 | 68 | 69 | ## Feature requests 70 | 71 | Feature requests are welcome. But take a moment to find out whether your idea 72 | fits with the scope and aims of the project. It's up to *you* to make a strong 73 | case to convince the project's developers of the merits of this feature. Please 74 | provide as much detail and context as possible. 75 | 76 | 77 | 78 | ## Pull requests 79 | 80 | Good pull requests - patches, improvements, new features - are a fantastic 81 | help. They should remain focused in scope and avoid containing unrelated 82 | commits. 83 | 84 | **Please ask first** before embarking on any significant pull request (e.g. 85 | implementing features, refactoring code, porting to a different language), 86 | otherwise you risk spending a lot of time working on something that the 87 | project's developers might not want to merge into the project. 88 | 89 | Please adhere to the coding conventions used throughout a project (indentation, 90 | accurate comments, etc.) and any other requirements (such as test coverage). 91 | 92 | Follow this process if you'd like your work considered for inclusion in the 93 | project: 94 | 95 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 96 | and configure the remotes: 97 | 98 | ```bash 99 | # Clone your fork of the repo into the current directory 100 | git clone https://github.com// 101 | # Navigate to the newly cloned directory 102 | cd 103 | # Assign the original repo to a remote called "upstream" 104 | git remote add upstream https://github.com// 105 | ``` 106 | 107 | 2. If you cloned a while ago, get the latest changes from upstream: 108 | 109 | ```bash 110 | git checkout 111 | git pull upstream 112 | ``` 113 | 114 | 3. Create a new topic branch (off the main project development branch) to 115 | contain your feature, change, or fix: 116 | 117 | ```bash 118 | git checkout -b 119 | ``` 120 | 121 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 122 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 123 | or your code is unlikely be merged into the main project. Use Git's 124 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 125 | feature to tidy up your commits before making them public. 126 | 127 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 128 | 129 | ```bash 130 | git pull [--rebase] upstream 131 | ``` 132 | 133 | 6. Push your topic branch up to your fork: 134 | 135 | ```bash 136 | git push origin 137 | ``` 138 | 139 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 140 | with a clear title and description to the `dev` branch. Avoid direct pull requests to `master` 141 | 142 | ### Checklist before making a pull request 143 | - Make sure your code does not have nay linting errors. 144 | - Make sure you have written proper test cases for every piece of code you have added. Every line and branch should be covered. 145 | - Execute the command `npm run ci` before making a pull request and fix any errors if you see. 146 | 147 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to 148 | license your work under the same license as that used by the project. 149 | 150 | And last but not least: Always write your commit messages in the present tense. Your commit message should describe what the commit, when applied, does to the code – not what you did to the code. 151 | 152 | Your contributions are welcome! 153 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/lib/pipes/filter-by.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { FilterByPipe } from './filter-by.pipe'; 2 | const testData = [ 3 | { 4 | _id: '5ab9c81febf774900a0ff849', 5 | index: 0, 6 | guid: '3ed3d5d4-7b27-4091-a207-427c196a76fc', 7 | balance: '$1,367.97', 8 | age: 28, 9 | eyeColor: 'brown', 10 | firstName: 'Bertie', 11 | lastName: 'Marks', 12 | company: 'ISOTRONIC', 13 | email: 'bertie.marks@isotronic.info', 14 | phone: '+1 (817) 557-2705', 15 | address: '853 Cook Street, Deercroft, Northern Mariana Islands, 1714', 16 | about: 'Quis aliqua et mollit mollit ad velit ut deserunt tempor sint sunt. Enim occaecat consectetur cillum ipsum cupidatat. Labore occaecat qui tempor veniam laborum deserunt. Elit magna commodo laborum voluptate incididunt ad. Sit elit enim culpa pariatur proident fugiat quis eu magna.' 17 | }, 18 | { 19 | _id: '5ab9c820de24c4ea5ff96665', 20 | index: 1, 21 | guid: 'c40f62a2-7294-4a75-a805-cb5957b55727', 22 | balance: '$2,063.42', 23 | age: 24, 24 | eyeColor: 'brown', 25 | firstName: 'Ramona', 26 | lastName: 'Walter', 27 | company: 'SOPRANO', 28 | email: 'ramona.walter@soprano.com', 29 | phone: '+1 (903) 477-2993', 30 | address: '676 Hendrix Street, Groveville, Marshall Islands, 9712', 31 | about: 'Sint quis cillum ad Lorem in nulla ipsum sunt ut ad quis ad ex. Anim do cillum culpa commodo duis ad nulla fugiat labore fugiat irure aute est elit. Mollit nisi voluptate anim consequat deserunt ullamco. Qui ex laboris sit dolor officia fugiat elit eu commodo nulla aute. Voluptate laborum labore ut tempor consectetur ipsum commodo do irure culpa officia. Nostrud cillum eu ex dolore adipisicing occaecat id nulla dolore.' 32 | }, 33 | { 34 | _id: '5ab9c820d668abc6eb572961', 35 | index: 2, 36 | guid: '6c858720-d0c2-4fba-8316-3a974a2c5ccc', 37 | balance: '$3,890.59', 38 | age: 31, 39 | eyeColor: 'brown', 40 | firstName: 'Holmes', 41 | lastName: 'Ratliff', 42 | company: 'ACLIMA', 43 | email: 'holmes.ratliff@aclima.co.uk', 44 | phone: '+1 (977) 541-2880', 45 | address: '736 Dikeman Street, Vallonia, Wyoming, 1370', 46 | about: 'Reprehenderit et sint eu sunt occaecat sint dolore minim aliqua aute enim incididunt. Labore officia qui proident esse cupidatat sint deserunt. Velit qui incididunt ullamco ullamco qui. Nostrud in sit laboris sit pariatur esse ea dolore elit enim.' 47 | }, 48 | { 49 | _id: '5ab9c820ad13b4f8707133e7', 50 | index: 3, 51 | guid: '29042a8d-f992-4ff7-8609-7cf1cb44ea0c', 52 | balance: '$1,743.95', 53 | age: 28, 54 | eyeColor: 'blue', 55 | firstName: 'Lara', 56 | lastName: 'Cox', 57 | company: 'CENTREE', 58 | email: 'lara.cox@centree.biz', 59 | phone: '+1 (894) 505-3882', 60 | address: '482 Sackman Street, Goodville, Kentucky, 3683', 61 | about: 'Mollit anim nostrud duis anim mollit reprehenderit ad velit mollit anim. Velit veniam reprehenderit minim fugiat. Aliqua est anim esse exercitation deserunt magna ad dolor minim labore. Occaecat deserunt elit occaecat irure eiusmod sit nisi ad in mollit in cupidatat. Ex consequat dolore amet laborum qui.' 62 | }, 63 | { 64 | _id: '5ab9c8203b395f39aa34997d', 65 | index: 4, 66 | guid: '72e39252-f7e7-49d0-9e3b-03ae8261bfeb', 67 | balance: '$2,630.52', 68 | age: 33, 69 | eyeColor: 'blue', 70 | firstName: 'Toni', 71 | lastName: 'Simpson', 72 | company: 'GOKO', 73 | email: 'toni.simpson@goko.io', 74 | phone: '+1 (976) 583-3167', 75 | address: '574 Prescott Place, Lowgap, Rhode Island, 3696', 76 | about: 'Nisi et magna voluptate aute dolor minim commodo laboris nisi est. Reprehenderit sunt occaecat deserunt ea fugiat dolor dolor exercitation ut. Laboris laboris amet quis consectetur eiusmod. Excepteur qui ut et magna tempor magna. Nulla adipisicing commodo mollit velit velit nisi consequat laborum voluptate.' 77 | }, 78 | { 79 | _id: '5ab9c820c181d1a8468a64ae', 80 | index: 5, 81 | guid: 'be7b9b92-d3c8-4175-a57d-d918304e410f', 82 | balance: '$1,601.57', 83 | age: 26, 84 | eyeColor: 'brown', 85 | firstName: 'Megan', 86 | lastName: 'Barr', 87 | company: 'GRAINSPOT', 88 | email: 'megan.barr@grainspot.tv', 89 | phone: '+1 (831) 576-3439', 90 | address: '707 Dewey Place, Gerber, West Virginia, 794', 91 | about: 'Officia mollit dolore occaecat occaecat. Voluptate adipisicing ad sit aliquip laborum proident aliquip nulla incididunt. Aute laboris elit nisi aute exercitation esse sit velit enim duis. Sunt fugiat ad non qui dolor Lorem.' 92 | } 93 | ]; 94 | const result = [ 95 | { 96 | _id: '5ab9c81febf774900a0ff849', 97 | index: 0, 98 | guid: '3ed3d5d4-7b27-4091-a207-427c196a76fc', 99 | balance: '$1,367.97', 100 | age: 28, 101 | eyeColor: 'brown', 102 | firstName: 'Bertie', 103 | lastName: 'Marks', 104 | company: 'ISOTRONIC', 105 | email: 'bertie.marks@isotronic.info', 106 | phone: '+1 (817) 557-2705', 107 | address: '853 Cook Street, Deercroft, Northern Mariana Islands, 1714', 108 | about: 'Quis aliqua et mollit mollit ad velit ut deserunt tempor sint sunt. Enim occaecat consectetur cillum ipsum cupidatat. Labore occaecat qui tempor veniam laborum deserunt. Elit magna commodo laborum voluptate incididunt ad. Sit elit enim culpa pariatur proident fugiat quis eu magna.' 109 | }, 110 | { 111 | _id: '5ab9c820c181d1a8468a64ae', 112 | index: 5, 113 | guid: 'be7b9b92-d3c8-4175-a57d-d918304e410f', 114 | balance: '$1,601.57', 115 | age: 26, 116 | eyeColor: 'brown', 117 | firstName: 'Megan', 118 | lastName: 'Barr', 119 | company: 'GRAINSPOT', 120 | email: 'megan.barr@grainspot.tv', 121 | phone: '+1 (831) 576-3439', 122 | address: '707 Dewey Place, Gerber, West Virginia, 794', 123 | about: 'Officia mollit dolore occaecat occaecat. Voluptate adipisicing ad sit aliquip laborum proident aliquip nulla incididunt. Aute laboris elit nisi aute exercitation esse sit velit enim duis. Sunt fugiat ad non qui dolor Lorem.' 124 | } 125 | ]; 126 | describe('FilterByPipe', () => { 127 | it('create an instance', () => { 128 | const pipe = new FilterByPipe(); 129 | expect(pipe).toBeTruthy(); 130 | }); 131 | 132 | it('should return the value when not array', () => { 133 | const pipe = new FilterByPipe(); 134 | expect(pipe.transform('test' as any)).toEqual('test' as any); 135 | }); 136 | 137 | it('should return the filtered array of objects', () => { 138 | const pipe = new FilterByPipe(); 139 | expect(pipe.transform(testData, 'B')).toEqual([...testData]); 140 | }); 141 | 142 | it('should return the filtered array of objects when no item matches', () => { 143 | const pipe = new FilterByPipe(); 144 | expect(pipe.transform(testData, 'xxxxxxx')).toEqual([]); 145 | }); 146 | 147 | it('should return the filtered array of objects', () => { 148 | const pipe = new FilterByPipe(); 149 | expect(pipe.transform(testData, 'B', 'firstName')).toEqual([result[0]]); 150 | }); 151 | 152 | it('should return the filtered array of objects', () => { 153 | const pipe = new FilterByPipe(); 154 | const arr = ['star', 'galaxy', 'sun', 'moon', 'earth']; 155 | expect(pipe.transform(arr, 'ar')).toEqual(['star', 'earth']); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx-select-dropdown 2 | 3 | [![GitHub license](https://img.shields.io/github/license/manishjanky/ngx-select-dropdown.svg)](https://github.com/me-and/mdf/blob/master/LICENSE) 4 | [![npm](https://img.shields.io/npm/v/ngx-select-dropdown.svg)]() 5 | [![Build Status](https://travis-ci.org/manishjanky/ngx-select-dropdown.svg?branch=master)](https://travis-ci.org/manishjanky/ngx-select-dropdown) 6 | [![Codecov branch](https://codecov.io/gh/manishjanky/ngx-select-dropdown/branch/master/graphs/badge.svg)]() 7 | ![npm](https://img.shields.io/npm/dy/ngx-select-dropdown) 8 | [![GitHub top language](https://img.shields.io/github/languages/top/manishjanky/ngx-select-dropdown.svg)]() 9 | [![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/manishjanky/ngx-select-dropdown.svg)]() 10 | 11 | `ngx-select-dropdown` Custom Dropdown component for Angular 4+ with multiple and single selection options, with customization options 12 | 13 | ## Features 14 | * single select dropdown 15 | * multi select dropdown 16 | * search dropdown list 17 | * arrows keys support 18 | * limit number of items displayed in dropdown 19 | * custom sort 20 | * angular forms support 21 | * angular v4 and above supported 22 | * cross browser support 23 | * custom template options 24 | 25 | 26 | ## Examples 27 | 28 | * [demo-page](https://manishjanky.github.io/ngx-select-dropdown/) 29 | 30 | ## Installation 31 | 32 | * `npm install ngx-select-dropdown` 33 | 34 | ### Using with webpack and tsc builds/ angular-cli builds 35 | 36 | * import `SelectDropDownModule` into your app.module; 37 | ```` 38 | import { SelectDropDownModule } from 'ngx-select-dropdown' 39 | ```` 40 | * add `SelectDropDownModule` to the imports of your NgModule: 41 | ````` 42 | @NgModule({ 43 | imports: [ 44 | ..., 45 | SelectDropDownModule 46 | ], 47 | ... 48 | }) 49 | class YourModule { ... } 50 | ````` 51 | 52 | * include css styles in you `angular-cli.json` for versions earlier than 1.4.0 53 | 54 | ````` 55 | "styles": [ 56 | "../node_modules/ngx-select-dropdown/dist/assets/style.css" 57 | ], 58 | ````` 59 | 60 | 61 | * use `` in your templates to add the custom dropdown in your view like below 62 | 63 | ```` 64 | 65 | ```` 66 | * use with reactive form like 67 | ```` 68 | 69 | ```` 70 | * use custom templates options like below. `item` and `config` variables available in the template context 71 | ```` 72 | 73 | 74 | {{item.name}} 75 | 76 | 77 | 78 | 80 | 81 | ```` 82 | 83 | ## Configuration 84 | 85 | ### Input 86 | 87 | * `multiple: boolean` - `true/false` beased if multiple selection required or not `Defaults to false`. 88 | * `options: Array` - Array of string/objects that are to be the dropdown options. 89 | * `disabled: boolean` - disabled attribute to disable the dropdown when required. 90 | * `instanceId: any` - instanceId of the dropdwon component instance. 91 | * `config: NgxDropdownConfig` - configuration object. 92 | * `selectedItemTemplate: TemplateRef` - Custom template ref for the selected item 93 | * `optionItemTemplate: TemplateRef` - Custom template ref for the options items(available options) 94 | * `dropdownButtonTemplate: TemplateRef` - Custom template ref for the dropdwon button element 95 | * `notFoundTemplate: TemplateRef` - Custom template ref for the no matched found message 96 | ```` 97 | config = { 98 | displayFn:(item: any) => { return item.hello.world; } //to support flexible text displaying for each item 99 | displayKey:"description", //if objects array passed which key to be displayed defaults to description 100 | search:true //true/false for the search functionlity defaults to false, 101 | height: 'auto' //height of the list so that if there are more no of items it can show a scroll defaults to auto. With auto height scroll will never appear 102 | placeholder:'Select' // text to be displayed when no item is selected defaults to Select, 103 | customComparator: ()=>{} // a custom function using which user wants to sort the items. default is undefined and Array.sort() will be used in that case, 104 | limitTo: 0 // number thats limits the no of options displayed in the UI (if zero, options will not be limited) 105 | moreText: 'more' // text to be displayed whenmore than one items are selected like Option 1 + 5 more 106 | noResultsFound: 'No results found!' // text to be displayed when no items are found while searching 107 | searchPlaceholder:'Search' // label thats displayed in search input, 108 | searchOnKey: 'name' // key on which search should be performed this will be selective search. if undefined this will be extensive search on all keys 109 | clearOnSelection: false // clears search criteria when an option is selected if set to true, default is false 110 | inputDirection: 'ltr' // the direction of the search input can be rtl or ltr(default) 111 | selectAllLabel: 'Select all' // label that is displayed in multiple selection for select all 112 | enableSelectAll: false, // enable select all option to select all available items, default is false 113 | } 114 | ```` 115 | 116 | ### Output 117 | 118 | * `change: Event` - change event when user changes the selected options 119 | * `open: Event` - open event when the dropdown toogles on 120 | * `close: Event` - close event when the dropdown toogles off 121 | * `searchChange: Event` - search change event when the search text changes 122 | 123 | ### Dropdown service 124 | * `openDropdown(instanceId:string)` - method to open a particular dropdown instance 125 | * `closeDropdown(instanceId:string)` - method to close a particular dropdown instance 126 | * `isOpen(instanceId:string)` - method to check if a particular instance dropdown is open 127 | * `openInstances` - instanceId list of all the open instances 128 | 129 | ### Change detection 130 | 131 | As of now `ngx-select-dropdown` uses Default change detection startegy which means dirty checking checks for immutable data types. And in Javascript Objects and arrays are mutable. So when changing any of the @Input parameters if you mutate an object change detection will not detect it. For ex:- 132 | ```` 133 | this.options.push({id: 34, description: 'Adding new item'}); 134 | 135 | // or 136 | 137 | config.height = '200px'; 138 | 139 | ````` 140 | Both the above scenarios will not trigger the change detection. In order for the componet to detect the changes what you need to do is:- 141 | ```` 142 | this.options = [...this.options, {id: 34, description: 'Adding new item'}]; 143 | 144 | // or 145 | 146 | config = {...config, height:'200px'}; 147 | 148 | ```` 149 | 150 | ## Changelog 151 | * v0.1.0 152 | ```` 153 | Added a change event so that user can attach a change event handler. 154 | If multiselect the selected text will display first item and + count for eg. (Option 1 + 2 more) . 155 | ```` 156 | * v0.2.0 157 | ```` 158 | Angular 4 and above support. 159 | Bug with search functionality fixed. 160 | ```` 161 | * v0.3.0 162 | ```` 163 | Support for Observable data source for options and async pipe. 164 | IE bug with styling. 165 | Few other minor bug fixes. 166 | ```` 167 | * v0.4.0 168 | ```` 169 | Use arrows keys and enter to select items from available options. 170 | Case insensitive search. 171 | Few other minor bug fixes. 172 | ```` 173 | * v0.5.0 174 | ```` 175 | Support for scroll bar with too many list items. 176 | Few other minor bug fixes. 177 | ```` 178 | * v0.7.0 179 | ```` 180 | Support for limito pipe to limit number of options displayed in case of too many options. 181 | Support for customComparator / custom sort function 182 | Few other minor bug fixes. 183 | ```` 184 | * v0.7.2 185 | ```` 186 | Support for angular 6 187 | Removed dependency on rxjs 188 | ```` 189 | * v0.8.0 190 | ```` 191 | No Results found indicator with custom text passed with config 192 | Custom text for *more* when more than 1 items selected 193 | Open event emitted 194 | Close event emitted 195 | Search placeholder text 196 | ```` 197 | * v1.0.0 198 | ```` 199 | Search on a specified key value. 200 | Support for Reactive forms 201 | Few other minor imoprovements and fixes 202 | ```` 203 | * v1.2.0 204 | ```` 205 | Search text change event searchChange 206 | ```` 207 | * v1.3.0 208 | ```` 209 | Clear search on selection config 210 | Disable with reactive forms .disable() 211 | ```` 212 | * v1.5.0 213 | ```` 214 | Custom function for displaying text for each option 215 | Add disabled class to different items based on item.disabled 216 | ```` 217 | * v2.0.0 218 | ```` 219 | Angular library approach opted for development 220 | Angular Ivy compatibility 221 | ```` 222 | * v3.0.0 223 | ```` 224 | Dropdown singleton service to interact with dropdown instances 225 | Instance identifier 226 | Upgraded to Angular v14 development environment 227 | ```` 228 | * v3.0.1 229 | ```` 230 | Auto drop based on the screen position 231 | Over-ride css styles for available and selected items using class names `selected-item(s)` and `available-item(s)` 232 | ```` 233 | * v3.2.0 234 | ```` 235 | Ability to select all available items using a select all checkbox 236 | ```` 237 | * v3.3.0 238 | ```` 239 | Custom templates for available options, selected items and the dropdown button 240 | Other minor fixes 241 | ```` 242 | ## Help Improve 243 | 244 | Found a bug or an issue with this? [Open a new issue](https://github.com/manishjanky/ngx-select-dropdown/issues) here on GitHub. 245 | 246 | ## Contributing to this project 247 | 248 | Anyone and everyone is welcome to contribute. Please take a moment to 249 | review the [guidelines for contributing](CONTRIBUTING.md). 250 | 251 | * [Bug reports](CONTRIBUTING.md#bugs) 252 | * [Feature requests](CONTRIBUTING.md#features) 253 | * [Pull requests](CONTRIBUTING.md#pull-requests) 254 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/README.md: -------------------------------------------------------------------------------- 1 | # ngx-select-dropdown 2 | 3 | [![GitHub license](https://img.shields.io/github/license/manishjanky/ngx-select-dropdown.svg)](https://github.com/me-and/mdf/blob/master/LICENSE) 4 | [![npm](https://img.shields.io/npm/v/ngx-select-dropdown.svg)]() 5 | [![Build Status](https://travis-ci.org/manishjanky/ngx-select-dropdown.svg?branch=master)](https://travis-ci.org/manishjanky/ngx-select-dropdown) 6 | [![Codecov branch](https://codecov.io/gh/manishjanky/ngx-select-dropdown/branch/master/graphs/badge.svg)]() 7 | ![npm](https://img.shields.io/npm/dy/ngx-select-dropdown) 8 | [![GitHub top language](https://img.shields.io/github/languages/top/manishjanky/ngx-select-dropdown.svg)]() 9 | [![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/manishjanky/ngx-select-dropdown.svg)]() 10 | 11 | `ngx-select-dropdown` Custom Dropdown component for Angular 4+ with multiple and single selection options, with customization options 12 | 13 | ## Features 14 | * single select dropdown 15 | * multi select dropdown 16 | * search dropdown list 17 | * arrows keys support 18 | * limit number of items displayed in dropdown 19 | * custom sort 20 | * angular forms support 21 | * angular v4 and above supported 22 | * cross browser support 23 | * custom template options 24 | 25 | 26 | ## Examples 27 | 28 | * [demo-page](https://manishjanky.github.io/ngx-select-dropdown/) 29 | 30 | ## Installation 31 | 32 | * `npm install ngx-select-dropdown` 33 | 34 | ### Using with webpack and tsc builds/ angular-cli builds 35 | 36 | * import `SelectDropDownModule` into your app.module; 37 | ```` 38 | import { SelectDropDownModule } from 'ngx-select-dropdown' 39 | ```` 40 | * add `SelectDropDownModule` to the imports of your NgModule: 41 | ````` 42 | @NgModule({ 43 | imports: [ 44 | ..., 45 | SelectDropDownModule 46 | ], 47 | ... 48 | }) 49 | class YourModule { ... } 50 | ````` 51 | 52 | * include css styles in you `angular-cli.json` for versions earlier than 1.4.0 53 | 54 | ````` 55 | "styles": [ 56 | "../node_modules/ngx-select-dropdown/dist/assets/style.css" 57 | ], 58 | ````` 59 | 60 | 61 | * use `` in your templates to add the custom dropdown in your view like below 62 | 63 | ```` 64 | 65 | ```` 66 | * use with reactive form like 67 | ```` 68 | 69 | ```` 70 | * use custom templates options like below. `item` and `config` variables available in the template context 71 | ```` 72 | 73 | 74 | {{item.name}} 75 | 76 | 77 | 78 | 80 | 81 | ```` 82 | 83 | ## Configuration 84 | 85 | ### Input 86 | 87 | * `multiple: boolean` - `true/false` beased if multiple selection required or not `Defaults to false`. 88 | * `options: Array` - Array of string/objects that are to be the dropdown options. 89 | * `disabled: boolean` - disabled attribute to disable the dropdown when required. 90 | * `instanceId: any` - instanceId of the dropdwon component instance. 91 | * `config: NgxDropdownConfig` - configuration object. 92 | * `selectedItemTemplate: TemplateRef` - Custom template ref for the selected item 93 | * `optionItemTemplate: TemplateRef` - Custom template ref for the options items(available options) 94 | * `dropdownButtonTemplate: TemplateRef` - Custom template ref for the dropdwon button element 95 | * `notFoundTemplate: TemplateRef` - Custom template ref for the no matched found message 96 | ```` 97 | config = { 98 | displayFn:(item: any) => { return item.hello.world; } //to support flexible text displaying for each item 99 | displayKey:"description", //if objects array passed which key to be displayed defaults to description 100 | search:true //true/false for the search functionlity defaults to false, 101 | height: 'auto' //height of the list so that if there are more no of items it can show a scroll defaults to auto. With auto height scroll will never appear 102 | placeholder:'Select' // text to be displayed when no item is selected defaults to Select, 103 | customComparator: ()=>{} // a custom function using which user wants to sort the items. default is undefined and Array.sort() will be used in that case, 104 | limitTo: 0 // number thats limits the no of options displayed in the UI (if zero, options will not be limited) 105 | moreText: 'more' // text to be displayed whenmore than one items are selected like Option 1 + 5 more 106 | noResultsFound: 'No results found!' // text to be displayed when no items are found while searching 107 | searchPlaceholder:'Search' // label thats displayed in search input, 108 | searchOnKey: 'name' // key on which search should be performed this will be selective search. if undefined this will be extensive search on all keys 109 | clearOnSelection: false // clears search criteria when an option is selected if set to true, default is false 110 | inputDirection: 'ltr' // the direction of the search input can be rtl or ltr(default) 111 | selectAllLabel: 'Select all' // label that is displayed in multiple selection for select all 112 | enableSelectAll: false, // enable select all option to select all available items, default is false 113 | } 114 | ```` 115 | 116 | ### Output 117 | 118 | * `change: Event` - change event when user changes the selected options 119 | * `open: Event` - open event when the dropdown toogles on 120 | * `close: Event` - close event when the dropdown toogles off 121 | * `searchChange: Event` - search change event when the search text changes 122 | 123 | ### Dropdown service 124 | * `openDropdown(instanceId:string)` - method to open a particular dropdown instance 125 | * `closeDropdown(instanceId:string)` - method to close a particular dropdown instance 126 | * `isOpen(instanceId:string)` - method to check if a particular instance dropdown is open 127 | * `openInstances` - instanceId list of all the open instances 128 | 129 | ### Change detection 130 | 131 | As of now `ngx-select-dropdown` uses Default change detection startegy which means dirty checking checks for immutable data types. And in Javascript Objects and arrays are mutable. So when changing any of the @Input parameters if you mutate an object change detection will not detect it. For ex:- 132 | ```` 133 | this.options.push({id: 34, description: 'Adding new item'}); 134 | 135 | // or 136 | 137 | config.height = '200px'; 138 | 139 | ````` 140 | Both the above scenarios will not trigger the change detection. In order for the componet to detect the changes what you need to do is:- 141 | ```` 142 | this.options = [...this.options, {id: 34, description: 'Adding new item'}]; 143 | 144 | // or 145 | 146 | config = {...config, height:'200px'}; 147 | 148 | ```` 149 | 150 | ## Changelog 151 | * v0.1.0 152 | ```` 153 | Added a change event so that user can attach a change event handler. 154 | If multiselect the selected text will display first item and + count for eg. (Option 1 + 2 more) . 155 | ```` 156 | * v0.2.0 157 | ```` 158 | Angular 4 and above support. 159 | Bug with search functionality fixed. 160 | ```` 161 | * v0.3.0 162 | ```` 163 | Support for Observable data source for options and async pipe. 164 | IE bug with styling. 165 | Few other minor bug fixes. 166 | ```` 167 | * v0.4.0 168 | ```` 169 | Use arrows keys and enter to select items from available options. 170 | Case insensitive search. 171 | Few other minor bug fixes. 172 | ```` 173 | * v0.5.0 174 | ```` 175 | Support for scroll bar with too many list items. 176 | Few other minor bug fixes. 177 | ```` 178 | * v0.7.0 179 | ```` 180 | Support for limito pipe to limit number of options displayed in case of too many options. 181 | Support for customComparator / custom sort function 182 | Few other minor bug fixes. 183 | ```` 184 | * v0.7.2 185 | ```` 186 | Support for angular 6 187 | Removed dependency on rxjs 188 | ```` 189 | * v0.8.0 190 | ```` 191 | No Results found indicator with custom text passed with config 192 | Custom text for *more* when more than 1 items selected 193 | Open event emitted 194 | Close event emitted 195 | Search placeholder text 196 | ```` 197 | * v1.0.0 198 | ```` 199 | Search on a specified key value. 200 | Support for Reactive forms 201 | Few other minor imoprovements and fixes 202 | ```` 203 | * v1.2.0 204 | ```` 205 | Search text change event searchChange 206 | ```` 207 | * v1.3.0 208 | ```` 209 | Clear search on selection config 210 | Disable with reactive forms .disable() 211 | ```` 212 | * v1.5.0 213 | ```` 214 | Custom function for displaying text for each option 215 | Add disabled class to different items based on item.disabled 216 | ```` 217 | * v2.0.0 218 | ```` 219 | Angular library approach opted for development 220 | Angular Ivy compatibility 221 | ```` 222 | * v3.0.0 223 | ```` 224 | Dropdown singleton service to interact with dropdown instances 225 | Instance identifier 226 | Upgraded to Angular v14 development environment 227 | ```` 228 | * v3.0.1 229 | ```` 230 | Auto drop based on the screen position 231 | Over-ride css styles for available and selected items using class names `selected-item(s)` and `available-item(s)` 232 | ```` 233 | * v3.2.0 234 | ```` 235 | Ability to select all available items using a select all checkbox 236 | ```` 237 | * v3.3.0 238 | ```` 239 | Custom templates for available options, selected items and the dropdown button 240 | Other minor fixes 241 | ```` 242 | ## Help Improve 243 | 244 | Found a bug or an issue with this? [Open a new issue](https://github.com/manishjanky/ngx-select-dropdown/issues) here on GitHub. 245 | 246 | ## Contributing to this project 247 | 248 | Anyone and everyone is welcome to contribute. Please take a moment to 249 | review the [guidelines for contributing](CONTRIBUTING.md). 250 | 251 | * [Bug reports](CONTRIBUTING.md#bugs) 252 | * [Feature requests](CONTRIBUTING.md#features) 253 | * [Pull requests](CONTRIBUTING.md#pull-requests) 254 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
ngx-select-dropdown demo
5 |
6 | 9 |
10 |
11 |

ngx-select-dropdown

12 | 13 |
Custom Dropdown component for Angular 2+ applications with multiple and single 14 | selection 15 | options.
16 | 24 |
25 | 37 |
38 |
39 |

1.Installation

40 |
 41 |         //using npm
 42 |          npm install ngx-select-dropdown
 43 | 
 44 |         //using yarn
 45 |          yarn add ngx-select-dropdown
 46 |          yarn install
47 |
    48 |
  • include styles in you angular-cli.json.
  • 49 |
50 | 51 |
 52 |         //in angular-cli.json
 53 |          "styles": [
 54 |             "../node_modules/ngx-select-dropdown/dist/assets/style.css"
 55 |           ]
56 |
57 |
58 |

2.Using with webpack and tsc builds/ angular-cli builds

59 |
    60 |
  • import SelectDropDownModule from ngx-select-dropdown
  • 61 |
  • add SelectDropDownModule to the imports of your NgModule
  • 62 |
63 |
 64 |         
 65 |           import {{'{'}} SelectDropDownModule {{'}'}} from 'ngx-select-dropdown'
 66 | 
 67 |           @NgModule({{'{'}}
 68 |             imports: [
 69 |               ...,
 70 | 
 71 |               SelectDropDownModule
 72 |             ],
 73 |             ...
 74 |           })
 75 |           class YourModule {{'{'}} ... {{'}'}}
76 |
77 |
78 |
    79 |
  • 80 | use 81 | <ngx-select-dropdown></ngx-select-dropdown> in your templates to add select 82 | dropdown in your view like below 83 |
  • 84 |
  • a tag in you html will look like below with all the config
  • 85 |
86 | 87 |
 88 |           <ngx-select-dropdown [config]="config" [options]="dropdownOptions"
 89 |             [(ngModel)]="dataModel" [multiple]="true" ></ngx-select-dropdown>
90 | 91 |
92 |
93 |

3.Config

94 |
Input
95 |
    96 |
  • 97 | multiple: boolean - true/false beased if multiple selection required or not Defaults to 98 | false 99 |
  • 100 |
  • 101 | options: Array - Array of string/objects that are to be the dropdown options. 102 |
  • 103 |
  • 104 | disabled: boolean - disabled attribute to disable the dropdown when required. 105 |
  • 106 |
  • 107 | config: Object - configuration object. 108 |
  • 109 |
110 |
111 |           config = {{ '{' }}
112 |             displayFn:(item: any) => {{'{'}} return item.hello.world; {{'}'}} //a replacement ofr displayKey to support flexible text displaying for each item
113 |             displayKey:"description", //if objects array passed which key to be displayed defaults to description
114 |             search:true //true/false for the search functionlity defaults to false,
115 |             height: 'auto' //height of the list so that if there are more no of items it can show a scroll defaults to auto. With auto height scroll will never appear
116 |             placeholder:'Select' // text to be displayed when no item is selected defaults to Select,
117 |             customComparator: ()=>{{ '{' }}{{ '}' }} // a custom function using which user wants to sort the items. default is undefined and Array.sort() will be used in that case,
118 |             limitTo: 0 // number thats limits the no of options displayed in the UI (if zero, options will not be limited)
119 |             moreText: 'more' // text to be displayed whenmore than one items are selected like Option 1 + 5 more
120 |             noResultsFound: 'No results found!' // text to be displayed when no items are found while searching
121 |             searchPlaceholder:'Search' // label thats displayed in search input,
122 |             searchOnKey: 'name' // key on which search should be performed this will be selective search. if undefined this will be extensive search on all keys
123 |             {{ '}' }}
124 |
    125 |
  • 126 | selectedItemTemplate: TemplateRef - a template reference for the selectedItems 127 |
  • 128 |
  • 129 | optionItemTemplate: TemplateRef - a template reference for the available options 130 |
  • 131 |
  • 132 | notFoundTemplate: TemplateRef - a template reference in case no matching items for search 133 |
  • 134 |
  • 135 | dropdownButtonTemplate: TemplateRef - a template reference for the dropdown action button 136 |
  • 137 |
138 | See beloe on how to use the custom templates. The context contains two properties the item and config which can be used in the template. 139 |
 
140 |           <ng-template> #optionTemplate let-item="item" let-config="config">
141 |             <i class="fa fa-plus"> </i>
142 |               '{{' item.name '}}'
143 |             <span> class="new badge"> </span>
144 |           </ng-template>
145 | 
146 |           <ngx-select-dropdown> [optionItemTemplate]="optionTemplate" 
147 |             [selectedItemTemplate]="optionTemplate"
148 |             tabindex="0" [multiple]="true" [(ngModel)]="optTemplate" [options]="options"
149 |             [config]="config"></ngx-select-dropdown>
150 |         
151 |
Output
152 |
    153 |
  • 154 | change: Event - change event when user changes the selected options. 155 |
  • 156 |
  • 157 | open: Event - open event when the dropdown toogles on. 158 |
  • 159 |
  • 160 | close: Event - close event when the dropdown toogles off. 161 |
  • 162 |
163 |
164 | 165 |
166 | 167 |
168 |
169 |

Demos

170 |
Single Select Dropdown
171 |
172 |
173 | 175 |
176 |
177 |

Balance of selected user: {{singleSelect? singleSelect?.balance : ''}}

178 |
179 |
180 | 181 |
Disabled Dropdown
182 |
183 |
184 | 186 | 187 |
188 |
189 |

Balance of selected user: {{singleSelect?singleSelect?.balance:''}}

190 |
191 |
192 | 193 |
Multi Select Dropdown
194 |
195 |
196 | 198 |
199 |
200 |

Selected Options:-

201 |

Name: {{user.name}} , 202 | Balance: 203 | {{user.balance}} 204 |

205 |
206 |
207 | 208 |
Options as array of string
209 |
210 |
211 | 213 | 214 |
215 |
216 |

Selected Options:-

217 |

{{user}}

218 |
219 |
220 | 221 |
Options as array of Objects
222 |
223 |
224 | 226 |
227 |
228 |

Selected Options:-

229 |

{{user | json}}

230 |
231 |
232 | 233 |
Already selected options passed as value
234 |
235 |
236 | 238 |
239 |
240 |

Selected Options:-

241 |

Name: {{user.name}} , 242 | Balance: 243 | {{user.balance}} 244 |

245 |
246 |
247 | 248 |
Reset
249 |
250 |
251 | 253 |
254 |
255 |

Selected Options:-

256 |

Name: {{user.name}} , 257 | Balance: 258 | {{user.balance}} 259 |

260 |
261 |
262 |
263 |
264 | 265 |
266 |
267 | 268 |
Custom option template
269 |
270 |
271 | 274 |
275 |
276 |

Selected Options:-

277 |

Name: {{user.name}} , 278 | Balance: 279 | {{user.balance}} 280 |

281 |
282 |
283 |
284 |
285 | 286 |
287 | 288 | 289 | 290 | {{item.name}} 291 | 292 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/lib/ngx-select-dropdown.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgxDropdownConfig } from "./types/ngx-select-dropdown.types"; 2 | import { FilterByPipe } from "./pipes/filter-by.pipe"; 3 | import { LimitToPipe } from "./pipes/limit-to.pipe"; 4 | import { QueryList } from "@angular/core"; 5 | import { ElementRef } from "@angular/core"; 6 | import { FormsModule } from "@angular/forms"; 7 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 8 | 9 | import { NgxSelectDropdownComponent } from "./ngx-select-dropdown.component"; 10 | 11 | const options = ["Option 1", "Option 2", "Option 3"]; 12 | const config: NgxDropdownConfig = { 13 | displayKey: "description", 14 | height: "auto", 15 | search: false, 16 | placeholder: "Select", 17 | searchPlaceholder: "Search...", 18 | limitTo: 0, 19 | customComparator: undefined, 20 | noResultsFound: "No results found!", 21 | moreText: "more", 22 | searchOnKey: null, 23 | clearOnSelection: false, 24 | inputDirection: "ltr", 25 | }; 26 | interface Account { 27 | _id: string; 28 | index: number; 29 | balance: string; 30 | picture: string; 31 | name: string; 32 | firstName: string; 33 | thirdPartyProfile: { 34 | name: string; 35 | }; 36 | } 37 | const objOptions: Account[] = [ 38 | { 39 | _id: "5a66d6c31d5e4e36c7711b7a", 40 | index: 0, 41 | balance: "$2,806.37", 42 | picture: "http://placehold.it/32x32", 43 | name: "Burns Dalton", 44 | firstName: "Burns", 45 | thirdPartyProfile: { 46 | name: "Burns.D", 47 | }, 48 | }, 49 | { 50 | _id: "5a66d6c3657e60c6073a2d22", 51 | index: 1, 52 | balance: "$2,984.98", 53 | picture: "http://placehold.it/32x32", 54 | name: "Mcintyre Lawson", 55 | firstName: "Mcintyre", 56 | thirdPartyProfile: { 57 | name: "Mcint", 58 | }, 59 | }, 60 | { 61 | _id: "5a66d6c376be165a5a7fae33", 62 | index: 2, 63 | balance: "$2,794.16", 64 | picture: "http://placehold.it/32x32", 65 | name: "Amie Franklin", 66 | firstName: "Amie", 67 | thirdPartyProfile: { 68 | name: "Frank", 69 | }, 70 | }, 71 | { 72 | _id: "5a66d6c3f7854b6b4d96333b", 73 | index: 3, 74 | balance: "$2,537.14", 75 | picture: "http://placehold.it/32x32", 76 | name: "Jocelyn Horton", 77 | firstName: "Jocelyn", 78 | thirdPartyProfile: { 79 | name: "Joce", 80 | }, 81 | }, 82 | ]; 83 | describe("NgxSelectDropdownComponent", () => { 84 | let component: NgxSelectDropdownComponent; 85 | let fixture: ComponentFixture; 86 | 87 | beforeEach(async(() => { 88 | TestBed.configureTestingModule({ 89 | imports: [FormsModule], 90 | declarations: [NgxSelectDropdownComponent, LimitToPipe, FilterByPipe], 91 | }).compileComponents(); 92 | })); 93 | 94 | beforeEach(() => { 95 | fixture = TestBed.createComponent(NgxSelectDropdownComponent); 96 | component = fixture.componentInstance; 97 | component.options = options; 98 | fixture.detectChanges(); 99 | }); 100 | 101 | it("should create", () => { 102 | expect(component).toBeTruthy(); 103 | }); 104 | 105 | it("should reinitialize options if data source changes", (done) => { 106 | setTimeout(() => { 107 | component.options = objOptions; 108 | component.ngOnChanges({}); 109 | expect(component.options).toEqual(objOptions); 110 | done(); 111 | }, 3000); 112 | }); 113 | 114 | it("should reinitialize options if data source changes and not the value", (done) => { 115 | setTimeout(() => { 116 | component.options = [...objOptions]; 117 | component.ngOnChanges({ options: { firstChange: false } } as any); 118 | component.selectItem(component.options[0], 0); 119 | expect(component.availableItems).toEqual([ 120 | objOptions[1], 121 | objOptions[2], 122 | objOptions[3], 123 | ]); 124 | done(); 125 | }, 3000); 126 | }); 127 | 128 | it("should handle value change in ngOnchanges", (done) => { 129 | setTimeout(() => { 130 | component.options = objOptions; 131 | component.value = [objOptions[0]]; 132 | component.ngOnChanges({ value: { firstChange: false } } as any); 133 | expect(component.selectedItems.length).toEqual(1); 134 | done(); 135 | }, 3000); 136 | }); 137 | 138 | it("should handle value changes to [] in ngOnchanges", (done) => { 139 | setTimeout(() => { 140 | component.options = objOptions; 141 | component.value = [objOptions[0]]; 142 | component.ngOnChanges({ value: { firstChange: false } } as any); 143 | component.value = []; 144 | component.ngOnChanges({ 145 | value: { 146 | firstChange: false, 147 | currentValue: [], 148 | previousValue: [objOptions[0]], 149 | }, 150 | } as any); 151 | expect(component.availableItems).toEqual(objOptions); 152 | done(); 153 | }, 3000); 154 | }); 155 | 156 | it("should handle change in ngOnchanges when options undefined", (done) => { 157 | component.options = undefined; 158 | component.ngOnChanges({ value: { firstChange: false } } as any); 159 | setTimeout(() => { 160 | component.options = objOptions; 161 | component.selectedItems = [objOptions[0]]; 162 | component.value = [objOptions[0], objOptions[1]]; 163 | component.ngOnChanges({ value: { firstChange: false } } as any); 164 | expect(component.selectedItems.length).toEqual(2); 165 | done(); 166 | }, 3000); 167 | }); 168 | it("when options are not passed ", () => { 169 | const undefinedOptions = undefined; 170 | component.options = undefinedOptions; 171 | component.ngOnInit(); 172 | expect(component.options).toEqual(undefined); 173 | }); 174 | 175 | it("should set available items when initializing", () => { 176 | component.options = objOptions; 177 | component.ngOnInit(); 178 | expect(component.availableItems).toEqual(component.options); 179 | }); 180 | 181 | it("should set the initial value passed while initializing", () => { 182 | component.options = objOptions; 183 | component.value = [objOptions[0]]; 184 | component.ngOnInit(); 185 | expect(component.selectedItems).toEqual([objOptions[0]]); 186 | }); 187 | 188 | it("should set the availableitems when initial value passed while initializing", () => { 189 | component.options = objOptions; 190 | component.value = [objOptions[0]]; 191 | component.ngOnInit(); 192 | expect(component.availableItems.length).toEqual(objOptions.length - 1); 193 | }); 194 | 195 | it("Should init the config", () => { 196 | (component as any).initDropdownValuesAndOptions(); 197 | expect(component.config).toBeTruthy(); 198 | }); 199 | 200 | it("Should init the config with display key", () => { 201 | (component as any).initDropdownValuesAndOptions(); 202 | expect(component.config.displayKey).toEqual("description"); 203 | }); 204 | 205 | it("Should handle the value initilization", () => { 206 | component.value = objOptions[0]; 207 | (component as any).initDropdownValuesAndOptions(); 208 | expect(component.selectedItems).toEqual([objOptions[0]]); 209 | }); 210 | 211 | it("Should write the value", () => { 212 | component.writeValue(objOptions[0], true); 213 | expect(component.value).toEqual(objOptions[0]); 214 | expect(component.selectedItems).toEqual([objOptions[0]]); 215 | }); 216 | 217 | // it('Should write the value and seleted items when already selected passed in initilization', () => { 218 | // component.writeValue(objOptions[0]); 219 | // expect(component.value).toEqual(objOptions[0]); 220 | // }); 221 | 222 | it("Should init the config with display one key missing", () => { 223 | component.config = { ...config, displayKey: "description" }; 224 | (component as any).initDropdownValuesAndOptions(); 225 | expect(component.config.height).toEqual("auto"); 226 | }); 227 | 228 | it("Should set the select text for selected items", () => { 229 | component.selectedItems = ["Option 1"]; 230 | (component as any).setSelectedDisplayText(); 231 | expect(component.selectedDisplayText).toEqual("Option 1"); 232 | }); 233 | 234 | it("Should set the select text with 0 selected items", () => { 235 | (component as any).setSelectedDisplayText(); 236 | expect(component.selectedDisplayText).toEqual("Select"); 237 | }); 238 | 239 | it("Should set the select text with 1 object as selected items", () => { 240 | component.selectedItems = [objOptions[0]]; 241 | component.multiple = false; 242 | component.config.displayKey = "name"; 243 | (component as any).setSelectedDisplayText(); 244 | expect(component.selectedDisplayText).toEqual("Burns Dalton"); 245 | }); 246 | 247 | it("Should set the select text with 1 object as selected items and use display function to display sub-object", () => { 248 | component.config.displayFn = (item: Account) => { 249 | return item.thirdPartyProfile.name; 250 | }; 251 | component.selectedItems = [objOptions[0]]; 252 | component.multiple = false; 253 | (component as any).setSelectedDisplayText(); 254 | expect(component.selectedDisplayText).toEqual("Burns.D"); 255 | }); 256 | 257 | it("Should Toggle Dropdown", () => { 258 | component.openSelectDropdown(); 259 | expect(component.toggleDropdown).toBeTruthy(); 260 | }); 261 | 262 | it("Should Toggle Dropdown", () => { 263 | component.toggleDropdown = true; 264 | component.closeSelectDropdown(); 265 | expect(component.toggleDropdown).toBeFalsy(); 266 | }); 267 | it("Should Toggle Dropdown and emit open event", () => { 268 | component.toggleDropdown = false; 269 | spyOn(component.open, "emit"); 270 | component.openSelectDropdown(); 271 | expect(component.open.emit).toHaveBeenCalled(); 272 | }); 273 | 274 | it("Should set the showNotfound text when no items found", () => { 275 | component.availableOptions = new QueryList(); 276 | component.availableOptions.reset([]); 277 | component.setNotFoundState(); 278 | expect(component.showNotFound).toEqual(true); 279 | }); 280 | 281 | it("Should set the showNotfound text when no items found", () => { 282 | component.availableOptions = new QueryList(); 283 | component.availableOptions.reset([ 284 | new ElementRef(document.createElement("li")), 285 | ]); 286 | component.setNotFoundState(); 287 | expect(component.showNotFound).toEqual(false); 288 | }); 289 | 290 | it("Should Toggle Dropdown and emit close event", () => { 291 | component.toggleDropdown = true; 292 | spyOn(component.close, "emit"); 293 | component.closeSelectDropdown(); 294 | expect(component.close.emit).toHaveBeenCalled(); 295 | }); 296 | 297 | it("Click inside component", () => { 298 | component.toggleDropdown = true; 299 | component.clickInsideComponent(); 300 | expect(component.toggleDropdown).toEqual(true); 301 | }); 302 | 303 | it("Click outside component", () => { 304 | component.toggleDropdown = true; 305 | component.clickOutsideComponent(); 306 | expect(component.toggleDropdown).toEqual(false); 307 | }); 308 | 309 | it("Click inside component propagates to documnet", () => { 310 | component.toggleDropdown = true; 311 | component.clickedInside = true; 312 | component.clickOutsideComponent(); 313 | expect(component.toggleDropdown).toEqual(true); 314 | }); 315 | 316 | it("Should select an item items with multiple false", () => { 317 | component.selectItem("Option 1", 0); 318 | expect(component.selectedItems).toEqual(["Option 1"]); 319 | }); 320 | 321 | it("Should select an item items with multiple true", () => { 322 | component.multiple = true; 323 | component.selectItem("Option 1", 0); 324 | expect(component.selectedItems).toEqual(["Option 1"]); 325 | }); 326 | 327 | it("Should select an item items with multiple false and already selected items", () => { 328 | component.selectedItems = ["Option 1"]; 329 | component.selectItem("Option 2", 1); 330 | expect(component.selectedItems).toEqual(["Option 2"]); 331 | }); 332 | 333 | it("Should deselect an item", () => { 334 | component.selectedItems = ["Option 1"]; 335 | component.deselectItem("Option 1", 0); 336 | expect(component.selectedItems).toEqual([]); 337 | }); 338 | 339 | it("Should set the value", () => { 340 | component.multiple = true; 341 | component.selectedItems = ["Option 1", "Option 2"]; 342 | component.valueChanged(); 343 | expect(component.value).toEqual(["Option 1", "Option 2"]); 344 | }); 345 | 346 | it("Should set the selected text for multi select", () => { 347 | component.selectedItems = ["Option 1", "Option 2"]; 348 | component.multiple = true; 349 | component.valueChanged(); 350 | expect(component.selectedDisplayText).toEqual("Option 1 + 1 more"); 351 | }); 352 | 353 | it("Should set the selected text for single select with string options", () => { 354 | component.selectedItems = ["Option 1"]; 355 | component.valueChanged(); 356 | expect(component.selectedDisplayText).toEqual("Option 1"); 357 | }); 358 | 359 | it("Should set the selected text for single select when nothing selected", () => { 360 | component.selectedItems = []; 361 | component.valueChanged(); 362 | expect(component.selectedDisplayText).toEqual("Select"); 363 | }); 364 | 365 | it("Should handle the key pressed outside event", () => { 366 | component.insideKeyPress = true; 367 | component.KeyPressOutsideComponent(); 368 | expect(component.insideKeyPress).toEqual(false); 369 | }); 370 | 371 | it("Should handle the key pressed outside event toggle dropdown", () => { 372 | component.insideKeyPress = false; 373 | component.KeyPressOutsideComponent(); 374 | expect(component.toggleDropdown).toEqual(false); 375 | }); 376 | 377 | it("Should not duplicate items when deselect in avaialable items", (done) => { 378 | component.options = objOptions; 379 | component.selectedItems = [objOptions[0], objOptions[1]]; 380 | component.availableItems = [objOptions[0]]; 381 | component.deselectItem(objOptions[1], 1); 382 | setTimeout(() => { 383 | expect(component.availableItems).toEqual([objOptions[0], objOptions[1]]); 384 | done(); 385 | }, 300); 386 | }); 387 | 388 | it("Should handle the up arrow key", () => { 389 | component.options = objOptions; 390 | component.availableItems = [...objOptions]; 391 | (component as any).onArrowKeyUp(); 392 | expect(component.focusedItemIndex).toEqual(0); 393 | }); 394 | 395 | it("Should handle the down arrow key", () => { 396 | component.options = objOptions; 397 | component.availableItems = [...objOptions]; 398 | (component as any).onArrowKeyDown(); 399 | expect(component.focusedItemIndex).toEqual(0); 400 | }); 401 | 402 | it("Should handle the down arrow key event when focus index already set", () => { 403 | component.options = objOptions; 404 | component.availableItems = [...objOptions]; 405 | component.focusedItemIndex = 0; 406 | (component as any).onArrowKeyDown(); 407 | expect(component.focusedItemIndex).toEqual(1); 408 | }); 409 | 410 | it("Should handle the down arrow key event when focus index already set to last item", () => { 411 | component.options = objOptions; 412 | component.availableItems = [...objOptions]; 413 | component.focusedItemIndex = component.availableItems.length - 1; 414 | (component as any).onArrowKeyDown(); 415 | expect(component.focusedItemIndex).toEqual(0); 416 | }); 417 | 418 | it("Should handle the up arrow key event when focus index already set", () => { 419 | component.options = objOptions; 420 | component.availableItems = [...objOptions]; 421 | component.focusedItemIndex = 1; 422 | (component as any).onArrowKeyUp(); 423 | expect(component.focusedItemIndex).toEqual(0); 424 | }); 425 | 426 | it("Should handle the up arrow key event when focus index already set to first item", () => { 427 | component.options = objOptions; 428 | component.availableItems = [...objOptions]; 429 | component.focusedItemIndex = 0; 430 | (component as any).onArrowKeyUp(); 431 | expect(component.focusedItemIndex).toEqual(objOptions.length - 1); 432 | }); 433 | 434 | it("Should handle the arrow key event for up", () => { 435 | component.options = objOptions; 436 | component.focusedItemIndex = null; 437 | component.availableItems = [...objOptions]; 438 | component.availableOptions = new QueryList(); 439 | component.availableOptions.reset([ 440 | new ElementRef(document.createElement("li")), 441 | ]); 442 | component.handleKeyboardEvent({ 443 | keyCode: 38, 444 | preventDefault: () => { 445 | return; 446 | }, 447 | } as any); 448 | expect(component.focusedItemIndex).toEqual(0); 449 | }); 450 | 451 | it("Should handle the arrow key event for down", () => { 452 | component.options = objOptions; 453 | component.focusedItemIndex = null; 454 | component.availableItems = [...objOptions]; 455 | component.availableOptions = new QueryList(); 456 | component.availableOptions.reset([ 457 | new ElementRef(document.createElement("li")), 458 | ]); 459 | component.handleKeyboardEvent({ 460 | keyCode: 40, 461 | preventDefault: () => { 462 | return; 463 | }, 464 | } as any); 465 | expect(component.focusedItemIndex).toEqual(0); 466 | }); 467 | 468 | it("Should handle the arrow key event for down when escape key", () => { 469 | component.handleKeyboardEvent({ 470 | keyCode: 27, 471 | preventDefault: () => { 472 | return; 473 | }, 474 | } as any); 475 | expect(component.toggleDropdown).toEqual(false); 476 | }); 477 | 478 | it("Should handle the arrow key event for down", () => { 479 | component.options = objOptions; 480 | // const event = new KeyboardEvent('Enter'); 481 | component.availableItems = [...objOptions]; 482 | component.focusedItemIndex = 1; 483 | component.handleKeyboardEvent({ 484 | keyCode: 13, 485 | preventDefault: () => { 486 | return; 487 | }, 488 | } as any); 489 | expect(component.selectedItems).toEqual([objOptions[1]]); 490 | }); 491 | 492 | it("Should register registerOnChange", () => { 493 | const func = () => { 494 | return; 495 | }; 496 | component.registerOnChange(func); 497 | expect(component.onChange).toEqual(func); 498 | }); 499 | 500 | it("Should register registerOnTouched", () => { 501 | const fun = () => { 502 | return; 503 | }; 504 | component.registerOnTouched(fun); 505 | expect(component.onTouched).toEqual(fun); 506 | }); 507 | 508 | it("Should emit the search text changed event", () => { 509 | spyOn(component.searchChange, "emit"); 510 | (component as any).searchTextChanged(); 511 | expect(component.searchChange.emit).toHaveBeenCalled(); 512 | }); 513 | 514 | it("Should reset", () => { 515 | component.selectedItems = [...options]; 516 | component.reset(); 517 | expect(component.selectedItems.length).toEqual(0); 518 | }); 519 | 520 | it("Should focus", () => { 521 | component.focus(); 522 | expect(component.toggleDropdown).toBeTruthy(); 523 | }); 524 | 525 | it("Should not focus", () => { 526 | component.disabled = true; 527 | component.focus(); 528 | expect(component.toggleDropdown).toBeFalsy(); 529 | }); 530 | 531 | it("Should blur", () => { 532 | component.toggleDropdown = true; 533 | component.blur(new KeyboardEvent("keydown")); 534 | expect(component.toggleDropdown).toBeFalsy(); 535 | }); 536 | 537 | it("Should blur not close", () => { 538 | component.toggleDropdown = true; 539 | component.blur(new MouseEvent('click')); 540 | expect(component.toggleDropdown).toBeTruthy(); 541 | }); 542 | 543 | }); 544 | -------------------------------------------------------------------------------- /projects/ngx-select-dropdown/src/lib/ngx-select-dropdown.component.ts: -------------------------------------------------------------------------------- 1 | import { NgxDropdownConfig } from "./types/ngx-select-dropdown.types"; 2 | import { SelectDropDownService } from "./ngx-select-dropdown.service"; 3 | import { FilterByPipe } from "./pipes/filter-by.pipe"; 4 | import { 5 | Component, 6 | OnInit, 7 | Input, 8 | EventEmitter, 9 | Output, 10 | HostListener, 11 | OnChanges, 12 | SimpleChanges, 13 | ViewChildren, 14 | ElementRef, 15 | QueryList, 16 | AfterViewInit, 17 | ChangeDetectorRef, 18 | forwardRef, 19 | ViewChild, 20 | HostBinding, 21 | TemplateRef, 22 | } from "@angular/core"; 23 | import { NG_VALUE_ACCESSOR } from "@angular/forms"; 24 | const config: NgxDropdownConfig = { 25 | displayKey: "description", 26 | height: "auto", 27 | search: false, 28 | placeholder: "Select", 29 | searchPlaceholder: "Search...", 30 | limitTo: 0, 31 | customComparator: undefined, 32 | noResultsFound: "No results found!", 33 | moreText: "more", 34 | searchOnKey: null, 35 | clearOnSelection: false, 36 | inputDirection: "ltr", 37 | selectAllLabel: "Select all", 38 | enableSelectAll: false, 39 | }; 40 | @Component({ 41 | selector: "ngx-select-dropdown", 42 | templateUrl: "./ngx-select-dropdown.component.html", 43 | styleUrls: ["./ngx-select-dropdown.component.scss"], 44 | providers: [ 45 | { 46 | provide: NG_VALUE_ACCESSOR, 47 | useExisting: forwardRef(() => NgxSelectDropdownComponent), 48 | multi: true, 49 | }, 50 | ], 51 | }) 52 | export class NgxSelectDropdownComponent 53 | implements OnInit, OnChanges, AfterViewInit 54 | { 55 | /** value of the dropdown */ 56 | @Input() public _value: any; 57 | 58 | /** 59 | * Get the required inputs 60 | */ 61 | @Input() public options: any = []; 62 | 63 | /** 64 | * configuration options 65 | */ 66 | @Input() public config: NgxDropdownConfig = config; 67 | 68 | /** 69 | * Whether multiple selection or single selection allowed 70 | */ 71 | @Input() public multiple = false; 72 | 73 | /** 74 | * Flag to disbale the dropdown 75 | */ 76 | @Input() public disabled: boolean; 77 | 78 | /** unique identifier to uniquely identify particular instance */ 79 | @Input() public instanceId: any; 80 | 81 | /** Template ref for the selected item */ 82 | @Input() selectedItemTemplate: TemplateRef; 83 | 84 | /** Template ref for the avilable item */ 85 | @Input() optionItemTemplate: TemplateRef; 86 | 87 | /** Template ref for the no matched found case */ 88 | @Input() notFoundTemplate: TemplateRef; 89 | 90 | /** Template ref for the button */ 91 | @Input() dropdownButtonTemplate: TemplateRef; 92 | 93 | /** 94 | * change event when value changes to provide user to handle things in change event 95 | */ 96 | @Output() public change: EventEmitter = new EventEmitter(); 97 | 98 | /** 99 | * The search text change event emitter emitted when user type in the search input 100 | */ 101 | @Output() public searchChange: EventEmitter = new EventEmitter(); 102 | 103 | /** 104 | * Event emitted when dropdown is open. 105 | */ 106 | @Output() public open: EventEmitter = new EventEmitter(); 107 | 108 | /** 109 | * Event emitted when dropdown is open. 110 | */ 111 | @Output() public close: EventEmitter = new EventEmitter(); 112 | 113 | /** 114 | * Toogle the dropdown list 115 | */ 116 | public toggleDropdown = false; 117 | 118 | /** 119 | * Available items for selection 120 | */ 121 | public availableItems: any = []; 122 | 123 | /** 124 | * Selected Items 125 | */ 126 | public selectedItems: any = []; 127 | 128 | /** 129 | * Selection text to be Displayed 130 | */ 131 | public selectedDisplayText = "Select"; 132 | 133 | /** 134 | * Search text 135 | */ 136 | public searchText: string; 137 | 138 | /** 139 | * variable to track if clicked inside or outside of component 140 | */ 141 | public clickedInside = false; 142 | 143 | /** 144 | * variable to track keypress event inside and outsid of component 145 | */ 146 | public insideKeyPress = false; 147 | 148 | /** 149 | * variable to track the focused item whenuser uses arrow keys to select item 150 | */ 151 | public focusedItemIndex: number = null; 152 | 153 | /** 154 | * element to show not found text when not itmes match the search 155 | */ 156 | 157 | public showNotFound = false; 158 | 159 | /** 160 | * The position from the top of the element in pixels to drop according to the visibility in viewport 161 | */ 162 | public top: string; 163 | 164 | /** 165 | * Flag to indicate that the click initiation was on one of the availabe or selected options 166 | * This is to track the mouse down event especially in Safari. 167 | */ 168 | public optionMouseDown: boolean; 169 | 170 | /** 171 | * Element ref of the dropdown list DOM element 172 | */ 173 | private dropdownList: ElementRef; 174 | 175 | /** 176 | * Flag for select all option 177 | */ 178 | public selectAll: boolean; 179 | 180 | /** 181 | * Hold the reference to available items in the list to focus on the item when scrolling 182 | */ 183 | @ViewChildren("availableOption") 184 | public availableOptions: QueryList; 185 | 186 | get value() { 187 | return this._value; 188 | } 189 | set value(val) { 190 | this._value = val; 191 | this.onChange(val); 192 | this.onTouched(); 193 | } 194 | 195 | constructor( 196 | private cdref: ChangeDetectorRef, 197 | public _elementRef: ElementRef, 198 | private dropdownService: SelectDropDownService 199 | ) { 200 | this.multiple = false; 201 | this.selectAll = false; 202 | } 203 | 204 | public onChange: any = () => { 205 | // empty 206 | }; 207 | public onTouched: any = () => { 208 | // empty 209 | }; 210 | 211 | /** 212 | * click listener for host inside this component i.e 213 | * if many instances are there, this detects if clicked inside 214 | * this instance 215 | */ 216 | @HostListener("click") 217 | public clickInsideComponent() { 218 | this.clickedInside = true; 219 | } 220 | /** 221 | * View reference for the dorpdown list 222 | */ 223 | @ViewChild("dropdownList") set dropDownElement(ref: ElementRef) { 224 | if (ref) { 225 | // initially setter gets called with undefined 226 | this.dropdownList = ref; 227 | } 228 | } 229 | 230 | /** 231 | * Event listener for the blur event to hide the dropdown 232 | */ 233 | @HostListener("blur") public blur($event: Event) { 234 | if ( 235 | !this.insideKeyPress && 236 | !this.optionMouseDown && 237 | $event instanceof KeyboardEvent 238 | ) { 239 | this.toggleDropdown = false; 240 | this.openStateChange(); 241 | } 242 | } 243 | 244 | /** 245 | * Event listener for the focus event to show the dropdown when using tab key 246 | */ 247 | @HostListener("focus") public focus() { 248 | /* istanbul ignore else */ 249 | if (!this.disabled) { 250 | this.toggleDropdown = true; 251 | this.openStateChange(); 252 | } 253 | } 254 | /** 255 | * click handler on documnent to hide the open dropdown if clicked outside 256 | */ 257 | @HostListener("document:click") 258 | public clickOutsideComponent() { 259 | /* istanbul ignore else */ 260 | if (!this.clickedInside) { 261 | this.toggleDropdown = false; 262 | this.openStateChange(); 263 | this.resetArrowKeyActiveElement(); 264 | // clear searh on close 265 | this.searchText = null; 266 | this.close.emit(); 267 | } 268 | this.clickedInside = false; 269 | } 270 | 271 | /** 272 | * click handler on documnent to hide the open dropdown if clicked outside 273 | */ 274 | @HostListener("document:keydown") 275 | public KeyPressOutsideComponent() { 276 | /* istanbul ignore else */ 277 | if (!this.insideKeyPress) { 278 | this.toggleDropdown = false; 279 | this.openStateChange(); 280 | this.resetArrowKeyActiveElement(); 281 | } 282 | this.insideKeyPress = false; 283 | } 284 | 285 | /** 286 | * Binding to set the tabindex property to set to 0 for accessibilty 287 | */ 288 | @HostBinding("attr.tabindex") tabindex = 0; 289 | /** 290 | * Event handler for key up and down event and enter press for selecting element 291 | */ 292 | @HostListener("keydown", ["$event"]) 293 | public handleKeyboardEvent($event: KeyboardEvent | any) { 294 | this.insideKeyPress = true; 295 | /* istanbul ignore else */ 296 | if ($event.keyCode === 27 || this.disabled) { 297 | this.toggleDropdown = false; 298 | this.openStateChange(); 299 | this.insideKeyPress = false; 300 | return; 301 | } 302 | const avaOpts = this.availableOptions.toArray(); 303 | /* istanbul ignore else */ 304 | if ($event.keyCode !== 9 && avaOpts.length === 0 && !this.toggleDropdown) { 305 | this.toggleDropdown = true; 306 | this.openStateChange(); 307 | } 308 | // Arrow Down 309 | /* istanbul ignore else */ 310 | if ($event.keyCode === 40 && avaOpts.length > 0) { 311 | this.onArrowKeyDown(); 312 | /* istanbul ignore else */ 313 | if (this.focusedItemIndex >= avaOpts.length) { 314 | this.focusedItemIndex = 0; 315 | } 316 | avaOpts[this.focusedItemIndex].nativeElement.focus(); 317 | $event.preventDefault(); 318 | } 319 | // Arrow Up 320 | /* istanbul ignore else */ 321 | if ($event.keyCode === 38 && avaOpts.length) { 322 | this.onArrowKeyUp(); 323 | /* istanbul ignore else */ 324 | if (this.focusedItemIndex >= avaOpts.length) { 325 | this.focusedItemIndex = avaOpts.length - 1; 326 | } 327 | avaOpts[this.focusedItemIndex].nativeElement.focus(); 328 | $event.preventDefault(); 329 | } 330 | // Enter 331 | /* istanbul ignore else */ 332 | if ($event.keyCode === 13 && this.focusedItemIndex !== null) { 333 | const filteredItems = new FilterByPipe().transform( 334 | this.availableItems, 335 | this.searchText, 336 | this.config.searchOnKey 337 | ); 338 | this.selectItem( 339 | filteredItems[this.focusedItemIndex], 340 | this.availableItems.indexOf(filteredItems[this.focusedItemIndex]) 341 | ); 342 | return false; 343 | } 344 | } 345 | 346 | /** 347 | * Component onInit 348 | */ 349 | public ngOnInit() { 350 | /* istanbul ignore else */ 351 | if ( 352 | typeof this.options !== "undefined" && 353 | typeof this.config !== "undefined" && 354 | Array.isArray(this.options) 355 | ) { 356 | this.availableItems = [ 357 | ...this.options.sort(this.config.customComparator), 358 | ]; 359 | this.initDropdownValuesAndOptions(); 360 | } 361 | this.serviceSubscriptions(); 362 | } 363 | 364 | isVisible() { 365 | if (!this.dropdownList) { 366 | return { visible: false, element: null }; 367 | } 368 | const el = this.dropdownList.nativeElement; 369 | if (!el) { 370 | return { visible: false, element: el }; 371 | } 372 | const rect = el.getBoundingClientRect(); 373 | const topShown = rect.top >= 0; 374 | const bottomShown = rect.bottom <= window.innerHeight; 375 | return { visible: topShown && bottomShown, element: el }; 376 | } 377 | 378 | serviceSubscriptions() { 379 | this.dropdownService.openDropdownInstance.subscribe((instanceId) => { 380 | if (this.instanceId === instanceId) { 381 | this.toggleDropdown = true; 382 | this.openStateChange(); 383 | this.resetArrowKeyActiveElement(); 384 | } 385 | }); 386 | this.dropdownService.closeDropdownInstance.subscribe((instanceId) => { 387 | if (this.instanceId === instanceId) { 388 | this.toggleDropdown = false; 389 | this.openStateChange(); 390 | this.resetArrowKeyActiveElement(); 391 | } 392 | }); 393 | } 394 | 395 | /** 396 | * after view init to subscribe to available option changes 397 | */ 398 | public ngAfterViewInit() { 399 | this.availableOptions.changes.subscribe(this.setNotFoundState.bind(this)); 400 | } 401 | 402 | public registerOnChange(fn: any) { 403 | this.onChange = fn; 404 | } 405 | 406 | public registerOnTouched(fn: any) { 407 | this.onTouched = fn; 408 | } 409 | 410 | public setDisabledState(isDisabled: boolean) { 411 | this.disabled = isDisabled; 412 | } 413 | 414 | public writeValue(value: any, internal?: boolean) { 415 | if (value) { 416 | if (Array.isArray(value)) { 417 | if (this.multiple) { 418 | this.value = value; 419 | } else if (value.length > 0) { 420 | this.value = value[0]; 421 | } 422 | } else { 423 | this.value = value; 424 | } 425 | /* istanbul ignore else */ 426 | if (this.selectedItems.length === 0) { 427 | if (Array.isArray(value)) { 428 | this.selectedItems = value; 429 | } else { 430 | this.selectedItems.push(value); 431 | } 432 | this.initDropdownValuesAndOptions(); 433 | } 434 | } else { 435 | this.value = []; 436 | /* istanbul ignore else */ 437 | if (!internal) { 438 | this.reset(); 439 | } 440 | } 441 | /* istanbul ignore else */ 442 | if (!internal) { 443 | this.reset(); 444 | } 445 | } 446 | 447 | public reset() { 448 | if (!this.config) { 449 | return; 450 | } 451 | this.selectedItems = []; 452 | this.availableItems = [...this.options.sort(this.config.customComparator)]; 453 | this.initDropdownValuesAndOptions(); 454 | } 455 | /** 456 | * function sets whether to show items not found text or not 457 | */ 458 | public setNotFoundState() { 459 | if ( 460 | this.availableOptions.length === 0 && 461 | this.selectedItems.length !== this.options.length 462 | ) { 463 | this.showNotFound = true; 464 | } else { 465 | this.showNotFound = false; 466 | } 467 | this.cdref.detectChanges(); 468 | } 469 | /** 470 | * Component onchage i.e when any of the input properties change 471 | */ 472 | public ngOnChanges(changes: SimpleChanges) { 473 | if (!this.config) { 474 | return; 475 | } 476 | this.selectedItems = []; 477 | // this.searchText = null; 478 | this.options = this.options || []; 479 | /* istanbul ignore else */ 480 | if (changes.options) { 481 | this.availableItems = [ 482 | ...this.options.sort(this.config.customComparator), 483 | ]; 484 | } 485 | /* istanbul ignore else */ 486 | if (changes.value) { 487 | /* istanbul ignore else */ 488 | if ( 489 | JSON.stringify(changes.value.currentValue) === JSON.stringify([]) || 490 | changes.value.currentValue === "" || 491 | changes.value.currentValue === null 492 | ) { 493 | this.availableItems = [ 494 | ...this.options.sort(this.config.customComparator), 495 | ]; 496 | } 497 | } 498 | this.initDropdownValuesAndOptions(); 499 | } 500 | 501 | /** 502 | * Deselct a selected items 503 | * @param item: item to be deselected 504 | * @param index: index of the item 505 | */ 506 | public deselectItem(item: any, index: number) { 507 | this.selectedItems.forEach((element: any, i: number) => { 508 | /* istanbul ignore else */ 509 | if (item === element) { 510 | this.selectedItems.splice(i, 1); 511 | } 512 | }); 513 | let sortedItems = [...this.availableItems]; 514 | /* istanbul ignore else */ 515 | if (!this.availableItems.includes(item)) { 516 | this.availableItems.push(item); 517 | sortedItems = this.availableItems.sort(this.config.customComparator); 518 | } 519 | this.selectedItems = [...this.selectedItems]; 520 | this.availableItems = [...sortedItems]; 521 | /* istanbul ignore else */ 522 | if (!Array.isArray(this.value)) { 523 | this.value = []; 524 | } 525 | if (!this.areAllSelected()) { 526 | this.selectAll = false; 527 | } 528 | this.valueChanged(); 529 | this.resetArrowKeyActiveElement(); 530 | } 531 | 532 | /** 533 | * Select an item 534 | * @param item: item to be selected 535 | * @param index: index of the item 536 | */ 537 | public selectItem(item: string, index?: number) { 538 | /* istanbul ignore else */ 539 | if (!this.multiple) { 540 | /* istanbul ignore else */ 541 | if (this.selectedItems.length > 0) { 542 | this.availableItems.push(this.selectedItems[0]); 543 | } 544 | this.selectedItems = []; 545 | this.toggleDropdown = false; 546 | } 547 | 548 | this.availableItems.forEach((element: any, i: number) => { 549 | /* istanbul ignore else */ 550 | if (item === element) { 551 | this.selectedItems.push(item); 552 | this.availableItems.splice(i, 1); 553 | } 554 | }); 555 | 556 | /* istanbul ignore else */ 557 | if (this.config.clearOnSelection) { 558 | this.searchText = null; 559 | } 560 | 561 | this.selectedItems = [...this.selectedItems]; 562 | this.availableItems = [...this.availableItems]; 563 | this.selectedItems.sort(this.config.customComparator); 564 | this.availableItems.sort(this.config.customComparator); 565 | // this.searchText = null; 566 | /* istanbul ignore else */ 567 | if (this.areAllSelected()) { 568 | this.selectAll = true; 569 | } 570 | this.valueChanged(); 571 | this.resetArrowKeyActiveElement(); 572 | } 573 | 574 | /** 575 | * When selected items changes trigger the chaange back to parent 576 | */ 577 | public valueChanged() { 578 | this.writeValue(this.selectedItems, true); 579 | // this.valueChange.emit(this.value); 580 | this.change.emit({ value: this.value }); 581 | this.setSelectedDisplayText(); 582 | } 583 | 584 | /** 585 | * Toggle the dropdownlist on/off 586 | */ 587 | public openSelectDropdown() { 588 | this.toggleDropdown = true; 589 | this.top = "30px"; 590 | this.openStateChange(); 591 | this.resetArrowKeyActiveElement(); 592 | setTimeout(() => { 593 | const { visible, element } = this.isVisible(); 594 | if (element) { 595 | this.top = visible 596 | ? "30px" 597 | : `-${element.getBoundingClientRect().height}px`; 598 | } 599 | }, 3); 600 | } 601 | 602 | public closeSelectDropdown() { 603 | this.toggleDropdown = false; 604 | this.openStateChange(); 605 | this.resetArrowKeyActiveElement(); 606 | } 607 | 608 | public openStateChange() { 609 | if (this.toggleDropdown) { 610 | this.dropdownService.openInstances.push(this.instanceId); 611 | this.open.emit(); 612 | } else { 613 | this.searchText = null; 614 | this.optionMouseDown = false; 615 | this.close.emit(); 616 | this.dropdownService.openInstances.splice( 617 | this.dropdownService.openInstances.indexOf(this.instanceId), 618 | 1 619 | ); 620 | } 621 | } 622 | 623 | /** 624 | * The change handler for search text 625 | */ 626 | public searchTextChanged() { 627 | this.searchChange.emit(this.searchText); 628 | } 629 | 630 | public changeSearchText($event: any) { 631 | $event.stopPropagation(); 632 | } 633 | 634 | /** 635 | * initialize the config and other properties 636 | */ 637 | private initDropdownValuesAndOptions() { 638 | /* istanbul ignore else */ 639 | if ( 640 | typeof this.config === "undefined" || 641 | Object.keys(this.config).length === 0 642 | ) { 643 | this.config = { ...config }; 644 | } 645 | for (const key of Object.keys(config)) { 646 | this.config[key] = this.config[key] ? this.config[key] : config[key]; 647 | } 648 | this.config = { ...this.config }; 649 | // Adding placeholder in config as default param 650 | this.selectedDisplayText = this.config["placeholder"]; 651 | /* istanbul ignore else */ 652 | if (this.value !== "" && typeof this.value !== "undefined") { 653 | if (Array.isArray(this.value)) { 654 | this.selectedItems = this.value; 655 | } else if (this.value !== "" && this.value !== null) { 656 | this.selectedItems[0] = this.value; 657 | } else { 658 | this.selectedItems = []; 659 | this.value = []; 660 | } 661 | 662 | this.selectedItems.forEach((item: any) => { 663 | const ind = this.availableItems.findIndex( 664 | (aItem: any) => JSON.stringify(item) === JSON.stringify(aItem) 665 | ); 666 | if (ind !== -1) { 667 | this.availableItems.splice(ind, 1); 668 | } 669 | }); 670 | } 671 | this.setSelectedDisplayText(); 672 | } 673 | 674 | /** 675 | * set the text to be displayed 676 | */ 677 | private setSelectedDisplayText() { 678 | let text: string = this.selectedItems[0]; 679 | /* istanbul ignore else */ 680 | if (typeof this.selectedItems[0] === "object") { 681 | text = this.config.displayFn 682 | ? this.config.displayFn(this.selectedItems[0]) 683 | : this.selectedItems[0][this.config.displayKey]; 684 | } 685 | if (this.multiple && this.selectedItems.length > 0) { 686 | this.selectedDisplayText = 687 | this.selectedItems.length === 1 688 | ? text 689 | : text + 690 | ` + ${this.selectedItems.length - 1} ${this.config.moreText}`; 691 | } else { 692 | this.selectedDisplayText = 693 | this.selectedItems.length === 0 ? this.config.placeholder : text; 694 | } 695 | } 696 | 697 | /** 698 | * Event handler for arrow key up event thats focuses on a item 699 | */ 700 | private onArrowKeyUp() { 701 | /* istanbul ignore else */ 702 | if (this.focusedItemIndex === 0) { 703 | this.focusedItemIndex = this.availableItems.length - 1; 704 | return; 705 | } 706 | /* istanbul ignore else */ 707 | if (this.onArrowKey()) { 708 | this.focusedItemIndex--; 709 | } 710 | } 711 | 712 | /** 713 | * Event handler for arrow key down event thats focuses on a item 714 | */ 715 | private onArrowKeyDown() { 716 | /* istanbul ignore else */ 717 | if (this.focusedItemIndex === this.availableItems.length - 1) { 718 | this.focusedItemIndex = 0; 719 | return; 720 | } 721 | /* istanbul ignore else */ 722 | if (this.onArrowKey()) { 723 | this.focusedItemIndex++; 724 | } 725 | } 726 | 727 | private onArrowKey() { 728 | /* istanbul ignore else */ 729 | if (this.focusedItemIndex === null) { 730 | this.focusedItemIndex = 0; 731 | return false; 732 | } 733 | return true; 734 | } 735 | 736 | /** 737 | * will reset the element that is marked active using arrow keys 738 | */ 739 | private resetArrowKeyActiveElement() { 740 | this.focusedItemIndex = null; 741 | } 742 | 743 | /** 744 | * Toggle the select all option 745 | */ 746 | public toggleSelectAll(close?: boolean, emitChange?: boolean): void { 747 | this.selectAll = !this.selectAll; 748 | if (this.selectAll) { 749 | this.selectedItems = [...this.selectedItems, ...this.availableItems]; 750 | this.availableItems = []; 751 | } else { 752 | this.availableItems = [...this.selectedItems, ...this.availableItems]; 753 | this.selectedItems = []; 754 | } 755 | this.selectedItems.sort(this.config.customComparator); 756 | this.availableItems.sort(this.config.customComparator); 757 | this.valueChanged(); 758 | this.closeSelectDropdown(); 759 | this.openStateChange(); 760 | this.resetArrowKeyActiveElement(); 761 | } 762 | 763 | /** 764 | * Check if all options selected 765 | */ 766 | areAllSelected() { 767 | return this.selectedItems.length === this.options.length; 768 | } 769 | } 770 | --------------------------------------------------------------------------------