├── .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://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://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 |
35 |
36 |
37 |
39 |
40 |
0 && heading">
41 |
{{heading}}
42 |
43 |
44 |
45 |
46 |
47 |
49 |
51 |
52 |
53 |
54 |
56 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
67 |
68 |
0 && historyHeading">
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 | Back
2 |
3 |
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 | Example with forms
2 |
3 |
4 |
5 |
Static data
6 |
7 | Open
8 | Close
9 | Focus
10 | Clear
11 | Clear & Close
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 | Open
51 | Close
52 | Focus
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 | Open
91 | Close
92 | Focus
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 |
--------------------------------------------------------------------------------