├── .browserslistrc ├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── pull_request_template.md ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── projects └── autocomplete-lib │ ├── .browserslistrc │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── autocomplete-lib.module.ts │ │ ├── autocomplete.component.html │ │ ├── autocomplete.component.scss │ │ ├── autocomplete.component.spec.ts │ │ ├── autocomplete.component.ts │ │ └── highlight.pipe.ts │ ├── public_api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── forms │ │ ├── forms.component.html │ │ ├── forms.component.scss │ │ ├── forms.component.spec.ts │ │ └── forms.component.ts │ ├── home │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.spec.ts │ │ └── home.component.ts │ ├── models │ │ └── countries.ts │ └── services │ │ ├── data.service.spec.ts │ │ └── data.service.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.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 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://paypal.me/gmerabishvili?locale.x=en_US'] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | #### Bug description 12 | 13 | 14 | #### Expected result 15 | 16 | 17 | #### Actual result 18 | 19 | 20 | #### Steps to reproduce 21 | 22 | 23 | 1. 24 | 2. 25 | 3. 26 | #### Context 27 | 28 | 29 | 30 | #### Your Environment 31 | 32 | 33 | * Version used: 34 | * Browser Name and version: 35 | * Operating System and version: 36 | * Link to your project: 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: Giorgi Merabishvili 7 | 8 | --- 9 | 10 | #### Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | #### Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | #### Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | #### Additional context 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | #### Changes 2 | 3 | 4 | 5 | 6 | Fixes # (issue) 7 | 8 | #### Type of change 9 | 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | #### Checklist 17 | 18 | - [ ] My code follows the style guidelines of this project 19 | - [ ] I have performed a self-review of my own code 20 | - [ ] I have commented my code, particularly in hard-to-understand areas 21 | - [ ] I have made corresponding changes to the documentation 22 | - [ ] My changes generate no new warnings 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | *.iml 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.angular/cache 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | yarn-error.log 36 | testem.log 37 | /typings 38 | 39 | # System Files 40 | .DS_Store 41 | Thumbs.db 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## Version 2.0.10 6 | 7 | ### Bug Fixes 8 | * Infinite scroll bug fixed on browser resize. 9 | 10 | ## Version 2.0.6 11 | 12 | ### Features 13 | * **selectedValueRender:** Custom renderer function to render selected value inside input field. 14 | * Customizable icons. 15 | 16 | 17 | ### Bug Fixes 18 | * Angular upgraded to V 12. 19 | * Removed automatic dependency on fonts.google.com. 20 | * Fixed 'scrolledToEnd event fires together with closed event' issue. 21 | * Accessibility improved. 22 | * Minor fixes and improvements. 23 | 24 | 25 | ## Version 2.0.5 26 | 27 | ### Features 28 | * **customFilter:** Custom filter function. You can use it to provide your own filtering function. 29 | 30 | 31 | ## Since version 2.0.2, 2.0.3 and 2.0.4 32 | 33 | ### Features 34 | * **focusFirst:** Automatically focus the first matched item on the list. 35 | 36 | ### Bug Fixes 37 | * Set initial value during run time. 38 | * Set/patch initial value when using reactive forms. 39 | * Clear the input value when using template driven forms. 40 | * Make autocomplete work with classes also, it used to work with objects only. 41 | * Remove potential security vulnerability in one of the project dependencies. 42 | * Other small fixes and improvements. 43 | 44 | ### 45 | CHANGES 46 | * 'placeHolder' changed to 'placeholder'. 47 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Giorgi Merabishvili 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Autocomplete 2 | * See [Demo](https://gmerabishvili.github.io/angular-ng-autocomplete/) or try in [Stackblitz](https://stackblitz.com/edit/angular-ng-autocomplete) 3 | * Example with images [Stackblitz](https://stackblitz.com/edit/angular-ng-autocomplete-with-images) 4 | * Example with Angular forms API [Stackblitz](https://stackblitz.com/edit/angular-ng-autocomplete-with-forms) 5 | 6 | 7 | Table of contents 8 | ================= 9 | 10 | * [Features](#features) 11 | * [Getting started](#getting-started) 12 | * [Usage](#usage-sample) 13 | * [API](#api) 14 | * [Styles](#styles) 15 | 16 | ## Features 17 | - [x] Flexible autocomplete with client/server filtering. 18 | - [x] Variable properties and event bindings. 19 | - [x] Selection history. 20 | - [x] Custom item and 'not found' templates. 21 | - [x] Infinite scroll. 22 | - [x] Compatible with Angular forms API (Both Reactive and Template-driven forms). 23 | - [x] Keyboard navigation. 24 | - [x] Accessibility. 25 | 26 | ## Getting started 27 | ### Step 1: Install `angular-ng-autocomplete`: 28 | 29 | #### NPM 30 | ```shell 31 | npm i angular-ng-autocomplete 32 | ``` 33 | ### Step 2: Import the AutocompleteLibModule: 34 | ```js 35 | import {AutocompleteLibModule} from 'angular-ng-autocomplete'; 36 | 37 | @NgModule({ 38 | declarations: [AppComponent], 39 | imports: [AutocompleteLibModule], 40 | bootstrap: [AppComponent] 41 | }) 42 | export class AppModule {} 43 | ``` 44 | ### Usage sample 45 | 46 | ```html 47 |
48 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 |
66 |
67 | 68 | ``` 69 | ```javascript 70 | 71 | class TestComponent { 72 | keyword = 'name'; 73 | data = [ 74 | { 75 | id: 1, 76 | name: 'Georgia' 77 | }, 78 | { 79 | id: 2, 80 | name: 'Usa' 81 | }, 82 | { 83 | id: 3, 84 | name: 'England' 85 | } 86 | ]; 87 | 88 | 89 | selectEvent(item) { 90 | // do something with selected item 91 | } 92 | 93 | onChangeSearch(val: string) { 94 | // fetch remote data from here 95 | // And reassign the 'data' which is binded to 'data' property. 96 | } 97 | 98 | onFocused(e){ 99 | // do something when input is focused 100 | } 101 | } 102 | ``` 103 | 104 | ## API 105 | ### Inputs 106 | | Input | Type | Default | Required | Description | 107 | | ------------- | ------------- | ------------- | ------------- | ------------- | 108 | | [data] | `Array` | `null` | yes | Items array. It can be array of strings or array of objects. | 109 | | searchKeyword | `string` | `-` | yes | Variable name to filter data with. | 110 | | customFilter | `(items: any[], query: string) => any[]` | `undefined` | no | Custom filter function. You can use it to provide your own filtering function, as e.g. fuzzy-matching filtering, or to disable filtering at all (just pass `(items) => items` as a filter). Do not change the `items` argument given, return filtered list instead. | 111 | | selectedValueRender | `(value: any) => string` | `undefined` | no | Custom renderer function to render selected value inside input field. | 112 | | placeholder | `string` | `-` | no | HTML `` placeholder text. | 113 | | heading | `string` | `-` | no | Heading text of items list. If it is null then heading is hidden. | 114 | | initialValue | `any` | `_` | no | Initial/default selected value. | 115 | | focusFirst | `boolean` | `false` | no | Automatically focus the first matched item on the list. | 116 | | historyIdentifier | `string` | `_` | no | History identifier of history list. When valid history identifier is given, then component stores selected item to local storage of user's browser. If it is null then history is hidden. History list is visible if at least one history item is stored. History identifier must be unique. | 117 | | historyHeading | `string` | `Recently selected` | no | Heading text of history list. If it is null then history heading is hidden. | 118 | | historyListMaxNumber | `number` | `15` | no | Maximum number of items in the history list. | 119 | | notFoundText | `string` | `Not found` | no | Set custom text when filter returns empty result. | 120 | | isLoading | `boolean` | `false` | no | Set the loading state when data is being loaded, (e.g. async items loading) and show loading spinner. | 121 | | minQueryLength | `number` | `1` | no | The minimum number of characters the user must type before a search is performed. | 122 | | debounceTime | `number` | `_` | no | Delay time while typing. | 123 | | disabled | `boolean` | `false` | no | HTML `` disable/enable. | 124 | 125 | ### Outputs 126 | | Output | Description | 127 | | ------------- | ------------- | 128 | | (selected) | Event is emitted when an item from the list is selected. | 129 | | (inputChanged) | Event is emitted when an input is changed. | 130 | | (inputFocused) | Event is emitted when an input is focused. | 131 | | (inputCleared) | Event is emitted when an input is cleared. | 132 | | (opened) | Event is emitted when the autocomplete panel is opened. | 133 | | (closed) | Event is emitted when the autocomplete panel is closed. | 134 | | (scrolledToEnd) | Event is emitted when scrolled to the end of items. Can be used for loading more items in chunks. | 135 | 136 | 137 | ### Methods (controls) 138 | Name | Description | 139 | | ------------- | ------------- | 140 | | open | Opens the autocomplete panel | 141 | | close | Closes the autocomplete panel | 142 | | focus | Focuses the autocomplete input element | 143 | | clear | Clears the autocomplete input element | 144 | 145 | To access the control methods of the component you should use `@ViewChild` decorator. 146 | See the example below: 147 | 148 | ```html 149 | 150 | ``` 151 | 152 | ```javascript 153 | class TestComponent { 154 | @ViewChild('auto') auto; 155 | 156 | openPanel(e): void { 157 | e.stopPropagation(); 158 | this.auto.open(); 159 | } 160 | 161 | closePanel(e): void { 162 | e.stopPropagation(); 163 | this.auto.close(); 164 | } 165 | 166 | focus(e): void { 167 | e.stopPropagation(); 168 | this.auto.focus(); 169 | } 170 | } 171 | ``` 172 | 173 | ## Styles 174 | If you are not happy with default styles you can easily override them: 175 | 176 | ```html 177 |
178 | 179 |
180 | ``` 181 | 182 | ```css 183 | .ng-autocomplete { 184 | width: 400px; 185 | } 186 | ``` 187 | 188 | ## Support Angular autocomplete! 189 | If you do love angular-ng-autocomplete I would appreciate a donation :) 190 | 191 | [![](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://paypal.me/gmerabishvili?locale.x=en_US) 192 | 193 | 194 | ### Author 195 | * [Giorgi Merabishvili](https://www.linkedin.com/in/giorgi-merabishvili-3719a2121/) 196 | 197 | 198 | ## License 199 | 200 | MIT 201 | 202 | 203 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": false 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "angular-autocomplete": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | }, 15 | "@schematics/angular:application": { 16 | "strict": true 17 | } 18 | }, 19 | "root": "", 20 | "sourceRoot": "src", 21 | "prefix": "app", 22 | "architect": { 23 | "build": { 24 | "builder": "@angular-devkit/build-angular:browser", 25 | "options": { 26 | "outputPath": "dist/angular-autocomplete", 27 | "index": "src/index.html", 28 | "main": "src/main.ts", 29 | "polyfills": "src/polyfills.ts", 30 | "tsConfig": "tsconfig.app.json", 31 | "assets": [ 32 | "src/favicon.ico", 33 | "src/assets" 34 | ], 35 | "styles": [ 36 | "src/styles.scss" 37 | ], 38 | "scripts": [], 39 | "aot": false, 40 | "vendorChunk": true, 41 | "extractLicenses": false, 42 | "buildOptimizer": false, 43 | "sourceMap": true, 44 | "optimization": false, 45 | "namedChunks": true 46 | }, 47 | "configurations": { 48 | "production": { 49 | "budgets": [ 50 | { 51 | "type": "initial", 52 | "maximumWarning": "10mb", 53 | "maximumError": "15mb" 54 | }, 55 | { 56 | "type": "anyComponentStyle", 57 | "maximumWarning": "100kb", 58 | "maximumError": "150b" 59 | } 60 | ], 61 | "fileReplacements": [ 62 | { 63 | "replace": "src/environments/environment.ts", 64 | "with": "src/environments/environment.prod.ts" 65 | } 66 | ], 67 | "outputHashing": "all" 68 | } 69 | }, 70 | "defaultConfiguration": "" 71 | }, 72 | "serve": { 73 | "builder": "@angular-devkit/build-angular:dev-server", 74 | "options": { 75 | "browserTarget": "angular-autocomplete:build" 76 | }, 77 | "configurations": { 78 | "production": { 79 | "browserTarget": "angular-autocomplete:build:production" 80 | } 81 | } 82 | }, 83 | "extract-i18n": { 84 | "builder": "@angular-devkit/build-angular:extract-i18n", 85 | "options": { 86 | "browserTarget": "angular-autocomplete:build" 87 | } 88 | }, 89 | "test": { 90 | "builder": "@angular-devkit/build-angular:karma", 91 | "options": { 92 | "main": "src/test.ts", 93 | "polyfills": "src/polyfills.ts", 94 | "tsConfig": "tsconfig.spec.json", 95 | "karmaConfig": "karma.conf.js", 96 | "styles": [ 97 | "src/styles.scss" 98 | ], 99 | "scripts": [], 100 | "assets": [ 101 | "src/favicon.ico", 102 | "src/assets" 103 | ] 104 | } 105 | } 106 | } 107 | }, 108 | "autocomplete-lib": { 109 | "root": "projects/autocomplete-lib", 110 | "sourceRoot": "projects/autocomplete-lib/src", 111 | "projectType": "library", 112 | "prefix": "ng", 113 | "architect": { 114 | "build": { 115 | "builder": "@angular-devkit/build-angular:ng-packagr", 116 | "options": { 117 | "project": "projects/autocomplete-lib/ng-package.json" 118 | }, 119 | "configurations": { 120 | "production": { 121 | "tsConfig": "projects/autocomplete-lib/tsconfig.lib.prod.json" 122 | }, 123 | "development": { 124 | "tsConfig": "projects/autocomplete-lib/tsconfig.lib.json" 125 | } 126 | }, 127 | "defaultConfiguration": "production" 128 | }, 129 | "test": { 130 | "builder": "@angular-devkit/build-angular:karma", 131 | "options": { 132 | "main": "projects/autocomplete-lib/src/test.ts", 133 | "tsConfig": "projects/autocomplete-lib/tsconfig.spec.json", 134 | "karmaConfig": "projects/autocomplete-lib/karma.conf.js" 135 | } 136 | } 137 | } 138 | } 139 | }, 140 | "defaultProject": "angular-autocomplete" 141 | } 142 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to angular-autocomplete!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /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'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/angular-playground'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ng-autocomplete", 3 | "version": "2.0.12", 4 | "description": "Angular autocomplete", 5 | "keywords": [ 6 | "angular", 7 | "angular-ng-autocomplete", 8 | "angular autocomplete", 9 | "angular-autocomplete", 10 | "angular-auto-complete", 11 | "autocomplete-component", 12 | "ng-autocomplete", 13 | "auto complete", 14 | "autocomplete", 15 | "angular-6", 16 | "angular-7", 17 | "angular-8", 18 | "angular-9", 19 | "angular-10", 20 | "angular-11", 21 | "angular-12", 22 | "angular-13", 23 | "angular-14" 24 | ], 25 | "author": { 26 | "name": "Giorgi Merabishvili", 27 | "email": "gtmerabishvili@gmail.com", 28 | "url": "https://www.linkedin.com/in/giorgi-merabishvili-3719a2121/" 29 | }, 30 | "license": "MIT", 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/gmerabishvili/angular-autocomplete" 34 | }, 35 | "homepage": "https://gmerabishvili.github.io/angular-ng-autocomplete/", 36 | "scripts": { 37 | "ng": "ng", 38 | "start": "ng serve", 39 | "build_lib": "ng build angular-autocomplete", 40 | "npm_pack": "cd dist/autocomplete-lib && npm pack", 41 | "package": "npm run build_lib && npm run npm_pack", 42 | "watch": "ng build --watch --configuration development", 43 | "test": "ng test", 44 | "e2e": "ng e2e", 45 | "postinstall": "ngcc" 46 | }, 47 | "private": true, 48 | "dependencies": { 49 | "@angular/animations": "^13.2.1", 50 | "@angular/common": "^13.2.1", 51 | "@angular/compiler": "^13.2.1", 52 | "@angular/core": "^13.2.1", 53 | "@angular/forms": "^13.2.1", 54 | "@angular/platform-browser": "^13.2.1", 55 | "@angular/platform-browser-dynamic": "^13.2.1", 56 | "@angular/platform-server": "^13.2.1", 57 | "@angular/router": "^13.2.1", 58 | "rxjs": "^6.5.4", 59 | "tslib": "^2.3.1", 60 | "zone.js": "~0.11.4" 61 | }, 62 | "devDependencies": { 63 | "@angular-devkit/build-angular": "^13.2.2", 64 | "@angular/cli": "^13.2.2", 65 | "@angular/compiler-cli": "^13.2.1", 66 | "@angular/language-service": "^13.2.1", 67 | "@types/jasmine": "^3.5.3", 68 | "@types/node": "^13.7.1", 69 | "jasmine-core": "^3.5.0", 70 | "karma": "^6.3.16", 71 | "karma-chrome-launcher": "^3.1.0", 72 | "karma-coverage": "~2.1.0", 73 | "karma-jasmine": "^3.1.1", 74 | "karma-jasmine-html-reporter": "^1.5.2", 75 | "ng-packagr": "^13.2.1", 76 | "ts-node": "~5.0.1", 77 | "typescript": "~4.5.2" 78 | }, 79 | "peerDependencies": {} 80 | } 81 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/.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 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/README.md: -------------------------------------------------------------------------------- 1 | # Angular Autocomplete 2 | * See [Demo](https://gmerabishvili.github.io/angular-ng-autocomplete/) or try in [Stackblitz](https://stackblitz.com/edit/angular-ng-autocomplete) 3 | * Example with images [Stackblitz](https://stackblitz.com/edit/angular-ng-autocomplete-with-images) 4 | * Example with Angular forms API [Stackblitz](https://stackblitz.com/edit/angular-ng-autocomplete-with-forms) 5 | 6 | 7 | Table of contents 8 | ================= 9 | 10 | * [Features](#features) 11 | * [Getting started](#getting-started) 12 | * [Usage](#usage-sample) 13 | * [API](#api) 14 | * [Styles](#styles) 15 | 16 | ## Features 17 | - [x] Flexible autocomplete with client/server filtering. 18 | - [x] Variable properties and event bindings. 19 | - [x] Selection history. 20 | - [x] Custom item and 'not found' templates. 21 | - [x] Infinite scroll. 22 | - [x] Compatible with Angular forms API (Both Reactive and Template-driven forms). 23 | - [x] Keyboard navigation. 24 | - [x] Accessibility. 25 | 26 | ## Getting started 27 | ### Step 1: Install `angular-ng-autocomplete`: 28 | 29 | #### NPM 30 | ```shell 31 | npm i angular-ng-autocomplete 32 | ``` 33 | ### Step 2: Import the AutocompleteLibModule: 34 | ```js 35 | import {AutocompleteLibModule} from 'angular-ng-autocomplete'; 36 | 37 | @NgModule({ 38 | declarations: [AppComponent], 39 | imports: [AutocompleteLibModule], 40 | bootstrap: [AppComponent] 41 | }) 42 | export class AppModule {} 43 | ``` 44 | ### Usage sample 45 | 46 | ```html 47 |
48 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 |
66 |
67 | 68 | ``` 69 | ```javascript 70 | 71 | class TestComponent { 72 | keyword = 'name'; 73 | data = [ 74 | { 75 | id: 1, 76 | name: 'Georgia' 77 | }, 78 | { 79 | id: 2, 80 | name: 'Usa' 81 | }, 82 | { 83 | id: 3, 84 | name: 'England' 85 | } 86 | ]; 87 | 88 | 89 | selectEvent(item) { 90 | // do something with selected item 91 | } 92 | 93 | onChangeSearch(val: string) { 94 | // fetch remote data from here 95 | // And reassign the 'data' which is binded to 'data' property. 96 | } 97 | 98 | onFocused(e){ 99 | // do something when input is focused 100 | } 101 | } 102 | ``` 103 | 104 | ## API 105 | ### Inputs 106 | | Input | Type | Default | Required | Description | 107 | | ------------- | ------------- | ------------- | ------------- | ------------- | 108 | | [data] | `Array` | `null` | yes | Items array. It can be array of strings or array of objects. | 109 | | searchKeyword | `string` | `-` | yes | Variable name to filter data with. | 110 | | customFilter | `(items: any[], query: string) => any[]` | `undefined` | no | Custom filter function. You can use it to provide your own filtering function, as e.g. fuzzy-matching filtering, or to disable filtering at all (just pass `(items) => items` as a filter). Do not change the `items` argument given, return filtered list instead. | 111 | | selectedValueRender | `(value: any) => string` | `undefined` | no | Custom renderer function to render selected value inside input field. | 112 | | placeholder | `string` | `-` | no | HTML `` placeholder text. | 113 | | heading | `string` | `-` | no | Heading text of items list. If it is null then heading is hidden. | 114 | | initialValue | `any` | `_` | no | Initial/default selected value. | 115 | | focusFirst | `boolean` | `false` | no | Automatically focus the first matched item on the list. | 116 | | historyIdentifier | `string` | `_` | no | History identifier of history list. When valid history identifier is given, then component stores selected item to local storage of user's browser. If it is null then history is hidden. History list is visible if at least one history item is stored. History identifier must be unique. | 117 | | historyHeading | `string` | `Recently selected` | no | Heading text of history list. If it is null then history heading is hidden. | 118 | | historyListMaxNumber | `number` | `15` | no | Maximum number of items in the history list. | 119 | | notFoundText | `string` | `Not found` | no | Set custom text when filter returns empty result. | 120 | | isLoading | `boolean` | `false` | no | Set the loading state when data is being loaded, (e.g. async items loading) and show loading spinner. | 121 | | minQueryLength | `number` | `1` | no | The minimum number of characters the user must type before a search is performed. | 122 | | debounceTime | `number` | `_` | no | Delay time while typing. | 123 | | disabled | `boolean` | `false` | no | HTML `` disable/enable. | 124 | 125 | ### Outputs 126 | | Output | Description | 127 | | ------------- | ------------- | 128 | | (selected) | Event is emitted when an item from the list is selected. | 129 | | (inputChanged) | Event is emitted when an input is changed. | 130 | | (inputFocused) | Event is emitted when an input is focused. | 131 | | (inputCleared) | Event is emitted when an input is cleared. | 132 | | (opened) | Event is emitted when the autocomplete panel is opened. | 133 | | (closed) | Event is emitted when the autocomplete panel is closed. | 134 | | (scrolledToEnd) | Event is emitted when scrolled to the end of items. Can be used for loading more items in chunks. | 135 | 136 | 137 | ### Methods (controls) 138 | Name | Description | 139 | | ------------- | ------------- | 140 | | open | Opens the autocomplete panel | 141 | | close | Closes the autocomplete panel | 142 | | focus | Focuses the autocomplete input element | 143 | | clear | Clears the autocomplete input element | 144 | 145 | To access the control methods of the component you should use `@ViewChild` decorator. 146 | See the example below: 147 | 148 | ```html 149 | 150 | ``` 151 | 152 | ```javascript 153 | class TestComponent { 154 | @ViewChild('auto') auto; 155 | 156 | openPanel(e): void { 157 | e.stopPropagation(); 158 | this.auto.open(); 159 | } 160 | 161 | closePanel(e): void { 162 | e.stopPropagation(); 163 | this.auto.close(); 164 | } 165 | 166 | focus(e): void { 167 | e.stopPropagation(); 168 | this.auto.focus(); 169 | } 170 | } 171 | ``` 172 | 173 | ## Styles 174 | If you are not happy with default styles you can easily override them: 175 | 176 | ```html 177 |
178 | 179 |
180 | ``` 181 | 182 | ```css 183 | .ng-autocomplete { 184 | width: 400px; 185 | } 186 | ``` 187 | 188 | ## Support Angular autocomplete! 189 | If you do love angular-ng-autocomplete I would appreciate a donation :) 190 | 191 | [![](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://paypal.me/gmerabishvili?locale.x=en_US) 192 | 193 | 194 | ### Author 195 | * [Giorgi Merabishvili](https://www.linkedin.com/in/giorgi-merabishvili-3719a2121/) 196 | 197 | 198 | ## License 199 | 200 | MIT 201 | 202 | 203 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/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'), 20 | reports: ['html', '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 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/autocomplete-lib", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ng-autocomplete", 3 | "version": "2.0.12", 4 | "description": "Angular autocomplete", 5 | "keywords": [ 6 | "angular", 7 | "angular-ng-autocomplete", 8 | "angular autocomplete", 9 | "angular-autocomplete", 10 | "angular-auto-complete", 11 | "autocomplete-component", 12 | "ng-autocomplete", 13 | "auto complete", 14 | "autocomplete", 15 | "angular-6", 16 | "angular-7", 17 | "angular-8", 18 | "angular-9", 19 | "angular-10", 20 | "angular-11", 21 | "angular-12", 22 | "angular-13", 23 | "angular-14" 24 | ], 25 | "author": { 26 | "name": "Giorgi Merabishvili", 27 | "email": "gtmerabishvili@gmail.com", 28 | "url": "https://www.linkedin.com/in/giorgi-merabishvili-3719a2121/" 29 | }, 30 | "license": "MIT", 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/gmerabishvili/angular-autocomplete" 34 | }, 35 | "homepage": "https://gmerabishvili.github.io/angular-ng-autocomplete/", 36 | "peerDependencies": { 37 | "@angular/common": "*", 38 | "@angular/core": "*" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/src/lib/autocomplete-lib.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {AutocompleteComponent} from './autocomplete.component'; 3 | import {FormsModule} from '@angular/forms'; 4 | import {CommonModule} from '@angular/common'; 5 | import {HighlightPipe} from './highlight.pipe'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | FormsModule 11 | ], 12 | declarations: [AutocompleteComponent, HighlightPipe], 13 | exports: [ AutocompleteComponent, HighlightPipe] 14 | }) 15 | export class AutocompleteLibModule { 16 | } 17 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/src/lib/autocomplete.component.html: -------------------------------------------------------------------------------- 1 |
3 |
4 | 16 |
17 | close 18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | 36 | 37 |
39 | 40 |
41 |
{{heading}}
42 |
43 | 44 |
    45 |
  • 46 | 47 |
    49 | 51 | 52 |
    53 | 54 |
    56 | 58 | 59 |
    60 |
  • 61 |
62 |
63 | 64 | 65 |
67 | 68 |
69 |
{{historyHeading}}
70 |
71 | delete 72 |
73 |
74 | 75 |
    76 |
  • 77 | 78 |
    79 | 81 | 82 |
    83 | 84 |
    85 | 87 | 88 |
    89 |
    90 | close 91 |
    92 |
  • 93 |
94 |
95 | 96 | 97 |
98 | 100 | 101 |
102 |
103 |
104 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/src/lib/autocomplete.component.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | @import url('https://fonts.googleapis.com/icon?family=Material+Icons'); 3 | 4 | 5 | .ng-autocomplete { 6 | width: 600px; 7 | } 8 | 9 | .autocomplete-container { 10 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .2), 0 1px 1px 0 rgba(0, 0, 0, .14), 0 2px 1px -1px rgba(0, 0, 0, .12); 11 | position: relative; 12 | overflow: visible; 13 | height: 40px; 14 | 15 | .input-container { 16 | input { 17 | font-size: 14px; 18 | box-sizing: border-box; 19 | border: none; 20 | box-shadow: none; 21 | outline: none; 22 | background-color: #FFFFFF; 23 | color: rgba(0, 0, 0, 0.87); 24 | width: 100%; 25 | padding: 0 15px; 26 | line-height: 40px; 27 | height: 40px; 28 | 29 | &:disabled { 30 | background-color: #eee; 31 | color: #666; 32 | } 33 | } 34 | 35 | .x { 36 | position: absolute; 37 | right: 10px; 38 | margin: auto; 39 | cursor: pointer; 40 | top: 50%; 41 | transform: translateY(-50%); 42 | 43 | i { 44 | color: rgba(0, 0, 0, 0.54); 45 | font-size: 22px; 46 | vertical-align: middle; 47 | } 48 | } 49 | } 50 | 51 | .suggestions-container { 52 | position: absolute; 53 | width: 100%; 54 | background: white; 55 | height: auto; 56 | box-shadow: 0 2px 5px rgba(0, 0, 0, .25); 57 | -webkit-box-sizing: border-box; 58 | -moz-box-sizing: border-box; 59 | box-sizing: border-box; 60 | 61 | ul { 62 | padding: 0; 63 | margin: 0; 64 | max-height: 240px; 65 | overflow-y: auto; 66 | 67 | li { 68 | position: relative; 69 | list-style: none; 70 | padding: 0; 71 | margin: 0; 72 | cursor: pointer; 73 | 74 | a { 75 | padding: 14px 15px; 76 | display: block; 77 | text-decoration: none; 78 | color: #333333; 79 | cursor: pointer; 80 | color: rgba(0, 0, 0, 0.87); 81 | font-size: 15px; 82 | } 83 | 84 | &:hover { 85 | background-color: rgba(158, 158, 158, 0.18); 86 | } 87 | } 88 | } 89 | 90 | .complete-selected { 91 | background-color: rgba(158, 158, 158, 0.18); 92 | } 93 | 94 | .heading { 95 | position: relative; 96 | padding: 10px 15px; 97 | border: solid 1px #f1f1f1; 98 | 99 | .text { 100 | font-size: 0.85em; 101 | } 102 | } 103 | 104 | .x { 105 | position: absolute; 106 | right: 10px; 107 | margin: auto; 108 | cursor: pointer; 109 | top: 50%; 110 | transform: translateY(-50%); 111 | 112 | i { 113 | color: rgba(0, 0, 0, 0.54); 114 | font-size: 18px; 115 | vertical-align: middle; 116 | } 117 | } 118 | 119 | &.is-hidden { 120 | visibility: hidden; 121 | } 122 | 123 | &.is-visible { 124 | visibility: visible; 125 | } 126 | } 127 | 128 | .not-found { 129 | padding: 0 0.75em; 130 | border: solid 1px #f1f1f1; 131 | background: white; 132 | 133 | div { 134 | padding: .4em 0; 135 | font-size: .95em; 136 | line-height: 1.4; 137 | border-bottom: 1px solid rgba(230, 230, 230, 0.7); 138 | } 139 | } 140 | 141 | &.active { 142 | z-index: 999; 143 | } 144 | } 145 | 146 | .highlight { 147 | font-weight: bold; 148 | } 149 | 150 | .autocomplete-overlay { 151 | position: fixed; 152 | background-color: transparent; 153 | width: 100%; 154 | height: 100%; 155 | top: 0; 156 | right: 0; 157 | bottom: 0; 158 | left: 0; 159 | z-index: 50; 160 | } 161 | 162 | input[type=text]::-ms-clear { 163 | display: none; 164 | } 165 | 166 | /*Loading spinner*/ 167 | 168 | .sk-fading-circle { 169 | $circleCount: 12; 170 | $animationDuration: 1.2s; 171 | 172 | width: 20px; 173 | height: 20px; 174 | position: absolute; 175 | right: 10px; 176 | top: 0; 177 | bottom: 0; 178 | margin: auto; 179 | 180 | .sk-circle { 181 | width: 100%; 182 | height: 100%; 183 | position: absolute; 184 | left: 0; 185 | top: 0; 186 | } 187 | 188 | .sk-circle:before { 189 | content: ''; 190 | display: block; 191 | margin: 0 auto; 192 | width: 15%; 193 | height: 15%; 194 | background-color: #333; 195 | border-radius: 100%; 196 | animation: sk-circleFadeDelay $animationDuration infinite ease-in-out both; 197 | } 198 | 199 | @for $i from 2 through $circleCount { 200 | .sk-circle#{$i} { transform: rotate(math.div(360deg, $circleCount) * ($i - 1)); } 201 | } 202 | 203 | @for $i from 2 through $circleCount { 204 | .sk-circle#{$i}:before { animation-delay: - $animationDuration + math.div($animationDuration, $circleCount) * ($i - 1); } 205 | } 206 | 207 | } 208 | 209 | @keyframes sk-circleFadeDelay { 210 | 0%, 39%, 100% { opacity: 0 } 211 | 40% { opacity: 1 } 212 | } 213 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/src/lib/autocomplete.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AutocompleteComponent } from './autocomplete.component'; 4 | 5 | describe('AutocompleteComponent', () => { 6 | let component: AutocompleteComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AutocompleteComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AutocompleteComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/src/lib/autocomplete.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | Component, ContentChild, 4 | ElementRef, 5 | EventEmitter, forwardRef, 6 | Input, OnChanges, 7 | OnInit, 8 | Output, 9 | Renderer2, 10 | SimpleChanges, TemplateRef, 11 | ViewChild, 12 | ViewEncapsulation 13 | } from '@angular/core'; 14 | import {fromEvent, Observable} from 'rxjs'; 15 | import {debounceTime, filter, map} from 'rxjs/operators'; 16 | import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; 17 | 18 | /** 19 | * Keyboard events 20 | */ 21 | const isArrowUp = keyCode => keyCode === 38; 22 | const isArrowDown = keyCode => keyCode === 40; 23 | const isArrowUpDown = keyCode => isArrowUp(keyCode) || isArrowDown(keyCode); 24 | const isEnter = keyCode => keyCode === 13; 25 | const isBackspace = keyCode => keyCode === 8; 26 | const isDelete = keyCode => keyCode === 46; 27 | const isESC = keyCode => keyCode === 27; 28 | const isTab = keyCode => keyCode === 9; 29 | 30 | 31 | @Component({ 32 | selector: 'ng-autocomplete', 33 | templateUrl: './autocomplete.component.html', 34 | styleUrls: ['./autocomplete.component.scss'], 35 | providers: [ 36 | { 37 | provide: NG_VALUE_ACCESSOR, 38 | useExisting: forwardRef(() => AutocompleteComponent), 39 | multi: true 40 | } 41 | ], 42 | encapsulation: ViewEncapsulation.None, 43 | host: { 44 | '(document:click)': 'handleClick($event)', 45 | 'class': 'ng-autocomplete' 46 | }, 47 | }) 48 | 49 | export class AutocompleteComponent implements OnInit, OnChanges, AfterViewInit, ControlValueAccessor { 50 | @ViewChild('searchInput') searchInput: ElementRef; // input element 51 | @ViewChild('filteredListElement') filteredListElement: ElementRef; // element of items 52 | @ViewChild('historyListElement') historyListElement: ElementRef; // element of history items 53 | 54 | inputKeyUp$: Observable; 55 | inputKeyDown$: Observable; 56 | 57 | public query = ''; // search query 58 | public filteredList = []; // list of items 59 | public historyList = []; // list of history items 60 | public isHistoryListVisible = true; 61 | public elementRef; 62 | public selectedIdx = -1; 63 | public toHighlight = ''; 64 | public notFound = false; 65 | public isFocused = false; 66 | public isOpen = false; 67 | public isScrollToEnd = false; 68 | public overlay = false; 69 | private manualOpen = undefined; 70 | private manualClose = undefined; 71 | 72 | 73 | // @Inputs 74 | /** 75 | * Data of items list. 76 | * It can be array of strings or array of objects. 77 | */ 78 | @Input() public data = []; 79 | @Input() public searchKeyword: string; // keyword to filter the list 80 | @Input() public placeholder = ''; 81 | @Input() public heading = ''; 82 | @Input() public initialValue: any; 83 | /** 84 | * History identifier of history list 85 | * When valid history identifier is given, then component stores selected item to local storage of user's browser. 86 | * If it is null then history is hidden. 87 | * History list is visible if at least one history item is stored. 88 | */ 89 | @Input() public historyIdentifier: string; 90 | /** 91 | * Heading text of history list. 92 | * If it is null then history heading is hidden. 93 | */ 94 | @Input() public historyHeading = 'Recently selected'; 95 | @Input() public historyListMaxNumber = 15; // maximum number of items in the history list. 96 | @Input() public notFoundText = 'Not found'; // set custom text when filter returns empty result 97 | @Input() public isLoading: boolean; // loading mask 98 | @Input() public debounceTime: number; // delay time while typing 99 | @Input() public disabled: boolean; // input disable/enable 100 | /** 101 | * The minimum number of characters the user must type before a search is performed. 102 | */ 103 | @Input() public minQueryLength = 1; 104 | 105 | /** 106 | * Focus first item in the list 107 | */ 108 | @Input() public focusFirst = false; 109 | 110 | /** 111 | * Custom filter function 112 | */ 113 | @Input() public customFilter: (items: any[], query: string) => any[]; 114 | 115 | /** 116 | * Custom result render function 117 | * @param value - selected value to be rendered inside input field 118 | */ 119 | @Input() public selectedValueRender: (value: any) => string; 120 | 121 | // @Output events 122 | /** Event that is emitted whenever an item from the list is selected. */ 123 | @Output() selected = new EventEmitter(); 124 | 125 | /** Event that is emitted whenever an input is changed. */ 126 | @Output() inputChanged = new EventEmitter(); 127 | 128 | /** Event that is emitted whenever an input is focused. */ 129 | @Output() readonly inputFocused: EventEmitter = new EventEmitter(); 130 | 131 | /** Event that is emitted whenever an input is cleared. */ 132 | @Output() readonly inputCleared: EventEmitter = new EventEmitter(); 133 | 134 | /** Event that is emitted when the autocomplete panel is opened. */ 135 | @Output() readonly opened: EventEmitter = new EventEmitter(); 136 | 137 | /** Event that is emitted when the autocomplete panel is closed. */ 138 | @Output() readonly closed: EventEmitter = new EventEmitter(); 139 | 140 | /** Event that is emitted when scrolled to the end of items. */ 141 | @Output() readonly scrolledToEnd: EventEmitter = new EventEmitter(); 142 | 143 | 144 | // Custom templates 145 | @Input() itemTemplate !: TemplateRef; 146 | @Input() notFoundTemplate !: TemplateRef; 147 | @ContentChild(TemplateRef) customTemplate !: TemplateRef; 148 | 149 | /** 150 | * Propagates new value when model changes 151 | */ 152 | propagateChange: any = () => { 153 | }; 154 | 155 | onTouched: any = () => { 156 | }; 157 | 158 | /** 159 | * Writes a new value from the form model into the view, 160 | * Updates model 161 | */ 162 | writeValue(value: any = '') { 163 | this.query = this.selectedValueRender !== undefined ? this.selectedValueRender(value) : this.defaultWriteValue(value); 164 | } 165 | 166 | private defaultWriteValue(value: any) { 167 | return value && !this.isTypeString(value) ? value[this.searchKeyword] : value; 168 | } 169 | 170 | /** 171 | * Registers a handler that is called when something in the view has changed 172 | */ 173 | registerOnChange(fn) { 174 | this.propagateChange = fn; 175 | } 176 | 177 | /** 178 | * Registers a handler specifically for when a control receives a touch event 179 | */ 180 | registerOnTouched(fn: () => void): void { 181 | this.onTouched = fn; 182 | } 183 | 184 | /** 185 | * Event that is called when the value of an input element is changed 186 | */ 187 | onChange(event) { 188 | this.propagateChange(event.target.value); 189 | } 190 | 191 | constructor(elementRef: ElementRef, private renderer: Renderer2) { 192 | this.elementRef = elementRef; 193 | } 194 | 195 | /** 196 | * Event that is called when the control status changes to or from DISABLED 197 | */ 198 | setDisabledState(isDisabled: boolean): void { 199 | this.disabled = isDisabled; 200 | } 201 | 202 | ngOnInit(): void { 203 | } 204 | 205 | ngAfterViewInit() { 206 | this.initEventStream(); 207 | this.handleScroll(); 208 | } 209 | 210 | /** 211 | * Set initial value 212 | * @param value 213 | */ 214 | public setInitialValue(value: any) { 215 | if (this.initialValue) { 216 | this.select(value); 217 | } 218 | } 219 | 220 | /** 221 | * Update search results 222 | */ 223 | ngOnChanges(changes: SimpleChanges): void { 224 | this.setInitialValue(this.initialValue); 225 | if ( 226 | changes && 227 | changes.data && 228 | Array.isArray(changes.data.currentValue) 229 | ) { 230 | this.handleItemsChange(); 231 | if (!changes.data.firstChange && this.isFocused) { 232 | this.handleOpen(); 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * Items change 239 | */ 240 | public handleItemsChange() { 241 | this.isScrollToEnd = false; 242 | if (!this.isOpen) { 243 | return; 244 | } 245 | 246 | this.filteredList = this.data; 247 | this.notFound = !this.filteredList || this.filteredList.length === 0; 248 | 249 | // Filter list when updating data and panel is open 250 | if (this.isOpen) { 251 | this.filterList(); 252 | } 253 | } 254 | 255 | /** 256 | * Filter data 257 | */ 258 | public filterList() { 259 | this.selectedIdx = -1; 260 | this.initSearchHistory(); 261 | if (this.query != null && this.data) { 262 | this.toHighlight = this.query; 263 | this.filteredList = this.customFilter !== undefined ? this.customFilter([...this.data], this.query) : this.defaultFilterFunction(); 264 | // If [focusFirst]="true" automatically focus the first match 265 | if (this.filteredList.length > 0 && this.focusFirst) { 266 | this.selectedIdx = 0; 267 | } 268 | } else { 269 | this.notFound = false; 270 | } 271 | } 272 | 273 | /** 274 | * Default filter function, used unless customFilter is provided 275 | */ 276 | public defaultFilterFunction(): any[] { 277 | return this.data.filter((item: any) => { 278 | if (typeof item === 'string') { 279 | // string logic, check equality of strings 280 | return item.toLowerCase().indexOf(this.query.toLowerCase()) > -1; 281 | } else if (typeof item === 'object' && item instanceof Object) { 282 | // object logic, check property equality 283 | return item[this.searchKeyword] ? item[this.searchKeyword].toLowerCase().indexOf(this.query.toLowerCase()) > -1 : ""; 284 | } 285 | }); 286 | } 287 | 288 | 289 | /** 290 | * Check if item is a string in the list. 291 | * @param item 292 | */ 293 | isTypeString(item) { 294 | return typeof item === 'string'; 295 | } 296 | 297 | /** 298 | * Select item in the list. 299 | * @param item 300 | */ 301 | public select(item) { 302 | this.query = !this.isTypeString(item) ? item[this.searchKeyword] : item; 303 | this.isOpen = true; 304 | this.overlay = false; 305 | this.selected.emit(item); 306 | this.propagateChange(item); 307 | 308 | if (this.initialValue) { 309 | // check if history already exists in localStorage and then update 310 | const history = window.localStorage.getItem(`${this.historyIdentifier}`); 311 | if (history) { 312 | let existingHistory = JSON.parse(localStorage[`${this.historyIdentifier}`]); 313 | if (!(existingHistory instanceof Array)) existingHistory = []; 314 | 315 | // check if selected item exists in existingHistory 316 | if (!existingHistory.some((existingItem) => !this.isTypeString(existingItem) 317 | ? existingItem[this.searchKeyword] == item[this.searchKeyword] : existingItem == item)) { 318 | existingHistory.unshift(item); 319 | localStorage.setItem(`${this.historyIdentifier}`, JSON.stringify(existingHistory)); 320 | 321 | // check if items don't exceed max allowed number 322 | if (existingHistory.length >= this.historyListMaxNumber) { 323 | existingHistory.splice(existingHistory.length - 1, 1); 324 | localStorage.setItem(`${this.historyIdentifier}`, JSON.stringify(existingHistory)); 325 | } 326 | } else { 327 | // if selected item exists in existingHistory swap to top in array 328 | if (!this.isTypeString(item)) { 329 | // object logic 330 | const copiedExistingHistory = existingHistory.slice(); // copy original existingHistory array 331 | const selectedIndex = copiedExistingHistory.map((el) => el[this.searchKeyword]).indexOf(item[this.searchKeyword]); 332 | copiedExistingHistory.splice(selectedIndex, 1); 333 | copiedExistingHistory.splice(0, 0, item); 334 | localStorage.setItem(`${this.historyIdentifier}`, JSON.stringify(copiedExistingHistory)); 335 | } else { 336 | // string logic 337 | const copiedExistingHistory = existingHistory.slice(); // copy original existingHistory array 338 | copiedExistingHistory.splice(copiedExistingHistory.indexOf(item), 1); 339 | copiedExistingHistory.splice(0, 0, item); 340 | localStorage.setItem(`${this.historyIdentifier}`, JSON.stringify(copiedExistingHistory)); 341 | } 342 | } 343 | } else { 344 | this.saveHistory(item); 345 | } 346 | } else { 347 | this.saveHistory(item); 348 | } 349 | this.handleClose(); 350 | } 351 | 352 | /** 353 | * Document click 354 | * @param e event 355 | */ 356 | public handleClick(e) { 357 | let clickedComponent = e.target; 358 | let inside = false; 359 | do { 360 | if (clickedComponent === this.elementRef.nativeElement) { 361 | inside = true; 362 | if (this.filteredList.length) { 363 | this.handleOpen(); 364 | } 365 | } 366 | clickedComponent = clickedComponent.parentNode; 367 | } while (clickedComponent); 368 | if (!inside) { 369 | this.handleClose(); 370 | } 371 | } 372 | 373 | /** 374 | * Handle body overlay 375 | */ 376 | handleOverlay() { 377 | this.overlay = false; 378 | } 379 | 380 | /** 381 | * Scroll items 382 | */ 383 | public handleScroll() { 384 | this.renderer.listen(this.filteredListElement.nativeElement, 'scroll', () => { 385 | this.scrollToEnd(); 386 | }); 387 | } 388 | 389 | /** 390 | * Define panel state 391 | */ 392 | setPanelState(event) { 393 | if (event) { 394 | event.stopPropagation(); 395 | } 396 | // If controls are untouched 397 | if (typeof this.manualOpen === 'undefined' 398 | && typeof this.manualClose === 'undefined') { 399 | this.isOpen = false; 400 | this.handleOpen(); 401 | } 402 | 403 | // If one of the controls is untouched and other is deactivated 404 | if (typeof this.manualOpen === 'undefined' 405 | && this.manualClose === false 406 | || typeof this.manualClose === 'undefined' 407 | && this.manualOpen === false) { 408 | this.isOpen = false; 409 | this.handleOpen(); 410 | } 411 | 412 | // if controls are touched but both are deactivated 413 | if (this.manualOpen === false && this.manualClose === false) { 414 | this.isOpen = false; 415 | this.handleOpen(); 416 | } 417 | 418 | // if open control is touched and activated 419 | if (this.manualOpen) { 420 | this.isOpen = false; 421 | this.handleOpen(); 422 | this.manualOpen = false; 423 | } 424 | 425 | // if close control is touched and activated 426 | if (this.manualClose) { 427 | this.isOpen = true; 428 | this.handleClose(); 429 | this.manualClose = false; 430 | } 431 | } 432 | 433 | /** 434 | * Manual controls 435 | */ 436 | open() { 437 | this.manualOpen = true; 438 | this.isOpen = false; 439 | this.handleOpen(); 440 | } 441 | 442 | close() { 443 | this.manualClose = true; 444 | this.isOpen = true; 445 | this.handleClose(); 446 | } 447 | 448 | focus() { 449 | this.handleFocus(event); 450 | } 451 | 452 | clear() { 453 | this.remove(event); 454 | } 455 | 456 | /** 457 | * Remove search query 458 | */ 459 | public remove(e) { 460 | e.stopPropagation(); 461 | this.query = ''; 462 | this.inputCleared.emit(); 463 | this.propagateChange(this.query); 464 | this.setPanelState(e); 465 | 466 | if (this.data && !this.data.length) { 467 | this.notFound = false; 468 | } 469 | } 470 | 471 | /** 472 | * Initialize historyList search 473 | */ 474 | initSearchHistory() { 475 | this.isHistoryListVisible = false; 476 | if (this.historyIdentifier && !this.query) { 477 | const history = window.localStorage.getItem(`${this.historyIdentifier}`); 478 | if (history) { 479 | this.isHistoryListVisible = true; 480 | this.filteredList = []; 481 | this.historyList = history ? JSON.parse(history) : []; 482 | } else { 483 | this.isHistoryListVisible = false; 484 | } 485 | } else { 486 | this.isHistoryListVisible = false; 487 | } 488 | } 489 | 490 | handleOpen() { 491 | if (this.isOpen || this.isOpen && !this.isLoading) { 492 | return; 493 | } 494 | // If data exists 495 | if (this.data && this.data.length) { 496 | this.isOpen = true; 497 | this.overlay = true; 498 | this.filterList(); 499 | this.opened.emit(); 500 | } 501 | } 502 | 503 | handleClose() { 504 | if (!this.isOpen) { 505 | this.isFocused = false; 506 | return; 507 | } 508 | this.isOpen = false; 509 | this.overlay = false; 510 | this.filteredList = []; 511 | this.selectedIdx = -1; 512 | this.notFound = false; 513 | this.isHistoryListVisible = false; 514 | this.isFocused = false; 515 | this.closed.emit(); 516 | } 517 | 518 | handleFocus(e) { 519 | this.searchInput.nativeElement.focus(); 520 | if (this.isFocused) { 521 | return; 522 | } 523 | this.inputFocused.emit(e); 524 | // if data exists then open 525 | if (this.data && this.data.length) { 526 | this.setPanelState(e); 527 | } 528 | this.isFocused = true; 529 | } 530 | 531 | scrollToEnd(): void { 532 | if (this.isScrollToEnd) { 533 | return; 534 | } 535 | 536 | const scrollTop = this.filteredListElement.nativeElement 537 | .scrollTop; 538 | const scrollHeight = this.filteredListElement.nativeElement 539 | .scrollHeight; 540 | const elementHeight = this.filteredListElement.nativeElement 541 | .clientHeight; 542 | const atBottom = elementHeight !=0 && Math.abs(scrollHeight - elementHeight - scrollTop) < 1; 543 | 544 | if (atBottom) { 545 | this.scrolledToEnd.emit(); 546 | this.isScrollToEnd = true; 547 | } 548 | } 549 | 550 | /** 551 | * Initialize keyboard events 552 | */ 553 | initEventStream() { 554 | this.inputKeyUp$ = fromEvent( 555 | this.searchInput.nativeElement, 'keyup' 556 | ).pipe(map( 557 | (e: any) => e 558 | )); 559 | 560 | this.inputKeyDown$ = fromEvent( 561 | this.searchInput.nativeElement, 562 | 'keydown' 563 | ).pipe(map( 564 | (e: any) => e 565 | )); 566 | 567 | this.listenEventStream(); 568 | } 569 | 570 | /** 571 | * Listen keyboard events 572 | */ 573 | listenEventStream() { 574 | // key up event 575 | this.inputKeyUp$ 576 | .pipe( 577 | filter(e => 578 | !isArrowUpDown(e.keyCode) && 579 | !isEnter(e.keyCode) && 580 | !isESC(e.keyCode) && 581 | !isTab(e.keyCode)), 582 | debounceTime(this.debounceTime) 583 | ).subscribe(e => { 584 | this.onKeyUp(e); 585 | }); 586 | 587 | // cursor up & down 588 | this.inputKeyDown$.pipe(filter( 589 | e => isArrowUpDown(e.keyCode) 590 | )).subscribe(e => { 591 | e.preventDefault(); 592 | this.onFocusItem(e); 593 | }); 594 | 595 | // enter keyup 596 | this.inputKeyUp$.pipe(filter(e => isEnter(e.keyCode))).subscribe(e => { 597 | //this.onHandleEnter(); 598 | }); 599 | 600 | // enter keydown 601 | this.inputKeyDown$.pipe(filter(e => isEnter(e.keyCode))).subscribe(e => { 602 | this.onHandleEnter(); 603 | }); 604 | 605 | // ESC 606 | this.inputKeyUp$.pipe( 607 | filter(e => isESC(e.keyCode), 608 | debounceTime(100)) 609 | ).subscribe(e => { 610 | this.onEsc(); 611 | }); 612 | 613 | // TAB 614 | this.inputKeyDown$.pipe( 615 | filter(e => isTab(e.keyCode)) 616 | ).subscribe(e => { 617 | this.onTab(); 618 | }); 619 | 620 | // delete 621 | this.inputKeyDown$.pipe( 622 | filter(e => isBackspace(e.keyCode) || isDelete(e.keyCode)) 623 | ).subscribe(e => { 624 | this.onDelete(); 625 | }); 626 | } 627 | 628 | /** 629 | * on keyup == when input changed 630 | * @param e event 631 | */ 632 | onKeyUp(e) { 633 | this.notFound = false; // search results are unknown while typing 634 | // if input is empty 635 | if (!this.query) { 636 | this.notFound = false; 637 | this.inputChanged.emit(e.target.value); 638 | this.inputCleared.emit(); 639 | this.setPanelState(e); 640 | } 641 | // note that '' can be a valid query 642 | if (!this.query && this.query !== '') { 643 | return; 644 | } 645 | // if query >= to minQueryLength 646 | if (this.query.length >= this.minQueryLength) { 647 | this.inputChanged.emit(e.target.value); 648 | this.filterList(); 649 | 650 | // If no results found 651 | if (!this.filteredList.length && !this.isLoading) { 652 | this.notFoundText ? this.notFound = true : this.notFound = false; 653 | } 654 | 655 | if (this.data && !this.data.length) { 656 | this.isOpen = true; 657 | } 658 | } 659 | } 660 | 661 | 662 | /** 663 | * Keyboard arrow top and arrow bottom 664 | * @param e event 665 | */ 666 | onFocusItem(e) { 667 | // move arrow up and down on filteredList or historyList 668 | if (!this.historyList.length || !this.isHistoryListVisible) { 669 | // filteredList 670 | const totalNumItem = this.filteredList.length; 671 | if (e.key === 'ArrowDown') { 672 | let sum = this.selectedIdx; 673 | sum = (this.selectedIdx === null) ? 0 : sum + 1; 674 | this.selectedIdx = (totalNumItem + sum) % totalNumItem; 675 | this.scrollToFocusedItem(this.selectedIdx); 676 | } else if (e.key === 'ArrowUp') { 677 | if (this.selectedIdx == -1) { 678 | this.selectedIdx = 0; 679 | } 680 | this.selectedIdx = (totalNumItem + this.selectedIdx - 1) % totalNumItem; 681 | this.scrollToFocusedItem(this.selectedIdx); 682 | } 683 | } else { 684 | // historyList 685 | const totalNumItem = this.historyList.length; 686 | if (e.key === 'ArrowDown') { 687 | let sum = this.selectedIdx; 688 | sum = (this.selectedIdx === null) ? 0 : sum + 1; 689 | this.selectedIdx = (totalNumItem + sum) % totalNumItem; 690 | this.scrollToFocusedItem(this.selectedIdx); 691 | } else if (e.key === 'ArrowUp') { 692 | if (this.selectedIdx == -1) { 693 | this.selectedIdx = 0; 694 | } 695 | this.selectedIdx = (totalNumItem + this.selectedIdx - 1) % totalNumItem; 696 | this.scrollToFocusedItem(this.selectedIdx); 697 | } 698 | } 699 | } 700 | 701 | /** 702 | * Scroll to focused item 703 | * * @param index 704 | */ 705 | scrollToFocusedItem(index) { 706 | let listElement = null; 707 | // Define list element 708 | if (!this.historyList.length || !this.isHistoryListVisible) { 709 | // filteredList element 710 | listElement = this.filteredListElement.nativeElement; 711 | } else { 712 | // historyList element 713 | listElement = this.historyListElement.nativeElement; 714 | } 715 | 716 | const items = Array.prototype.slice.call(listElement.childNodes).filter((node: any) => { 717 | if (node.nodeType === 1) { 718 | // if node is element 719 | return node.className.includes('item'); 720 | } else { 721 | return false; 722 | } 723 | }); 724 | 725 | if (!items.length) { 726 | return; 727 | } 728 | 729 | const listHeight = listElement.offsetHeight; 730 | const itemHeight = items[index].offsetHeight; 731 | const visibleTop = listElement.scrollTop; 732 | const visibleBottom = listElement.scrollTop + listHeight - itemHeight; 733 | const targetPosition = items[index].offsetTop; 734 | 735 | if (targetPosition < visibleTop) { 736 | listElement.scrollTop = targetPosition; 737 | } 738 | 739 | if (targetPosition > visibleBottom) { 740 | listElement.scrollTop = targetPosition - listHeight + itemHeight; 741 | } 742 | } 743 | 744 | /** 745 | * Select item on enter click 746 | */ 747 | onHandleEnter() { 748 | // click enter to choose item from filteredList or historyList 749 | if (this.selectedIdx > -1) { 750 | if (!this.historyList.length || !this.isHistoryListVisible) { 751 | // filteredList 752 | this.query = !this.isTypeString(this.filteredList[this.selectedIdx]) 753 | ? this.filteredList[this.selectedIdx][this.searchKeyword] 754 | : this.filteredList[this.selectedIdx]; 755 | 756 | this.saveHistory(this.filteredList[this.selectedIdx]); 757 | this.select(this.filteredList[this.selectedIdx]); 758 | } else { 759 | // historyList 760 | this.query = !this.isTypeString(this.historyList[this.selectedIdx]) 761 | ? this.historyList[this.selectedIdx][this.searchKeyword] 762 | : this.historyList[this.selectedIdx]; 763 | this.saveHistory(this.historyList[this.selectedIdx]); 764 | this.select(this.historyList[this.selectedIdx]); 765 | } 766 | } 767 | this.isHistoryListVisible = false; 768 | this.handleClose(); 769 | } 770 | 771 | /** 772 | * Esc click 773 | */ 774 | onEsc() { 775 | this.searchInput.nativeElement.blur(); 776 | this.handleClose(); 777 | } 778 | 779 | /** 780 | * Tab click 781 | */ 782 | onTab() { 783 | this.searchInput.nativeElement.blur(); 784 | this.handleClose(); 785 | } 786 | 787 | /** 788 | * Delete click 789 | */ 790 | onDelete() { 791 | this.isOpen = true; 792 | } 793 | 794 | 795 | /** 796 | * Select item to save in localStorage 797 | * @param selected 798 | */ 799 | saveHistory(selected) { 800 | if (this.historyIdentifier) { 801 | // check if selected item exists in historyList 802 | if (!this.historyList.some((item) => !this.isTypeString(item) 803 | ? item[this.searchKeyword] == selected[this.searchKeyword] : item == selected)) { 804 | this.saveHistoryToLocalStorage([selected, ...this.historyList]); 805 | 806 | // check if items don't exceed max allowed number 807 | if (this.historyList.length >= this.historyListMaxNumber) { 808 | this.historyList.splice(this.historyList.length - 1, 1); 809 | this.saveHistoryToLocalStorage([selected, ...this.historyList]); 810 | } 811 | } else { 812 | // if selected item exists in historyList swap to top in array 813 | if (!this.isTypeString(selected)) { 814 | // object logic 815 | const copiedHistoryList = this.historyList.slice(); // copy original historyList array 816 | const selectedIndex = copiedHistoryList.map((item) => item[this.searchKeyword]).indexOf(selected[this.searchKeyword]); 817 | copiedHistoryList.splice(selectedIndex, 1); 818 | copiedHistoryList.splice(0, 0, selected); 819 | this.saveHistoryToLocalStorage([...copiedHistoryList]); 820 | } else { 821 | // string logic 822 | const copiedHistoryList = this.historyList.slice(); // copy original historyList array 823 | copiedHistoryList.splice(this.historyList.indexOf(selected), 1); 824 | copiedHistoryList.splice(0, 0, selected); 825 | this.saveHistoryToLocalStorage([...copiedHistoryList]); 826 | } 827 | } 828 | } 829 | } 830 | 831 | /** 832 | * Save item in localStorage 833 | * @param selected 834 | */ 835 | saveHistoryToLocalStorage(selected) { 836 | window.localStorage.setItem( 837 | `${this.historyIdentifier}`, 838 | JSON.stringify(selected) 839 | ); 840 | } 841 | 842 | /** 843 | * Remove item from localStorage 844 | * @param index 845 | * @param e event 846 | */ 847 | removeHistoryItem(index, e) { 848 | e.stopPropagation(); 849 | this.historyList = this.historyList.filter((v, i) => i !== index); 850 | this.saveHistoryToLocalStorage(this.historyList); 851 | if (this.historyList.length == 0) { 852 | window.localStorage.removeItem(`${this.historyIdentifier}`); 853 | this.filterList(); 854 | } 855 | } 856 | 857 | /** 858 | * Reset localStorage 859 | * @param e event 860 | */ 861 | resetHistoryList(e) { 862 | e.stopPropagation(); 863 | this.historyList = []; 864 | window.localStorage.removeItem(`${this.historyIdentifier}`); 865 | this.filterList(); 866 | } 867 | } 868 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/src/lib/highlight.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'highlight' 5 | }) 6 | export class HighlightPipe implements PipeTransform { 7 | transform(text: any, search: any, searchKeyword?: any): any { 8 | let pattern = search.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 9 | pattern = pattern.split(' ').filter((t) => { 10 | return t.length > 0; 11 | }).join('|'); 12 | const regex = new RegExp(pattern, 'gi'); 13 | 14 | if (!search) { 15 | return text; 16 | } 17 | 18 | if (searchKeyword) { 19 | const name = text[searchKeyword].replace(regex, (match) => `${match}`); 20 | // copy original object 21 | const textCopied = {...text}; 22 | // set bold value into searchKeyword of copied object 23 | textCopied[searchKeyword] = name; 24 | return textCopied; 25 | } else { 26 | return search ? text.replace(regex, (match) => `${match}`) : text; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of autocomplete-lib 3 | */ 4 | 5 | export * from './lib/autocomplete-lib.module'; 6 | export * from './lib/autocomplete.component'; 7 | export * from './lib/highlight.pipe'; 8 | 9 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: { 12 | context(path: string, deep?: boolean, filter?: RegExp): { 13 | (id: string): T; 14 | keys(): string[]; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment( 20 | BrowserDynamicTestingModule, 21 | platformBrowserDynamicTesting(), { 22 | teardown: { destroyAfterEach: false } 23 | }, 24 | ); 25 | 26 | // Then we find all the tests. 27 | const context = require.context('./', true, /\.spec\.ts$/); 28 | // And load the modules. 29 | context.keys().map(context); 30 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "src/test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial", 9 | "enableResourceInlining": true, 10 | "enableIvy": false 11 | }, 12 | "exclude": [ 13 | "src/test.ts", 14 | "**/*.spec.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /projects/autocomplete-lib/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {Routes, RouterModule} from '@angular/router'; 3 | import {FormsComponent} from './forms/forms.component'; 4 | import {HomeComponent} from './home/home.component'; 5 | 6 | const routes: Routes = [ 7 | {path: '', pathMatch: 'full', redirectTo: 'home'}, 8 | {path: 'home', component: HomeComponent}, 9 | {path: 'forms', component: FormsComponent} 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forRoot(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class AppRoutingModule { 17 | } 18 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmerabishvili/angular-ng-autocomplete/23208cc2ea0f8b41d02c46b8bb6fe4b70453ce8f/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach(async(() => { 5 | TestBed.configureTestingModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | }).compileComponents(); 10 | })); 11 | it('should create the app', async(() => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | })); 16 | it(`should have as title 'app'`, async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app.title).toEqual('app'); 20 | })); 21 | it('should render title in a h1 tag', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.debugElement.nativeElement; 25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-autocomplete!'); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent implements OnInit { 9 | 10 | 11 | constructor() { 12 | } 13 | 14 | ngOnInit() { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | 4 | import {AppComponent} from './app.component'; 5 | import {AutocompleteLibModule} from 'autocomplete-lib'; 6 | import {HttpClientModule} from '@angular/common/http'; 7 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 8 | import {FormsComponent} from './forms/forms.component'; 9 | import {AppRoutingModule} from './app-routing.module'; 10 | import {HomeComponent} from './home/home.component'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | AppComponent, 15 | FormsComponent, 16 | HomeComponent 17 | ], 18 | imports: [ 19 | BrowserModule, 20 | HttpClientModule, 21 | AutocompleteLibModule, 22 | ReactiveFormsModule, 23 | FormsModule, 24 | AppRoutingModule 25 | ], 26 | providers: [], 27 | bootstrap: [AppComponent] 28 | }) 29 | export class AppModule { 30 | } 31 | -------------------------------------------------------------------------------- /src/app/forms/forms.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |

Template Driven Forms

6 |
{{ form.value | json }}
7 |
8 |
9 |
10 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 |
39 |

Reactive Forms

40 |
{{ reactiveForm.value | json }}
41 |
42 |
43 |
44 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |
65 |
66 | 67 |
68 |
69 |
70 |
71 | 72 | -------------------------------------------------------------------------------- /src/app/forms/forms.component.scss: -------------------------------------------------------------------------------- 1 | .forms { 2 | display: flex; 3 | justify-content: space-around; 4 | .container { 5 | display: flex; 6 | .ng-autocomplete { 7 | width: 500px; 8 | } 9 | .btn { 10 | background-color: #3f51b5; 11 | border: none; 12 | color: white; 13 | padding: 8px 22px; 14 | margin: 5px 10px; 15 | text-align: center; 16 | text-decoration: none; 17 | display: inline-block; 18 | font-size: 12px; 19 | cursor: pointer; 20 | outline: none; 21 | &:disabled { 22 | background-color: gray; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/forms/forms.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FormsComponent } from './forms.component'; 4 | 5 | describe('FormsComponent', () => { 6 | let component: FormsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FormsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FormsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/forms/forms.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'app-forms', 6 | templateUrl: './forms.component.html', 7 | styleUrls: ['./forms.component.scss'] 8 | }) 9 | export class FormsComponent implements OnInit { 10 | name = ''; 11 | /** 12 | * Form 13 | */ 14 | reactiveForm: FormGroup; 15 | 16 | 17 | public placeholder: string = 'Enter the Country Name'; 18 | public keyword = 'name'; 19 | public historyHeading: string = 'Recently selected'; 20 | 21 | public countriesTemplate = ['Albania', 'Andorra', 'Armenia', 'Austria', 'Azerbaijan', 'Belarus', 22 | 'Belgium', 'Bosnia & Herzegovina', 'Bulgaria', 'Croatia', 'Cyprus', 23 | 'Czech Republic', 'Denmark', 'Estonia', 'Finland', 'France', 'Georgia', 24 | 'Germany', 'Greece', 'Hungary', 'Iceland', 'India', 'Ireland', 'Italy', 'Kosovo', 25 | 'Latvia', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Macedonia', 'Malta', 26 | 'Moldova', 'Monaco', 'Montenegro', 'Netherlands', 'Norway', 'Poland', 27 | 'Portugal', 'Romania', 'Russia', 'San Marino', 'Serbia', 'Slovakia', 'Slovenia', 28 | 'Spain', 'Sweden', 'Switzerland', 'Turkey', 'Ukraine', 'United Kingdom', 'Vatican City']; 29 | 30 | /* 31 | public countriesTemplate = [{name: 'Albania'}, {name: 'Andorra'}, {name: 'Armenia'}, {name: 'Austria'}]; 32 | */ 33 | 34 | 35 | public countriesReactive = [ 36 | { 37 | id: 1, 38 | name: 'Albania', 39 | }, 40 | { 41 | id: 2, 42 | name: 'Belgium', 43 | }, 44 | { 45 | id: 3, 46 | name: 'Denmark', 47 | }, 48 | { 49 | id: 4, 50 | name: 'Montenegro', 51 | }, 52 | ]; 53 | 54 | 55 | constructor(private _fb: FormBuilder) { 56 | this.reactiveForm = _fb.group({ 57 | name: [{value: '', disabled: false}, Validators.required] 58 | }); 59 | 60 | this.reactiveForm.patchValue({ 61 | name: 62 | {value: 1, name: 'Albania'} 63 | }); 64 | } 65 | 66 | ngOnInit() { 67 | } 68 | 69 | set() { 70 | this.name = 'test'; 71 | console.log('countriesTemplate', this.countriesTemplate); 72 | console.log('selected', this.name); 73 | } 74 | 75 | reset() { 76 | this.name = ''; 77 | } 78 | 79 | /** 80 | * Submit template form 81 | */ 82 | submitTemplateForm(value) { 83 | console.log(value); 84 | } 85 | 86 | /** 87 | * Submit reactive form 88 | */ 89 | submitReactiveForm() { 90 | if (this.reactiveForm.valid) { 91 | console.log(this.reactiveForm.value); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |

Static data

6 |
7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 |
45 |
46 | 47 |
48 |

API data (Filter on server)

49 |
50 | 51 | 52 | 53 |
54 | 55 |
56 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
83 |
84 |
85 |
86 | 87 |
88 |

API data (Filter on local)

89 |
90 | 91 | 92 | 93 |
94 | 95 |
96 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
116 |
117 |
118 |
119 |
120 | 121 |
122 |

Custom filters

123 | 124 | 125 |
126 |
127 |

Static data with custom filter (e.g. case-sensitive)

128 | 129 |
130 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 |
145 |
146 |
147 |
148 | 149 |
150 |

API data (Disable local filter, you should change the data by re-fetching the data from server)

151 | 152 |
153 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 |
168 |
169 |
170 |
171 |
172 | -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | .example { 2 | display: flex; 3 | justify-content: space-around; 4 | flex-wrap: wrap; 5 | .control-buttons { 6 | display: flex; 7 | margin-bottom: 20px; 8 | .btn { 9 | background-color: #3f51b5; 10 | border: none; 11 | color: white; 12 | padding: 8px 22px; 13 | margin: 5px; 14 | text-align: center; 15 | text-decoration: none; 16 | display: inline-block; 17 | font-size: 12px; 18 | cursor: pointer; 19 | outline: none; 20 | } 21 | } 22 | } 23 | 24 | .custom-filters { 25 | display: flex; 26 | justify-content: space-around; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HomeComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, ViewChild} from '@angular/core'; 2 | import {DataService} from '../services/data.service'; 3 | import {Country} from '../models/countries'; 4 | import {Observable} from 'rxjs'; 5 | import {tap} from 'rxjs/operators'; 6 | 7 | @Component({ 8 | selector: 'app-home', 9 | templateUrl: './home.component.html', 10 | styleUrls: ['./home.component.scss'] 11 | }) 12 | export class HomeComponent implements OnInit { 13 | 14 | @ViewChild('ngAutoCompleteStatic') ngAutocompleteStatic; 15 | @ViewChild('ngAutoCompleteApi') ngAutocompleteApi; 16 | @ViewChild('userAuto') userAuto; 17 | 18 | public users$: Observable; 19 | userInitialValue = null; 20 | 21 | items; 22 | public placeholder: string = 'Enter the Country Name'; 23 | public keyword = 'name'; 24 | public historyHeading: string = 'Recently selected'; 25 | public isLoading: boolean; 26 | initialValue = { 27 | id: 9, 28 | name: 'Georgia', 29 | population: 200 30 | }; 31 | 32 | /** 33 | * Static Data 34 | */ 35 | 36 | public countries: Country[] = [ 37 | { 38 | id: 1, 39 | name: 'Albania', 40 | population: 100 41 | }, 42 | { 43 | id: 2, 44 | name: 'Belgium', 45 | population: 200 46 | }, 47 | { 48 | id: 3, 49 | name: 'Denmark', 50 | population: 200 51 | }, 52 | { 53 | id: 4, 54 | name: 'Montenegro', 55 | population: 200 56 | }, 57 | { 58 | id: 5, 59 | name: 'Turkey', 60 | population: 200 61 | }, 62 | { 63 | id: 6, 64 | name: 'Ukraine', 65 | population: 200 66 | }, 67 | { 68 | id: 7, 69 | name: 'Macedonia', 70 | population: 200 71 | }, 72 | { 73 | id: 8, 74 | name: 'Slovenia', 75 | population: 200 76 | }, 77 | { 78 | id: 9, 79 | name: 'Georgia', 80 | population: 200 81 | }, 82 | { 83 | id: 10, 84 | name: 'India', 85 | population: 200 86 | }, 87 | { 88 | id: 11, 89 | name: 'Russia', 90 | population: 200 91 | }, 92 | { 93 | id: 12, 94 | name: 'Switzerland', 95 | population: 200 96 | } 97 | ]; 98 | 99 | constructor(private _dataService: DataService) { 100 | } 101 | 102 | ngOnInit() { 103 | this.countries.push(new Country(1, 'Yeah', 100)); 104 | this.countries.push(new Country(2, 'Yep', 200)); 105 | 106 | this.users$ = this._dataService.getUsers().pipe( 107 | tap(users => this.userInitialValue = users[0]), 108 | ); 109 | } 110 | 111 | /** 112 | * API Data (Filter on server) 113 | */ 114 | onChangeSearch(term: string) { 115 | console.log('term', term); 116 | this.isLoading = true; 117 | this._dataService.getRepos(term).subscribe(res => { 118 | console.log('res', res); 119 | //this.items = this.items ? this.items.concat(res['items']) : res['items']; 120 | this.items = res['items']; 121 | this.isLoading = false; 122 | }, (err) => { 123 | console.log('err', err); 124 | this.isLoading = false; 125 | }); 126 | } 127 | 128 | selectEvent(item) { 129 | console.log('Selected item', item); 130 | } 131 | 132 | /** 133 | * Static 134 | */ 135 | 136 | changeEventStatic(string: string) { 137 | console.log('string', string); 138 | } 139 | 140 | focusEventStatic(e) { 141 | console.log('focused', e); 142 | //this.ngAutocompleteStatic.close(); 143 | } 144 | 145 | clearEventStatic() { 146 | console.log('cleared'); 147 | //this.ngAutocompleteStatic.close(); 148 | } 149 | 150 | scrollToEndStatic() { 151 | console.log('scrolled-to-bottom'); 152 | //this.countries = [...this.countries, ...this.test]; 153 | //console.log('countriesssss', this.countries); 154 | } 155 | 156 | openedStatic() { 157 | console.log('opened'); 158 | } 159 | 160 | closedStatic() { 161 | console.log('closed'); 162 | } 163 | 164 | openStaticPanel(e): void { 165 | console.log('open'); 166 | e.stopPropagation(); 167 | this.ngAutocompleteStatic.open(); 168 | } 169 | 170 | closeStaticPanel(e): void { 171 | console.log('close'); 172 | e.stopPropagation(); 173 | this.ngAutocompleteStatic.close(); 174 | } 175 | 176 | focusStaticPanel(e): void { 177 | console.log('focus'); 178 | e.stopPropagation(); 179 | this.ngAutocompleteStatic.focus(); 180 | } 181 | 182 | clearStatic(e): void { 183 | console.log('clear'); 184 | e.stopPropagation(); 185 | this.ngAutocompleteStatic.clear(); 186 | } 187 | 188 | clearAndCloseStatic() { 189 | this.ngAutocompleteStatic.close(); 190 | this.ngAutocompleteStatic.clear(); 191 | } 192 | 193 | /** 194 | * End of Static 195 | */ 196 | 197 | 198 | /** 199 | * API 200 | */ 201 | 202 | focusedEventApi(e) { 203 | console.log('focused'); 204 | // Fetch API data on Load 205 | this.onChangeSearch(null); 206 | } 207 | 208 | 209 | openedEventApi() { 210 | console.log('opened'); 211 | } 212 | 213 | closedEventApi() { 214 | console.log('closed'); 215 | } 216 | 217 | clearEventApi() { 218 | console.log('cleared'); 219 | } 220 | 221 | scrollToEndApi() { 222 | this.onChangeSearch('w'); 223 | } 224 | 225 | openApiPanel(e): void { 226 | console.log('open'); 227 | e.stopPropagation(); 228 | this.ngAutocompleteApi.open(); 229 | } 230 | 231 | closeApiPanel(e): void { 232 | console.log('close'); 233 | e.stopPropagation(); 234 | this.ngAutocompleteApi.close(); 235 | } 236 | 237 | focusApiPanel(e): void { 238 | console.log('focus'); 239 | e.stopPropagation(); 240 | this.ngAutocompleteApi.focus(); 241 | } 242 | 243 | /** 244 | * End of API 245 | */ 246 | 247 | 248 | /** 249 | * API Data (Filter on local) 250 | */ 251 | 252 | userFocused(e) { 253 | console.log('focused'); 254 | } 255 | 256 | selectUser(user) { 257 | console.log('Selected user', user); 258 | } 259 | 260 | onUserChange(term: string) { 261 | console.log('term', term); 262 | } 263 | 264 | scrollToEndUsers() { 265 | console.log('scrolled-to-bottom'); 266 | } 267 | 268 | openUserPanel(e): void { 269 | console.log('open'); 270 | e.stopPropagation(); 271 | this.userAuto.open(); 272 | } 273 | 274 | closeUserPanel(e): void { 275 | console.log('close'); 276 | e.stopPropagation(); 277 | this.userAuto.close(); 278 | } 279 | 280 | focusUserPanel(e): void { 281 | console.log('focus'); 282 | e.stopPropagation(); 283 | this.userAuto.focus(); 284 | } 285 | 286 | /*Custom filters*/ 287 | 288 | customFilter(items: any, query: string) { 289 | return items.filter((item: any) => { 290 | return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1; 291 | }); 292 | } 293 | 294 | disableFilter = (items: any[]) => items; 295 | } 296 | -------------------------------------------------------------------------------- /src/app/models/countries.ts: -------------------------------------------------------------------------------- 1 | export interface Icountry { 2 | id: number; 3 | name: string; 4 | population: number; 5 | } 6 | 7 | export class Country { 8 | id: number; 9 | name: string; 10 | population: number; 11 | 12 | constructor(id: number, name: string, population: number) { 13 | this.id = id; 14 | this.name = name; 15 | this.population = population; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/services/data.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { DataService } from './data.service'; 4 | 5 | describe('DataService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [DataService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([DataService], (service: DataService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/data.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class DataService { 8 | constructor(private _http: HttpClient) { 9 | } 10 | 11 | getRepos(value) { 12 | return this._http.get(`https://api.github.com/search/repositories?q=${value}&sort=stars&order=desc&limit=10`); 13 | } 14 | 15 | getUsers() { 16 | return this._http.get(`https://jsonplaceholder.typicode.com/users`); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmerabishvili/angular-ng-autocomplete/23208cc2ea0f8b41d02c46b8bb6fe4b70453ce8f/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` 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/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmerabishvili/angular-ng-autocomplete/23208cc2ea0f8b41d02c46b8bb6fe4b70453ce8f/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularAutocomplete 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | (id: string): T; 13 | keys(): string[]; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), { 21 | teardown: { destroyAfterEach: false } 22 | }, 23 | ); 24 | 25 | // Then we find all the tests. 26 | const context = require.context('./', true, /\.spec\.ts$/); 27 | // And load the modules. 28 | context.keys().map(context); 29 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ], 12 | "files": [ 13 | "src/main.ts", 14 | "src/polyfills.ts" 15 | ], 16 | "include": [ 17 | "src/**/*.d.ts" 18 | ], 19 | "angularCompilerOptions": { 20 | "enableIvy": false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es2015", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ], 19 | "paths": { 20 | "autocomplete-lib": [ 21 | "dist/autocomplete-lib" 22 | ], 23 | "autocomplete-lib/*": [ 24 | "dist/autocomplete-lib/*" 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------