├── src ├── demo │ ├── app │ │ ├── examples │ │ │ ├── tags-backend-example │ │ │ │ ├── tags-backend-example.component.scss │ │ │ │ ├── tags-backend-example.component.html │ │ │ │ └── tags-backend-example.component.ts │ │ │ ├── tags-custom-example │ │ │ │ ├── tags-custom-example.component.scss │ │ │ │ ├── tags-custom-example.component.html │ │ │ │ └── tags-custom-example.component.ts │ │ │ ├── tags-default-example │ │ │ │ ├── tags-default-example.component.scss │ │ │ │ ├── tags-default-example.component.html │ │ │ │ └── tags-default-example.component.ts │ │ │ ├── group-children-example │ │ │ │ ├── group-children-example.component.scss │ │ │ │ ├── group-children-example.component.html │ │ │ │ └── group-children-example.component.ts │ │ │ ├── group-default-example │ │ │ │ ├── group-default-example.component.scss │ │ │ │ ├── group-default-example.component.html │ │ │ │ └── group-default-example.component.ts │ │ │ ├── group-function-example │ │ │ │ ├── group-function-example.component.scss │ │ │ │ ├── group-function-example.component.html │ │ │ │ └── group-function-example.component.ts │ │ │ ├── multi-checkbox-example │ │ │ │ ├── multi-checkbox-example.component.scss │ │ │ │ ├── multi-checkbox-example.component.html │ │ │ │ └── multi-checkbox-example.component.ts │ │ │ ├── output-events-example │ │ │ │ ├── output-events-example.component.scss │ │ │ │ ├── output-events-example.component.html │ │ │ │ └── output-events-example.component.ts │ │ │ ├── search-custom-example │ │ │ │ ├── search-custom-example.component.scss │ │ │ │ ├── search-custom-example.component.html │ │ │ │ └── search-custom-example.component.ts │ │ │ ├── search-default-example │ │ │ │ ├── search-default-example.component.scss │ │ │ │ ├── search-default-example.component.html │ │ │ │ └── search-default-example.component.ts │ │ │ ├── template-label-example │ │ │ │ ├── template-label-example.component.scss │ │ │ │ ├── template-label-example.component.html │ │ │ │ └── template-label-example.component.ts │ │ │ ├── virtual-scroll-example │ │ │ │ ├── virtual-scroll-example.component.scss │ │ │ │ ├── virtual-scroll-example.component.html │ │ │ │ └── virtual-scroll-example.component.ts │ │ │ ├── bindings-custom-example │ │ │ │ ├── bindings-custom-example.component.scss │ │ │ │ ├── bindings-custom-example.component.html │ │ │ │ └── bindings-custom-example.component.ts │ │ │ ├── bindings-default-example │ │ │ │ ├── bindings-default-example.component.scss │ │ │ │ ├── bindings-default-example.component.html │ │ │ │ └── bindings-default-example.component.ts │ │ │ ├── bindings-nested-example │ │ │ │ ├── bindings-nested-example.component.scss │ │ │ │ ├── bindings-nested-example.component.html │ │ │ │ └── bindings-nested-example.component.ts │ │ │ ├── data-source-array-example │ │ │ │ ├── data-source-array-example.component.scss │ │ │ │ ├── data-source-array-example.component.html │ │ │ │ └── data-source-array-example.component.ts │ │ │ ├── dropdown-position-example │ │ │ │ ├── dropdown-position-example.component.scss │ │ │ │ ├── dropdown-position-example.component.html │ │ │ │ └── dropdown-position-example.component.ts │ │ │ ├── forms-async-data-example │ │ │ │ ├── forms-async-data-example.component.scss │ │ │ │ ├── forms-async-data-example.component.html │ │ │ │ └── forms-async-data-example.component.ts │ │ │ ├── group-selectable-example │ │ │ │ ├── group-selectable-example.component.scss │ │ │ │ ├── group-selectable-example.component.html │ │ │ │ └── group-selectable-example.component.ts │ │ │ ├── search-editable-example │ │ │ │ ├── search-editable-example.component.scss │ │ │ │ ├── search-editable-example.component.html │ │ │ │ └── search-editable-example.component.ts │ │ │ ├── template-display-example │ │ │ │ ├── template-display-example.component.scss │ │ │ │ ├── template-display-example.component.html │ │ │ │ └── template-display-example.component.ts │ │ │ ├── template-optgroup-example │ │ │ │ ├── template-optgroup-example.component.scss │ │ │ │ ├── template-optgroup-example.component.html │ │ │ │ └── template-optgroup-example.component.ts │ │ │ ├── template-option-example │ │ │ │ ├── template-option-example.component.scss │ │ │ │ ├── template-option-example.component.html │ │ │ │ └── template-option-example.component.ts │ │ │ ├── template-search-example │ │ │ │ ├── template-search-example.component.scss │ │ │ │ ├── template-search-example.component.html │ │ │ │ └── template-search-example.component.ts │ │ │ ├── data-source-backend-example │ │ │ │ ├── data-source-backend-example.component.scss │ │ │ │ ├── data-source-backend-example.component.html │ │ │ │ └── data-source-backend-example.component.ts │ │ │ ├── data-source-options-example │ │ │ │ ├── data-source-options-example.component.scss │ │ │ │ ├── data-source-options-example.component.html │ │ │ │ └── data-source-options-example.component.ts │ │ │ ├── forms-multi-select-example │ │ │ │ ├── forms-multi-select-example.component.scss │ │ │ │ ├── forms-multi-select-example.component.html │ │ │ │ └── forms-multi-select-example.component.ts │ │ │ ├── forms-single-select-example │ │ │ │ ├── forms-single-select-example.component.scss │ │ │ │ ├── forms-single-select-example.component.html │ │ │ │ └── forms-single-select-example.component.ts │ │ │ ├── forms-with-options-example │ │ │ │ ├── forms-with-options-example.component.scss │ │ │ │ ├── forms-with-options-example.component.ts │ │ │ │ └── forms-with-options-example.component.html │ │ │ ├── multi-select-custom-example │ │ │ │ ├── multi-select-custom-example.component.scss │ │ │ │ ├── multi-select-custom-example.component.ts │ │ │ │ └── multi-select-custom-example.component.html │ │ │ ├── multi-select-hidden-example │ │ │ │ ├── multi-select-hidden-example.component.scss │ │ │ │ ├── multi-select-hidden-example.component.html │ │ │ │ └── multi-select-hidden-example.component.ts │ │ │ ├── multi-select-limit-example │ │ │ │ ├── multi-select-limit-example.component.scss │ │ │ │ ├── multi-select-limit-example.component.html │ │ │ │ └── multi-select-limit-example.component.ts │ │ │ ├── search-autocomplete-example │ │ │ │ ├── search-autocomplete-example.component.scss │ │ │ │ ├── search-autocomplete-example.component.html │ │ │ │ └── search-autocomplete-example.component.ts │ │ │ ├── forms-custom-template-example │ │ │ │ ├── forms-custom-template-example.component.scss │ │ │ │ ├── forms-custom-template-example.component.ts │ │ │ │ └── forms-custom-template-example.component.html │ │ │ ├── multi-checkbox-group-example │ │ │ │ ├── multi-checkbox-group-example.component.scss │ │ │ │ ├── multi-checkbox-group-example.component.html │ │ │ │ └── multi-checkbox-group-example.component.ts │ │ │ ├── multi-select-default-example │ │ │ │ ├── multi-select-default-example.component.scss │ │ │ │ ├── multi-select-default-example.component.html │ │ │ │ └── multi-select-default-example.component.ts │ │ │ ├── multi-select-disabled-example │ │ │ │ ├── multi-select-disabled-example.component.scss │ │ │ │ ├── multi-select-disabled-example.component.html │ │ │ │ └── multi-select-disabled-example.component.ts │ │ │ ├── multi-select-template-example │ │ │ │ ├── multi-select-template-example.component.scss │ │ │ │ ├── multi-select-template-example.component.ts │ │ │ │ └── multi-select-template-example.component.html │ │ │ ├── tags-closed-dropdown-example │ │ │ │ ├── tags-closed-dropdown-example.component.scss │ │ │ │ ├── tags-closed-dropdown-example.component.html │ │ │ │ └── tags-closed-dropdown-example.component.ts │ │ │ ├── template-header-footer-example │ │ │ │ ├── template-header-footer-example.component.scss │ │ │ │ ├── template-header-footer-example.component.html │ │ │ │ └── template-header-footer-example.component.ts │ │ │ ├── group-selectable-hidden-example │ │ │ │ ├── group-selectable-hidden-example.component.scss │ │ │ │ ├── group-selectable-hidden-example.component.html │ │ │ │ └── group-selectable-hidden-example.component.ts │ │ │ ├── template-loading-example │ │ │ │ ├── template-loading-example.component.html │ │ │ │ ├── template-loading-example.component.ts │ │ │ │ └── template-loading-example.component.scss │ │ │ └── append-to-example │ │ │ │ ├── append-to-example.component.scss │ │ │ │ ├── append-to-example.component.ts │ │ │ │ └── append-to-example.component.html │ │ ├── shared │ │ │ ├── route-viewer │ │ │ │ ├── route-viewer.component.html │ │ │ │ └── route-viewer.component.ts │ │ │ └── example-viewer │ │ │ │ ├── example-viewer.component.html │ │ │ │ ├── stackblitz-button │ │ │ │ ├── stackblitz-button.component.html │ │ │ │ ├── stackblitz-button.component.ts │ │ │ │ └── stackblitz.service.ts │ │ │ │ └── example-viewer.component.ts │ │ ├── app.component.html │ │ ├── layout │ │ │ ├── sidenav-component.ts │ │ │ └── header.component.ts │ │ ├── app.module.ts │ │ ├── routes.ts │ │ └── app.component.ts │ ├── favicon.ico │ ├── assets │ │ ├── thor.png │ │ ├── angular.png │ │ ├── batman.png │ │ ├── spidey.png │ │ └── stackblitz │ │ │ ├── polyfills.ts │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ ├── styles.css │ │ │ ├── app.module.ts │ │ │ └── angular.json │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── style │ │ ├── _content.scss │ │ └── _sidebar.scss │ ├── tsconfig.app.json │ ├── main.ts │ ├── .browserslistrc │ ├── index.html │ ├── styles.scss │ └── polyfills.ts ├── ng-select │ ├── themes │ │ └── _mixins.scss │ ├── ng-package.json │ ├── lib │ │ ├── console.service.ts │ │ ├── id.ts │ │ ├── ng-select.types.ts │ │ ├── config.service.ts │ │ ├── value-utils.ts │ │ ├── ng-option.component.ts │ │ ├── ng-select.module.ts │ │ ├── ng-templates.directive.ts │ │ ├── ng-dropdown-panel.service.spec.ts │ │ ├── ng-dropdown-panel.service.ts │ │ ├── selection-model.ts │ │ └── ng-select.component.html │ ├── tsconfig.spec.json │ ├── tslint.json │ ├── public-api.ts │ ├── testing │ │ ├── mocks.ts │ │ └── helpers.ts │ ├── tsconfig.lib.json │ ├── test.ts │ ├── karma.conf.js │ └── package.json └── ng-option-highlight │ ├── public-api.ts │ ├── ng-package.json │ ├── lib │ ├── ng-option-highlight.module.ts │ ├── ng-option-highlight.directive.ts │ └── ng-option-highlight.directive.spec.ts │ ├── tslint.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── test.ts │ ├── package.json │ ├── karma.conf.js │ └── README.md ├── .gitignore ├── release.sh ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── FUNDING.yml ├── no-response.yml ├── workflows │ ├── ci.yml │ ├── stale.yml │ ├── release.yml │ └── codeql-analysis.yml └── config.yml ├── CONTRIBUTING.md ├── tsconfig.json ├── LICENSE ├── tslint.json ├── package.json └── angular.json /src/demo/app/examples/tags-backend-example/tags-backend-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/tags-custom-example/tags-custom-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/tags-default-example/tags-default-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-children-example/group-children-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-default-example/group-default-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-function-example/group-function-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-checkbox-example/multi-checkbox-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/output-events-example/output-events-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/search-custom-example/search-custom-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/search-default-example/search-default-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-label-example/template-label-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/virtual-scroll-example/virtual-scroll-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/bindings-custom-example/bindings-custom-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/bindings-default-example/bindings-default-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/bindings-nested-example/bindings-nested-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/data-source-array-example/data-source-array-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/dropdown-position-example/dropdown-position-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-async-data-example/forms-async-data-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-selectable-example/group-selectable-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/search-editable-example/search-editable-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-display-example/template-display-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-optgroup-example/template-optgroup-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-option-example/template-option-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-search-example/template-search-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/data-source-backend-example/data-source-backend-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/data-source-options-example/data-source-options-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-multi-select-example/forms-multi-select-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-single-select-example/forms-single-select-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-with-options-example/forms-with-options-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-custom-example/multi-select-custom-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-hidden-example/multi-select-hidden-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-limit-example/multi-select-limit-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/search-autocomplete-example/search-autocomplete-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clear/ng-select/master/src/demo/favicon.ico -------------------------------------------------------------------------------- /src/demo/app/examples/forms-custom-template-example/forms-custom-template-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-checkbox-group-example/multi-checkbox-group-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-default-example/multi-select-default-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-disabled-example/multi-select-disabled-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-template-example/multi-select-template-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/tags-closed-dropdown-example/tags-closed-dropdown-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-header-footer-example/template-header-footer-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-selectable-hidden-example/group-selectable-hidden-example.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/assets/thor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clear/ng-select/master/src/demo/assets/thor.png -------------------------------------------------------------------------------- /src/demo/assets/angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clear/ng-select/master/src/demo/assets/angular.png -------------------------------------------------------------------------------- /src/demo/assets/batman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clear/ng-select/master/src/demo/assets/batman.png -------------------------------------------------------------------------------- /src/demo/assets/spidey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clear/ng-select/master/src/demo/assets/spidey.png -------------------------------------------------------------------------------- /src/demo/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/ng-select/themes/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin rtl { 2 | @at-root [dir="rtl"] #{&} { 3 | @content 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/demo/assets/stackblitz/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es6/reflect'; 2 | import 'core-js/es7/reflect'; 3 | import 'zone.js'; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | out-tsc 5 | .idea 6 | *.iml 7 | .DS_Store 8 | travis_rsa 9 | travis_rsa.pub 10 | .vscode/** 11 | dist.tgz 12 | -------------------------------------------------------------------------------- /src/demo/app/shared/route-viewer/route-viewer.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/demo/style/_content.scss: -------------------------------------------------------------------------------- 1 | .bd-title { 2 | margin-top: .5rem; 3 | margin-bottom: .5rem; 4 | font-weight: 300; 5 | } 6 | 7 | .bd-content { 8 | margin-bottom: 300px; 9 | } 10 | -------------------------------------------------------------------------------- /src/ng-select/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ng-select", 4 | "lib": { 5 | "entryFile": "public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /src/ng-option-highlight/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ng-option-highlight 3 | */ 4 | 5 | export * from './lib/ng-option-highlight.directive'; 6 | export * from './lib/ng-option-highlight.module'; 7 | -------------------------------------------------------------------------------- /src/ng-option-highlight/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ng-option-highlight", 4 | "lib": { 5 | "entryFile": "public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/ng-select/lib/console.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class ConsoleService { 5 | warn(message: string) { 6 | console.warn(message) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [ 6 | "node" 7 | ] 8 | }, 9 | "files": [ 10 | "main.ts", 11 | "polyfills.ts" 12 | ], 13 | "include": [ 14 | "**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/demo/app/examples/tags-closed-dropdown-example/tags-closed-dropdown-example.component.html: -------------------------------------------------------------------------------- 1 |

Tagging without dropdown. Press enter to add item

2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /src/demo/assets/stackblitz/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ng-option-highlight/lib/ng-option-highlight.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NgOptionHighlightDirective } from './ng-option-highlight.directive'; 3 | 4 | @NgModule({ 5 | declarations: [NgOptionHighlightDirective], 6 | exports: [NgOptionHighlightDirective] 7 | }) 8 | export class NgOptionHighlightModule {} 9 | -------------------------------------------------------------------------------- /src/ng-select/lib/id.ts: -------------------------------------------------------------------------------- 1 | export function newId() { 2 | // First character is an 'a', it's good practice to tag id to begin with a letter 3 | return 'axxxxxxxxxxx'.replace(/[x]/g, function (_) { 4 | // tslint:disable-next-line:no-bitwise 5 | const val = Math.random() * 16 | 0; 6 | return val.toString(16); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/ng-select/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "angularCompilerOptions": { 8 | "enableIvy": false 9 | }, 10 | "files": ["test.ts"], 11 | "include": ["**/*.spec.ts", "**/*.d.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /src/ng-select/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ng-option-highlight/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ng-option-highlight/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 | "test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/demo/app/examples/tags-default-example/tags-default-example.component.html: -------------------------------------------------------------------------------- 1 |

If option doesn't exist among items you can add new one using [addTag]="true"

2 | 3 | 6 | 7 | 8 |
9 | 10 | Selected value: {{selectedCompany | json}} 11 | -------------------------------------------------------------------------------- /src/demo/app/examples/bindings-custom-example/bindings-custom-example.component.html: -------------------------------------------------------------------------------- 1 |

You can provide custom property for display and selected value.

2 | 3 | 7 | 8 | 9 |
10 | Selected city ID: {{selectedCityId | json}} 11 | -------------------------------------------------------------------------------- /src/demo/app/examples/search-default-example/search-default-example.component.html: -------------------------------------------------------------------------------- 1 |

2 | By default ng-select will search using label text. You can also use loading input to set 3 | loading state manually if [typeahead] is not used. 4 |

5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/demo/app/examples/bindings-default-example/bindings-default-example.component.html: -------------------------------------------------------------------------------- 1 |

2 | By default ng-select binds to default label property for display, and keeps whole object as selected 3 | value 4 |

5 | 6 | 8 | 9 | 10 |
11 | Selected city object: {{selectedCity | json}} 12 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-hidden-example/multi-select-hidden-example.component.html: -------------------------------------------------------------------------------- 1 |

Selected items won't appear among dropdown options

2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-label-example/template-label-example.component.html: -------------------------------------------------------------------------------- 1 |

Custom selected item label using ng-label-tmp

2 | 3 | 4 | 5 | 6 | {{item.name}} 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/demo/assets/stackblitz/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { AppModule } from './app.module'; 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule).then(ref => { 7 | if (window['ngRef']) { 8 | window['ngRef'].destroy(); 9 | } 10 | window['ngRef'] = ref; 11 | 12 | }).catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/ng-select/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ng-select 3 | */ 4 | 5 | export { DropdownPosition, NgSelectComponent, SELECTION_MODEL_FACTORY } from './lib/ng-select.component'; 6 | export { NgSelectModule } from './lib/ng-select.module'; 7 | export { NgOption } from './lib/ng-select.types'; 8 | export { SelectionModel } from './lib/selection-model'; 9 | export { NgSelectConfig } from './lib/config.service'; 10 | -------------------------------------------------------------------------------- /src/demo/app/examples/bindings-nested-example/bindings-nested-example.component.html: -------------------------------------------------------------------------------- 1 |

Bind label and value to nested custom property

2 | 3 | 8 | 9 | 10 |
11 | Selected country ID: {{selectedCountryId}} 12 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-search-example/template-search-example.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/demo/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 | -------------------------------------------------------------------------------- /src/demo/app/examples/tags-default-example/tags-default-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'tags-default-example', 5 | templateUrl: './tags-default-example.component.html', 6 | styleUrls: ['./tags-default-example.component.scss'] 7 | }) 8 | export class TagsDefaultExampleComponent implements OnInit { 9 | 10 | selectedCompany; 11 | 12 | ngOnInit() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/demo/app/examples/tags-custom-example/tags-custom-example.component.html: -------------------------------------------------------------------------------- 1 |

By providing custom function to [addTag] you can modify result of new tag

2 | 3 | 9 | 10 | 11 |
12 | Selected value: {{selectedCompanies | json}} 13 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git pull 3 | 4 | echo update changelog 5 | cd ./src/ng-select/ 6 | node ../../node_modules/standard-version/bin/cli.js --infile ../../CHANGELOG.md 7 | cd ../.. 8 | 9 | echo build lib 10 | yarn run build 11 | 12 | echo push tags 13 | git push --follow-tags origin master 14 | 15 | echo push to npm 16 | cp README.md ./dist/ng-select/ 17 | cd ./dist/ng-select/ 18 | yarn publish --access=public 19 | cd ./dist/ng-option-highlight/ 20 | yarn publish --access=public 21 | -------------------------------------------------------------------------------- /src/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'. -------------------------------------------------------------------------------- /src/demo/app/examples/search-custom-example/search-custom-example.component.html: -------------------------------------------------------------------------------- 1 |

Use search term and filter on custom fields. Type female to see only females.

2 | 3 | 7 | 8 | {{item.name}}
9 | {{item.gender}} 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-loading-example/template-loading-example.component.html: -------------------------------------------------------------------------------- 1 |

Custom loading spinner using ng-loadingspinner-tmp

2 | 3 | 4 | 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/demo/app/examples/append-to-example/append-to-example.component.scss: -------------------------------------------------------------------------------- 1 | // as we are using ViewEncapsulation.ShadowDom we need to make sure all styles are accessible within this component 2 | // @import "~@ng-select/ng-select/themes/default.theme.css"; 3 | 4 | .overflow-box { 5 | padding: 5px; 6 | height: 100px; 7 | border: 1px solid #999; 8 | overflow: hidden; 9 | } 10 | 11 | .scrollable-box { 12 | position: relative; 13 | height: 400px; 14 | overflow: auto; 15 | } 16 | -------------------------------------------------------------------------------- /src/ng-select/testing/mocks.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgZone } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class MockNgZone extends NgZone { 5 | constructor() { 6 | super({ enableLongStackTrace: true }); 7 | } 8 | 9 | run(fn: Function): any { 10 | return fn(); 11 | } 12 | 13 | runOutsideAngular(fn: Function): any { 14 | return fn(); 15 | } 16 | } 17 | 18 | @Injectable() 19 | export class MockConsole { 20 | warn() { 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/demo/app/examples/tags-closed-dropdown-example/tags-closed-dropdown-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'tags-closed-dropdown-example', 5 | templateUrl: './tags-closed-dropdown-example.component.html', 6 | styleUrls: ['./tags-closed-dropdown-example.component.scss'] 7 | }) 8 | export class TagsClosedDropdownExampleComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js,html,scss}] 14 | charset = utf-8 15 | 16 | # Indentation override for all JS under lib directory 17 | [src/**.{js,html,scss}] 18 | indent_style = space 19 | indent_size = 4 20 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-disabled-example/multi-select-disabled-example.component.html: -------------------------------------------------------------------------------- 1 |

Disabled multiple elements

2 | 3 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ng-select/lib/ng-select.types.ts: -------------------------------------------------------------------------------- 1 | export interface NgOption { 2 | [name: string]: any; 3 | index?: number; 4 | htmlId?: string; 5 | selected?: boolean; 6 | disabled?: boolean; 7 | marked?: boolean; 8 | label?: string; 9 | value?: string | Object; 10 | parent?: NgOption; 11 | children?: NgOption[]; 12 | } 13 | 14 | export enum KeyCode { 15 | Tab = 9, 16 | Enter = 13, 17 | Esc = 27, 18 | Space = 32, 19 | ArrowUp = 38, 20 | ArrowDown = 40, 21 | Backspace = 8 22 | } 23 | -------------------------------------------------------------------------------- /src/demo/app/examples/tags-backend-example/tags-backend-example.component.html: -------------------------------------------------------------------------------- 1 |

[addTag] also accepts promise if you need to verify new option with backend

2 | 3 | 9 | 10 | 11 | create new: {{search}} 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/demo/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /src/demo/app/examples/dropdown-position-example/dropdown-position-example.component.html: -------------------------------------------------------------------------------- 1 |

2 | By default the dropdown position is set to auto and will be shown above if there is not space placing it at the 3 | bottom. 4 |

5 | 6 | 7 | 8 | 9 |
10 |

11 | You can force position to bottom or top by setting dropdownPosition 12 |

13 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /src/demo/app/examples/data-source-backend-example/data-source-backend-example.component.html: -------------------------------------------------------------------------------- 1 |

2 | Most common case is showing data from backend 3 | API and with ng-select this is extremely simple since you can bind directly to 4 | observable when using angular | async pipe 5 |

6 | 7 | 12 | 13 | 14 |
Selected: {{selectedPersonId}} 15 | -------------------------------------------------------------------------------- /src/ng-select/lib/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class NgSelectConfig { 5 | placeholder: string; 6 | notFoundText = 'No items found'; 7 | typeToSearchText = 'Type to search'; 8 | addTagText = 'Add item'; 9 | loadingText = 'Loading...'; 10 | clearAllText = 'Clear all'; 11 | disableVirtualScroll = true; 12 | openOnEnter = true; 13 | appendTo: string; 14 | bindValue: string; 15 | bindLabel: string; 16 | appearance = 'underline'; 17 | } 18 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-default-example/group-default-example.component.html: -------------------------------------------------------------------------------- 1 |

2 | You can group by item key providing key name as a string to groupBy input 3 |

4 | 5 | 10 | 11 | {{item.country || 'Unnamed group'}} 12 | 13 | 14 | 15 |
16 | Selected: {{selectedAccount | json}} 17 | 18 | -------------------------------------------------------------------------------- /src/demo/app/examples/data-source-array-example/data-source-array-example.component.html: -------------------------------------------------------------------------------- 1 |

2 | You can also set array of objects as items input 3 |

4 | 5 | 9 | 10 | 11 |
12 |

13 | While array of objects is the most common items source, you may want to set simple array of strings, numbers, 14 | booleans 15 |

16 | 17 | 19 | 20 | -------------------------------------------------------------------------------- /src/demo/app/examples/search-editable-example/search-editable-example.component.html: -------------------------------------------------------------------------------- 1 |

2 | By default, ng-select doesn't allow edit the current value for search. You can enable it by setting [editableSearchTerm] 3 | input to true. It's useful when you want to modify part of a long search query 4 |

5 | 6 | 11 | 12 | 13 |
Selected: {{selectedPersonId}} 14 | -------------------------------------------------------------------------------- /src/demo/app/examples/dropdown-position-example/dropdown-position-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'dropdown-position-example', 5 | templateUrl: './dropdown-position-example.component.html', 6 | styleUrls: ['./dropdown-position-example.component.scss'] 7 | }) 8 | export class DropdownPositionExampleComponent implements OnInit { 9 | 10 | cities = [ 11 | { value: 1, label: 'Vilnius' }, 12 | { value: 2, label: 'Kaunas' }, 13 | { value: 3, label: 'Pavilnys' } 14 | ]; 15 | 16 | ngOnInit() { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /src/demo/app/shared/example-viewer/example-viewer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{title}} 4 | 11 |
12 | 13 |
14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/ng-select/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 | "skipTemplateCodegen": true, 16 | "strictMetadataEmit": true, 17 | "fullTemplateTypeCheck": true, 18 | "strictInjectionParameters": true, 19 | "enableResourceInlining": true, 20 | "enableIvy": false 21 | }, 22 | "exclude": [ 23 | "test.ts", 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/ng-option-highlight/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 | "skipTemplateCodegen": true, 16 | "strictMetadataEmit": true, 17 | "fullTemplateTypeCheck": true, 18 | "strictInjectionParameters": true, 19 | "enableResourceInlining": true, 20 | "enableIvy": false 21 | }, 22 | "exclude": [ 23 | "test.ts", 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/demo/app/examples/data-source-options-example/data-source-options-example.component.html: -------------------------------------------------------------------------------- 1 |

2 | If you have simple use case, you can omit items array and bind options directly in html using ng-option 3 | component. 4 |

5 | 6 |
7 | 8 |
9 |
10 | 11 | {{car.name}} 12 | Custom 13 | 14 | 15 |
Selected car ID: {{selectedCars | json}} 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [varnastadeus] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/demo/app/shared/example-viewer/stackblitz-button/stackblitz-button.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | StackBlitz 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/demo/app/examples/bindings-custom-example/bindings-custom-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'bindings-custom-example', 5 | templateUrl: './bindings-custom-example.component.html', 6 | styleUrls: ['./bindings-custom-example.component.scss'] 7 | }) 8 | export class BindingsCustomExampleComponent implements OnInit { 9 | 10 | cities = [ 11 | { id: 1, name: 'Vilnius' }, 12 | { id: 2, name: 'Kaunas' }, 13 | { id: 3, name: 'Pavilnys', disabled: true } 14 | ]; 15 | selectedCityId: number = null; 16 | 17 | ngOnInit() { 18 | this.selectedCityId = this.cities[0].id; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-selectable-hidden-example/group-selectable-hidden-example.component.html: -------------------------------------------------------------------------------- 1 |

Selecting group will hide all of its items

2 | 3 | 12 | 13 | {{item.country || 'Unnamed group'}} 14 | 15 | 16 | 17 |
18 | Selected: {{selectedAccounts | json}} 19 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an Issue is closed for lack of response 4 | daysUntilClose: 14 5 | # Label requiring a response 6 | responseRequiredLabel: needs info 7 | # Comment to post when closing an Issue for lack of response. Set to `false` to disable 8 | closeComment: > 9 | This issue has been automatically closed because there has been no response 10 | to our request for more information from the original author. With only the 11 | description that is currently in the issue, we don't have enough information 12 | to take action. Feel free to create new issue if you have more info to provide. 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in ng-select. All forms of contribution are 4 | welcome, from issue reports to PRs and documentation / write-ups. 5 | 6 | 7 | Please do the following before creating PR. 8 | * If you're planning to add or change a major feature in a PR, please ensure the change is aligned with the project roadmap by opening an issue first, especially if you're going to spend a lot of time on it. 9 | * Test cases for your change are present. Generally we don't accept PR's without a test case. 10 | * All tests are passing and build is not failing. 11 | * In development run `yarn start` to start demo app. 12 | * In development run `yarn test:watch` to start running tests in watch mode. 13 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-default-example/multi-select-default-example.component.html: -------------------------------------------------------------------------------- 1 |

Select multiple elements

2 | 3 | 11 | 12 | 13 |
14 | Selected value:
15 | 18 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /src/demo/app/examples/bindings-default-example/bindings-default-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'bindings-default-example', 5 | templateUrl: './bindings-default-example.component.html', 6 | styleUrls: ['./bindings-default-example.component.scss'] 7 | }) 8 | export class BindingsDefaultExampleComponent implements OnInit { 9 | 10 | defaultBindingsList = [ 11 | { value: 1, label: 'Vilnius' }, 12 | { value: 2, label: 'Kaunas' }, 13 | { value: 3, label: 'Pavilnys', disabled: true } 14 | ]; 15 | 16 | selectedCity = null; 17 | 18 | ngOnInit() { 19 | this.selectedCity = this.defaultBindingsList[0]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ng-select/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 | ); 18 | // Then we find all the tests. 19 | const context = require.context('./', true, /\.spec\.ts$/); 20 | // And load the modules. 21 | context.keys().map(context); 22 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-children-example/group-children-example.component.html: -------------------------------------------------------------------------------- 1 |

2 | Group by children array. Note that when grouping by already grouped items ng-optgroup-tmp is 3 | required to display correct headers. 4 |

5 | 6 | 12 | 13 | {{item.title}} 14 | 15 | 16 | {{item.title}} 17 | 18 | 19 | 20 |
21 | Selected: {{selectedProjects | json}} 22 | -------------------------------------------------------------------------------- /src/demo/app/shared/route-viewer/route-viewer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { EXAMPLE_COMPONENTS } from '../../examples/examples'; 4 | 5 | @Component({ 6 | selector: 'route-viewer', 7 | templateUrl: './route-viewer.component.html' 8 | }) 9 | export class RouteViewerComponent implements OnInit { 10 | 11 | examples: string[]; 12 | 13 | constructor( 14 | private route: ActivatedRoute) { 15 | } 16 | 17 | ngOnInit() { 18 | this.route.data.subscribe((data: { examples: string }) => { 19 | this.examples = Object.keys(EXAMPLE_COMPONENTS).filter(x => x.startsWith(data.examples)); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/demo/app/examples/tags-custom-example/tags-custom-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'tags-custom-example', 5 | templateUrl: './tags-custom-example.component.html', 6 | styleUrls: ['./tags-custom-example.component.scss'] 7 | }) 8 | export class TagsCustomExampleComponent implements OnInit { 9 | 10 | selectedCompanies; 11 | companies: any[] = []; 12 | companiesNames = ['Uber', 'Microsoft', 'Flexigen']; 13 | 14 | ngOnInit() { 15 | this.companiesNames.forEach((c, i) => { 16 | this.companies.push({ id: i, name: c }); 17 | }); 18 | } 19 | 20 | addTagFn(name) { 21 | return { name: name, tag: true }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/demo/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 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-hidden-example/multi-select-hidden-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { DataService } from '../data.service'; 4 | 5 | @Component({ 6 | selector: 'multi-select-hidden-example', 7 | templateUrl: './multi-select-hidden-example.component.html', 8 | styleUrls: ['./multi-select-hidden-example.component.scss'] 9 | }) 10 | export class MultiSelectHiddenExampleComponent implements OnInit { 11 | 12 | people$: Observable; 13 | selectedPeople = []; 14 | 15 | constructor(private dataService: DataService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.people$ = this.dataService.getPeople(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/demo/app/examples/search-editable-example/search-editable-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Person, DataService } from '../data.service'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'app-search-editable-example', 7 | templateUrl: './search-editable-example.component.html', 8 | styleUrls: ['./search-editable-example.component.scss'] 9 | }) 10 | export class SearchEditableExampleComponent implements OnInit { 11 | 12 | people$: Observable; 13 | selectedPersonId = '5a15b13c36e7a7f00cf0d7cb'; 14 | 15 | constructor( 16 | private dataService: DataService) { 17 | } 18 | 19 | ngOnInit() { 20 | this.people$ = this.dataService.getPeople(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/ng-option-highlight/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 | ); 18 | // Then we find all the tests. 19 | const context = require.context('./', true, /\.spec\.ts$/); 20 | // And load the modules. 21 | context.keys().map(context); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "module": "es2020", 10 | "moduleResolution": "node", 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ], 21 | "paths": { 22 | "@ng-select/ng-select": [ 23 | "src/ng-select/public-api.ts" 24 | ], 25 | "@ng-select/ng-option-highlight": [ 26 | "src/ng-option-highlight/public-api.ts" 27 | ] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/demo/app/examples/append-to-example/append-to-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { DataService } from '../data.service'; 3 | 4 | @Component({ 5 | selector: 'append-to-example', 6 | templateUrl: './append-to-example.component.html', 7 | styleUrls: ['./append-to-example.component.scss'], 8 | // encapsulation: ViewEncapsulation.ShadowDom, 9 | }) 10 | export class AppendToExampleComponent implements OnInit { 11 | 12 | people: any = []; 13 | selected: any; 14 | selected2: any; 15 | selected3: any; 16 | 17 | constructor(private dataService: DataService) { 18 | } 19 | 20 | ngOnInit() { 21 | this.people = this.dataService.getPeople(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-optgroup-example/template-optgroup-example.component.html: -------------------------------------------------------------------------------- 1 |

Custom label option and optgroup templates

2 | 3 | 4 | 5 | 6 | {{item.name}} 7 | 8 | 9 | City group logo 10 | 11 | 12 | {{item.name}} 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-limit-example/multi-select-limit-example.component.html: -------------------------------------------------------------------------------- 1 |

Select multiple elements with a limit number of selections (e.g.: 3)

2 | 3 |
4 | Max selection reached 5 |
6 | 7 | 14 | 15 | 16 |
17 | Selected value:
18 |
    19 |
  • {{item.name}}
  • 20 |
21 | 22 |
23 | -------------------------------------------------------------------------------- /src/demo/app/examples/data-source-backend-example/data-source-backend-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DataService, Person } from '../data.service'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'data-source-backend-example', 7 | templateUrl: './data-source-backend-example.component.html', 8 | styleUrls: ['./data-source-backend-example.component.scss'] 9 | }) 10 | export class DataSourceBackendExampleComponent implements OnInit { 11 | 12 | people$: Observable; 13 | selectedPersonId = '5a15b13c36e7a7f00cf0d7cb'; 14 | 15 | constructor(private dataService: DataService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.people$ = this.dataService.getPeople(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-template-example/multi-select-template-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { DataService } from '../data.service'; 4 | 5 | @Component({ 6 | selector: 'multi-select-template-example', 7 | templateUrl: './multi-select-template-example.component.html', 8 | styleUrls: ['./multi-select-template-example.component.scss'] 9 | }) 10 | export class MultiSelectTemplateExampleComponent implements OnInit { 11 | 12 | githubUsers$: Observable; 13 | selectedUsers = ['anjmao']; 14 | 15 | constructor(private dataService: DataService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.githubUsers$ = this.dataService.getGithubAccounts('anjm'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/demo/app/examples/bindings-nested-example/bindings-nested-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'bindings-nested-example', 5 | templateUrl: './bindings-nested-example.component.html', 6 | styleUrls: ['./bindings-nested-example.component.scss'] 7 | }) 8 | export class BindingsNestedExampleComponent implements OnInit { 9 | 10 | countries = [ 11 | { id: 1, nested: { countryId: 'L', name: 'Lithuania' } }, 12 | { id: 2, nested: { countryId: 'U', name: 'USA' } }, 13 | { id: 3, nested: { countryId: 'A', name: 'Australia' } } 14 | ]; 15 | 16 | selectedCountryId: string = null; 17 | 18 | ngOnInit() { 19 | this.selectedCountryId = this.countries[0].nested.countryId; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-custom-example/multi-select-custom-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { DataService } from '../data.service'; 4 | 5 | @Component({ 6 | selector: 'multi-select-custom-example', 7 | templateUrl: './multi-select-custom-example.component.html', 8 | styleUrls: ['./multi-select-custom-example.component.scss'] 9 | }) 10 | export class MultiSelectCustomExampleComponent implements OnInit { 11 | 12 | githubUsers$: Observable; 13 | selectedUsers = ['anjmao', 'anjmittu', 'anjmendoza']; 14 | 15 | constructor(private dataService: DataService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.githubUsers$ = this.dataService.getGithubAccounts('anjm'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/demo/app/examples/search-autocomplete-example/search-autocomplete-example.component.html: -------------------------------------------------------------------------------- 1 |

Use typeahead to subscribe to search term and load async items.

2 | 3 | 4 | 15 | 16 |
17 | 18 |
19 | Selected persons: {{selectedPersons | json}} 20 |
21 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-header-footer-example/template-header-footer-example.component.html: -------------------------------------------------------------------------------- 1 |

Custom header and footer using ng-header-tmp and ng-footer-tmp

2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | Selected count: {{selectedPeople.length}} 16 | 17 | 18 | 19 |
20 | 21 | Selected people: {{selectedPeople}} 22 | -------------------------------------------------------------------------------- /src/demo/app/examples/data-source-options-example/data-source-options-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'data-source-options-example', 5 | templateUrl: './data-source-options-example.component.html', 6 | styleUrls: ['./data-source-options-example.component.scss'] 7 | }) 8 | export class DataSourceOptionsExampleComponent implements OnInit { 9 | 10 | selectedCars = [3]; 11 | cars = [ 12 | { id: 1, name: 'Volvo' }, 13 | { id: 2, name: 'Saab', disabled: true }, 14 | { id: 3, name: 'Opel' }, 15 | { id: 4, name: 'Audi' }, 16 | ]; 17 | 18 | ngOnInit() { 19 | 20 | } 21 | 22 | toggleDisabled() { 23 | const car: any = this.cars[1]; 24 | car.disabled = !car.disabled; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-limit-example/multi-select-limit-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { DataService } from '../data.service'; 4 | 5 | @Component({ 6 | selector: 'multi-select-limit-example', 7 | templateUrl: './multi-select-limit-example.component.html', 8 | styleUrls: ['./multi-select-limit-example.component.scss'] 9 | }) 10 | export class MultiSelectLimitExampleComponent implements OnInit { 11 | 12 | people$: Observable; 13 | selectedPeople = []; 14 | 15 | constructor(private dataService: DataService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.people$ = this.dataService.getPeople(); 20 | } 21 | 22 | clearModel() { 23 | this.selectedPeople = []; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-template-example/multi-select-template-example.component.html: -------------------------------------------------------------------------------- 1 |

Custom template for each selected item using ng-label-tmp

2 | 3 | 9 | 10 | 11 | {{item.login}} 12 | 13 | 14 | 15 | 16 | {{item.login}} 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-function-example/group-function-example.component.html: -------------------------------------------------------------------------------- 1 |

2 | Having more complex use case, you can use function expression as groupBy input. Also you can use [groupValue] 3 | to set value of grouped item. 4 |

5 | 6 | 13 | 14 | {{item.name}} 15 | {{item.total}} 16 | 17 | 18 | 19 |
20 | Selected: {{selectedAccounts | json}} 21 | -------------------------------------------------------------------------------- /src/demo/app/layout/sidenav-component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { appRoutes } from '../routes'; 3 | 4 | @Component({ 5 | selector: 'layout-sidenav', 6 | template: ` 7 | 12 | ` 13 | }) 14 | export class LayoutSidenavComponent { 15 | routes = []; 16 | 17 | constructor() { 18 | this.routes = appRoutes.filter(route => route.component) 19 | .map(route => ({ 20 | title: route.data.title, 21 | url: `/${route.path}` 22 | })); 23 | } 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-checkbox-group-example/multi-checkbox-group-example.component.html: -------------------------------------------------------------------------------- 1 |

Group selects children

2 | 3 | 13 | 14 | {{item.gender | uppercase}} 15 | 16 | 17 | {{item.name}} 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/ng-option-highlight/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ng-select/ng-option-highlight", 3 | "version": "0.0.6", 4 | "author": "@ng-select/ng-select", 5 | "license": "MIT", 6 | "repository": "ng-select/ng-select", 7 | "peerDependencies": { 8 | "@angular/common": ">=12.0.0 <13.0.0", 9 | "@angular/core": "^>=12.0.0 <13.0.0" 10 | }, 11 | "main": "bundles/ng-select-ng-option-highlight.umd.js", 12 | "module": "fesm2015/ng-select-ng-option-highlight.js", 13 | "es2015": "fesm2015/ng-select-ng-option-highlight.js", 14 | "esm2015": "esm2015/ng-select-ng-option-highlight.js", 15 | "fesm2015": "fesm2015/ng-select-ng-option-highlight.js", 16 | "typings": "ng-select-ng-option-highlight.d.ts", 17 | "metadata": "ng-select-ng-option-highlight.metadata.json", 18 | "sideEffects": false, 19 | "dependencies": { 20 | "tslib": "^2.1.0" 21 | } 22 | } -------------------------------------------------------------------------------- /src/demo/app/examples/forms-with-options-example/forms-with-options-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup } from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'forms-with-options-example', 6 | templateUrl: './forms-with-options-example.component.html', 7 | styleUrls: ['./forms-with-options-example.component.scss'] 8 | }) 9 | export class FormsWithOptionsExampleComponent implements OnInit { 10 | 11 | basePath; 12 | heroForm: FormGroup; 13 | 14 | constructor( 15 | private fb: FormBuilder) { 16 | } 17 | 18 | ngOnInit() { 19 | this.basePath = window.location.host.includes('localhost') ? '' : '/ng-select'; 20 | this.heroForm = this.fb.group({ 21 | heroId: 'batman', 22 | agree: null 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/demo/app/examples/data-source-array-example/data-source-array-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DataService, Person } from '../data.service'; 3 | 4 | @Component({ 5 | selector: 'data-source-array-example', 6 | templateUrl: './data-source-array-example.component.html', 7 | styleUrls: ['./data-source-array-example.component.scss'] 8 | }) 9 | export class DataSourceArrayExampleComponent implements OnInit { 10 | 11 | people: Person[] = []; 12 | selectedPersonId = '5a15b13c36e7a7f00cf0d7cb'; 13 | 14 | selectedSimpleItem = 'Two'; 15 | simpleItems = []; 16 | 17 | constructor(private dataService: DataService) { 18 | } 19 | 20 | ngOnInit() { 21 | this.dataService.getPeople().subscribe(items => this.people = items); 22 | this.simpleItems = [true, 'Two', 3]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-multi-select-example/forms-multi-select-example.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 13 | 14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | paths: 7 | - '**.ts' 8 | - '**.html' 9 | - '**.scss' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Setup Node 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: '12.x' 21 | 22 | - name: Install dependencies 23 | run: yarn install 24 | 25 | - name: Run lint 26 | run: yarn lint 27 | 28 | - name: Run tests 29 | run: yarn test:ci 30 | 31 | - name: Build 32 | run: yarn build:demo 33 | 34 | - name: Coveralls 35 | uses: coverallsapp/github-action@master 36 | with: 37 | github-token: ${{ secrets.GITHUB_TOKEN }} 38 | path-to-lcov: ./coverage/ng-select/lcov.info 39 | -------------------------------------------------------------------------------- /src/demo/app/shared/example-viewer/stackblitz-button/stackblitz-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { environment } from '../../../../environments/environment'; 3 | import { StackblitzService } from './stackblitz.service'; 4 | 5 | @Component({ 6 | selector: 'stackblitz-button', 7 | templateUrl: './stackblitz-button.component.html', 8 | styles: [` 9 | svg { 10 | height: 18px; 11 | vertical-align: sub; 12 | } 13 | `] 14 | }) 15 | export class StackblitzButtonComponent { 16 | 17 | @Input() example: string; 18 | 19 | constructor( 20 | private stackblitz: StackblitzService) { 21 | } 22 | 23 | async openExample() { 24 | if (!environment.production) { 25 | return; 26 | } 27 | 28 | await this.stackblitz.openNewProject(this.example); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-checkbox-example/multi-checkbox-example.component.html: -------------------------------------------------------------------------------- 1 |

Select multiple elements using custom templates with checkboxes

2 | 3 | 12 | 13 | {{item.gender | uppercase}} 14 | 15 | 16 | {{item.name}} 17 | 18 | 19 | 20 |
21 | {{selectedPeople | json}} 22 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v3 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions :fireworks:' 17 | stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions :fireworks:' 18 | stale-issue-label: 'stale' 19 | exempt-issue-labels: 'improvement,needs triage,good for contribution,bug' 20 | stale-pr-label: 'stale' 21 | -------------------------------------------------------------------------------- /src/demo/app/examples/virtual-scroll-example/virtual-scroll-example.component.html: -------------------------------------------------------------------------------- 1 |

2 | In this example we are loading many items but only ~30 of them are rendered in the DOM. 3 | This allows to load as big data as you want. 4 |

5 | 6 | 15 | 16 | Loaded {{photosBuffer.length}} of {{photos.length}} 17 | 18 | 19 | {{index}} {{item.title}} 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/demo/assets/stackblitz/styles.css: -------------------------------------------------------------------------------- 1 | @import "~@ng-select/ng-select/themes/default.theme.css"; 2 | 3 | body { 4 | font-size: 1rem; 5 | font-weight: 400; 6 | line-height: 1.5; 7 | /* Body position must be relative while using appendTo */ 8 | position: relative; 9 | 10 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 11 | } 12 | 13 | .card { 14 | position: relative; 15 | display: -webkit-box; 16 | display: flex; 17 | flex-direction: column; 18 | min-width: 0; 19 | word-wrap: break-word; 20 | background-color: #fff; 21 | background-clip: border-box; 22 | border: 1px solid rgba(0, 0, 0, 0.125); 23 | border-radius: 0.25rem; 24 | } 25 | 26 | .card-body { 27 | flex: 1 1 auto; 28 | padding: 1.25rem; 29 | } 30 | -------------------------------------------------------------------------------- /src/ng-select/lib/value-utils.ts: -------------------------------------------------------------------------------- 1 | const unescapedHTMLExp = /[&<>"']/g; 2 | const hasUnescapedHTMLExp = RegExp(unescapedHTMLExp.source); 3 | const htmlEscapes = { 4 | '&': '&', 5 | '<': '<', 6 | '>': '>', 7 | '"': '"', 8 | '\'': ''' 9 | }; 10 | 11 | export function escapeHTML(string: string) { 12 | return (string && hasUnescapedHTMLExp.test(string)) ? 13 | string.replace(unescapedHTMLExp, chr => htmlEscapes[chr]) : 14 | string; 15 | } 16 | 17 | export function isDefined(value: any) { 18 | return value !== undefined && value !== null; 19 | } 20 | 21 | export function isObject(value: any) { 22 | return typeof value === 'object' && isDefined(value); 23 | } 24 | 25 | export function isPromise(value: any) { 26 | return value instanceof Promise; 27 | } 28 | 29 | export function isFunction(value: any) { 30 | return value instanceof Function; 31 | } 32 | -------------------------------------------------------------------------------- /src/demo/app/examples/search-default-example/search-default-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DataService, Person } from '../data.service'; 3 | 4 | @Component({ 5 | selector: 'search-default-example', 6 | templateUrl: './search-default-example.component.html', 7 | styleUrls: ['./search-default-example.component.scss'] 8 | }) 9 | export class SearchDefaultExampleComponent implements OnInit { 10 | 11 | people: Person[] = []; 12 | peopleLoading = false; 13 | 14 | constructor( 15 | private dataService: DataService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.loadPeople(); 20 | } 21 | 22 | private loadPeople() { 23 | this.peopleLoading = true; 24 | this.dataService.getPeople().subscribe(x => { 25 | this.people = x; 26 | this.peopleLoading = false; 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-checkbox-example/multi-checkbox-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DataService, Person } from '../data.service'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'multi-checkbox-example', 7 | templateUrl: './multi-checkbox-example.component.html', 8 | styleUrls: ['./multi-checkbox-example.component.scss'] 9 | }) 10 | export class MultiCheckboxExampleComponent implements OnInit { 11 | 12 | people: Person[] = []; 13 | selectedPeople = []; 14 | 15 | constructor(private dataService: DataService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.dataService.getPeople() 20 | .pipe(map(x => x.filter(y => !y.disabled))) 21 | .subscribe((res) => { 22 | this.people = res; 23 | this.selectedPeople = [this.people[0].id, this.people[1].id]; 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-custom-example/multi-select-custom-example.component.html: -------------------------------------------------------------------------------- 1 |

Custom template for all selected items using ng-multi-label-tmp

2 | 3 | 10 | 11 |
12 | {{item.login}} 13 | 14 |
15 |
16 | {{items.length - 2}} more... 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-default-example/multi-select-default-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { DataService } from '../data.service'; 4 | 5 | @Component({ 6 | selector: 'multi-select-default-example', 7 | templateUrl: './multi-select-default-example.component.html', 8 | styleUrls: ['./multi-select-default-example.component.scss'] 9 | }) 10 | export class MultiSelectDefaultExampleComponent implements OnInit { 11 | 12 | people$: Observable; 13 | selectedPeople = [{ name: 'Karyn Wright' }]; 14 | 15 | constructor(private dataService: DataService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.people$ = this.dataService.getPeople(); 20 | } 21 | 22 | clearModel() { 23 | this.selectedPeople = []; 24 | } 25 | 26 | changeModel() { 27 | this.selectedPeople = [{ name: 'New person' }]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-header-footer-example/template-header-footer-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DataService } from '../data.service'; 3 | 4 | @Component({ 5 | selector: 'template-header-footer-example', 6 | templateUrl: './template-header-footer-example.component.html', 7 | styleUrls: ['./template-header-footer-example.component.scss'] 8 | }) 9 | export class TemplateHeaderFooterExampleComponent implements OnInit { 10 | 11 | people = []; 12 | selectedPeople = []; 13 | 14 | constructor(private dataService: DataService) { 15 | } 16 | 17 | ngOnInit() { 18 | this.dataService.getPeople().subscribe(items => { 19 | this.people = items; 20 | }); 21 | } 22 | 23 | selectAll() { 24 | this.selectedPeople = this.people.map(x => x.name); 25 | } 26 | 27 | unselectAll() { 28 | this.selectedPeople = []; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-checkbox-group-example/multi-checkbox-group-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DataService, Person } from '../data.service'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'multi-checkbox-group-example', 7 | templateUrl: './multi-checkbox-group-example.component.html', 8 | styleUrls: ['./multi-checkbox-group-example.component.scss'] 9 | }) 10 | export class MultiCheckboxGroupExampleComponent implements OnInit { 11 | 12 | people: Person[] = []; 13 | selectedPeople = []; 14 | 15 | constructor(private dataService: DataService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.dataService.getPeople() 20 | .pipe(map(x => x.filter(y => !y.disabled))) 21 | .subscribe((res) => { 22 | this.people = res; 23 | this.selectedPeople = [this.people[0].id]; 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ng-select component demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Loading ng-select 14 | 15 | 16 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/demo/app/examples/output-events-example/output-events-example.component.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 |
20 |
21 | 22 |
23 |
24 |
25 | 26 |
27 | {{event.name}} - {{event.value | json}} 28 |
29 |
30 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-display-example/template-display-example.component.html: -------------------------------------------------------------------------------- 1 |

Custom not found, type to search and loading templates

2 | 10 | 11 |
12 | Start typing... 13 |
14 |
15 | 16 |
17 | No data found for "{{searchTerm}}" 18 |
19 |
20 | 21 |
22 | Fetching data for "{{searchTerm}}" 23 |
24 |
25 |
26 | 27 |
28 | 29 | Selected people: {{selectedPeople}} 30 | -------------------------------------------------------------------------------- /src/demo/app/examples/tags-backend-example/tags-backend-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'tags-backend-example', 5 | templateUrl: './tags-backend-example.component.html', 6 | styleUrls: ['./tags-backend-example.component.scss'] 7 | }) 8 | export class TagsBackendExampleComponent implements OnInit { 9 | 10 | selectedCompanies; 11 | companies: any[] = []; 12 | loading = false; 13 | companiesNames = ['Uber', 'Microsoft', 'Flexigen']; 14 | 15 | ngOnInit() { 16 | this.companiesNames.forEach((c, i) => { 17 | this.companies.push({ id: i, name: c }); 18 | }); 19 | } 20 | 21 | addTagPromise(name) { 22 | return new Promise((resolve) => { 23 | this.loading = true; 24 | // Simulate backend call. 25 | setTimeout(() => { 26 | resolve({ id: 5, name: name, valid: true }); 27 | this.loading = false; 28 | }, 1000); 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/demo/assets/stackblitz/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { NgSelectModule } from '@ng-select/ng-select'; 5 | import { NgOptionHighlightModule } from '@ng-select/ng-option-highlight'; 6 | import { CommonModule } from '@angular/common'; 7 | import { HttpClientModule } from '@angular/common/http'; 8 | 9 | //example-import 10 | 11 | @Component({ 12 | selector: 'app-component', 13 | template: '//example-template' 14 | }) 15 | export class AppComponent {} 16 | 17 | @NgModule({ 18 | imports: [ 19 | BrowserModule, 20 | FormsModule, 21 | NgSelectModule, 22 | NgOptionHighlightModule, 23 | CommonModule, 24 | ReactiveFormsModule, 25 | HttpClientModule 26 | ], 27 | declarations: [ 28 | AppComponent, 29 | //example-cmp 30 | ], 31 | bootstrap: [AppComponent] 32 | }) 33 | export class AppModule { 34 | } 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-children-example/group-children-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'group-children-example', 5 | templateUrl: './group-children-example.component.html', 6 | styleUrls: ['./group-children-example.component.scss'] 7 | }) 8 | export class GroupChildrenExampleComponent implements OnInit { 9 | 10 | selectedProjects = []; 11 | projects = [ 12 | { 13 | id: 'p1', 14 | title: 'Project A', 15 | subprojects: [ 16 | { title: 'Subproject 1 of A', id: 's1p1' }, 17 | { title: 'Subproject 2 of A', id: 's2p1' }, 18 | ] 19 | }, 20 | { 21 | id: 'p2', 22 | title: 'Project B', 23 | subprojects: [ 24 | { title: 'Subproject 1 of B', id: 's1p2' }, 25 | { title: 'Subproject 2 of B', id: 's2p2' }, 26 | ] 27 | } 28 | ] 29 | 30 | constructor() { 31 | } 32 | 33 | ngOnInit() { 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 14 | steps: 15 | - uses: actions/checkout@v1 16 | 17 | - name: Setup Node 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: '12.x' 21 | 22 | - name: Install dependencies 23 | run: npm install 24 | 25 | - name: Build ng-select 26 | run: npm run-script build 27 | 28 | - name: Copy README 29 | run: cp README.md ./dist/ng-select/ 30 | 31 | - name: Semantic release 32 | run: npx semantic-release 33 | 34 | - name: build demo 35 | run: npm run-script build:demo 36 | 37 | - name: Deploy demo 🚀 38 | uses: JamesIves/github-pages-deploy-action@releases/v3 39 | with: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | BRANCH: gh-pages 42 | FOLDER: dist/demo 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **Reproducbile example** 11 | [Example](https://stackblitz.com) which reproduces described behaviour 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior (if example is not provided): 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/demo/app/examples/search-custom-example/search-custom-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DataService, Person } from '../data.service'; 3 | 4 | @Component({ 5 | selector: 'search-custom-example', 6 | templateUrl: './search-custom-example.component.html', 7 | styleUrls: ['./search-custom-example.component.scss'] 8 | }) 9 | export class SearchCustomExampleComponent implements OnInit { 10 | 11 | people: Person[] = []; 12 | peopleLoading = false; 13 | 14 | constructor( 15 | private dataService: DataService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.loadPeople(); 20 | } 21 | 22 | private loadPeople() { 23 | this.peopleLoading = true; 24 | this.dataService.getPeople().subscribe(x => { 25 | this.people = x; 26 | this.peopleLoading = false; 27 | }); 28 | } 29 | 30 | customSearchFn(term: string, item: Person) { 31 | term = term.toLowerCase(); 32 | return item.name.toLowerCase().indexOf(term) > -1 || item.gender.toLowerCase() === term; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Andžej Maciusovič 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-loading-example/template-loading-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'template-loading-example', 5 | templateUrl: './template-loading-example.component.html', 6 | styleUrls: ['./template-loading-example.component.scss'] 7 | }) 8 | export class TemplateLoadingExampleComponent implements OnInit { 9 | 10 | cities = [ 11 | { 12 | id: 1, 13 | name: 'Vilnius', 14 | avatar: '//www.gravatar.com/avatar/b0d8c6e5ea589e6fc3d3e08afb1873bb?d=retro&r=g&s=30 2x' 15 | }, 16 | { id: 2, name: 'Kaunas', avatar: '//www.gravatar.com/avatar/ddac2aa63ce82315b513be9dc93336e5?d=retro&r=g&s=15' }, 17 | { 18 | id: 3, 19 | name: 'Pavilnys', 20 | avatar: '//www.gravatar.com/avatar/6acb7abf486516ab7fb0a6efa372042b?d=retro&r=g&s=15' 21 | }, 22 | { 23 | id: 4, 24 | name: 'Siauliai', 25 | avatar: '//www.gravatar.com/avatar/b0d8c6e5ea589e6fc3d3e08afb1873bb?d=retro&r=g&s=30 2x' 26 | }, 27 | ]; 28 | 29 | ngOnInit() { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-multi-select-example/forms-multi-select-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup } from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'forms-multi-select-example', 6 | templateUrl: './forms-multi-select-example.component.html', 7 | styleUrls: ['./forms-multi-select-example.component.scss'] 8 | }) 9 | export class FormsMultiSelectExampleComponent implements OnInit { 10 | 11 | heroForm: FormGroup; 12 | isCitiesControlVisible = true; 13 | cities: any[] = [ 14 | { id: 1, name: 'Vilnius' }, 15 | { id: 2, name: 'Kaunas' }, 16 | { id: 3, name: 'Pavilnys (Disabled)', disabled: true }, 17 | { id: 4, name: 'Pabradė' }, 18 | ]; 19 | 20 | constructor(private fb: FormBuilder) { 21 | } 22 | 23 | ngOnInit() { 24 | this.heroForm = this.fb.group({ 25 | selectedCitiesIds: [] 26 | }); 27 | } 28 | 29 | toggleCitiesControl() { 30 | this.isCitiesControlVisible = !this.isCitiesControlVisible; 31 | } 32 | 33 | clearCities() { 34 | this.heroForm.get('selectedCitiesIds').patchValue([]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-option-example/template-option-example.component.html: -------------------------------------------------------------------------------- 1 |

Custom dropdown panel option template using ng-option-tmp

2 | 3 | 4 | 5 |
{{item.name}}
6 |
7 |
8 |
{{item.name}}
9 |
Card subtitle
10 |

11 | 12 | Some quick example text to build 13 |

14 |
15 | Card link 16 | Another link 17 |
18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-label-example/template-label-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'template-label-example', 5 | templateUrl: './template-label-example.component.html', 6 | styleUrls: ['./template-label-example.component.scss'] 7 | }) 8 | export class TemplateLabelExampleComponent implements OnInit { 9 | cities = [ 10 | { 11 | id: 1, 12 | name: 'Vilnius', 13 | avatar: '//www.gravatar.com/avatar/b0d8c6e5ea589e6fc3d3e08afb1873bb?d=retro&r=g&s=30 2x' 14 | }, 15 | { id: 2, name: 'Kaunas', avatar: '//www.gravatar.com/avatar/ddac2aa63ce82315b513be9dc93336e5?d=retro&r=g&s=15' }, 16 | { 17 | id: 3, 18 | name: 'Pavilnys', 19 | avatar: '//www.gravatar.com/avatar/6acb7abf486516ab7fb0a6efa372042b?d=retro&r=g&s=15' 20 | }, 21 | { 22 | id: 4, 23 | name: 'Siauliai', 24 | avatar: '//www.gravatar.com/avatar/b0d8c6e5ea589e6fc3d3e08afb1873bb?d=retro&r=g&s=30 2x' 25 | }, 26 | ]; 27 | 28 | selectedCity = this.cities[0].name; 29 | 30 | ngOnInit() { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-optgroup-example/template-optgroup-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'template-optgroup-example', 5 | templateUrl: './template-optgroup-example.component.html', 6 | styleUrls: ['./template-optgroup-example.component.scss'] 7 | }) 8 | export class TemplateOptgroupExampleComponent implements OnInit { 9 | 10 | cities = [ 11 | { 12 | id: 1, 13 | name: 'Vilnius', 14 | avatar: '//www.gravatar.com/avatar/b0d8c6e5ea589e6fc3d3e08afb1873bb?d=retro&r=g&s=30 2x' 15 | }, 16 | { id: 2, name: 'Kaunas', avatar: '//www.gravatar.com/avatar/ddac2aa63ce82315b513be9dc93336e5?d=retro&r=g&s=15' }, 17 | { 18 | id: 3, 19 | name: 'Pavilnys', 20 | avatar: '//www.gravatar.com/avatar/6acb7abf486516ab7fb0a6efa372042b?d=retro&r=g&s=15' 21 | }, 22 | { 23 | id: 4, 24 | name: 'Siauliai', 25 | avatar: '//www.gravatar.com/avatar/b0d8c6e5ea589e6fc3d3e08afb1873bb?d=retro&r=g&s=30 2x' 26 | }, 27 | ]; 28 | 29 | selectedCity = this.cities[2].name; 30 | 31 | ngOnInit() { 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-search-example/template-search-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'template-search-example', 5 | templateUrl: './template-search-example.component.html', 6 | styleUrls: ['./template-search-example.component.scss'] 7 | }) 8 | export class TemplateSearchExampleComponent implements OnInit { 9 | 10 | cities = [ 11 | { 12 | id: 1, 13 | name: 'Vilnius', 14 | avatar: '//www.gravatar.com/avatar/b0d8c6e5ea589e6fc3d3e08afb1873bb?d=retro&r=g&s=30 2x' 15 | }, 16 | { id: 2, name: 'Kaunas', avatar: '//www.gravatar.com/avatar/ddac2aa63ce82315b513be9dc93336e5?d=retro&r=g&s=15' }, 17 | { 18 | id: 3, 19 | name: 'Pavilnys', 20 | avatar: '//www.gravatar.com/avatar/6acb7abf486516ab7fb0a6efa372042b?d=retro&r=g&s=15' 21 | }, 22 | { 23 | id: 4, 24 | name: 'Siauliai', 25 | avatar: '//www.gravatar.com/avatar/b0d8c6e5ea589e6fc3d3e08afb1873bb?d=retro&r=g&s=30 2x' 26 | }, 27 | ]; 28 | 29 | selectedCity = this.cities[0].name; 30 | 31 | constructor() { } 32 | 33 | ngOnInit() { 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-display-example/template-display-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, OnInit } from '@angular/core'; 2 | import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; 3 | import { DataService } from '../data.service'; 4 | 5 | @Component({ 6 | selector: 'template-display-example', 7 | templateUrl: './template-display-example.component.html', 8 | styleUrls: ['./template-display-example.component.scss'] 9 | }) 10 | export class TemplateDisplayExampleComponent implements OnInit { 11 | 12 | peopleTypeahead = new EventEmitter(); 13 | serverSideFilterItems = []; 14 | selectedPeople; 15 | 16 | constructor(private dataService: DataService) { 17 | } 18 | 19 | ngOnInit() { 20 | this.serverSideSearch(); 21 | } 22 | 23 | private serverSideSearch() { 24 | this.peopleTypeahead.pipe( 25 | distinctUntilChanged(), 26 | debounceTime(300), 27 | switchMap(term => this.dataService.getPeople(term)) 28 | ).subscribe(x => { 29 | this.serverSideFilterItems = x; 30 | }, (err) => { 31 | console.log(err); 32 | this.serverSideFilterItems = []; 33 | }); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-option-example/template-option-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'template-option-example', 5 | templateUrl: './template-option-example.component.html', 6 | styleUrls: ['./template-option-example.component.scss'] 7 | }) 8 | export class TemplateOptionExampleComponent implements OnInit { 9 | 10 | cities = [ 11 | { 12 | id: 1, 13 | name: 'Vilnius', 14 | avatar: '//www.gravatar.com/avatar/b0d8c6e5ea589e6fc3d3e08afb1873bb?d=retro&r=g&s=30 2x' 15 | }, 16 | { id: 2, name: 'Kaunas', avatar: '//www.gravatar.com/avatar/ddac2aa63ce82315b513be9dc93336e5?d=retro&r=g&s=15' }, 17 | { 18 | id: 3, 19 | name: 'Pavilnys', 20 | avatar: '//www.gravatar.com/avatar/6acb7abf486516ab7fb0a6efa372042b?d=retro&r=g&s=15' 21 | }, 22 | { 23 | id: 4, 24 | name: 'Siauliai', 25 | avatar: '//www.gravatar.com/avatar/b0d8c6e5ea589e6fc3d3e08afb1873bb?d=retro&r=g&s=30 2x' 26 | }, 27 | ]; 28 | 29 | selectedCity = this.cities[1].name; 30 | 31 | constructor() { 32 | } 33 | 34 | ngOnInit() { 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/ng-select/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/ng-select'), 20 | reports: ['lcovonly'], 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 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-single-select-example/forms-single-select-example.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/ng-option-highlight/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/ng-option-highlight'), 20 | reports: ['lcovonly'], 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 | -------------------------------------------------------------------------------- /src/ng-select/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@clear/ng-select", 3 | "version": "7.1.1", 4 | "description": "Angular ng-select - All in One UI Select, Multiselect and Autocomplete", 5 | "author": "@ng-select/ng-select", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com:clear/ng-select.git" 10 | }, 11 | "publishConfig": { 12 | "access": "restricted", 13 | "registry": "https://npm.pkg.github.com" 14 | }, 15 | "engines": { 16 | "node": ">= 12.14.0", 17 | "npm": ">= 6.0.0" 18 | }, 19 | "keywords": [ 20 | "angular", 21 | "select", 22 | "ui-select", 23 | "dropdown", 24 | "multiselect", 25 | "autocomplete", 26 | "angular2" 27 | ], 28 | "peerDependencies": { 29 | "@angular/common": ">=12.0.0 <13.0.0", 30 | "@angular/core": ">=12.0.0 <13.0.0", 31 | "@angular/forms": ">=12.0.0 <13.0.0" 32 | }, 33 | "dependencies": { 34 | "tslib": "^2.0.0" 35 | }, 36 | "main": "bundles/clear-ng-select.umd.js", 37 | "module": "fesm2015/clear-ng-select.js", 38 | "es2015": "fesm2015/clear-ng-select.js", 39 | "esm2015": "esm2015/clear-ng-select.js", 40 | "fesm2015": "fesm2015/clear-ng-select.js", 41 | "typings": "clear-ng-select.d.ts", 42 | "metadata": "clear-ng-select.metadata.json", 43 | "sideEffects": false 44 | } -------------------------------------------------------------------------------- /src/ng-select/testing/helpers.ts: -------------------------------------------------------------------------------- 1 | import { DebugElement } from '@angular/core'; 2 | import { ComponentFixture, tick } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | import { KeyCode } from '../lib/ng-select.types'; 5 | 6 | export class TestsErrorHandler {} 7 | 8 | export function tickAndDetectChanges(fixture: ComponentFixture) { 9 | fixture.detectChanges(); 10 | tick(); 11 | } 12 | 13 | export function selectOption(fixture, key: KeyCode, index: number) { 14 | triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space); // open 15 | tickAndDetectChanges(fixture); // need to tick and detect changes, since dropdown fully inits after promise is resolved 16 | for (let i = 0; i < index; i++) { 17 | triggerKeyDownEvent(getNgSelectElement(fixture), key); 18 | } 19 | triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Enter); // select 20 | } 21 | 22 | export function getNgSelectElement(fixture: ComponentFixture): DebugElement { 23 | return fixture.debugElement.query(By.css('ng-select')); 24 | } 25 | 26 | export function triggerKeyDownEvent(element: DebugElement, which: number, key = ''): void { 27 | element.triggerEventHandler('keydown', { 28 | which: which, 29 | key: key, 30 | preventDefault: () => { }, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-single-select-example/forms-single-select-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; 4 | 5 | @Component({ 6 | selector: 'forms-single-select-example', 7 | templateUrl: './forms-single-select-example.component.html', 8 | styleUrls: ['./forms-single-select-example.component.scss'] 9 | }) 10 | export class FormsSingleSelectExampleComponent implements OnInit { 11 | 12 | heroForm: FormGroup; 13 | ages: any[] = [ 14 | { value: '<18', label: 'Under 18' }, 15 | { value: '18', label: '18' }, 16 | { value: '>18', label: 'More than 18' }, 17 | ]; 18 | 19 | constructor(private fb: FormBuilder, private modalService: NgbModal) { 20 | } 21 | 22 | ngOnInit() { 23 | this.heroForm = this.fb.group({ 24 | age: [null, Validators.required], 25 | }); 26 | } 27 | 28 | toggleAgeDisable() { 29 | if (this.heroForm.controls.age.disabled) { 30 | this.heroForm.controls.age.enable(); 31 | } else { 32 | this.heroForm.controls.age.disable(); 33 | } 34 | } 35 | 36 | showConfirm(content) { 37 | this.modalService.open(content); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-selectable-example/group-selectable-example.component.html: -------------------------------------------------------------------------------- 1 |

Groups can be made selectable. By default group acts as a separate option.

2 | 9 | 10 | {{item.country || 'Unnamed group'}} 11 | 12 | 13 | 14 |
15 | Selected: {{selectedAccount | json}} 16 |
17 | 18 |

Using [selectableGroupAsModel]="false" selecting group will select all of its children but not group itself. 19 |

20 | 21 | 30 | 31 | {{item.country || 'Unnamed group'}} 32 | 33 | 34 | 35 |
36 | Selected: {{selectedAccounts | json}} 37 | -------------------------------------------------------------------------------- /src/demo/app/examples/multi-select-disabled-example/multi-select-disabled-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { DataService } from '../data.service'; 4 | 5 | @Component({ 6 | selector: 'multi-select-disabled-example', 7 | templateUrl: './multi-select-disabled-example.component.html', 8 | styleUrls: ['./multi-select-disabled-example.component.scss'] 9 | }) 10 | export class MultiSelectDisabledExampleComponent implements OnInit { 11 | 12 | people$: Observable; 13 | selectedPeople = []; 14 | disable = true; 15 | 16 | constructor(private dataService: DataService) { 17 | } 18 | 19 | ngOnInit() { 20 | this.people$ = this.dataService.getPeople(); 21 | this.setSelectedPeople(); 22 | } 23 | 24 | toggleModel() { 25 | if (this.selectedPeople.length > 0) { 26 | this.selectedPeople = []; 27 | } else { 28 | this.setSelectedPeople(); 29 | } 30 | } 31 | 32 | setSelectedPeople() { 33 | this.selectedPeople = [ 34 | { id: '5a15b13c2340978ec3d2c0ea', name: 'Rochelle Estes', disabled: true }, 35 | { id: '5a15b13c663ea0af9ad0dae8', name: 'Mendoza Ruiz' }, 36 | { id: '5a15b13c728cd3f43cc0fe8a', name: 'Marquez Nolan', disabled: true } 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/demo/app/examples/append-to-example/append-to-example.component.html: -------------------------------------------------------------------------------- 1 |

By default dropdown panel is rendered as child element of ng-select, but you can append dropdown to any element using 2 | appendTo.

3 |

If you place ng-select inside container with fixed height and hidden overflow then dropdown will not be fully 4 | visible.

5 | 6 |
7 | 11 | 12 |
13 | 14 |
15 |

It can be fixed by appending dropdown to body or other parent element.

16 | 17 |
18 | 25 | 26 |
27 | 28 |
29 |
30 |
31 | 36 | 37 |
38 |
39 | -------------------------------------------------------------------------------- /src/demo/app/examples/template-loading-example/template-loading-example.component.scss: -------------------------------------------------------------------------------- 1 | .lds-ellipsis { 2 | display: inline-block; 3 | position: relative; 4 | width: 32px; 5 | height: 32px; 6 | margin-right: 10px; 7 | } 8 | 9 | .lds-ellipsis div { 10 | position: absolute; 11 | top: 14px; 12 | width: 8px; 13 | height: 8px; 14 | border-radius: 50%; 15 | background: #c2c2c2; 16 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 17 | } 18 | 19 | .lds-ellipsis div:nth-child(1) { 20 | left: -9px; 21 | animation: lds-ellipsis1 0.6s infinite; 22 | } 23 | 24 | .lds-ellipsis div:nth-child(2) { 25 | left: -10px; 26 | animation: lds-ellipsis2 0.6s infinite; 27 | } 28 | 29 | .lds-ellipsis div:nth-child(3) { 30 | left: 2px; 31 | animation: lds-ellipsis2 0.6s infinite; 32 | } 33 | 34 | .lds-ellipsis div:nth-child(4) { 35 | left: 24px; 36 | animation: lds-ellipsis3 0.6s infinite; 37 | } 38 | 39 | @keyframes lds-ellipsis1 { 40 | 0% { 41 | transform: scale(0); 42 | } 43 | 100% { 44 | transform: scale(1); 45 | } 46 | } 47 | 48 | @keyframes lds-ellipsis3 { 49 | 0% { 50 | transform: scale(1); 51 | } 52 | 100% { 53 | transform: scale(0); 54 | } 55 | } 56 | 57 | @keyframes lds-ellipsis2 { 58 | 0% { 59 | transform: translate(0, 0); 60 | } 61 | 100% { 62 | transform: translate(19px, 0); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ng-option-highlight/README.md: -------------------------------------------------------------------------------- 1 | ## Getting started 2 | ### Step 1: Install `ng-option-highlight`: 3 | 4 | #### NPM 5 | ```shell 6 | npm install --save @ng-select/ng-option-highlight 7 | ``` 8 | #### YARN 9 | ```shell 10 | yarn add @ng-select/ng-option-highlight 11 | ``` 12 | ### Step 2: Import the NgOptionHighlightModule: 13 | ```js 14 | import { NgSelectModule } from '@ng-select/ng-select'; 15 | import { NgOptionHighlightModule } from '@ng-select/ng-option-highlight'; 16 | 17 | @NgModule({ 18 | declarations: [AppComponent], 19 | imports: [ 20 | NgSelectModule, 21 | NgOptionHighlightModule 22 | ], 23 | bootstrap: [AppComponent] 24 | }) 25 | export class AppModule {} 26 | ``` 27 | 28 | ### Step 3: Add directive in your template: 29 | 30 | ```html 31 | 32 | ... 33 | 34 | {{item.title}} 35 | 36 | 37 | ``` 38 | 39 | 40 | ## Development 41 | 42 | ### Build 43 | 44 | Run `ng build ng-option-highlight` to build the project. The build artifacts will be stored in the `dist/` directory. 45 | 46 | ### Publishing 47 | 48 | After building your library with `ng build ng-option-highlight`, go to the dist folder `cd dist/ng-option-highlight` and run `npm publish`. 49 | 50 | ### Running unit tests 51 | 52 | Run `ng test ng-option-highlight` to execute the unit tests via [Karma](https://karma-runner.github.io). 53 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-async-data-example/forms-async-data-example.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 13 | 14 |
Title: {{item.title}}
15 | Id: {{item.id}} | UserId: {{item.userId}} 16 |
17 |
18 | Albums data from backend using HttpClient. 19 |
20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 28 | -------------------------------------------------------------------------------- /src/demo/app/examples/search-autocomplete-example/search-autocomplete-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { concat, Observable, of, Subject } from 'rxjs'; 3 | import { DataService, Person } from '../data.service'; 4 | import { catchError, debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators'; 5 | 6 | @Component({ 7 | selector: 'search-autocomplete-example', 8 | templateUrl: './search-autocomplete-example.component.html', 9 | styleUrls: ['./search-autocomplete-example.component.scss'] 10 | }) 11 | export class SearchAutocompleteExampleComponent implements OnInit { 12 | 13 | people$: Observable; 14 | peopleLoading = false; 15 | peopleInput$ = new Subject(); 16 | selectedPersons: Person[] = [{ name: 'Karyn Wright' }, { name: 'Other' }]; 17 | 18 | constructor(private dataService: DataService) { 19 | } 20 | 21 | ngOnInit() { 22 | this.loadPeople(); 23 | } 24 | 25 | trackByFn(item: Person) { 26 | return item.id; 27 | } 28 | 29 | private loadPeople() { 30 | this.people$ = concat( 31 | of([]), // default items 32 | this.peopleInput$.pipe( 33 | distinctUntilChanged(), 34 | tap(() => this.peopleLoading = true), 35 | switchMap(term => this.dataService.getPeople(term).pipe( 36 | catchError(() => of([])), // empty list on error 37 | tap(() => this.peopleLoading = false) 38 | )) 39 | ) 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/demo/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "~bootstrap/scss/bootstrap"; 3 | @import "style/sidebar"; 4 | @import "style/content"; 5 | 6 | body { 7 | position: relative; 8 | } 9 | 10 | html, 11 | body, 12 | demo-app { 13 | height: 100%; 14 | } 15 | 16 | body { 17 | background-color: #fafafa; 18 | } 19 | 20 | nav.navbar { 21 | @include media-breakpoint-between(xs, sm) { 22 | height: auto; 23 | } 24 | height: 80px; 25 | z-index: 20; 26 | position: relative; 27 | box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1); 28 | border-bottom: 1px solid #d4dadf; 29 | background-color: #FFFFFF; 30 | 31 | a { 32 | color: #242A31; 33 | font-family: Content-font, Roboto, sans-serif; 34 | font-weight: 500; 35 | } 36 | } 37 | 38 | demo-app, 39 | layout-header, 40 | layout-sidenav { 41 | display: block; 42 | } 43 | 44 | .fill { 45 | height: 100%; 46 | } 47 | 48 | .card { 49 | label { 50 | margin-bottom: 5px; 51 | } 52 | 53 | box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12); 54 | border: 0; 55 | border-radius: 4px; 56 | } 57 | 58 | .bd-main { 59 | order: 1; 60 | } 61 | 62 | ng-select.ng-invalid.ng-touched .ng-select-container { 63 | border-color: #dc3545; 64 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 3px #fde6e8; 65 | } 66 | 67 | .btn + .btn { 68 | margin-left: 5px 69 | } 70 | 71 | .card-body .form-group { 72 | margin-bottom: 0; 73 | } 74 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-with-options-example/forms-with-options-example.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | Yes 7 | No 8 | 9 |
10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | Batman 18 | 19 | 20 | 21 | Spider-Man & Goblin 22 | 23 | 24 | 25 | Thor 26 | 27 | 28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-async-data-example/forms-async-data-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup } from '@angular/forms'; 3 | import { NgSelectComponent } from '@ng-select/ng-select' 4 | import { delay } from 'rxjs/operators'; 5 | import { DataService } from '../data.service'; 6 | 7 | @Component({ 8 | selector: 'forms-async-data-example', 9 | templateUrl: './forms-async-data-example.component.html', 10 | styleUrls: ['./forms-async-data-example.component.scss'] 11 | }) 12 | export class FormsAsyncDataExampleComponent implements OnInit { 13 | 14 | heroForm: FormGroup; 15 | albums = []; 16 | allAlbums = []; 17 | 18 | constructor( 19 | private fb: FormBuilder, 20 | private dataService: DataService) { 21 | } 22 | 23 | ngOnInit() { 24 | this.loadAlbums(); 25 | this.heroForm = this.fb.group({ 26 | album: '', 27 | }); 28 | } 29 | 30 | openSelect(select: NgSelectComponent) { 31 | select.open(); 32 | } 33 | 34 | closeSelect(select: NgSelectComponent) { 35 | select.close(); 36 | } 37 | 38 | selectAlbumsRange(from, to) { 39 | this.albums = this.allAlbums.slice(from, to); 40 | } 41 | 42 | selectFirstAlbum() { 43 | this.heroForm.get('album').patchValue(this.albums[0].id); 44 | } 45 | 46 | private loadAlbums() { 47 | this.dataService.getAlbums().pipe(delay(500)).subscribe(albums => { 48 | this.allAlbums = albums; 49 | this.albums = [...this.allAlbums]; 50 | this.selectFirstAlbum(); 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for request-info - https://github.com/behaviorbot/request-info 2 | 3 | # *OPTIONAL* Comment to reply with 4 | # Can be either a string : 5 | requestInfoReplyComment: > 6 | Hey there! It seems that you didn't give us whole info about this issue. It will help a lot if you would use issue template and provide reproducible demo forking stackblitz example from demo page :tada: 7 | 8 | # Or an array: 9 | # requestInfoReplyComment: 10 | # - Ah no! young blade! That was a trifle short! 11 | # - Tell me more ! 12 | # - I am sure you can be more effusive 13 | 14 | # *OPTIONAL* default titles to check against for lack of descriptiveness 15 | # MUST BE ALL LOWERCASE 16 | requestInfoDefaultTitles: 17 | - update readme.md 18 | - updates 19 | - not working 20 | 21 | # *OPTIONAL* Label to be added to Issues and Pull Requests with insufficient information given 22 | requestInfoLabelToAdd: needs info 23 | 24 | # *OPTIONAL* Require Issues to contain more information than what is provided in the issue templates 25 | # Will fail if the issue's body is equal to a provided template 26 | checkIssueTemplate: true 27 | 28 | # *OPTIONAL* Require Pull Requests to contain more information than what is provided in the PR template 29 | # Will fail if the pull request's body is equal to the provided template 30 | checkPullRequestTemplate: true 31 | 32 | # *OPTIONAL* Only warn about insufficient information on these events type 33 | # Keys must be lowercase. Valid values are 'issue' and 'pullRequest' 34 | requestInfoOn: 35 | pullRequest: false 36 | issue: true 37 | 38 | # *OPTIONAL* Add a list of people whose Issues/PRs will not be commented on 39 | # keys must be GitHub usernames 40 | requestInfoUserstoExclude: 41 | - anjmao 42 | -------------------------------------------------------------------------------- /src/demo/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { RouterModule } from '@angular/router'; 5 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 6 | 7 | import { AppComponent } from './app.component'; 8 | import { DataService } from './examples/data.service'; 9 | import { ExamplesModule } from './examples/examples.module'; 10 | import { LayoutHeaderComponent } from './layout/header.component'; 11 | import { LayoutSidenavComponent } from './layout/sidenav-component'; 12 | import { appRoutes } from './routes'; 13 | import { ExampleHostDirective, ExampleViewerComponent } from './shared/example-viewer/example-viewer.component'; 14 | import { StackblitzButtonComponent } from './shared/example-viewer/stackblitz-button/stackblitz-button.component'; 15 | import { RouteViewerComponent } from './shared/route-viewer/route-viewer.component'; 16 | 17 | @NgModule({ 18 | imports: [ 19 | BrowserModule, 20 | HttpClientModule, 21 | ExamplesModule, 22 | NgbModule, 23 | RouterModule.forRoot( 24 | appRoutes, 25 | { 26 | useHash: true, 27 | relativeLinkResolution: 'legacy' 28 | } 29 | ) 30 | ], 31 | providers: [ 32 | DataService, 33 | ], 34 | declarations: [ 35 | ExampleHostDirective, 36 | AppComponent, 37 | LayoutHeaderComponent, 38 | LayoutSidenavComponent, 39 | ExampleViewerComponent, 40 | StackblitzButtonComponent, 41 | RouteViewerComponent, 42 | ], 43 | entryComponents: [], 44 | bootstrap: [AppComponent] 45 | }) 46 | export class AppModule { 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/demo/app/examples/virtual-scroll-example/virtual-scroll-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | 4 | @Component({ 5 | selector: 'virtual-scroll-example', 6 | templateUrl: './virtual-scroll-example.component.html', 7 | styleUrls: ['./virtual-scroll-example.component.scss'] 8 | }) 9 | export class VirtualScrollExampleComponent implements OnInit { 10 | 11 | photos = []; 12 | photosBuffer = []; 13 | bufferSize = 50; 14 | numberOfItemsFromEndBeforeFetchingMore = 10; 15 | loading = false; 16 | 17 | constructor(private http: HttpClient) { 18 | } 19 | 20 | ngOnInit() { 21 | this.http.get('https://jsonplaceholder.typicode.com/photos').subscribe(photos => { 22 | this.photos = photos; 23 | this.photosBuffer = this.photos.slice(0, this.bufferSize); 24 | }); 25 | } 26 | 27 | onScrollToEnd() { 28 | this.fetchMore(); 29 | } 30 | 31 | onScroll({ end }) { 32 | if (this.loading || this.photos.length <= this.photosBuffer.length) { 33 | return; 34 | } 35 | 36 | if (end + this.numberOfItemsFromEndBeforeFetchingMore >= this.photosBuffer.length) { 37 | this.fetchMore(); 38 | } 39 | } 40 | 41 | private fetchMore() { 42 | const len = this.photosBuffer.length; 43 | const more = this.photos.slice(len, this.bufferSize + len); 44 | this.loading = true; 45 | // using timeout here to simulate backend API delay 46 | setTimeout(() => { 47 | this.loading = false; 48 | this.photosBuffer = this.photosBuffer.concat(more); 49 | }, 200) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-custom-template-example/forms-custom-template-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup } from '@angular/forms'; 3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; 4 | import { DataService } from '../data.service'; 5 | 6 | @Component({ 7 | selector: 'forms-custom-template-example', 8 | templateUrl: './forms-custom-template-example.component.html', 9 | styleUrls: ['./forms-custom-template-example.component.scss'] 10 | }) 11 | export class FormsCustomTemplateExampleComponent implements OnInit { 12 | 13 | heroForm: FormGroup; 14 | photos = []; 15 | 16 | constructor( 17 | private fb: FormBuilder, 18 | private modalService: NgbModal, 19 | private dataService: DataService) { 20 | } 21 | 22 | ngOnInit() { 23 | this.loadPhotos(); 24 | 25 | this.heroForm = this.fb.group({ 26 | photo: '' 27 | }); 28 | } 29 | 30 | selectFirstPhoto() { 31 | this.heroForm.get('photo').patchValue(this.photos[0].thumbnailUrl); 32 | } 33 | 34 | openModal(content) { 35 | this.modalService.open(content); 36 | } 37 | 38 | changePhoto(photo) { 39 | this.heroForm.get('photo').patchValue(photo ? photo.thumbnailUrl : null); 40 | } 41 | 42 | togglePhotoDisabled() { 43 | const photo = this.heroForm.get('photo'); 44 | if (photo.disabled) { 45 | photo.enable(); 46 | } else { 47 | photo.disable(); 48 | } 49 | } 50 | 51 | private loadPhotos() { 52 | this.dataService.getPhotos().subscribe(photos => { 53 | this.photos = photos; 54 | this.selectFirstPhoto(); 55 | }); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-default-example/group-default-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'group-default-example', 5 | templateUrl: './group-default-example.component.html', 6 | styleUrls: ['./group-default-example.component.scss'] 7 | }) 8 | export class GroupDefaultExampleComponent implements OnInit { 9 | 10 | selectedAccount = 'Adam'; 11 | accounts = [ 12 | { name: 'Adam', email: 'adam@email.com', age: 12, country: 'United States', child: { state: 'Active' } }, 13 | { name: 'Homer', email: 'homer@email.com', age: 47, country: '', child: { state: 'Active' } }, 14 | { name: 'Samantha', email: 'samantha@email.com', age: 30, country: 'United States', child: { state: 'Active' } }, 15 | { name: 'Amalie', email: 'amalie@email.com', age: 12, country: 'Argentina', child: { state: 'Active' } }, 16 | { name: 'Estefanía', email: 'estefania@email.com', age: 21, country: 'Argentina', child: { state: 'Active' } }, 17 | { name: 'Adrian', email: 'adrian@email.com', age: 21, country: 'Ecuador', child: { state: 'Active' } }, 18 | { name: 'Wladimir', email: 'wladimir@email.com', age: 30, country: 'Ecuador', child: { state: 'Inactive' } }, 19 | { name: 'Natasha', email: 'natasha@email.com', age: 54, country: 'Ecuador', child: { state: 'Inactive' } }, 20 | { name: 'Nicole', email: 'nicole@email.com', age: 43, country: 'Colombia', child: { state: 'Inactive' } }, 21 | { name: 'Michael', email: 'michael@email.com', age: 15, country: 'Colombia', child: { state: 'Inactive' } }, 22 | { name: 'Nicolás', email: 'nicole@email.com', age: 43, country: 'Colombia', child: { state: 'Inactive' } } 23 | ]; 24 | 25 | constructor() { 26 | } 27 | 28 | ngOnInit() { 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/demo/app/examples/output-events-example/output-events-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DataService } from '../data.service'; 3 | 4 | interface Event { 5 | name: string; 6 | value: any; 7 | } 8 | 9 | @Component({ 10 | selector: 'output-events-example', 11 | templateUrl: './output-events-example.component.html', 12 | styleUrls: ['./output-events-example.component.scss'] 13 | }) 14 | export class OutputEventsExampleComponent implements OnInit { 15 | 16 | selectedItems: any; 17 | items = []; 18 | 19 | events: Event[] = []; 20 | 21 | constructor(private dataService: DataService) { 22 | this.dataService.getPeople().subscribe(items => { 23 | this.items = items; 24 | }); 25 | } 26 | 27 | ngOnInit() { 28 | } 29 | 30 | onChange($event) { 31 | this.events.push({ name: '(change)', value: $event }); 32 | } 33 | 34 | onFocus($event: Event) { 35 | this.events.push({ name: '(focus)', value: $event }); 36 | } 37 | 38 | onBlur($event: Event) { 39 | this.events.push({ name: '(blur)', value: $event }); 40 | } 41 | 42 | onOpen() { 43 | this.events.push({ name: '(open)', value: null }); 44 | } 45 | 46 | onClose() { 47 | this.events.push({ name: '(close)', value: null }); 48 | } 49 | 50 | onAdd($event) { 51 | this.events.push({ name: '(add)', value: $event }); 52 | } 53 | 54 | onRemove($event) { 55 | this.events.push({ name: '(remove)', value: $event }); 56 | } 57 | 58 | onClear() { 59 | this.events.push({ name: '(clear)', value: null }); 60 | } 61 | 62 | onScrollToEnd($event) { 63 | this.events.push({ name: '(scrollToEnd)', value: $event }); 64 | } 65 | 66 | onSearch($event) { 67 | this.events.push({ name: '(search)', value: $event }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/ng-select/lib/ng-option.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewChecked, 3 | ChangeDetectionStrategy, 4 | Component, 5 | ElementRef, 6 | Input, 7 | OnChanges, 8 | OnDestroy, 9 | SimpleChanges 10 | } from '@angular/core'; 11 | import { Subject } from 'rxjs'; 12 | 13 | @Component({ 14 | selector: 'ng-option', 15 | changeDetection: ChangeDetectionStrategy.OnPush, 16 | template: `` 17 | }) 18 | export class NgOptionComponent implements OnChanges, AfterViewChecked, OnDestroy { 19 | 20 | @Input() value: any; 21 | @Input() 22 | get disabled() { return this._disabled; } 23 | set disabled(value: any) { this._disabled = this._isDisabled(value) } 24 | 25 | readonly stateChange$ = new Subject<{ value: any, disabled: boolean, label?: string }>(); 26 | 27 | private _disabled = false; 28 | private _previousLabel: string; 29 | 30 | constructor(public elementRef: ElementRef) { } 31 | 32 | get label(): string { 33 | return (this.elementRef.nativeElement.textContent || '').trim(); 34 | } 35 | 36 | ngOnChanges(changes: SimpleChanges) { 37 | if (changes.disabled) { 38 | this.stateChange$.next({ 39 | value: this.value, 40 | disabled: this._disabled 41 | }); 42 | } 43 | } 44 | 45 | ngAfterViewChecked() { 46 | if (this.label !== this._previousLabel) { 47 | this._previousLabel = this.label; 48 | this.stateChange$.next({ 49 | value: this.value, 50 | disabled: this._disabled, 51 | label: this.elementRef.nativeElement.innerHTML 52 | }); 53 | } 54 | } 55 | 56 | ngOnDestroy() { 57 | this.stateChange$.complete(); 58 | } 59 | 60 | private _isDisabled(value) { 61 | return value != null && `${value}` !== 'false'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/demo/app/shared/example-viewer/example-viewer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentFactoryResolver, Directive, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 2 | import { EXAMPLE_COMPONENTS } from '../../examples/examples'; 3 | 4 | @Directive({ 5 | selector: '[example-host]', 6 | }) 7 | export class ExampleHostDirective { 8 | constructor(public viewContainerRef: ViewContainerRef) { 9 | } 10 | } 11 | 12 | @Component({ 13 | selector: 'example-viewer', 14 | templateUrl: './example-viewer.component.html', 15 | styles: [` 16 | .card-header { 17 | display: flex; 18 | align-items: center; 19 | justify-content: space-between; 20 | font-weight: 500; 21 | color: rgba(0, 0, 0, 0.54); 22 | } 23 | 24 | a.btn { 25 | color: rgba(0, 0, 0, 0.54); 26 | } 27 | 28 | .card { 29 | margin-bottom: 20px; 30 | } 31 | `] 32 | }) 33 | export class ExampleViewerComponent implements OnInit { 34 | 35 | @Input() example: string; 36 | 37 | @ViewChild(ExampleHostDirective, { static: true }) exampleHost: ExampleHostDirective; 38 | 39 | title: string; 40 | 41 | constructor( 42 | private componentFactoryResolver: ComponentFactoryResolver) { 43 | } 44 | 45 | get sourcePath() { 46 | return `https://github.com/ng-select/ng-select/tree/master/src/demo/app/examples/${this.example}`; 47 | } 48 | 49 | ngOnInit() { 50 | this.loadComponent(); 51 | } 52 | 53 | private loadComponent() { 54 | const example = EXAMPLE_COMPONENTS[this.example]; 55 | this.title = example.title; 56 | const componentFactory = this.componentFactoryResolver.resolveComponentFactory(example.component); 57 | 58 | const viewContainerRef = this.exampleHost.viewContainerRef; 59 | viewContainerRef.clear(); 60 | viewContainerRef.createComponent(componentFactory); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/ng-option-highlight/lib/ng-option-highlight.directive.ts: -------------------------------------------------------------------------------- 1 | import * as searchHelper from './search-helper'; 2 | import { 3 | AfterViewInit, 4 | Directive, 5 | ElementRef, 6 | Input, 7 | OnChanges, 8 | Renderer2 9 | } from '@angular/core'; 10 | 11 | @Directive({ 12 | selector: '[ngOptionHighlight]' 13 | }) 14 | export class NgOptionHighlightDirective implements OnChanges, AfterViewInit { 15 | 16 | @Input('ngOptionHighlight') term: string; 17 | 18 | private element: HTMLElement; 19 | private label: string; 20 | 21 | constructor( 22 | private elementRef: ElementRef, 23 | private renderer: Renderer2) { 24 | this.element = this.elementRef.nativeElement; 25 | } 26 | 27 | ngOnChanges() { 28 | if (this._canHighlight) { 29 | this._highlightLabel(); 30 | } 31 | } 32 | 33 | ngAfterViewInit() { 34 | this.label = this.element.innerHTML; 35 | if (this._canHighlight) { 36 | this._highlightLabel(); 37 | } 38 | } 39 | 40 | private _escapeRegExp(str: string): string { 41 | return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 42 | } 43 | 44 | private _highlightLabel() { 45 | const label = this.label; 46 | if (!this.term) { 47 | this._setInnerHtml(label); 48 | return; 49 | } 50 | 51 | const alternationString = this._escapeRegExp(this.term).replace(' ', '|'); 52 | const termRegex = new RegExp(alternationString, 'gi'); 53 | this._setInnerHtml(label.replace(termRegex, `$&`)) 54 | } 55 | 56 | private get _canHighlight() { 57 | return this._isDefined(this.term) && this._isDefined(this.label); 58 | } 59 | 60 | private _setInnerHtml(html) { 61 | this.renderer.setProperty(this.elementRef.nativeElement, 'innerHTML', html); 62 | } 63 | 64 | private _isDefined(value: any) { 65 | return value !== undefined && value !== null; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/demo/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { RouteViewerComponent } from './shared/route-viewer/route-viewer.component'; 3 | 4 | export const appRoutes: Routes = [ 5 | { 6 | path: '', 7 | redirectTo: '/data-sources', 8 | pathMatch: 'full' 9 | }, 10 | { path: 'data-sources', component: RouteViewerComponent, data: { title: 'Data sources', examples: 'data-source' } }, 11 | { path: 'forms', component: RouteViewerComponent, data: { title: 'Reactive forms', examples: 'forms' } }, 12 | { path: 'bindings', component: RouteViewerComponent, data: { title: 'Data bindings', examples: 'bindings' } }, 13 | { path: 'search', component: RouteViewerComponent, data: { title: 'Search and autocomplete', examples: 'search' } }, 14 | { path: 'tags', component: RouteViewerComponent, data: { title: 'Tags', examples: 'tags' } }, 15 | { path: 'templates', component: RouteViewerComponent, data: { title: 'Templates', examples: 'template' } }, 16 | { path: 'multiselect', component: RouteViewerComponent, data: { title: 'Multiselect', examples: 'multi-select' } }, 17 | { 18 | path: 'multiselect-checkbox', 19 | component: RouteViewerComponent, 20 | data: { title: 'Multiselect checkbox', examples: 'multi-checkbox' } 21 | }, 22 | { path: 'events', component: RouteViewerComponent, data: { title: 'Output events', examples: 'output-events' } }, 23 | { 24 | path: 'virtual-scroll', 25 | component: RouteViewerComponent, 26 | data: { title: 'Virtual scroll', examples: 'virtual-scroll' } 27 | }, 28 | { 29 | path: 'dropdown-position', 30 | component: RouteViewerComponent, 31 | data: { title: 'Dropdown position', examples: 'dropdown-position' } 32 | }, 33 | { 34 | path: 'append-to-element', 35 | component: RouteViewerComponent, 36 | data: { title: 'Append to element', examples: 'append-to' } 37 | }, 38 | { path: 'grouping', component: RouteViewerComponent, data: { title: 'Grouping', examples: 'group' } }, 39 | ]; 40 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "curly": true, 9 | "eofline": true, 10 | "forin": true, 11 | "indent": [ 12 | true, 13 | "spaces" 14 | ], 15 | "label-position": true, 16 | "max-line-length": [ 17 | true, 18 | 140 19 | ], 20 | "member-access": false, 21 | "member-ordering": [ 22 | true, 23 | "static-before-instance" 24 | ], 25 | "no-arg": true, 26 | "no-bitwise": true, 27 | "no-console": [ 28 | true, 29 | "debug", 30 | "info", 31 | "time", 32 | "timeEnd", 33 | "trace" 34 | ], 35 | "no-construct": true, 36 | "no-debugger": true, 37 | "no-duplicate-variable": true, 38 | "no-empty": false, 39 | "no-eval": true, 40 | "no-inferrable-types": [true, "ignore-params"], 41 | "no-shadowed-variable": true, 42 | "no-string-literal": false, 43 | "no-switch-case-fall-through": true, 44 | "no-trailing-whitespace": false, 45 | "no-unused-expression": true, 46 | "no-use-before-declare": false, 47 | "no-var-keyword": true, 48 | "object-literal-sort-keys": false, 49 | "one-line": [ 50 | true, 51 | "check-open-brace", 52 | "check-catch", 53 | "check-else", 54 | "check-whitespace" 55 | ], 56 | "quotemark": [ 57 | true, 58 | "single" 59 | ], 60 | "radix": true, 61 | "semicolon": [ 62 | "always" 63 | ], 64 | "triple-equals": [ 65 | true, 66 | "allow-null-check" 67 | ], 68 | "typedef-whitespace": [ 69 | true, 70 | { 71 | "call-signature": "nospace", 72 | "index-signature": "nospace", 73 | "parameter": "nospace", 74 | "property-declaration": "nospace", 75 | "variable-declaration": "nospace" 76 | } 77 | ], 78 | "variable-name": false, 79 | "whitespace": [ 80 | true, 81 | "check-branch", 82 | "check-decl", 83 | "check-operator", 84 | "check-separator", 85 | "check-type" 86 | ] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/demo/assets/stackblitz/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "demo": { 7 | "root": "", 8 | "sourceRoot": "", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/demo", 17 | "index": "index.html", 18 | "main": "main.ts", 19 | "polyfills": "polyfills.ts", 20 | "assets": [ 21 | ], 22 | "styles": [ 23 | "styles.css" 24 | ], 25 | "scripts": [] 26 | }, 27 | "configurations": { 28 | "production": { 29 | "fileReplacements": [ 30 | { 31 | "replace": "src/environments/environment.ts", 32 | "with": "src/environments/environment.prod.ts" 33 | } 34 | ], 35 | "optimization": true, 36 | "outputHashing": "all", 37 | "sourceMap": false, 38 | "extractCss": true, 39 | "namedChunks": false, 40 | "aot": true, 41 | "extractLicenses": true, 42 | "vendorChunk": false, 43 | "buildOptimizer": true 44 | } 45 | } 46 | }, 47 | "serve": { 48 | "builder": "@angular-devkit/build-angular:dev-server", 49 | "options": { 50 | "browserTarget": "demo:build" 51 | }, 52 | "configurations": { 53 | "production": { 54 | "browserTarget": "demo:build:production" 55 | } 56 | } 57 | }, 58 | "extract-i18n": { 59 | "builder": "@angular-devkit/build-angular:extract-i18n", 60 | "options": { 61 | "browserTarget": "demo:build" 62 | } 63 | } 64 | } 65 | } 66 | }, 67 | "defaultProject": "demo" 68 | } 69 | -------------------------------------------------------------------------------- /src/ng-select/lib/ng-select.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { NgDropdownPanelComponent } from './ng-dropdown-panel.component'; 4 | import { NgOptionComponent } from './ng-option.component'; 5 | import { NgSelectComponent, SELECTION_MODEL_FACTORY } from './ng-select.component'; 6 | import { 7 | NgFooterTemplateDirective, 8 | NgHeaderTemplateDirective, 9 | NgLabelTemplateDirective, 10 | NgLoadingSpinnerTemplateDirective, 11 | NgLoadingTextTemplateDirective, 12 | NgMultiLabelTemplateDirective, 13 | NgNotFoundTemplateDirective, 14 | NgOptgroupTemplateDirective, 15 | NgOptionTemplateDirective, 16 | NgTagTemplateDirective, 17 | NgItemLabelDirective, 18 | NgTypeToSearchTemplateDirective 19 | } from './ng-templates.directive'; 20 | import { DefaultSelectionModelFactory } from './selection-model'; 21 | 22 | @NgModule({ 23 | declarations: [ 24 | NgDropdownPanelComponent, 25 | NgOptionComponent, 26 | NgSelectComponent, 27 | NgOptgroupTemplateDirective, 28 | NgOptionTemplateDirective, 29 | NgLabelTemplateDirective, 30 | NgMultiLabelTemplateDirective, 31 | NgHeaderTemplateDirective, 32 | NgFooterTemplateDirective, 33 | NgNotFoundTemplateDirective, 34 | NgTypeToSearchTemplateDirective, 35 | NgLoadingTextTemplateDirective, 36 | NgTagTemplateDirective, 37 | NgLoadingSpinnerTemplateDirective, 38 | NgItemLabelDirective 39 | ], 40 | imports: [ 41 | CommonModule 42 | ], 43 | exports: [ 44 | NgSelectComponent, 45 | NgOptionComponent, 46 | NgOptgroupTemplateDirective, 47 | NgOptionTemplateDirective, 48 | NgLabelTemplateDirective, 49 | NgMultiLabelTemplateDirective, 50 | NgHeaderTemplateDirective, 51 | NgFooterTemplateDirective, 52 | NgNotFoundTemplateDirective, 53 | NgTypeToSearchTemplateDirective, 54 | NgLoadingTextTemplateDirective, 55 | NgTagTemplateDirective, 56 | NgLoadingSpinnerTemplateDirective 57 | ], 58 | providers: [ 59 | { provide: SELECTION_MODEL_FACTORY, useValue: DefaultSelectionModelFactory } 60 | ] 61 | }) 62 | export class NgSelectModule {} 63 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-function-example/group-function-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'group-function-example', 5 | templateUrl: './group-function-example.component.html', 6 | styleUrls: ['./group-function-example.component.scss'] 7 | }) 8 | export class GroupFunctionExampleComponent implements OnInit { 9 | 10 | selectedAccounts = ['Michael']; 11 | accounts = [ 12 | { name: 'Jill', email: 'jill@email.com', age: 15, country: undefined, child: { state: 'Active' } }, 13 | { name: 'Henry', email: 'henry@email.com', age: 10, country: undefined, child: { state: 'Active' } }, 14 | { name: 'Meg', email: 'meg@email.com', age: 7, country: null, child: { state: 'Active' } }, 15 | { name: 'Adam', email: 'adam@email.com', age: 12, country: 'United States', child: { state: 'Active' } }, 16 | { name: 'Homer', email: 'homer@email.com', age: 47, country: '', child: { state: 'Active' } }, 17 | { name: 'Samantha', email: 'samantha@email.com', age: 30, country: 'United States', child: { state: 'Active' } }, 18 | { name: 'Amalie', email: 'amalie@email.com', age: 12, country: 'Argentina', child: { state: 'Active' } }, 19 | { name: 'Estefanía', email: 'estefania@email.com', age: 21, country: 'Argentina', child: { state: 'Active' } }, 20 | { name: 'Adrian', email: 'adrian@email.com', age: 21, country: 'Ecuador', child: { state: 'Active' } }, 21 | { name: 'Wladimir', email: 'wladimir@email.com', age: 30, country: 'Ecuador', child: { state: 'Inactive' } }, 22 | { name: 'Natasha', email: 'natasha@email.com', age: 54, country: 'Ecuador', child: { state: 'Inactive' } }, 23 | { name: 'Nicole', email: 'nicole@email.com', age: 43, country: 'Colombia', child: { state: 'Inactive' } }, 24 | { name: 'Michael', email: 'michael@email.com', age: 15, country: 'Colombia', child: { state: 'Inactive' } }, 25 | { name: 'Nicolás', email: 'nicole@email.com', age: 43, country: 'Colombia', child: { state: 'Inactive' } } 26 | ]; 27 | 28 | groupByFn = (item) => item.child.state; 29 | 30 | groupValueFn = (_: string, children: any[]) => ({ name: children[0].child.state, total: children.length }); 31 | 32 | constructor() { 33 | } 34 | 35 | ngOnInit() { 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/demo/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; 3 | import { Title } from '@angular/platform-browser'; 4 | import { filter, map, mergeMap } from 'rxjs/operators'; 5 | import { NgSelectConfig } from '@ng-select/ng-select'; 6 | 7 | @Component({ 8 | selector: 'demo-app', 9 | templateUrl: './app.component.html', 10 | changeDetection: ChangeDetectionStrategy.Default, 11 | }) 12 | export class AppComponent { 13 | 14 | title: string; 15 | version: string = window['ngSelectVersion']; 16 | exampleSourceUrl: string; 17 | dir: 'ltr' | 'rtl' = 'ltr'; 18 | 19 | constructor( 20 | private router: Router, 21 | private activatedRoute: ActivatedRoute, 22 | private titleService: Title, 23 | private config: NgSelectConfig 24 | ) { 25 | this.config.placeholder = 'Select item'; 26 | // This could be useful if you want to use appendTo in entire application without explicitly defining it. (eg: appendTo = 'body') 27 | this.config.appendTo = null; 28 | // set the bindValue to global config when you use the same bindValue in most of the place. 29 | // You can also override bindValue for the specified template by defining `bindValue` as property 30 | // Eg : 31 | // this.config.bindValue = 'value'; 32 | } 33 | 34 | ngOnInit() { 35 | this.setTitle(); 36 | } 37 | 38 | private setTitle() { 39 | this.router.events 40 | .pipe( 41 | filter((event) => event instanceof NavigationEnd), 42 | map(() => this.activatedRoute), 43 | map((route) => { 44 | while (route.firstChild) { 45 | route = route.firstChild; 46 | } 47 | return route; 48 | }), 49 | filter((route) => route.outlet === 'primary'), 50 | mergeMap((route) => route.data) 51 | ) 52 | .subscribe((event) => { 53 | this.title = event['title']; 54 | this.titleService.setTitle(this.title); 55 | this.exampleSourceUrl = `https://github.com/ng-select/ng-select/tree/master/demo/app/examples/${event['fileName']}`; 56 | }); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/ng-select/lib/ng-templates.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, Input, OnChanges, SimpleChanges, TemplateRef } from '@angular/core'; 2 | import { escapeHTML } from './value-utils'; 3 | 4 | @Directive({ selector: '[ngItemLabel]' }) 5 | export class NgItemLabelDirective implements OnChanges { 6 | @Input() ngItemLabel: string; 7 | @Input() escape = true; 8 | 9 | constructor(private element: ElementRef) { } 10 | 11 | ngOnChanges(changes: SimpleChanges) { 12 | this.element.nativeElement.innerHTML = this.escape ? 13 | escapeHTML(this.ngItemLabel) : 14 | this.ngItemLabel; 15 | } 16 | } 17 | 18 | @Directive({ selector: '[ng-option-tmp]' }) 19 | export class NgOptionTemplateDirective { 20 | constructor(public template: TemplateRef) { } 21 | } 22 | 23 | @Directive({ selector: '[ng-optgroup-tmp]' }) 24 | export class NgOptgroupTemplateDirective { 25 | constructor(public template: TemplateRef) { } 26 | } 27 | 28 | @Directive({ selector: '[ng-label-tmp]' }) 29 | export class NgLabelTemplateDirective { 30 | constructor(public template: TemplateRef) { } 31 | } 32 | 33 | @Directive({ selector: '[ng-multi-label-tmp]' }) 34 | export class NgMultiLabelTemplateDirective { 35 | constructor(public template: TemplateRef) { } 36 | } 37 | 38 | @Directive({ selector: '[ng-header-tmp]' }) 39 | export class NgHeaderTemplateDirective { 40 | constructor(public template: TemplateRef) { } 41 | } 42 | 43 | @Directive({ selector: '[ng-footer-tmp]' }) 44 | export class NgFooterTemplateDirective { 45 | constructor(public template: TemplateRef) { } 46 | } 47 | 48 | @Directive({ selector: '[ng-notfound-tmp]' }) 49 | export class NgNotFoundTemplateDirective { 50 | constructor(public template: TemplateRef) { } 51 | } 52 | 53 | @Directive({ selector: '[ng-typetosearch-tmp]' }) 54 | export class NgTypeToSearchTemplateDirective { 55 | constructor(public template: TemplateRef) { } 56 | } 57 | 58 | @Directive({ selector: '[ng-loadingtext-tmp]' }) 59 | export class NgLoadingTextTemplateDirective { 60 | constructor(public template: TemplateRef) { } 61 | } 62 | 63 | @Directive({ selector: '[ng-tag-tmp]' }) 64 | export class NgTagTemplateDirective { 65 | constructor(public template: TemplateRef) { } 66 | } 67 | 68 | @Directive({ selector: '[ng-loadingspinner-tmp]' }) 69 | export class NgLoadingSpinnerTemplateDirective { 70 | constructor(public template: TemplateRef) { } 71 | } 72 | -------------------------------------------------------------------------------- /src/ng-select/lib/ng-dropdown-panel.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { NgDropdownPanelService } from './ng-dropdown-panel.service'; 3 | 4 | describe('NgDropdownPanelService', () => { 5 | 6 | let service: NgDropdownPanelService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | providers: [NgDropdownPanelService] 11 | }); 12 | 13 | service = TestBed.inject(NgDropdownPanelService); 14 | }); 15 | 16 | describe('calculate items', () => { 17 | it('should calculate items from start', () => { 18 | const itemsLength = 100; 19 | const buffer = 4; 20 | 21 | service.setDimensions(25, 100); 22 | const res = service.calculateItems(0, itemsLength, buffer); 23 | 24 | expect(res).toEqual({ 25 | start: 0, 26 | end: 9, 27 | topPadding: 0, 28 | scrollHeight: 2500 29 | }) 30 | }); 31 | 32 | it('should calculate items when scrolled', () => { 33 | const itemsLength = 100; 34 | const buffer = 4; 35 | 36 | service.setDimensions(25, 100); 37 | const res = service.calculateItems(1250, itemsLength, buffer); 38 | 39 | expect(res).toEqual({ 40 | start: 46, 41 | end: 59, 42 | topPadding: 1150, 43 | scrollHeight: 2500 44 | }) 45 | }); 46 | }); 47 | 48 | describe('scroll to', () => { 49 | beforeEach(() => { 50 | service.setDimensions(40, 240); 51 | }); 52 | 53 | it('should not scroll if item is in visible area', () => { 54 | expect(service.getScrollTo(0, 40, 0)).toBeNull(); 55 | expect(service.getScrollTo(200, 40, 0)).toBeNull(); 56 | }); 57 | 58 | it('should not scroll if item is inside panel height', () => { 59 | expect(service.getScrollTo(40, 40, 40)).toBeNull(); 60 | }); 61 | 62 | it('should scroll by item height', () => { 63 | expect(service.getScrollTo(240, 40, 0)).toBe(40); 64 | }); 65 | 66 | it('should start from top when reached bottom', () => { 67 | expect(service.getScrollTo(0, 40, 400)).toBe(0); 68 | }); 69 | 70 | it('should move to bottom when reached top', () => { 71 | expect(service.getScrollTo(600, 40, 0)).toBe(400); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-selectable-example/group-selectable-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'group-selectable-example', 5 | templateUrl: './group-selectable-example.component.html', 6 | styleUrls: ['./group-selectable-example.component.scss'] 7 | }) 8 | export class GroupSelectableExampleComponent implements OnInit { 9 | 10 | selectedAccount = [{ country: 'Colombia' }]; 11 | selectedAccounts = [{ name: 'Adam' }]; 12 | accounts = [ 13 | { name: 'Jill', email: 'jill@email.com', age: 15, country: undefined, child: { state: 'Active' } }, 14 | { name: 'Henry', email: 'henry@email.com', age: 10, country: undefined, child: { state: 'Active' } }, 15 | { name: 'Meg', email: 'meg@email.com', age: 7, country: null, child: { state: 'Active' } }, 16 | { name: 'Adam', email: 'adam@email.com', age: 12, country: 'United States', child: { state: 'Active' } }, 17 | { name: 'Homer', email: 'homer@email.com', age: 47, country: '', child: { state: 'Active' } }, 18 | { name: 'Samantha', email: 'samantha@email.com', age: 30, country: 'United States', child: { state: 'Active' } }, 19 | { name: 'Amalie', email: 'amalie@email.com', age: 12, country: 'Argentina', child: { state: 'Active' } }, 20 | { name: 'Estefanía', email: 'estefania@email.com', age: 21, country: 'Argentina', child: { state: 'Active' } }, 21 | { name: 'Adrian', email: 'adrian@email.com', age: 21, country: 'Ecuador', child: { state: 'Active' } }, 22 | { name: 'Wladimir', email: 'wladimir@email.com', age: 30, country: 'Ecuador', child: { state: 'Inactive' } }, 23 | { name: 'Natasha', email: 'natasha@email.com', age: 54, country: 'Ecuador', child: { state: 'Inactive' }, disabled: true }, 24 | { name: 'Nicole', email: 'nicole@email.com', age: 43, country: 'Colombia', child: { state: 'Inactive' } }, 25 | { name: 'Michael', email: 'michael@email.com', age: 15, country: 'Colombia', child: { state: 'Inactive' } }, 26 | { name: 'Nicolás', email: 'nicole@email.com', age: 43, country: 'Colombia', child: { state: 'Inactive' } } 27 | ]; 28 | 29 | ngOnInit() { 30 | } 31 | 32 | compareAccounts = (item, selected) => { 33 | if (selected.country && item.country) { 34 | return item.country === selected.country; 35 | } 36 | if (item.name && selected.name) { 37 | return item.name === selected.name; 38 | } 39 | return false; 40 | }; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/demo/app/examples/group-selectable-hidden-example/group-selectable-hidden-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'group-selectable-hidden-example', 5 | templateUrl: './group-selectable-hidden-example.component.html', 6 | styleUrls: ['./group-selectable-hidden-example.component.scss'] 7 | }) 8 | export class GroupSelectableHiddenExampleComponent implements OnInit { 9 | 10 | accounts = [ 11 | { name: 'Jill', email: 'jill@email.com', age: 15, country: undefined, child: { state: 'Active' } }, 12 | { name: 'Henry', email: 'henry@email.com', age: 10, country: undefined, child: { state: 'Active' } }, 13 | { name: 'Meg', email: 'meg@email.com', age: 7, country: null, child: { state: 'Active' } }, 14 | { name: 'Adam', email: 'adam@email.com', age: 12, country: 'United States', child: { state: 'Active' } }, 15 | { name: 'Homer', email: 'homer@email.com', age: 47, country: '', child: { state: 'Active' } }, 16 | { name: 'Samantha', email: 'samantha@email.com', age: 30, country: 'United States', child: { state: 'Active' } }, 17 | { name: 'Amalie', email: 'amalie@email.com', age: 12, country: 'Argentina', child: { state: 'Active' } }, 18 | { name: 'Estefanía', email: 'estefania@email.com', age: 21, country: 'Argentina', child: { state: 'Active' } }, 19 | { name: 'Adrian', email: 'adrian@email.com', age: 21, country: 'Ecuador', child: { state: 'Active' } }, 20 | { name: 'Wladimir', email: 'wladimir@email.com', age: 30, country: 'Ecuador', child: { state: 'Inactive' } }, 21 | { name: 'Natasha', email: 'natasha@email.com', age: 54, country: 'Ecuador', child: { state: 'Inactive' } }, 22 | { name: 'Nicole', email: 'nicole@email.com', age: 43, country: 'Colombia', child: { state: 'Inactive' } }, 23 | { name: 'Michael', email: 'michael@email.com', age: 15, country: 'Colombia', child: { state: 'Inactive' } }, 24 | { name: 'Nicolás', email: 'nicole@email.com', age: 43, country: 'Colombia', child: { state: 'Inactive' } } 25 | ]; 26 | 27 | selectedAccounts = [{ country: 'Argentina' }, { name: 'Samantha' }]; 28 | 29 | constructor() { 30 | } 31 | 32 | ngOnInit() { 33 | } 34 | 35 | compareAccounts = (item, selected) => { 36 | if (selected.country && item.country) { 37 | return item.country === selected.country; 38 | } 39 | if (item.name && selected.name) { 40 | return item.name === selected.name; 41 | } 42 | return false; 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 23 * * 0' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['javascript'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v1 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v1 67 | -------------------------------------------------------------------------------- /src/ng-select/lib/ng-dropdown-panel.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | export interface ItemsRangeResult { 3 | scrollHeight: number; 4 | topPadding: number; 5 | start: number; 6 | end: number; 7 | } 8 | 9 | export interface PanelDimensions { 10 | itemHeight: number; 11 | panelHeight: number; 12 | itemsPerViewport: number; 13 | } 14 | 15 | @Injectable() 16 | export class NgDropdownPanelService { 17 | 18 | private _dimensions: PanelDimensions = { 19 | itemHeight: 0, 20 | panelHeight: 0, 21 | itemsPerViewport: 0 22 | }; 23 | 24 | get dimensions() { 25 | return this._dimensions; 26 | } 27 | 28 | calculateItems(scrollPos: number, itemsLength: number, buffer: number): ItemsRangeResult { 29 | const d = this._dimensions; 30 | const scrollHeight = d.itemHeight * itemsLength; 31 | 32 | const scrollTop = Math.max(0, scrollPos); 33 | const indexByScrollTop = scrollTop / scrollHeight * itemsLength; 34 | let end = Math.min(itemsLength, Math.ceil(indexByScrollTop) + (d.itemsPerViewport + 1)); 35 | 36 | const maxStartEnd = end; 37 | const maxStart = Math.max(0, maxStartEnd - d.itemsPerViewport); 38 | let start = Math.min(maxStart, Math.floor(indexByScrollTop)); 39 | 40 | let topPadding = d.itemHeight * Math.ceil(start) - (d.itemHeight * Math.min(start, buffer)); 41 | topPadding = !isNaN(topPadding) ? topPadding : 0; 42 | start = !isNaN(start) ? start : -1; 43 | end = !isNaN(end) ? end : -1; 44 | start -= buffer; 45 | start = Math.max(0, start); 46 | end += buffer; 47 | end = Math.min(itemsLength, end); 48 | 49 | return { 50 | topPadding, 51 | scrollHeight, 52 | start, 53 | end 54 | } 55 | } 56 | 57 | setDimensions(itemHeight: number, panelHeight: number) { 58 | const itemsPerViewport = Math.max(1, Math.floor(panelHeight / itemHeight)); 59 | this._dimensions = { 60 | itemHeight, 61 | panelHeight, 62 | itemsPerViewport 63 | }; 64 | } 65 | 66 | getScrollTo(itemTop: number, itemHeight: number, lastScroll: number) { 67 | const { panelHeight } = this.dimensions; 68 | const itemBottom = itemTop + itemHeight; 69 | const top = lastScroll; 70 | const bottom = top + panelHeight; 71 | 72 | if (panelHeight >= itemBottom && lastScroll === itemTop) { 73 | return null; 74 | } 75 | 76 | if (itemBottom > bottom) { 77 | return top + itemBottom - bottom; 78 | } else if (itemTop <= top) { 79 | return itemTop; 80 | } 81 | 82 | return null; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/demo/style/_sidebar.scss: -------------------------------------------------------------------------------- 1 | .section-nav { 2 | padding-left: 0; 3 | border-left: 1px solid #eee; 4 | 5 | ul { 6 | padding-left: 1rem; 7 | 8 | ul { 9 | display: none; 10 | } 11 | } 12 | } 13 | 14 | .toc-entry { 15 | display: block; 16 | 17 | a { 18 | display: block; 19 | padding: .125rem 1.5rem; 20 | color: #99979c; 21 | 22 | &:hover { 23 | color: $blue; 24 | text-decoration: none; 25 | } 26 | } 27 | } 28 | 29 | .bd-sidebar { 30 | order: 0; 31 | background-color: #fafafa; 32 | height: 100%; 33 | @include media-breakpoint-between(xs, sm) { 34 | height: auto; 35 | } 36 | @include media-breakpoint-up(xl) { 37 | max-width: 320px; 38 | } 39 | // All levels of nav 40 | .nav > li > a { 41 | display: block; 42 | color: rgba(0, 0, 0, .65); 43 | font-weight: 500; 44 | } 45 | 46 | .nav > li > a:hover { 47 | color: rgba(0, 0, 0, .85); 48 | text-decoration: none; 49 | background-color: transparent; 50 | } 51 | 52 | .nav > .active > a, 53 | .nav > .active:hover > a { 54 | font-weight: 500; 55 | color: rgba(0, 0, 0, .85); 56 | background-color: transparent; 57 | } 58 | 59 | .nav > .active > a.active { 60 | border: 0 #343a40 solid; 61 | border-left-width: 4px; 62 | background-color: rgba(248, 248, 248, 0.62); 63 | border-radius: 0; 64 | } 65 | } 66 | 67 | .bd-links { 68 | padding-top: 1rem; 69 | padding-bottom: 1rem; 70 | margin-right: -15px; 71 | margin-left: -15px; 72 | @include media-breakpoint-up(md) { 73 | @supports (position: sticky) { 74 | max-height: calc(100vh - 9rem); 75 | overflow-y: auto; 76 | } 77 | } 78 | // Override collapse behaviors 79 | @include media-breakpoint-up(md) { 80 | display: block !important; 81 | } 82 | } 83 | 84 | .bd-search-docs-toggle { 85 | line-height: 1; 86 | color: $gray-900; 87 | } 88 | 89 | .bd-sidenav { 90 | display: none; 91 | } 92 | 93 | .bd-toc-link { 94 | display: block; 95 | padding: .25rem 1.5rem; 96 | font-weight: 500; 97 | color: rgba(0, 0, 0, .65); 98 | 99 | &:hover { 100 | color: rgba(0, 0, 0, .85); 101 | text-decoration: none; 102 | } 103 | } 104 | 105 | .bd-toc-item { 106 | &.active { 107 | margin-bottom: 1rem; 108 | 109 | &:not(:first-child) { 110 | margin-top: 1rem; 111 | } 112 | 113 | > .bd-toc-link { 114 | color: rgba(0, 0, 0, .85); 115 | 116 | &:hover { 117 | background-color: transparent; 118 | } 119 | } 120 | 121 | > .bd-sidenav { 122 | display: block; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": "ng-select/ng-select", 3 | "engines": { 4 | "node": ">= 12.14.0", 5 | "npm": ">= 6.0.0" 6 | }, 7 | "scripts": { 8 | "build": "ng build ng-select && ng build ng-option-highlight && yarn build:themes && yarn copy-sass", 9 | "build:demo": "ng build demo --prod --baseHref=/ng-select && yarn copy-examples", 10 | "build:themes": "node-sass --output-style compressed src/ng-select/themes/ -o dist/ng-select/themes", 11 | "copy-sass": "mkdir -p dist/ng-select/scss && cp src/ng-select/**/*.scss dist/ng-select/scss", 12 | "copy-examples": "cp -r src/demo/app/examples dist/demo", 13 | "start": "ng serve", 14 | "test:watch": "ng test ng-select --watch", 15 | "test": "ng test ng-select --code-coverage && ng test ng-option-highlight --code-coverage", 16 | "test:ci": "ng test --watch=false --browsers=ChromeHeadless --code-coverage", 17 | "coveralls": "cat ./coverage/ng-select/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 18 | "lint": "ng lint ng-select && ng lint ng-option-highlight", 19 | "publish": "cd dist/ng-select && npm publish && cd ../..", 20 | "postpublish": "cp dist/ng-select/package.json src/ng-select && cp dist/ng-option-highlight/package.json src/ng-option-highlight", 21 | "release": "semantic-release" 22 | }, 23 | "release": { 24 | "branch": "master", 25 | "plugins": [ 26 | "@semantic-release/commit-analyzer", 27 | "@semantic-release/changelog", 28 | "@semantic-release/github", 29 | [ 30 | "@semantic-release/npm", 31 | { 32 | "pkgRoot": "dist/ng-select" 33 | } 34 | ], 35 | "@semantic-release/release-notes-generator" 36 | ] 37 | }, 38 | "dependencies": { 39 | "@angular/animations": "~12.0.0", 40 | "@angular/common": "~12.0.0", 41 | "@angular/compiler": "~12.0.0", 42 | "@angular/core": "~12.0.0", 43 | "@angular/forms": "~12.0.0", 44 | "@angular/localize": "^12.0.0", 45 | "@angular/platform-browser": "~12.0.0", 46 | "@angular/platform-browser-dynamic": "~12.0.0", 47 | "@angular/router": "~12.0.0", 48 | "@ng-bootstrap/ng-bootstrap": "^9.0.2", 49 | "bootstrap": "^4.5.0", 50 | "rxjs": "~6.5.5", 51 | "tslib": "^2.2.0", 52 | "zone.js": "~0.11.4" 53 | }, 54 | "devDependencies": { 55 | "@angular-devkit/build-angular": "~12.0.0", 56 | "@angular/cli": "~12.0.0", 57 | "@angular/compiler-cli": "~12.0.0", 58 | "@angular/language-service": "~12.0.0", 59 | "@semantic-release/changelog": "^3.0.4", 60 | "@stackblitz/sdk": "^1.3.0", 61 | "@types/jasmine": "~3.6.0", 62 | "@types/jasminewd2": "^2.0.6", 63 | "@types/node": "^12.11.1", 64 | "codelyzer": "^6.0.0", 65 | "coveralls": "^3.0.2", 66 | "gh-pages-travis": "^1.0.4", 67 | "jasmine-core": "~3.6.0", 68 | "jasmine-spec-reporter": "~5.0.0", 69 | "karma": "~6.3.2", 70 | "karma-chrome-launcher": "~3.1.0", 71 | "karma-coverage-istanbul-reporter": "~3.0.2", 72 | "karma-jasmine": "~4.0.0", 73 | "karma-jasmine-html-reporter": "^1.5.0", 74 | "ng-packagr": "^12.0.0", 75 | "node-sass": "^4.12.0", 76 | "semantic-release": "^17.2.3", 77 | "standard-version": "^8.0.1", 78 | "ts-node": "~8.3.0", 79 | "tslint": "~6.1.0", 80 | "typescript": "~4.2.4" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/demo/polyfills.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 3 | */ 4 | import '@angular/localize/init'; 5 | /** 6 | * This file includes polyfills needed by Angular and is loaded before the app. 7 | * You can add your own extra polyfills to this file. 8 | * 9 | * This file is divided into 2 sections: 10 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 11 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 12 | * file. 13 | * 14 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 15 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 16 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 17 | * 18 | * Learn more in https://angular.io/guide/browser-support 19 | */ 20 | 21 | /*************************************************************************************************** 22 | * BROWSER POLYFILLS 23 | */ 24 | 25 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 26 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 27 | 28 | /** 29 | * Web Animations `@angular/platform-browser/animations` 30 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 31 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 32 | */ 33 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 34 | 35 | /** 36 | * By default, zone.js will patch all possible macroTask and DomEvents 37 | * user can disable parts of macroTask/DomEvents patch by setting following flags 38 | * because those flags need to be set before `zone.js` being loaded, and webpack 39 | * will put import in the top of bundle, so user need to create a separate file 40 | * in this directory (for example: zone-flags.ts), and put the following flags 41 | * into that file, and then add the following code before importing zone.js. 42 | * import './zone-flags.ts'; 43 | * 44 | * The flags allowed in zone-flags.ts are listed here. 45 | * 46 | * The following flags will work for all browsers. 47 | * 48 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 49 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 50 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 51 | * 52 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 53 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 54 | * 55 | * (window as any).__Zone_enable_cross_context_check = true; 56 | * 57 | */ 58 | 59 | /*************************************************************************************************** 60 | * Zone JS is required by default for Angular itself. 61 | */ 62 | import 'zone.js'; // Included with Angular CLI. 63 | 64 | 65 | /*************************************************************************************************** 66 | * APPLICATION IMPORTS 67 | */ 68 | -------------------------------------------------------------------------------- /src/demo/app/examples/forms-custom-template-example/forms-custom-template-example.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 11 | 12 | 13 | {{item.title}} 14 | 15 | 16 | 17 | {{item.title}} 18 | 19 | 20 | 5000 items with virtual scroll 21 |
22 | 23 | 24 | 25 |
26 | 27 | 28 | 34 | 58 | 61 | 62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /src/ng-select/lib/selection-model.ts: -------------------------------------------------------------------------------- 1 | import { NgOption } from './ng-select.types'; 2 | 3 | export type SelectionModelFactory = () => SelectionModel; 4 | 5 | export function DefaultSelectionModelFactory() { 6 | return new DefaultSelectionModel(); 7 | } 8 | 9 | export interface SelectionModel { 10 | value: NgOption[]; 11 | select(item: NgOption, multiple: boolean, selectableGroupAsModel: boolean); 12 | unselect(item: NgOption, multiple: boolean); 13 | clear(keepDisabled: boolean); 14 | } 15 | 16 | export class DefaultSelectionModel implements SelectionModel { 17 | private _selected: NgOption[] = []; 18 | 19 | get value(): NgOption[] { 20 | return this._selected; 21 | } 22 | 23 | select(item: NgOption, multiple: boolean, groupAsModel: boolean) { 24 | item.selected = true; 25 | if (!item.children || (!multiple && groupAsModel)) { 26 | this._selected.push(item); 27 | } 28 | if (multiple) { 29 | if (item.parent) { 30 | const childrenCount = item.parent.children.length; 31 | const selectedCount = item.parent.children.filter(x => x.selected).length; 32 | item.parent.selected = childrenCount === selectedCount; 33 | } else if (item.children) { 34 | this._setChildrenSelectedState(item.children, true); 35 | this._removeChildren(item); 36 | if (groupAsModel && this._activeChildren(item)) { 37 | this._selected = [...this._selected.filter(x => x.parent !== item), item] 38 | } else { 39 | this._selected = [...this._selected, ...item.children.filter(x => !x.disabled)]; 40 | } 41 | } 42 | } 43 | } 44 | 45 | unselect(item: NgOption, multiple: boolean) { 46 | this._selected = this._selected.filter(x => x !== item); 47 | item.selected = false; 48 | if (multiple) { 49 | if (item.parent && item.parent.selected) { 50 | const children = item.parent.children; 51 | this._removeParent(item.parent); 52 | this._removeChildren(item.parent); 53 | this._selected.push(...children.filter(x => x !== item && !x.disabled)); 54 | item.parent.selected = false; 55 | } else if (item.children) { 56 | this._setChildrenSelectedState(item.children, false); 57 | this._removeChildren(item); 58 | } 59 | } 60 | } 61 | 62 | clear(keepDisabled: boolean) { 63 | this._selected = keepDisabled ? this._selected.filter(x => x.disabled) : []; 64 | } 65 | 66 | private _setChildrenSelectedState(children: NgOption[], selected: boolean) { 67 | for (const child of children) { 68 | if (child.disabled) { 69 | continue; 70 | } 71 | child.selected = selected; 72 | } 73 | } 74 | 75 | private _removeChildren(parent: NgOption) { 76 | this._selected = [ 77 | ...this._selected.filter(x => x.parent !== parent), 78 | ...parent.children.filter(x => x.parent === parent && x.disabled && x.selected) 79 | ]; 80 | } 81 | 82 | private _removeParent(parent: NgOption) { 83 | this._selected = this._selected.filter(x => x !== parent) 84 | } 85 | 86 | private _activeChildren(item: NgOption): boolean { 87 | return item.children.every(x => !x.disabled || x.selected); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/demo/app/shared/example-viewer/stackblitz-button/stackblitz.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import sdk from '@stackblitz/sdk'; 4 | import { Project } from '@stackblitz/sdk/typings/interfaces'; 5 | 6 | const EXAMPLE_PATH = '/ng-select/examples/'; 7 | const TEMPLATE_PATH = '/ng-select/assets/stackblitz/'; 8 | 9 | const TEMPLATE_FILES = [ 10 | 'index.html', 11 | 'styles.css', 12 | 'polyfills.ts', 13 | 'data.service.ts', 14 | 'main.ts', 15 | 'app.module.ts', 16 | 'angular.json', 17 | ]; 18 | 19 | const angularVersion = '>=7.0.0'; 20 | const dependencies = { 21 | '@angular/animations': angularVersion, 22 | '@angular/common': angularVersion, 23 | '@angular/compiler': angularVersion, 24 | '@angular/core': angularVersion, 25 | '@angular/forms': angularVersion, 26 | '@angular/http': angularVersion, 27 | '@angular/platform-browser': angularVersion, 28 | '@angular/platform-browser-dynamic': angularVersion, 29 | '@angular/router': angularVersion, 30 | '@ng-select/ng-select': '*', 31 | '@ng-select/ng-option-highlight': '*', 32 | '@ng-bootstrap/ng-bootstrap': '*', 33 | 'core-js': '^2.4.1', 34 | 'rxjs': '>=6.0.0-beta.0 <7.0.0', 35 | 'web-animations-js': '^2.3.1', 36 | 'zone.js': '^0.8.14', 37 | 'hammerjs': '^2.0.8' 38 | }; 39 | 40 | 41 | @Injectable({ 42 | providedIn: 'root' 43 | }) 44 | export class StackblitzService { 45 | 46 | private _exampleName: string; 47 | private _componentName: string; 48 | private _examplePath: string; 49 | 50 | constructor(private _http: HttpClient) { 51 | } 52 | 53 | async openNewProject(example: string) { 54 | this._mapExamplePath(example); 55 | 56 | const exampleFiles = await this.fetchExampleFiles(); 57 | const templateFiles = await this.fetchTemplateFiles(); 58 | 59 | const project: Project = { 60 | files: { 61 | ...templateFiles, 62 | ...exampleFiles 63 | }, 64 | title: 'ng-select example', 65 | description: 'ng-select example', 66 | template: 'angular-cli', 67 | dependencies: dependencies 68 | }; 69 | 70 | sdk.openProject(project, { openFile: `src/${this._exampleName}.component.html` }) 71 | } 72 | 73 | async fetchTemplateFiles() { 74 | const files: { [path: string]: string } = {}; 75 | for (const file of TEMPLATE_FILES) { 76 | let fileResult = await this._readFile(file, TEMPLATE_PATH); 77 | if (file.includes('app.module')) { 78 | fileResult = fileResult.replace('//example-import', this._exampleImport); 79 | fileResult = fileResult.replace('//example-template', this._exampleTemplate); 80 | fileResult = fileResult.replace('//example-cmp', this._componentName); 81 | } 82 | files[file] = fileResult; 83 | 84 | } 85 | return files; 86 | } 87 | 88 | async fetchExampleFiles() { 89 | const exampleFiles = ['html', 'scss', 'ts'].map(ex => (`${this._exampleName}.component.${ex}`)); 90 | const files: { [path: string]: string } = {}; 91 | 92 | for (const file of exampleFiles) { 93 | const path = `src/${file}`; 94 | files[path] = await this._readFile(file, this._examplePath); 95 | } 96 | 97 | return files; 98 | } 99 | 100 | async _readFile(file: string, path: string) { 101 | return await this._http.get(path + file, { responseType: 'text' }).toPromise(); 102 | } 103 | 104 | private _mapExamplePath(example: string) { 105 | const toUpperCase = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); 106 | 107 | this._exampleName = example; 108 | this._examplePath = EXAMPLE_PATH + example + '/'; 109 | this._componentName = `${this._exampleName.split('-').map(x => toUpperCase(x)).join('')}Component`; 110 | } 111 | 112 | private get _exampleImport() { 113 | return `import { ${this._componentName} } from \'./src/${this._exampleName}.component\'` 114 | } 115 | 116 | private get _exampleTemplate() { 117 | return `<${this._exampleName}>`; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/demo/app/layout/header.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | // @ts-ignore 3 | import antDesignTheme from '../../../ng-select/themes/ant.design.theme.scss'; 4 | // @ts-ignore 5 | import defaultTheme from '../../../ng-select/themes/default.theme.scss'; 6 | // @ts-ignore 7 | import materialTheme from '../../../ng-select/themes/material.theme.scss'; 8 | 9 | type langDir = 'ltr' | 'rtl'; 10 | 11 | @Component({ 12 | selector: 'layout-header', 13 | template: ` 14 | 64 | ` 65 | }) 66 | export class LayoutHeaderComponent implements AfterViewInit { 67 | theme = 'Default theme'; 68 | @Input() version: string; 69 | @Input() dir: langDir; 70 | @Output() dirChange = new EventEmitter(); 71 | 72 | private style: HTMLStyleElement; 73 | 74 | ngAfterViewInit() { 75 | setTimeout(() => { 76 | this.style = document.createElement('style'); 77 | this.style.type = 'text/css'; 78 | this.style.id = 'MyStyleTag' 79 | this.style.innerHTML = defaultTheme; 80 | document.getElementsByTagName('head')[0].appendChild(this.style); 81 | }, 100); 82 | } 83 | 84 | setTheme(theme) { 85 | this.theme = theme; 86 | if (this.theme === 'Default theme') { 87 | this.style.innerHTML = defaultTheme; 88 | } else if (this.theme === 'Material theme') { 89 | this.style.innerHTML = materialTheme; 90 | } else { 91 | this.style.innerHTML = antDesignTheme; 92 | } 93 | } 94 | 95 | changeDirTo(dir: langDir) { 96 | this.dir = dir; 97 | this.dirChange.emit(dir); 98 | } 99 | } 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/ng-option-highlight/lib/ng-option-highlight.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { NgOptionHighlightDirective } from './ng-option-highlight.directive'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | @Component({ 7 | template: ` 8 | My text is highlighted 9 | My text is not highlighted 10 | My text is highlighted 11 | My ťëxť is highlighted text 12 | New label 13 | +My text is) high\\lighted 14 | ` 15 | }) 16 | class TestComponent { 17 | term: string; 18 | showNew = false; 19 | } 20 | 21 | describe('NgOptionHighlightDirective', () => { 22 | 23 | let fixture: ComponentFixture; 24 | 25 | beforeEach(() => { 26 | fixture = TestBed.configureTestingModule({ 27 | declarations: [NgOptionHighlightDirective, TestComponent] 28 | }).createComponent(TestComponent); 29 | 30 | fixture.detectChanges(); 31 | }); 32 | 33 | it('should have five elements with highlight directive', () => { 34 | const highlightDirectives = fixture.debugElement.queryAll(By.directive(NgOptionHighlightDirective)); 35 | expect(highlightDirectives.length).toBe(5); 36 | }); 37 | 38 | it('should have one element with highlighted text when term matching', () => { 39 | const span = fixture.debugElement.query(By.css('#test1')); 40 | fixture.componentInstance.term = 'is high'; 41 | fixture.detectChanges(); 42 | expect(span.nativeElement.querySelectorAll('.highlighted')[0].innerHTML).toBe('is'); 43 | expect(span.nativeElement.querySelectorAll('.highlighted')[1].innerHTML).toBe('high'); 44 | expect(span.nativeElement.textContent).toBe('My text is highlighted'); 45 | }); 46 | 47 | it('should have one element with no highlighted text when term not matching', () => { 48 | const span = fixture.debugElement.query(By.css('#test2')); 49 | fixture.componentInstance.term = 'non matching'; 50 | fixture.detectChanges(); 51 | expect(span.nativeElement.querySelector('.highlighted')).toBeNull(); 52 | expect(span.nativeElement.innerHTML).toBe('My text is not highlighted'); 53 | }); 54 | 55 | it('should have multiple elements with highlighted text when term matching', () => { 56 | const span = fixture.debugElement.query(By.css('#test3')); 57 | fixture.componentInstance.term = 'text highlighted'; 58 | fixture.detectChanges(); 59 | expect(span.nativeElement.querySelectorAll('.highlighted')[0].innerHTML).toBe('text'); 60 | expect(span.nativeElement.querySelectorAll('.highlighted')[1].innerHTML).toBe('highlighted'); 61 | expect(span.nativeElement.textContent).toBe('My text is highlighted'); 62 | }); 63 | 64 | it('Highlights special characters', () => { 65 | const span = fixture.debugElement.query(By.css('#test4')); 66 | fixture.componentInstance.term = 'ťëxť'; 67 | fixture.detectChanges(); 68 | expect(span.nativeElement.querySelector('.highlighted').innerHTML).toBe('ťëxť'); 69 | expect(span.nativeElement.textContent).toBe('My ťëxť is highlighted text'); 70 | }); 71 | 72 | it('should highlight text when label changed', () => { 73 | fixture.componentInstance.term = 'new'; 74 | fixture.detectChanges(); 75 | fixture.componentInstance.showNew = true; 76 | fixture.detectChanges(); 77 | const span = fixture.debugElement.query(By.css('#test5')); 78 | expect(span.nativeElement.querySelector('.highlighted').innerHTML).toBe('New'); 79 | expect(span.nativeElement.textContent).toBe('New label'); 80 | }); 81 | 82 | it('should highlight text with an special character at the beginning of the term', () => { 83 | const span = fixture.debugElement.query(By.css('#test6')); 84 | 85 | fixture.componentInstance.term = '+My text'; 86 | fixture.detectChanges(); 87 | expect(span.nativeElement.querySelectorAll('.highlighted')[0].innerHTML).toBe('+My'); 88 | expect(span.nativeElement.querySelectorAll('.highlighted')[1].innerHTML).toBe('text'); 89 | expect(span.nativeElement.textContent).toBe('+My text is) high\\lighted'); 90 | }); 91 | 92 | it('should highlight text with an special character at the end of the term', () => { 93 | const span = fixture.debugElement.query(By.css('#test6')); 94 | 95 | fixture.componentInstance.term = 'is)'; 96 | fixture.detectChanges(); 97 | expect(span.nativeElement.querySelectorAll('.highlighted')[0].innerHTML).toBe('is)'); 98 | expect(span.nativeElement.textContent).toBe('+My text is) high\\lighted'); 99 | }); 100 | 101 | it('should highlight text with an special character in the middle of the term', () => { 102 | const span = fixture.debugElement.query(By.css('#test6')); 103 | 104 | fixture.componentInstance.term = 'high\\l'; 105 | fixture.detectChanges(); 106 | expect(span.nativeElement.querySelectorAll('.highlighted')[0].innerHTML).toBe('high\\l'); 107 | expect(span.nativeElement.textContent).toBe('+My text is) high\\lighted'); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "src", 5 | "projects": { 6 | "ng-select": { 7 | "projectType": "library", 8 | "root": "src/ng-select", 9 | "sourceRoot": "src/ng-select", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "tsConfig": "src/ng-select/tsconfig.lib.json", 16 | "project": "src/ng-select/ng-package.json" 17 | } 18 | }, 19 | "test": { 20 | "builder": "@angular-devkit/build-angular:karma", 21 | "options": { 22 | "main": "src/ng-select/test.ts", 23 | "tsConfig": "src/ng-select/tsconfig.spec.json", 24 | "karmaConfig": "src/ng-select/karma.conf.js", 25 | "watch": false, 26 | "codeCoverageExclude": [ 27 | "src/ng-select/testing/*", 28 | "src/ng-select/*.ts", 29 | "src/ng-select/lib/console.service.ts", 30 | "src/ng-select/lib/search-helper.ts", 31 | "src/ng-select/lib/ng-templates.directive.ts" 32 | ] 33 | } 34 | }, 35 | "lint": { 36 | "builder": "@angular-devkit/build-angular:tslint", 37 | "options": { 38 | "tsConfig": [ 39 | "src/ng-select/tsconfig.lib.json", 40 | "src/ng-select/tsconfig.spec.json" 41 | ], 42 | "exclude": ["**/node_modules/**"] 43 | } 44 | } 45 | } 46 | }, 47 | "demo": { 48 | "projectType": "application", 49 | "schematics": { 50 | "@schematics/angular:component": { 51 | "style": "scss", 52 | "skipTests": true 53 | }, 54 | "@schematics/angular:class": { 55 | "skipTests": true 56 | }, 57 | "@schematics/angular:directive": { 58 | "skipTests": true 59 | }, 60 | "@schematics/angular:guard": { 61 | "skipTests": true 62 | }, 63 | "@schematics/angular:module": { 64 | "skipTests": true 65 | }, 66 | "@schematics/angular:pipe": { 67 | "skipTests": true 68 | }, 69 | "@schematics/angular:service": { 70 | "skipTests": true 71 | } 72 | }, 73 | "root": "src/demo", 74 | "sourceRoot": "src/demo", 75 | "prefix": "", 76 | "architect": { 77 | "build": { 78 | "builder": "@angular-devkit/build-angular:browser", 79 | "options": { 80 | "outputPath": "dist/demo", 81 | "showCircularDependencies": false, 82 | "index": "src/demo/index.html", 83 | "main": "src/demo/main.ts", 84 | "polyfills": "src/demo/polyfills.ts", 85 | "tsConfig": "src/demo/tsconfig.app.json", 86 | "deployUrl": "/ng-select/", 87 | "assets": [ 88 | "src/demo/favicon.ico", 89 | "src/demo/assets" 90 | ], 91 | "styles": [ 92 | "src/demo/styles.scss" 93 | ], 94 | "scripts": [], 95 | "vendorChunk": true, 96 | "extractLicenses": false, 97 | "buildOptimizer": false, 98 | "sourceMap": true, 99 | "optimization": false, 100 | "namedChunks": true 101 | }, 102 | "configurations": { 103 | "production": { 104 | "fileReplacements": [ 105 | { 106 | "replace": "src/demo/environments/environment.ts", 107 | "with": "src/demo/environments/environment.prod.ts" 108 | } 109 | ], 110 | "optimization": true, 111 | "outputHashing": "all", 112 | "sourceMap": false, 113 | "namedChunks": false, 114 | "extractLicenses": true, 115 | "vendorChunk": false, 116 | "buildOptimizer": true, 117 | "budgets": [ 118 | { 119 | "type": "initial", 120 | "maximumWarning": "2mb", 121 | "maximumError": "5mb" 122 | }, 123 | { 124 | "type": "anyComponentStyle", 125 | "maximumWarning": "6kb" 126 | } 127 | ] 128 | } 129 | } 130 | }, 131 | "serve": { 132 | "builder": "@angular-devkit/build-angular:dev-server", 133 | "options": { 134 | "browserTarget": "demo:build", 135 | "deployUrl": "/" 136 | }, 137 | "configurations": { 138 | "production": { 139 | "browserTarget": "demo:build:production" 140 | } 141 | } 142 | } 143 | } 144 | }, 145 | "ng-option-highlight": { 146 | "projectType": "library", 147 | "root": "src/ng-option-highlight", 148 | "sourceRoot": "src/ng-option-highlight", 149 | "prefix": "lib", 150 | "architect": { 151 | "build": { 152 | "builder": "@angular-devkit/build-angular:ng-packagr", 153 | "options": { 154 | "tsConfig": "src/ng-option-highlight/tsconfig.lib.json", 155 | "project": "src/ng-option-highlight/ng-package.json" 156 | } 157 | }, 158 | "test": { 159 | "builder": "@angular-devkit/build-angular:karma", 160 | "options": { 161 | "main": "src/ng-option-highlight/test.ts", 162 | "tsConfig": "src/ng-option-highlight/tsconfig.spec.json", 163 | "karmaConfig": "src/ng-option-highlight/karma.conf.js", 164 | "watch": false 165 | } 166 | }, 167 | "lint": { 168 | "builder": "@angular-devkit/build-angular:tslint", 169 | "options": { 170 | "tsConfig": [ 171 | "src/ng-option-highlight/tsconfig.lib.json", 172 | "src/ng-option-highlight/tsconfig.spec.json" 173 | ], 174 | "exclude": ["**/node_modules/**"] 175 | } 176 | } 177 | } 178 | } 179 | }, 180 | "defaultProject": "demo" 181 | } 182 | -------------------------------------------------------------------------------- /src/ng-select/lib/ng-select.component.html: -------------------------------------------------------------------------------- 1 |
6 | 7 |
8 |
{{placeholder}}
9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 |
22 |
23 | 24 | 27 | 28 | 29 |
34 | 35 | 50 |
51 |
52 | 53 | 54 | 55 |
56 |
57 | 58 | 60 | 61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 | 72 | 92 | 93 | 94 |
104 | 105 | 106 | 107 | 108 | 109 | 112 | 113 |
114 | 115 |
116 | 117 | {{addTagText}}"{{searchTerm}}" 118 | 119 | 120 | 123 | 124 |
125 |
126 | 127 | 128 | 129 |
{{notFoundText}}
130 |
131 | 132 | 135 | 136 |
137 | 138 | 139 | 140 |
{{typeToSearchText}}
141 |
142 | 143 | 145 | 146 |
147 | 148 | 149 | 150 |
{{loadingText}}
151 |
152 | 153 | 156 | 157 |
158 | 159 |
160 | --------------------------------------------------------------------------------