├── .editorconfig ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── angular.json ├── demo.gif ├── demo2.gif ├── e2e ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── projects └── ng-auto-complete │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── classes │ │ │ ├── AutocompleteGroup.ts │ │ │ ├── AutocompleteItem.ts │ │ │ └── typing.ts │ │ ├── completer │ │ │ └── completer.component.ts │ │ ├── dropdown │ │ │ └── ng-dropdown.directive.ts │ │ ├── ng-auto-complete.component.ts │ │ ├── ng-auto-complete.module.ts │ │ ├── pipes │ │ │ ├── highlight.ts │ │ │ ├── key-value.ts │ │ │ └── pipes.module.ts │ │ └── utils │ │ │ └── utils.ts │ ├── public_api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── ng-holder │ │ ├── ng-holder.component.html │ │ └── ng-holder.component.ts │ ├── ng-view │ │ ├── ng-view.component.html │ │ └── ng-view.component.ts │ └── routes │ │ ├── route1 │ │ ├── route1.component.html │ │ └── route1.component.ts │ │ └── route2 │ │ ├── route2.component.html │ │ └── route2.component.ts ├── assets │ └── .gitkeep ├── browserslist ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.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 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at benjamin@codebridge.nl. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ### Do you like to code? 4 | 5 | - Fork & clone 6 | - npm install 7 | - ng serve 8 | - git checkout -b my-new-feature 9 | - component lives in src/app/ng-autocomplete/* 10 | - Submit a pull request 11 | 12 | ### Do you like organizing? 13 | - Link to duplicate issues, and suggest new issue labels, to keep things organized 14 | - Go through open issues and suggest closing old ones. 15 | - Ask clarifying questions on recently opened issues to move the discussion forward 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 sengirab 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 | # NgAutoComplete / Example 2 | Light-weight autocomplete component for Angular. 3 | 4 | [![Code Climate](https://codeclimate.com/github/sengirab/ngAutocomplete/badges/gpa.svg)](https://codeclimate.com/github/sengirab/ngAutocomplete) 5 | [![npm version](https://badge.fury.io/js/ng-auto-complete.svg)](https://badge.fury.io/js/ng-auto-complete) 6 | 7 | 8 | https://github.com/sengirab/ngAutocomplete 9 | 10 | ![](https://raw.githubusercontent.com/sengirab/ngAutocomplete/master/demo.gif) 11 | 12 | # Installation 13 | 14 | `npm i ng-auto-complete --save` 15 | 16 | # Styling !Important 17 | First thing to note, i've created this package to be as simple as possible. That's why i don't include any styling, 18 | this is so you could style it the way you want it. 19 | 20 | If you like the styling i did for the example .gif shown above, you can copy it from [here.](https://github.com/sengirab/ngAutocomplete/blob/master/src/styles.css) 21 | ### Classes 22 | - .ng-autocomplete-dropdown (.open .is-loading .is-async) 23 | - .ng-autocomplete-inputs 24 | - .ng-autocomplete-input 25 | - .ng-autocomplete-placeholder 26 | - .ng-autocomplete-dropdown-icon (.open) 27 | - .ng-dropdown (.open .is-initial-empty) 28 | - .dropdown-item (.active) 29 | 30 | 31 | # Responses !Important 32 | #### Response when selected 33 | ```json 34 | "{group: AutocompleteGroup, item: AutocompleteItem}" 35 | ``` 36 | #### Response when cleared 37 | ```json 38 | "{group: AutocompleteGroup, item: null}" 39 | ``` 40 | Note that when calling completer.ResetInput('completer'), this will clear the input. This means that the 41 | completer will emit `{group: AutocompleteGroup, item: null}`. If your listening to this within your component 42 | keep in mind that each clear the item will be null 43 | 44 | The input will also emit "null" when the input reaches a length of `<= 0`. 45 | 46 | 47 | # Usage 48 | 49 | #### app.module.ts 50 | ```typescript 51 | import {BrowserModule} from "@angular/platform-browser"; 52 | import {NgModule} from "@angular/core"; 53 | import {FormsModule} from "@angular/forms"; 54 | import {HttpModule} from "@angular/http"; 55 | 56 | import {AppComponent} from "./app.component"; 57 | import {NgAutoCompleteModule} from "ng-auto-complete"; 58 | 59 | @NgModule({ 60 | declarations: [ 61 | AppComponent 62 | ], 63 | imports: [ 64 | BrowserModule, 65 | FormsModule, 66 | HttpModule, 67 | NgAutoCompleteModule 68 | ], 69 | providers: [], 70 | bootstrap: [AppComponent] 71 | }) 72 | export class AppModule { 73 | } 74 | ``` 75 | 76 | #### app.component.ts 77 | ```typescript 78 | import {Component, ViewChild} from "@angular/core"; 79 | import {CreateNewAutocompleteGroup, SelectedAutocompleteItem, NgAutoCompleteComponent} from "ng-auto-complete"; 80 | 81 | @Component({ 82 | selector: 'app-root', 83 | templateUrl: './app.component.html', 84 | }) 85 | export class AppComponent { 86 | @ViewChild(NgAutoCompleteComponent) public completer: NgAutoCompleteComponent; 87 | 88 | public group = [ 89 | CreateNewAutocompleteGroup( 90 | 'Search / choose in / from list', 91 | 'completer', 92 | [ 93 | {title: 'Option 1', id: '1'}, 94 | {title: 'Option 2', id: '2'}, 95 | {title: 'Option 3', id: '3'}, 96 | {title: 'Option 4', id: '4'}, 97 | {title: 'Option 5', id: '5'}, 98 | ], 99 | {titleKey: 'title', childrenKey: null} 100 | ), 101 | ]; 102 | 103 | constructor() { 104 | 105 | } 106 | 107 | /** 108 | * 109 | * @param item 110 | * @constructor 111 | */ 112 | Selected(item: SelectedAutocompleteItem) { 113 | console.log(item); 114 | } 115 | } 116 | 117 | ``` 118 | 119 | #### app.component.html 120 | ```html 121 | 122 | ``` 123 | 124 | # Remove selected values 125 | ```typescript 126 | public selected: any[] = []; 127 | 128 | Selected(item: SelectedAutocompleteItem) { 129 | this.selected.push(item.item); 130 | 131 | /** 132 | * Remove selected values from list, 133 | * selected value must be of type: {id: string(based on GUID's), [value: string]: any}[] 134 | */ 135 | this.completer.RemovableValues('completer', this.selected) 136 | } 137 | ``` 138 | 139 | # Turn off completion 140 | In some cases you may want to disable auto completion. e.g you want a html select element. 141 | ### Example 142 | ![](https://raw.githubusercontent.com/sengirab/ngAutocomplete/master/demo2.gif) 143 | ### Usage 144 | ```typescript 145 | public group = [ 146 | CreateNewAutocompleteGroup( 147 | 'Search / choose in / from list', 148 | 'completer', 149 | [ 150 | {title: 'Option 1', id: '1'}, 151 | {title: 'Option 2', id: '2'}, 152 | {title: 'Option 3', id: '3'}, 153 | {title: 'Option 4', id: '4'}, 154 | {title: 'Option 5', id: '5'}, 155 | ], 156 | {titleKey: 'title', childrenKey: null}, 157 | '', 158 | false 159 | ) 160 | ]; 161 | ``` 162 | 163 | # With children 164 | ### Usage 165 | ```typescript 166 | public group = [ 167 | CreateNewAutocompleteGroup( 168 | 'Search / choose in / from list', 169 | 'completer_one', 170 | [ 171 | { 172 | title: 'Option 1', id: '1', 173 | children: [ 174 | {title: 'Option 1', id: '1'}, 175 | {title: 'Option 2', id: '2'} 176 | ] 177 | }, 178 | { 179 | title: 'Option 2', id: '2', 180 | children: [ 181 | {title: 'Option 3', id: '3'}, 182 | {title: 'Option 4', id: '4'} 183 | ] 184 | }, 185 | { 186 | title: 'Option 3', id: '3', 187 | children: [ 188 | {title: 'Option 5', id: '5'}, 189 | {title: 'Option 6', id: '6'} 190 | ] 191 | }, 192 | ], 193 | {titleKey: 'title', childrenKey: 'children'}, 194 | '' 195 | ), 196 | CreateNewAutocompleteGroup( 197 | 'Search / choose in / from list', 198 | 'completer_two', 199 | [ 200 | {title: 'Option 1', id: '1'}, 201 | {title: 'Option 2', id: '2'}, 202 | {title: 'Option 3', id: '3'}, 203 | {title: 'Option 4', id: '4'}, 204 | {title: 'Option 5', id: '5'}, 205 | {title: 'Option 6', id: '6'}, 206 | ], 207 | {titleKey: 'title', childrenKey: null}, 208 | 'completer_one' 209 | ) 210 | ]; 211 | ``` 212 | 213 | # Within a form 214 | 215 | ### Usage: 216 | ```typescript 217 | import {Component, OnInit, ViewChild} from "@angular/core"; 218 | import {FormArray, FormBuilder, FormGroup} from "@angular/forms"; 219 | import {CreateNewAutocompleteGroup, SelectedAutocompleteItem, NgAutoCompleteComponent} from "ng-auto-complete"; 220 | 221 | @Component({ 222 | selector: 'app-root', 223 | templateUrl: './app.component.html', 224 | }) 225 | export class AppComponent implements OnInit { 226 | @ViewChild(NgAutoCompleteComponent) public completer: NgAutoCompleteComponent; 227 | 228 | public form: FormGroup; 229 | public group = [ 230 | CreateNewAutocompleteGroup( 231 | 'Search / choose in / from list', 232 | 'completer', 233 | [ 234 | {title: 'Option 1', id: '1'}, 235 | {title: 'Option 2', id: '2'}, 236 | {title: 'Option 3', id: '3'}, 237 | {title: 'Option 4', id: '4'}, 238 | {title: 'Option 5', id: '5'}, 239 | ], 240 | {titleKey: 'title', childrenKey: null} 241 | ), 242 | ]; 243 | 244 | constructor(private _fb: FormBuilder) { 245 | 246 | } 247 | 248 | /** 249 | * 250 | */ 251 | ngOnInit() { 252 | this.form = this._fb.group({ 253 | items: new FormArray([]) 254 | }); 255 | } 256 | 257 | /** 258 | * 259 | * @param item 260 | * @constructor 261 | */ 262 | Selected(item: SelectedAutocompleteItem) { 263 | this.form.controls['items'] = this._fb.array([...this.form.controls['items'].value, item.original]); 264 | 265 | console.log(item); 266 | } 267 | } 268 | ``` 269 | 270 | # Changelog - (Read before updating.) 271 | ## [4.1.9] 272 | - Internal fixes & performance improvements. 273 | ## [4.1.6] 274 | - I.E fixes 275 | ## [4.1.5] 276 | - New functions 277 | - SetEnable(key: string) 278 | - SetDisable(key: string) 279 | ## [4.1.4] 280 | - ExpressionChangedAfterItHasBeenCheckedError 281 | - Fixed this error in a previous version, this ends up in propagating the error, this isn't user friendly. 282 | - Since i must check something in the `ngAfterViewChecked` there is no escaping this error unless setTimeout is used (which i did now). 283 | ## [4.1.2] 284 | - Internal changes 285 | - The way how the first value was selected is changed. Package used to wait until view was checked, this was checked 286 | periodically via a interval. Now a subject has been created to emit this value to all their subscribers. 287 | - BREAKING: 288 | - `FindInput` function has been removed. 289 | ## [4.0.2] - [4.0.1] - [4.0.0] 290 | - Updates regarding angular 7 291 | ## [3.1.0] 292 | ### Important. Breaking changes! 293 | - NgAutocompleteComponent renamed to NgAutoCompleteComponent 294 | - Very big changes internally. Stopped using rollup to create my npm package. 295 | - Using angular libraries now instead. 296 | ## [3.0.0] - 2018-09-14. 297 | - Updated to newest version of angular. 298 | ## [2.10.5] - 2018-06-07 299 | - Fixed an maximum callstack exceeded bug. 300 | ## [2.10.3] / [2.10.4] - 2018-06-06 301 | - Added searchLength to options when create an autocomplete group, it configures when to fire a search. The number given is the amount of characters in the input. 302 | ## [2.10.2] - 2018-05-15. 303 | - Removed dropdown it's own comparison when using Async. Assuming the user will probably filter the results. 304 | ## [2.10.1] - 2018-05-14. 305 | - Added new classes to wrong element. 306 | ## [2.10.0] - 2018-05-14. 307 | - Changes to behaviour of the dropdown (mainly for async). 308 | - Dropdown now only opens when there's a default value at start. It will stay closed until it has a list to show. -- Represented by the class: is-initial-empty. 309 | - Dropdown now has a loading class; is-loading. 310 | - Internal changes to keep the new code compatible with new behaviour. 311 | ## [2.9.12] - 2018-05-11. 312 | ### New Functionality. 313 | - Support for async functions. 314 | - Useful for when you want to use your own API to return results. 315 | ```typescript 316 | @ViewChild(NgAutoCompleteComponent) public completer: NgAutoCompleteComponent; 317 | 318 | const async = (str: string) => { 319 | return new Promise((resolve) => { 320 | setTimeout(() => { 321 | resolve([ 322 | { 323 | id: 0, 324 | title: `Test case 1 ${str}` 325 | }, 326 | { 327 | id: 1, 328 | title: `Test case 2 ${str}` 329 | }, 330 | { 331 | id: 2, 332 | title: `Test case 3 ${str}` 333 | } 334 | ]) 335 | }, 2000) 336 | }); 337 | }; 338 | 339 | this.completer.SetAsync('completer', async); 340 | ``` 341 | ## [2.8.12] - 2018-05-09. 342 | ### New Functionality. 343 | - Created new functions to add custom ng-templates. 344 | - Every ng template receives a context that's equal to an AutocompleteItem type. Except for the dropdownValue, this receives a hightlight too. see example 345 | - template parameter must be of type: dropdownValue | placeholderValue | noResults 346 | ```typescript 347 | @ViewChild(NgAutoCompleteComponent) public completer: NgAutoCompleteComponent; 348 | 349 | @ViewChild('dropdownValue') dropdownValue: TemplateRef; 350 | @ViewChild('placeholderValue') placeholderValue: TemplateRef; 351 | @ViewChild('noResults') noResults: TemplateRef; 352 | 353 | this.completer.SetTemplate('completer', 'dropdownValue', this.dropdownValue); 354 | this.completer.SetTemplate('completer', 'noResults', this.noResults); 355 | this.completer.SetTemplate('completer', 'placeholderValue', this.placeholderValue); 356 | ``` 357 | ```html 358 | 359 |
360 |
361 | 362 | 363 | {{value.title}} 364 | 365 | 366 | 367 | Hey, you searched for: {{value}}. But there are no results! 368 | 369 | ``` 370 | ## [2.7.12] - 2017-11-08. 371 | - Big performance refactor. 372 | - Instead of using arrays, now uses objects. Search by object key. 373 | - New @output, (no-results) emits GroupNoResult. 374 | ## [1.5.12], [1.6.12], [1.7.12] - 2017-08-29. 375 | - A lot of internal changes & bugfixes. 376 | - In some cases when the a view has a hidden ng-content, that shows if an expression evaluates to true and 377 | a completer function has been used e.g(`SelectItem('completer', 5)`) it would give an error; completer view is not 378 | initialized. 379 | - Functions that are called before the completer view has been initialized are now queued to be fired when the view 380 | is actually initialized. 381 | ## [1.4.12] - 2017-08-02. 382 | - Added tab to submit events. (not preventing default, so still goes to the next input, if there's one). 383 | - Added better support for navigating with arrows. Dropdown list now navigates to the selected item if the items exceed 384 | the dropdown its given height. 385 | - Internal code cleanup. 386 | ## [1.3.12] - 2017-07-21. 387 | - Mobile update: Remove mouseover when mobile. This prevents the user from needing to double tap the options. 388 | ## [1.3.11] - 2017-07-21. 389 | ### Styling 390 | - Some internal styling has changed. 391 | - When completer is turned of, input used to be disabled. This doesn't work on all browsers. Input now get 392 | `pointer-events: none;` 393 | - Browser compatibility. 394 | ### Fixes 395 | - Value has to be set on input (equal to ngModel). 396 | - This created an issue on safari, when an item was selected, the placeholder didn't go away. 397 | 398 | 399 | ## [1.3.9] - 2017-07-20. 400 | ### Styling 401 | - There's a new element `span.ng-autocomplete-dropdown-icon` this replaces the dropdown icon i did with css only. 402 | ### Other changes 403 | - Increase of internal performance. 404 | - Had some issues with Element refs. #fixed. 405 | ## [1.2.8] - 2017-07-15. 406 | ### New Functionality. 407 | - It's now possible to instantiate CreateNewAutocompleteGroup with an empty array and set its value later. This can be useful 408 | when you're waiting for an async task to complete. 409 | ``` 410 | const component = NgAutoCompleteComponent.FindCompleter('completer', this.completers); 411 | component.SetValues( 412 | 'late', // <-- NOTE: this is the key of the input. You can call this what you want. 413 | [ 414 | {title: 'Option 4', id: '1'}, 415 | {title: 'Option 5', id: '2'}, 416 | {title: 'Option 6', id: '3'}, 417 | {title: 'Option 7', id: '4'}, 418 | {title: 'Option 8', id: '5'}, 419 | {title: 'Option 9', id: '6'}, 420 | ] 421 | ) 422 | ``` 423 | 424 | - Created new pipe to highlight search query. class `dropdown-item-highlight` 425 | ### Changes 426 | - CreateNewAutocompleteGroup now accepts {id:string|number}, before only {id:string} 427 | - Some small changes. 428 | ## [1.1.6] - 2017-07-12. 429 | - KeyEvents now preventDefault 430 | - Close dropdown on tab. 431 | - Open dropdown on focus - was only on click. 432 | - Updated README.md. 433 | 434 | ## [1.1.5] - 2017-07-12. 435 | - Maintain active option when input is blurred - also for disabled completion inputs now. 436 | - Updated README.md. 437 | 438 | ## [1.1.4] - 2017-07-12. 439 | - Maintain active option when input is blurred. 440 | - Updated README.md. 441 | 442 | ## [1.1.3] - 2017-07-12. 443 | - Updated README.md. 444 | 445 | ## [1.1.2] - 2017-07-11. 446 | - Fixed an issue; when selecting a option from a completer that is parent, active child option didn't reset. 447 | - Updated README.md. 448 | 449 | ## [1.1.1] - 2017-07-11. 450 | ### New Functionality. 451 | - SelectItem(key: string, id: string) 452 | - NgAutoCompleteComponent function - Set value manual, by id. This can be useful when the list is loaded 453 | but there's a value set in the database. When the data is fetched from server, this method can be used. 454 | ```typescript 455 | @ViewChild(NgAutoCompleteComponent) public completer: NgAutoCompleteComponent; 456 | this.completer.SelectItem('completer', '1'); 457 | ``` 458 | ### Or if multiple completers in component. 459 | 460 | ```typescript 461 | @ViewChildren(NgAutoCompleteComponent) public completers: QueryList; 462 | 463 | const completer = NgAutoCompleteComponent.FindCompleter('group1', this.completers); 464 | completer.SelectItem('completer', '1'); 465 | ``` 466 | 467 | ## [1.0.1] - 2017-07-11. 468 | - Only groups with parents did a reset when the input reached a length <= 0. Now all inputs do, input with parents still get set to initial value. 469 | 470 | ## [1.0.0] - 2017-07-11. Releasing since it's being used. 471 | ### Renamed Functions. 472 | 473 | - ResetCompleters to ResetInputs. 474 | - ResetCompleter to ResetInput. 475 | - FindCompleter to FindInput. 476 | - TriggerChangeCompleters to TriggerChange. 477 | 478 | ### New Functionality. 479 | - - Added key on component. 480 | - static FindCompleter usage (NgAutoCompleteComponent.FindCompleter()) (not to be confused with the old FindCompleter, now FindInput) 481 | - (key: string, list: QueryList): NgAutoCompleteComponent. This can be useful when you have multiple ng-autocomplete components 482 | in one component. Note that this can only be used when the view has been init. 483 | 484 | # NgAutoCompleteComponent Functions 485 | 486 | ### Note: 487 | 488 |

I have made all NgAutoCompleteComponent and CompleterComponent functions public, so you could do a lot more than i'll show you

489 |

I've documented the functions of which i think their useful:

490 | 491 | ### Usage 492 | ```typescript 493 | @ViewChild(NgAutoCompleteComponent) public completer: NgAutoCompleteComponent; 494 | ``` 495 | 496 | | Function | Description | 497 | | :--- | :---: | 498 | | FindCompleter((key: string, list: QueryList)) (Static function) | Finds completer | 499 | | ResetInputs() | Resets all rendered completer inputs | 500 | | FindInput(key: string) | Find completer input by assigned key | 501 | | RemovableValues(key: string, list: {id: string or number, [value: string]: any}[]) | Remove options from rendered list (by id) | 502 | | SelectItem(key: string, id: string or number) | e.g set an initial value on the completers input | 503 | | SetValues(key: string, {id: string or number, [value: string]: any}[]) | Sets values for the input. Useful in async situations.| 504 | 505 | 506 | # CompleterComponent Functions 507 | 508 | ### Usage 509 | ```typescript 510 | @ViewChild(NgAutoCompleteComponent) public completer: NgAutoCompleteComponent; 511 | public input = this.completer.FindInput('completer'); 512 | ``` 513 | 514 | | Function | Description | 515 | | :--- | :---: | 516 | | ClearValue() | Clears found completer's input. | 517 | 518 | # Contributing 519 | 520 | ### Do you like to code? 521 | 522 | - Fork & clone 523 | - npm install 524 | - ng serve 525 | - git checkout -b my-new-feature 526 | - component lives in src/app/ng-autocomplete/* 527 | - Submit a pull request 528 | 529 | ### Do you like organizing? 530 | - Link to duplicate issues, and suggest new issue labels, to keep things organized 531 | - Go through open issues and suggest closing old ones. 532 | - Ask clarifying questions on recently opened issues to move the discussion forward 533 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": "6a89b4cc-88fe-4bb3-90b4-a863ca1e38cf" 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "ng-auto-complete-app": { 10 | "root": "", 11 | "sourceRoot": "src", 12 | "projectType": "application", 13 | "prefix": "app", 14 | "schematics": {}, 15 | "targets": { 16 | "build": { 17 | "builder": "@angular-devkit/build-angular:browser", 18 | "options": { 19 | "outputPath": "dist/ng-auto-complete-app", 20 | "index": "src/index.html", 21 | "main": "src/main.ts", 22 | "polyfills": "src/polyfills.ts", 23 | "tsConfig": "src/tsconfig.app.json", 24 | "assets": [ 25 | "src/favicon.ico", 26 | "src/assets" 27 | ], 28 | "styles": [ 29 | "src/styles.css" 30 | ], 31 | "scripts": [] 32 | }, 33 | "configurations": { 34 | "production": { 35 | "fileReplacements": [ 36 | { 37 | "replace": "src/environments/environment.ts", 38 | "with": "src/environments/environment.prod.ts" 39 | } 40 | ], 41 | "optimization": true, 42 | "outputHashing": "all", 43 | "sourceMap": false, 44 | "extractCss": true, 45 | "namedChunks": false, 46 | "aot": true, 47 | "extractLicenses": true, 48 | "vendorChunk": false, 49 | "buildOptimizer": true 50 | } 51 | } 52 | }, 53 | "serve": { 54 | "builder": "@angular-devkit/build-angular:dev-server", 55 | "options": { 56 | "browserTarget": "ng-auto-complete-app:build" 57 | }, 58 | "configurations": { 59 | "production": { 60 | "browserTarget": "ng-auto-complete-app:build:production" 61 | } 62 | } 63 | }, 64 | "extract-i18n": { 65 | "builder": "@angular-devkit/build-angular:extract-i18n", 66 | "options": { 67 | "browserTarget": "ng-auto-complete-app:build" 68 | } 69 | }, 70 | "test": { 71 | "builder": "@angular-devkit/build-angular:karma", 72 | "options": { 73 | "main": "src/test.ts", 74 | "polyfills": "src/polyfills.ts", 75 | "tsConfig": "src/tsconfig.spec.json", 76 | "karmaConfig": "src/karma.conf.js", 77 | "styles": [ 78 | "src/styles.css" 79 | ], 80 | "scripts": [], 81 | "assets": [ 82 | "src/favicon.ico", 83 | "src/assets" 84 | ] 85 | } 86 | }, 87 | "lint": { 88 | "builder": "@angular-devkit/build-angular:tslint", 89 | "options": { 90 | "tsConfig": [ 91 | "src/tsconfig.app.json", 92 | "src/tsconfig.spec.json" 93 | ], 94 | "exclude": [ 95 | "**/node_modules/**" 96 | ] 97 | } 98 | } 99 | } 100 | }, 101 | "ng-auto-complete-app-e2e": { 102 | "root": "e2e/", 103 | "projectType": "application", 104 | "targets": { 105 | "e2e": { 106 | "builder": "@angular-devkit/build-angular:protractor", 107 | "options": { 108 | "protractorConfig": "e2e/protractor.conf.js", 109 | "devServerTarget": "ng-auto-complete-app:serve" 110 | }, 111 | "configurations": { 112 | "production": { 113 | "devServerTarget": "ng-auto-complete-app:serve:production" 114 | } 115 | } 116 | }, 117 | "lint": { 118 | "builder": "@angular-devkit/build-angular:tslint", 119 | "options": { 120 | "tsConfig": "e2e/tsconfig.e2e.json", 121 | "exclude": [ 122 | "**/node_modules/**" 123 | ] 124 | } 125 | } 126 | } 127 | }, 128 | "ng-auto-complete": { 129 | "root": "projects/ng-auto-complete", 130 | "sourceRoot": "projects/ng-auto-complete/src", 131 | "projectType": "library", 132 | "prefix": "ng", 133 | "targets": { 134 | "build": { 135 | "builder": "@angular-devkit/build-ng-packagr:build", 136 | "options": { 137 | "tsConfig": "projects/ng-auto-complete/tsconfig.lib.json", 138 | "project": "projects/ng-auto-complete/ng-package.json" 139 | } 140 | }, 141 | "test": { 142 | "builder": "@angular-devkit/build-angular:karma", 143 | "options": { 144 | "main": "projects/ng-auto-complete/src/test.ts", 145 | "tsConfig": "projects/ng-auto-complete/tsconfig.spec.json", 146 | "karmaConfig": "projects/ng-auto-complete/karma.conf.js" 147 | } 148 | }, 149 | "lint": { 150 | "builder": "@angular-devkit/build-angular:tslint", 151 | "options": { 152 | "tsConfig": [ 153 | "projects/ng-auto-complete/tsconfig.lib.json", 154 | "projects/ng-auto-complete/tsconfig.spec.json" 155 | ], 156 | "exclude": [ 157 | "**/node_modules/**" 158 | ] 159 | } 160 | } 161 | } 162 | } 163 | }, 164 | "defaultProject": "ng-auto-complete-app" 165 | } -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sengirab/ngAutocomplete/9c26bc48196783bd0395ffe09070b32c13b29499/demo.gif -------------------------------------------------------------------------------- /demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sengirab/ngAutocomplete/9c26bc48196783bd0395ffe09070b32c13b29499/demo2.gif -------------------------------------------------------------------------------- /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 ng-auto-complete-app!'); 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 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-auto-complete-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "build_lib": "ng build ng-auto-complete", 12 | "copy-license": "cp ./LICENSE ./dist/ng-auto-complete", 13 | "copy-readme": "cp ./README.md ./dist/ng-auto-complete", 14 | "copy-files": "npm run copy-license && npm run copy-readme", 15 | "npm_pack": "cd dist/ng-auto-complete && npm pack", 16 | "package": "npm run build_lib && npm run copy-files && npm run npm_pack" 17 | }, 18 | "private": true, 19 | "dependencies": { 20 | "@angular/animations": "^11.2.13", 21 | "@angular/common": "^11.2.13", 22 | "@angular/compiler": "^11.2.13", 23 | "@angular/core": "^11.2.13", 24 | "@angular/forms": "^11.2.13", 25 | "@angular/platform-browser": "^11.2.13", 26 | "@angular/platform-browser-dynamic": "^11.2.13", 27 | "@angular/platform-server": "^11.2.13", 28 | "@angular/router": "^11.2.13", 29 | "core-js": "^3.12.0", 30 | "rxjs": "^6.6.7", 31 | "rxjs-compat": "^6.3.3", 32 | "zone.js": "^0.11.4" 33 | }, 34 | "devDependencies": { 35 | "@angular-devkit/build-angular": "^0.1102.12", 36 | "@angular-devkit/build-ng-packagr": "^0.1002.0", 37 | "@angular/cli": "^11.2.12", 38 | "@angular/compiler-cli": "^11.2.13", 39 | "@angular/language-service": "^11.2.13", 40 | "@types/jasmine": "^3.3.0", 41 | "@types/jasminewd2": "~2.0.3", 42 | "@types/node": "^10.12.10", 43 | "codelyzer": "^6.0.2", 44 | "jasmine-core": "^3.3.0", 45 | "jasmine-spec-reporter": "^4.2.1", 46 | "karma": "^3.1.1", 47 | "karma-chrome-launcher": "^2.2.0", 48 | "karma-cli": "^1.0.1", 49 | "karma-coverage-istanbul-reporter": "~2.0.1", 50 | "karma-jasmine": "^2.0.1", 51 | "karma-jasmine-html-reporter": "^1.4.0", 52 | "ng-packagr": "^11.2.4", 53 | "protractor": "^5.4.1", 54 | "rxjs-tslint": "^0.1.6", 55 | "ts-node": "~7.0.0", 56 | "tsickle": "^0.39.1", 57 | "tslib": "^2.2.0", 58 | "tslint": "^5.11.0", 59 | "typescript": "4.1.5" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/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/ng-auto-complete/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ng-auto-complete", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/ng-auto-complete/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-auto-complete", 3 | "version": "5.0.3", 4 | "homepage": "https://github.com/sengirab/ngAutocomplete", 5 | "url": "https://github.com/sengirab/ngAutocomplete/issues", 6 | "email": "benjaminbloot@gmail.com", 7 | "license": "(MIT OR Apache-2.0)", 8 | "peerDependencies": { 9 | "@angular/common": "^7.1.1", 10 | "@angular/core": "^7.1.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/lib/classes/AutocompleteGroup.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteItem, SearchableAutoCompleteItems } from './AutocompleteItem'; 2 | import { TemplateRef } from '@angular/core'; 3 | 4 | export class AutocompleteGroup { 5 | initialValue: { [value: string]: AutocompleteItem }; 6 | 7 | key: string; 8 | keys: { titleKey: string, childrenKey: string | null }; 9 | value: { [value: string]: AutocompleteItem }; 10 | remove: string[]; 11 | placeholder: string; 12 | parent: string; 13 | completion: boolean; 14 | searchLength: number; 15 | async: (str: string) => Promise<{ id: string | number; [value: string]: any }[]> = null; 16 | 17 | // Templates 18 | noResults: TemplateRef; 19 | dropdownValue: TemplateRef; 20 | placeholderValue: TemplateRef; 21 | 22 | private removals: string[] = []; 23 | private _copy: { [value: string]: AutocompleteItem }; 24 | 25 | constructor() { 26 | } 27 | 28 | /** 29 | * 30 | */ 31 | SetNewValue(value: { id: string | number; [value: string]: any }[], titleKey: string) { 32 | const values = SearchableAutoCompleteItems(value, titleKey); 33 | this.SetCopy(values); 34 | 35 | /** 36 | * 37 | */ 38 | this.value = this.FilterRemovals(this.removals, values); 39 | } 40 | 41 | /** 42 | * 43 | */ 44 | Removables(ids: string[]) { 45 | this.removals = ids; 46 | 47 | /** 48 | * 49 | */ 50 | this.value = this.FilterRemovals(this.removals, this._copy); 51 | } 52 | 53 | /** 54 | * 55 | */ 56 | InitialValue() { 57 | this.value = this.FilterRemovals(this.removals, this.initialValue); 58 | 59 | /** 60 | * 61 | */ 62 | this.SetCopy(this.initialValue); 63 | } 64 | 65 | /** 66 | * 67 | */ 68 | SetCopy(values: { [value: string]: AutocompleteItem }) { 69 | this._copy = Object.assign([], values); 70 | } 71 | 72 | /** 73 | * 74 | */ 75 | SetValues(value: { id?: string | number; [value: string]: any }[]) { 76 | this.value = SearchableAutoCompleteItems(value, this.keys.titleKey, this.keys.childrenKey); 77 | 78 | /** 79 | * 80 | */ 81 | this.initialValue = Object.assign({}, this.value); 82 | this.SetCopy(Object.assign({}, this.value)); 83 | } 84 | 85 | /** 86 | * 87 | */ 88 | FilterRemovals(removals: any[], value: { [value: string]: AutocompleteItem }): { [value: string]: AutocompleteItem } { 89 | let filtered = Object.assign({}, value); 90 | 91 | let key, keys = []; 92 | for (key in filtered) { 93 | if (filtered.hasOwnProperty(key)) { 94 | removals.forEach((id) => { 95 | // Comparable string and ID 96 | let f = `_id_${String(id)}`; 97 | let c = key.substring(key.indexOf(f), key.length); 98 | 99 | if (f === c) { 100 | keys.push(key); 101 | } 102 | }) 103 | } 104 | } 105 | 106 | keys.forEach((k) => { 107 | delete filtered[k]; 108 | }); 109 | 110 | return filtered; 111 | } 112 | } 113 | 114 | /** 115 | * 116 | */ 117 | export function CreateNewAutocompleteGroup(placeholder: string, key: string, value: { id?: string | number; [value: string]: any }[], keys: { titleKey: string, childrenKey: string | null }, parent: string = '', completion: boolean = true, searchLength: number = 2): AutocompleteGroup { 118 | const group = new AutocompleteGroup(); 119 | 120 | group.key = key; 121 | group.keys = keys; 122 | group.placeholder = placeholder; 123 | group.parent = parent; 124 | group.completion = completion; 125 | group.searchLength = searchLength; 126 | 127 | /** 128 | * Initial value needed, if we empty search box or want to clear it, it needs to be reset. 129 | * Setting copy, used if user wants to remove values (by id.). This _ list gets filtered. 130 | */ 131 | group.SetValues(value); 132 | 133 | return group; 134 | } 135 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/lib/classes/AutocompleteItem.ts: -------------------------------------------------------------------------------- 1 | export interface StrippedAutocompleteGroup { 2 | group: { key: string }; 3 | item: AutocompleteItem; 4 | } 5 | 6 | export class AutocompleteItem { 7 | title: string; 8 | id?: string | number; 9 | children: any[]; 10 | original: any; 11 | className: string; 12 | 13 | constructor(title: string, id: string | number, className: string, original: any) { 14 | this.title = title; 15 | this.id = id; 16 | this.className = className; 17 | this.original = original; 18 | } 19 | } 20 | 21 | /** 22 | * 23 | */ 24 | export function SearchableAutoCompleteItems(items: { id?: string | number; [value: string]: any }[], titleKey: string, childrenKey: string | null = null): { [value: string]: AutocompleteItem } { 25 | return items.reduce(function (r, i) { 26 | const t = SearchableAutoCompleteString(i[titleKey], i.id); 27 | 28 | if (typeof r[t] === 'undefined') { 29 | r[t] = TransformToAutocompleteItem(i, titleKey, childrenKey); 30 | } 31 | 32 | return r; 33 | }, {}); 34 | } 35 | 36 | /** 37 | * 38 | */ 39 | export function SearchableAutoCompleteString(key: string, id: string | number) { 40 | return `${key.replace(/ /g, '_')}_id_${String(id)}`; 41 | } 42 | 43 | /** 44 | * 45 | */ 46 | export function ComparableAutoCompleteString(str: string) { 47 | return str.replace(/_/g, ' '); 48 | } 49 | 50 | /** 51 | * object must have an ID 52 | */ 53 | 54 | export function TransformToAutocompleteItem(object: { id?: string | number; [value: string]: any, className?: string }, titleKey: string, childrenKey: string | null = null) { 55 | const item = new AutocompleteItem(object[titleKey], object.id, object.className, object); 56 | 57 | /** 58 | * if there are children, add these. 59 | */ 60 | if (childrenKey !== null) { 61 | item.children = object[childrenKey]; 62 | } 63 | 64 | return item; 65 | } 66 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/lib/classes/typing.ts: -------------------------------------------------------------------------------- 1 | import {AutocompleteGroup} from "./AutocompleteGroup"; 2 | import {AutocompleteItem} from "./AutocompleteItem"; 3 | 4 | export interface SelectedAutocompleteItem { 5 | group: AutocompleteGroup; 6 | item: AutocompleteItem 7 | } -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/lib/completer/completer.component.ts: -------------------------------------------------------------------------------- 1 | import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; 2 | import { 3 | ChangeDetectionStrategy, 4 | ChangeDetectorRef, 5 | Component, 6 | EventEmitter, 7 | Input, 8 | NgZone, 9 | OnInit, 10 | Output, 11 | ViewChild 12 | } from '@angular/core'; 13 | import { AutocompleteGroup } from '../classes/AutocompleteGroup'; 14 | import { 15 | AutocompleteItem, 16 | ComparableAutoCompleteString, 17 | SearchableAutoCompleteString, 18 | StrippedAutocompleteGroup 19 | } from '../classes/AutocompleteItem'; 20 | import { NgDropdownDirective } from '../dropdown/ng-dropdown.directive'; 21 | import { GroupNoResult } from '../utils/utils'; 22 | import { Observable, of, Subject } from 'rxjs'; 23 | 24 | @Component({ 25 | selector : 'ng-completer', 26 | template : ` 27 |

29 | 30 | 31 | 32 |
34 | 36 | 37 | 38 | 39 | 40 | {{_DOM.placeholder.title}} 41 | 42 | 43 | 49 | 50 | 52 |
53 | 54 |
62 | 67 | 68 | 77 |
78 |
`, 79 | styles: [` 80 | .ng-autocomplete-inputs { 81 | position: relative; 82 | } 83 | 84 | .ng-autocomplete-inputs input[type=text]::-ms-clear, 85 | .ng-autocomplete-inputs input[type=text]::-ms-reveal { 86 | display: none; 87 | width: 0; 88 | height: 0; 89 | } 90 | 91 | .ng-autocomplete-inputs.completion-off { 92 | cursor: pointer; 93 | } 94 | 95 | .ng-autocomplete-inputs.completion-off input { 96 | pointer-events: none; 97 | } 98 | 99 | .ng-autocomplete-placeholder { 100 | pointer-events: none; 101 | } 102 | 103 | .ng-autocomplete-dropdown-icon { 104 | 105 | } 106 | 107 | .ng-autocomplete-dropdown .ng-dropdown { 108 | display: none; 109 | } 110 | 111 | .ng-autocomplete-dropdown .ng-dropdown.is-empty { 112 | display: none; 113 | } 114 | 115 | .ng-autocomplete-dropdown .ng-dropdown.open { 116 | display: block; 117 | } 118 | `], 119 | }) 120 | export class CompleterComponent implements OnInit { 121 | @ViewChild(NgDropdownDirective, { static: true }) public dropdown: NgDropdownDirective; 122 | 123 | @Output() public cleared: EventEmitter = new EventEmitter(); 124 | @Output() public selected: EventEmitter = new EventEmitter(); 125 | // tslint:disable-next-line:no-output-rename 126 | @Output('no-result') public noResult: EventEmitter = new EventEmitter(); 127 | 128 | @Input() public group: AutocompleteGroup = {}; 129 | 130 | _change: Subject = new Subject(); 131 | _items: { [value: string]: AutocompleteItem } = {}; 132 | _completer = ''; 133 | _highlight = ''; 134 | _disabled = false; 135 | 136 | _DOM = { 137 | notFound : false, 138 | empty : false, 139 | placeholder: null, 140 | selected : '', 141 | isLoading : false 142 | 143 | }; 144 | 145 | asyncObservable = new Subject(); 146 | 147 | constructor(private _zone: NgZone, private cdr: ChangeDetectorRef) { 148 | } 149 | 150 | /** 151 | * 152 | */ 153 | ngOnInit() { 154 | this._change.pipe( 155 | debounceTime(300), 156 | distinctUntilChanged(), 157 | switchMap((value: string) => { 158 | if (this.group.async !== null) { 159 | return this.RunAsyncFunction(value).pipe(switchMap((res) => { 160 | if (res !== null) { 161 | this.ListenToAsyncFunction(res.value, res.values); 162 | } 163 | return of(true); 164 | })); 165 | } else { 166 | this.OnModelChange(value); 167 | return of(true); 168 | } 169 | }) 170 | ) 171 | .subscribe(); 172 | 173 | this.SetItems(); 174 | } 175 | 176 | /** 177 | * Only used when completion is off. 178 | */ 179 | RegisterClick() { 180 | if (!this.group.completion) { 181 | this.SwitchDropdownState(); 182 | } 183 | } 184 | 185 | /** 186 | * 187 | */ 188 | DropdownArray() { 189 | if (this.group.completion) { 190 | this.SwitchDropdownState(); 191 | } 192 | } 193 | 194 | /** 195 | * 196 | */ 197 | SwitchDropdownState() { 198 | if (this.dropdown._open) { 199 | this.CloseDropdown(); 200 | return; 201 | } 202 | 203 | /** 204 | * 205 | */ 206 | this.OpenDropdown(); 207 | } 208 | 209 | /** 210 | * 211 | */ 212 | CloseDropdown() { 213 | this.dropdown._open = false; 214 | 215 | /** 216 | * 217 | */ 218 | this._DOM.placeholder = null; 219 | } 220 | 221 | /** 222 | * 223 | */ 224 | OpenDropdown() { 225 | this.dropdown.Open(); 226 | 227 | /** 228 | * 229 | */ 230 | this._DOM.placeholder = null; 231 | } 232 | 233 | /** 234 | * 235 | */ 236 | SetItems() { 237 | this._items = this.group.value; 238 | this.IsInitialEmpty(); 239 | } 240 | 241 | /** 242 | * 243 | */ 244 | SelectItem(item: AutocompleteItem | string) { 245 | let i: AutocompleteItem; 246 | if (typeof item === 'string') { 247 | i = this._items[item]; 248 | this._DOM.selected = item; 249 | } else { 250 | i = item; 251 | this._DOM.selected = SearchableAutoCompleteString(item.title, item.id); 252 | } 253 | 254 | this._completer = i.title; 255 | this._highlight = ''; 256 | 257 | this.dropdown.Close(null, true); 258 | this.selected.emit({ group: { key: this.group.key }, item: i }); 259 | } 260 | 261 | ListenToAsyncFunction(value: string, values: {id: string | number, [p: string]: any}[]) { 262 | this.group.SetNewValue(values, this.group.keys.titleKey); 263 | 264 | this._DOM.isLoading = false; 265 | 266 | this._items = this.group.value; 267 | this.EmptySearch(this._items, value); 268 | 269 | // User has typed something now, results could be shown. We need to remove the "is-initial-empty" class. 270 | this.IsInitialEmpty(); 271 | this.dropdown.Open(); 272 | } 273 | 274 | /** 275 | * 276 | */ 277 | RunAsyncFunction(value: string): Observable<{value: string, values: {id: string | number, [p: string]: any}[]}> { 278 | this._completer = value; 279 | this._highlight = value; 280 | 281 | this._DOM.selected = null; 282 | 283 | if (value.length === 0) { 284 | this.group.InitialValue(); 285 | this.ClearModel(); 286 | 287 | this.dropdown.Close('', true); 288 | return of(null); 289 | } else if (value.length > this.group.searchLength) { 290 | this._DOM.isLoading = true; 291 | 292 | return new Observable((observer) => { 293 | this.group.async(value).then(values => { 294 | observer.next({value, values}); 295 | }); 296 | }); 297 | } 298 | } 299 | 300 | /** 301 | * 302 | */ 303 | OnModelChange(value: string) { 304 | this._completer = value; 305 | this._highlight = value; 306 | 307 | this._DOM.selected = null; 308 | 309 | if (value.length === 0) { 310 | this.ClearModel(); 311 | } else if (value.length > this.group.searchLength) { 312 | this.CompareItemsAndSet(value); 313 | } 314 | } 315 | 316 | /** 317 | * 318 | */ 319 | ClearModel() { 320 | this._DOM.selected = null; 321 | this._DOM.notFound = false; 322 | 323 | this.cleared.emit(this.group.key); 324 | } 325 | 326 | /** 327 | * 328 | */ 329 | CompareItemsAndSet(value: string) { 330 | const obj = {}; 331 | for (const key in this.group.value) { 332 | if (ComparableAutoCompleteString(key).toLowerCase().indexOf(value.toLowerCase()) > -1) { 333 | obj[key] = this.group.value[key]; 334 | } 335 | } 336 | 337 | this._items = obj; 338 | this.EmptySearch(this._items, value); 339 | 340 | // User has typed something now, results could be shown. We need to remove the "is-initial-empty" class. 341 | this.IsInitialEmpty(); 342 | this.dropdown.Open(); 343 | } 344 | 345 | /** 346 | * 347 | */ 348 | OnInputBlurred() { 349 | if (!this.HasChosenValue()) { 350 | /** 351 | * Let component know completer input has been cleared - 352 | * this function shows the list again, also resets children, if chosen. 353 | */ 354 | this.OnModelChange(''); 355 | } 356 | } 357 | 358 | /** 359 | * 360 | */ 361 | OnHoverDropdownItem(item: AutocompleteItem | string) { 362 | if (typeof item === 'string') { 363 | this._DOM.placeholder = this._items[item]; 364 | return; 365 | } 366 | if (item == null) { 367 | this._DOM.placeholder = null; 368 | return; 369 | } 370 | 371 | this._DOM.placeholder = item; 372 | } 373 | 374 | // =======================================================================// 375 | // ! Utils // 376 | // =======================================================================// 377 | 378 | IsInitialEmpty() { 379 | if (Object.keys(this._items).length === 0 && this._completer.length === 0) { 380 | this._DOM.empty = true; 381 | return; 382 | } 383 | 384 | this._DOM.empty = false; 385 | } 386 | 387 | /** 388 | * 389 | */ 390 | HasChosenValue(): boolean { 391 | return this._DOM.selected !== null; 392 | } 393 | 394 | /** 395 | * 396 | */ 397 | EmptySearch(obj: Object, query: string) { 398 | if (Object.keys(obj).length > 0) { 399 | this._DOM.notFound = false; 400 | return; 401 | } 402 | 403 | this._DOM.notFound = true; 404 | this.noResult.emit({ group: { key: this.group.key }, query: query }); 405 | } 406 | 407 | /** 408 | * 409 | */ 410 | ClearValue() { 411 | this._completer = ''; 412 | this._DOM.selected = null; 413 | 414 | this.group.InitialValue(); 415 | this.IsInitialEmpty(); 416 | /** 417 | * 418 | */ 419 | this.selected.emit({ group: { key: this.group.key }, item: null }); 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/lib/dropdown/ng-dropdown.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectorRef, 3 | Directive, 4 | ElementRef, 5 | EventEmitter, 6 | HostBinding, 7 | HostListener, 8 | Input, 9 | OnChanges, 10 | OnDestroy, 11 | OnInit, 12 | Output, 13 | SimpleChanges 14 | } from '@angular/core'; 15 | import {IsMobileOrTablet} from '../utils/utils'; 16 | 17 | @Directive({ 18 | selector: '[ngDropdown]' 19 | }) 20 | export class NgDropdownDirective implements OnChanges, OnInit, OnDestroy { 21 | @Input() public list: any[] = []; 22 | @Input() public active: any = null; 23 | 24 | @Input() public input: HTMLElement = null; 25 | @Input() public element: Element = null; 26 | 27 | @Input() public key = ''; 28 | @Input() public completion = true; 29 | 30 | @Output() public hover: EventEmitter = new EventEmitter(); 31 | @Output() public selected: EventEmitter = new EventEmitter(); 32 | @Output() public closed: EventEmitter = new EventEmitter(); 33 | 34 | _open = false; 35 | _list: { active: boolean, [value: string]: any }[] = []; 36 | _class = ''; 37 | 38 | private inputKeydownBind = this.inputKeydown.bind(this); 39 | private mouseoverListenerBind = this.mouseoverListener.bind(this); 40 | private documentKeydownBind = this.documentKeydown.bind(this); 41 | 42 | constructor(public _eref: ElementRef, private cdr: ChangeDetectorRef) { 43 | } 44 | 45 | /** 46 | * 47 | */ 48 | ngOnInit() { 49 | this._class = `dr-item-${this.key}-`; 50 | 51 | if (!IsMobileOrTablet()) { 52 | this._eref.nativeElement.addEventListener('mouseover', this.mouseoverListenerBind); 53 | } 54 | 55 | /** 56 | * 57 | */ 58 | this.PrepareList(); 59 | } 60 | 61 | /** 62 | * 63 | */ 64 | ngOnChanges(changes: SimpleChanges) { 65 | if (typeof changes['active'] !== 'undefined' && !changes['active'].firstChange) { 66 | this.PrepareList(); 67 | } 68 | if (typeof changes['list'] !== 'undefined') { 69 | this.list = changes['list'].currentValue; 70 | 71 | /** 72 | * 73 | */ 74 | this.PrepareList(); 75 | } 76 | } 77 | 78 | /** 79 | * 80 | */ 81 | keyDown(event: KeyboardEvent) { 82 | event.stopImmediatePropagation(); 83 | event.stopPropagation(); 84 | 85 | /** 86 | * 87 | */ 88 | switch (event.code) { 89 | case 'ArrowDown': 90 | this.Open(); 91 | 92 | /** 93 | * 94 | */ 95 | this.SetActive(this.FindActive() + 1); 96 | this.DropdownFocusAreaDown(); 97 | 98 | event.preventDefault(); 99 | break; 100 | case 'ArrowUp': 101 | this.Open(); 102 | 103 | /** 104 | * 105 | */ 106 | this.SetActive(this.FindActive() - 1); 107 | this.DropdownFocusAreaUp(); 108 | 109 | event.preventDefault(); 110 | break; 111 | case 'Enter': 112 | this.EmitSelected(); 113 | this.Close(null, true); 114 | 115 | if (this.RefExists()) { 116 | this.input.blur(); 117 | } 118 | 119 | event.preventDefault(); 120 | break; 121 | case 'Escape': 122 | this.Close(null, true); 123 | 124 | if (this.RefExists()) { 125 | this.input.blur(); 126 | } 127 | 128 | event.preventDefault(); 129 | break; 130 | case 'Tab': 131 | if (!event.shiftKey) { 132 | this.EmitSelected(); 133 | } 134 | 135 | this.Close(null, true); 136 | break; 137 | default: 138 | return; 139 | } 140 | } 141 | 142 | /** 143 | * 144 | */ 145 | OnMouseOver(event: MouseEvent) { 146 | // Mouse didn't actually move, so no logic needed. 147 | if (event.movementX === 0 && event.movementY === 0) { 148 | return; 149 | } 150 | 151 | /** 152 | * 153 | */ 154 | const el: any = event.target || event.srcElement; 155 | if (el.id.length > 0 && el.id.includes(this._class)) { 156 | this.SetActive(Number(el.id.slice(this._class.length, el.id.length))); 157 | } 158 | } 159 | 160 | /** 161 | * 162 | */ 163 | EmitSelected() { 164 | if (this.FindActive() > -1) { 165 | this.selected.emit(this._list[this.FindActive()].key); 166 | } 167 | } 168 | 169 | /** 170 | * 171 | */ 172 | DropdownFocusAreaDown() { 173 | const scroll = this._eref.nativeElement.offsetHeight + this._eref.nativeElement.scrollTop; 174 | 175 | /** 176 | * 177 | */ 178 | if ((this.GetElement(this.FindActive()).offsetTop + this.GetElement(this.FindActive()).offsetHeight) > scroll) { 179 | // tslint:disable-next-line:max-line-length 180 | this._eref.nativeElement.scrollTop = this.GetElement(this.FindActive()).offsetTop - (this._eref.nativeElement.offsetHeight - this.GetElement(this.FindActive()).offsetHeight); 181 | } 182 | } 183 | 184 | /** 185 | * 186 | */ 187 | DropdownFocusAreaUp() { 188 | const scroll = this._eref.nativeElement.scrollTop; 189 | 190 | /** 191 | * 192 | */ 193 | if (this.GetElement(this.FindActive()).offsetTop < scroll && scroll > 0) { 194 | this._eref.nativeElement.scrollTop = this.GetElement(this.FindActive()).offsetTop; 195 | } 196 | } 197 | 198 | // =======================================================================// 199 | // ! Bindings // 200 | // =======================================================================// 201 | 202 | /** 203 | * 204 | */ 205 | @HostBinding('class.open') 206 | get opened() { 207 | return this._open; 208 | } 209 | 210 | 211 | // =======================================================================// 212 | // ! Listeners // 213 | // =======================================================================// 214 | 215 | /** 216 | * 217 | */ 218 | @HostListener('document:click', ['$event']) 219 | Close(event, force: boolean = false) { 220 | if (!this._open) { 221 | return; 222 | } 223 | 224 | const close = () => { 225 | this._open = false; 226 | 227 | /** 228 | * Emit NULL so listening components know what to do. 229 | */ 230 | this.RemoveListeners(); 231 | this.ClearActive(); 232 | this.hover.emit(null); 233 | this.closed.emit(); 234 | this.cdr.detectChanges(); 235 | }; 236 | 237 | if (force) { 238 | close(); 239 | return; 240 | } 241 | 242 | if ((this._open && (!this.element.contains(event.target)))) { 243 | close(); 244 | } 245 | } 246 | 247 | /** 248 | * 249 | */ 250 | private inputKeydown(event: KeyboardEvent) { 251 | this.keyDown(event); 252 | } 253 | 254 | 255 | /** 256 | * 257 | */ 258 | private documentKeydown(event: KeyboardEvent) { 259 | this.keyDown(event); 260 | } 261 | 262 | 263 | /** 264 | * 265 | */ 266 | private mouseoverListener(event: MouseEvent) { 267 | this.OnMouseOver(event); 268 | } 269 | 270 | 271 | 272 | // =======================================================================// 273 | // ! Utils // 274 | // =======================================================================// 275 | 276 | /** 277 | * 278 | */ 279 | RegisterListeners() { 280 | if (this.RefExists()) { 281 | this.input.addEventListener('keydown', this.inputKeydownBind); 282 | } 283 | 284 | if (!this.completion) { 285 | document.addEventListener('keydown', this.documentKeydownBind); 286 | } 287 | } 288 | 289 | /** 290 | * 291 | */ 292 | RemoveListeners() { 293 | if (this.RefExists()) { 294 | this.input.removeEventListener('keydown', this.inputKeydownBind); 295 | } 296 | 297 | if (!this.completion) { 298 | document.removeEventListener('keydown', this.documentKeydownBind); 299 | } 300 | 301 | if (!IsMobileOrTablet()) { 302 | this._eref.nativeElement.removeEventListener('mouseover', this.mouseoverListenerBind); 303 | } 304 | } 305 | 306 | /** 307 | * 308 | */ 309 | Open() { 310 | setTimeout(() => { 311 | if (!this._open && !this._eref.nativeElement.classList.contains('is-initial-empty')) { 312 | this.RegisterListeners(); 313 | 314 | this._open = true; 315 | this.PrepareList(); 316 | 317 | /** 318 | * 319 | */ 320 | if (this.FindActive() < 0) { 321 | this._eref.nativeElement.scrollTop = 0; 322 | } else { 323 | this._eref.nativeElement.scrollTop = this.GetElement(this.FindActive()).offsetHeight * this.FindActive(); 324 | } 325 | 326 | this.cdr.detectChanges(); 327 | } 328 | }, 0); 329 | } 330 | 331 | /** 332 | * 333 | */ 334 | RefExists() { 335 | return typeof this.input !== 'undefined'; 336 | } 337 | 338 | /** 339 | * 340 | */ 341 | FindActive(): number { 342 | return this._list.reduce((result, item, index) => { 343 | if (item.active) { 344 | result = index; 345 | } 346 | 347 | return result; 348 | }, -1); 349 | } 350 | 351 | /** 352 | * 353 | */ 354 | SetActive(index: number) { 355 | if (index > this._list.length - 1 || index < 0) { 356 | return; 357 | } 358 | 359 | /** 360 | * 361 | */ 362 | this.ClearActive(); 363 | 364 | this._list[index].active = true; 365 | this.hover.emit(this._list[index].key); 366 | /** 367 | * 368 | */ 369 | this.GetElement(index).classList.add('active'); 370 | } 371 | 372 | /** 373 | * 374 | */ 375 | GetElement(index: number) { 376 | return this._eref.nativeElement.children[index]; 377 | } 378 | 379 | /** 380 | * 381 | */ 382 | ClearActive(): void { 383 | this._list.forEach((item, index) => { 384 | item.active = false; 385 | 386 | /** 387 | * 388 | */ 389 | this.GetElement(index).classList.remove('active'); 390 | }); 391 | } 392 | 393 | /** 394 | * 395 | */ 396 | PrepareList() { 397 | this._list = Object.keys(this.list).map((key) => { 398 | return { 399 | key, 400 | active: this.ActiveItem(key) 401 | }; 402 | }); 403 | 404 | /** 405 | * 406 | */ 407 | this.PrepareChildrenList(); 408 | } 409 | 410 | /** 411 | * 412 | */ 413 | ActiveItem(item: any) { 414 | return this.active !== null && item === this.active; 415 | } 416 | 417 | /** 418 | * 419 | */ 420 | DetermineActiveClass() { 421 | this._list.forEach((item, index) => { 422 | if (typeof this.GetElement(index) === 'undefined') { 423 | return; 424 | } 425 | 426 | /** 427 | * 428 | */ 429 | this.GetElement(index).classList.remove('active'); 430 | if (item.active) { 431 | this.GetElement(index).classList.add('active'); 432 | } 433 | }); 434 | } 435 | 436 | /** 437 | * 438 | */ 439 | PrepareChildrenList() { 440 | const list = this._eref.nativeElement.children; 441 | 442 | setTimeout(() => { 443 | for (let i = 0; i < list.length; i++) { 444 | list[i].id = this._class + i; 445 | } 446 | }, 0); 447 | 448 | /** 449 | * 450 | */ 451 | this.DetermineActiveClass(); 452 | 453 | } 454 | 455 | /** 456 | * 457 | */ 458 | DeReference(object: { active: boolean, [value: string]: any }) { 459 | const {item} = object; 460 | 461 | /** 462 | * 463 | */ 464 | return Object.assign({}, {...item}); 465 | } 466 | 467 | /** 468 | * 469 | */ 470 | ngOnDestroy() { 471 | this.RemoveListeners(); 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/lib/ng-auto-complete.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewChecked, 3 | ChangeDetectorRef, 4 | Component, 5 | ElementRef, 6 | EventEmitter, 7 | Input, 8 | OnInit, 9 | Output, 10 | QueryList, 11 | SimpleChanges, 12 | TemplateRef, 13 | ViewChild, 14 | ViewChildren 15 | } from '@angular/core'; 16 | import {AutocompleteGroup} from './classes/AutocompleteGroup'; 17 | import {SelectedAutocompleteItem} from './classes/typing'; 18 | import {CompleterComponent} from './completer/completer.component'; 19 | import {GroupNoResult, ReturnStringArrayByID} from './utils/utils'; 20 | import {Subject} from 'rxjs'; 21 | 22 | @Component({ 23 | selector: 'ng-auto-complete', 24 | template: ` 25 |
26 | 30 | `, 31 | }) 32 | export class NgAutoCompleteComponent implements OnInit, AfterViewChecked { 33 | @ViewChildren(CompleterComponent) public completers: QueryList; 34 | @ViewChild('init', { static: true }) public init: ElementRef; 35 | 36 | @Output() public selected: EventEmitter = new EventEmitter(); 37 | // tslint:disable-next-line:no-output-rename 38 | @Output('no-result') public noResult: EventEmitter = new EventEmitter(); 39 | 40 | @Input() public group: AutocompleteGroup[] = []; 41 | @Input() public key = ''; 42 | @Input() public classes: string[] = []; 43 | 44 | _viewHasBeenInit = false; 45 | _viewInitSubject: Subject = new Subject(); 46 | 47 | constructor(private cdr: ChangeDetectorRef) { 48 | } 49 | 50 | 51 | // =======================================================================// 52 | // ! Static (utils) // 53 | // =======================================================================// 54 | 55 | /** 56 | * 57 | */ 58 | static FindCompleter(key: string, list: QueryList): NgAutoCompleteComponent { 59 | const completer = list.filter((c: NgAutoCompleteComponent) => { 60 | return key === c.key; 61 | }); 62 | 63 | if (typeof completer[0] !== 'undefined') { 64 | return completer[0]; 65 | } 66 | 67 | return null; 68 | } 69 | 70 | /** 71 | * 72 | */ 73 | ngOnInit() { 74 | } 75 | 76 | /** 77 | * 78 | */ 79 | ngAfterViewChecked() { 80 | if (!this._viewHasBeenInit) { 81 | const el = this.init.nativeElement.querySelector('.after-view-init'); 82 | 83 | if (window.getComputedStyle(el).length > 0) { 84 | this._viewHasBeenInit = true; 85 | this._viewInitSubject.next(true); 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * 92 | */ 93 | ListenToSelected(selected: SelectedAutocompleteItem) { 94 | this.selected.emit(selected); 95 | 96 | /** 97 | * 98 | */ 99 | this.SetChildren(selected); 100 | } 101 | 102 | /** 103 | * 104 | */ 105 | NoResult(group: GroupNoResult) { 106 | this.noResult.emit(group); 107 | } 108 | 109 | /** 110 | * 111 | */ 112 | InputCleared(key: string) { 113 | this.group.forEach((group) => { 114 | if (group.key === key || group.parent === key) { 115 | this.ResetInput(group.key); 116 | } 117 | }); 118 | 119 | /** 120 | * Items may have changed, need to te re-set list in completer components. 121 | */ 122 | this.TriggerChange(); 123 | } 124 | 125 | /** 126 | * 127 | */ 128 | SetChildren(selected: SelectedAutocompleteItem) { 129 | this.group.forEach((item) => { 130 | 131 | if (item.parent === selected.group.key) { 132 | this.ResetInput(item.key); 133 | 134 | /** 135 | * 136 | */ 137 | if (selected.item !== null && typeof selected.item.children !== 'undefined') { 138 | item.SetNewValue(selected.item.children, selected.group.keys.titleKey); 139 | } 140 | } 141 | }); 142 | 143 | /** 144 | * Items may have changed, need to te re-set list in completer components. 145 | */ 146 | this.TriggerChange(); 147 | } 148 | 149 | /** 150 | * 151 | */ 152 | TriggerChange() { 153 | this.completers.forEach((completer) => { 154 | completer.SetItems(); 155 | }); 156 | } 157 | 158 | // =======================================================================// 159 | // ! Utils // 160 | // =======================================================================// 161 | 162 | /** 163 | * 164 | */ 165 | GetInput(key: string): CompleterComponent { 166 | return this.completers.reduce((result, completer) => { 167 | if (completer.group.key === key) { 168 | result = completer; 169 | } 170 | 171 | return result; 172 | }, {}); 173 | } 174 | 175 | /** 176 | * 177 | */ 178 | SubscribeInput(key: string, f: (completer: CompleterComponent) => void) { 179 | if (this._viewHasBeenInit) { 180 | const completer = this.GetInput(key); 181 | 182 | /** 183 | * 184 | */ 185 | f(completer); 186 | return; 187 | } 188 | 189 | this._viewInitSubject.subscribe((_bool) => { 190 | const completer = this.GetInput(key); 191 | setTimeout(() => { 192 | f(completer); 193 | }); 194 | 195 | this._viewInitSubject.unsubscribe(); 196 | }); 197 | } 198 | 199 | /** 200 | * 201 | */ 202 | ResetInput(key: string) { 203 | this.SubscribeInput( 204 | key, 205 | (completer) => { 206 | completer.ClearValue(); 207 | } 208 | ); 209 | } 210 | 211 | /** 212 | * 213 | */ 214 | SetValues(key: string, values: { id?: string | number; [value: string]: any }[]) { 215 | this.SubscribeInput( 216 | key, 217 | (completer) => { 218 | completer.group.SetValues(values); 219 | 220 | /** 221 | * Items may have changed, need to te re-set list in completer components. 222 | */ 223 | this.TriggerChange(); 224 | } 225 | ); 226 | } 227 | 228 | /** 229 | * 230 | */ 231 | SetTemplate(key: string, type: 'noResults' | 'placeholderValue' | 'dropdownValue', template: TemplateRef) { 232 | this.SubscribeInput( 233 | key, 234 | (completer) => { 235 | completer.group[type] = template; 236 | 237 | /** 238 | * Items may have changed, need to te re-set list in completer components. 239 | */ 240 | this.TriggerChange(); 241 | } 242 | ); 243 | } 244 | 245 | /** 246 | * 247 | */ 248 | SetAsync(key: string, promise: (str: string) => Promise<{ id: string | number; [value: string]: any }[]>) { 249 | this.SubscribeInput( 250 | key, 251 | (completer) => { 252 | completer.group.async = promise; 253 | 254 | /** 255 | * Items may have changed, need to te re-set list in completer components. 256 | */ 257 | this.TriggerChange(); 258 | } 259 | ); 260 | } 261 | 262 | /** 263 | * 264 | */ 265 | SetEnable(key: string) { 266 | this.SubscribeInput( 267 | key, 268 | (completer) => { 269 | completer._disabled = false; 270 | 271 | /** 272 | * Items may have changed, need to te re-set list in completer components. 273 | */ 274 | this.TriggerChange(); 275 | } 276 | ); 277 | } 278 | 279 | /** 280 | * 281 | */ 282 | SetDisable(key: string) { 283 | this.SubscribeInput( 284 | key, 285 | (completer) => { 286 | completer._disabled = true; 287 | 288 | /** 289 | * Items may have changed, need to te re-set list in completer components. 290 | */ 291 | this.TriggerChange(); 292 | } 293 | ); 294 | } 295 | 296 | /** 297 | * 298 | */ 299 | SelectItem(key: string, id: string | number) { 300 | this.SubscribeInput( 301 | key, 302 | (completer) => { 303 | Object.keys(completer._items).forEach((k) => { 304 | const f = `_id_${String(id)}`; 305 | const c = k.substring(k.indexOf(f), k.length); 306 | 307 | if (f === c) { 308 | completer.SelectItem(completer._items[k]); 309 | } 310 | }); 311 | 312 | } 313 | ); 314 | } 315 | 316 | /** 317 | * 318 | */ 319 | RemovableValues(key: string, ids: { id: string | number, [value: string]: any }[]) { 320 | this.SubscribeInput( 321 | key, 322 | (completer) => { 323 | completer.group.Removables(ReturnStringArrayByID(ids)); 324 | 325 | /** 326 | * Items may have changed, need to te re-set list in completer components. 327 | */ 328 | this.TriggerChange(); 329 | } 330 | ); 331 | } 332 | 333 | /** 334 | * 335 | */ 336 | ResetInputs() { 337 | this.group.forEach((item) => { 338 | this.ResetInput(item.key); 339 | }); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/lib/ng-auto-complete.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {NgAutoCompleteComponent} from './ng-auto-complete.component'; 3 | import {CommonModule} from '@angular/common'; 4 | import {FormsModule} from '@angular/forms'; 5 | import {CompleterComponent} from './completer/completer.component'; 6 | import {NgDropdownDirective} from './dropdown/ng-dropdown.directive'; 7 | import {PipeModule} from './pipes/pipes.module'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, 12 | FormsModule, 13 | PipeModule.forRoot() 14 | ], 15 | exports: [ 16 | NgAutoCompleteComponent, 17 | CompleterComponent, 18 | ], 19 | declarations: [ 20 | NgAutoCompleteComponent, 21 | CompleterComponent, 22 | NgDropdownDirective 23 | ] 24 | }) 25 | export class NgAutoCompleteModule { 26 | } 27 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/lib/pipes/highlight.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'highlight' 5 | }) 6 | export class HighlightPipe implements PipeTransform { 7 | 8 | transform(text: string, search: string): string { 9 | if (search.length > 0) { 10 | return this.Strip(text).replace(new RegExp(`${this.EscapeMatch(search)}`, 'gi'), (match: string) => { 11 | return `${match}`; 12 | }); 13 | } else { 14 | return text; 15 | } 16 | } 17 | 18 | EscapeMatch(match: string) { 19 | const entityMap = { 20 | '&': '\\&', 21 | '<': '\\<', 22 | '>': '\\>', 23 | '/': '\\/', 24 | '=': '\\=', 25 | '+': '\\+', 26 | '-': '\\-', 27 | '#': '\\#', 28 | '!': '\\!', 29 | '@': '\\@', 30 | '$': '\\$', 31 | '%': '\\%', 32 | '^': '\\^', 33 | '*': '\\*', 34 | '(': '\\(', 35 | ')': '\\)', 36 | }; 37 | 38 | return String(match).replace(/[&<>"'`=+\/]/g, function (s) { 39 | return entityMap[s]; 40 | }); 41 | } 42 | 43 | Strip(str_in: String = '') { 44 | return str_in.replace(/<[^>]*>/g, ''); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/lib/pipes/key-value.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'keys' 5 | }) 6 | export class KeyValuePipe implements PipeTransform { 7 | 8 | transform(value: any, args?: any): any { 9 | let keys = []; 10 | for (let key in value) { 11 | if (value.hasOwnProperty(key)) { 12 | keys.push(key); 13 | } 14 | } 15 | 16 | return keys; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/lib/pipes/pipes.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { HighlightPipe } from './highlight'; 3 | import { KeyValuePipe } from './key-value'; 4 | 5 | @NgModule({ 6 | imports : [], 7 | declarations: [ HighlightPipe, KeyValuePipe ], 8 | exports : [ HighlightPipe, KeyValuePipe ], 9 | }) 10 | export class PipeModule { 11 | 12 | static forRoot(): ModuleWithProviders { 13 | return { 14 | ngModule : PipeModule, 15 | providers: [], 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/lib/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteItem } from '../classes/AutocompleteItem'; 2 | 3 | const UsedCodeList = { 4 | ArrowDown: 40, 5 | ArrowUp: 38, 6 | Enter: 13, 7 | Escape: 27, 8 | Tab: 9, 9 | MetaLeft: 91, 10 | AltLeft: 18, 11 | ControlLeft: 17, 12 | ShiftLeft: 16, 13 | ArrowLeft: 37, 14 | ArrowRight: 39, 15 | MetaRight: 93, 16 | AltRight: 18 17 | }; 18 | 19 | export interface GroupNoResult { 20 | group: {key: string}; 21 | query: string; 22 | } 23 | 24 | /** 25 | * 26 | */ 27 | export function ReturnStringArrayByID(array: { id: string | number, [value: string]: any }[]) { 28 | return array.reduce((result, item) => { 29 | result.push(item.id.toString()); 30 | 31 | return result 32 | }, []) 33 | } 34 | 35 | /** 36 | * 37 | */ 38 | export function FilterRemovals(removals: string[], list: AutocompleteItem[]) { 39 | return list.filter((item) => { 40 | return removals.indexOf(item.id.toString()) <= -1; 41 | }); 42 | } 43 | 44 | /** 45 | * 46 | */ 47 | export function IsMobileOrTablet() { 48 | let isMobile = false; 49 | if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) 50 | || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4))) { 51 | isMobile = true; 52 | } 53 | 54 | return isMobile 55 | } 56 | 57 | export function NotUsedKey(code: string) { 58 | return typeof UsedCodeList[code] === 'undefined'; 59 | } 60 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ng-auto-complete 3 | */ 4 | 5 | export * from './lib/ng-auto-complete.component'; 6 | export * from './lib/completer/completer.component'; 7 | export * from './lib/ng-auto-complete.module'; 8 | export * from './lib/pipes/pipes.module'; 9 | export * from './lib/pipes/highlight'; 10 | export * from './lib/pipes/key-value'; 11 | export * from './lib/classes/AutocompleteItem'; 12 | export * from './lib/classes/AutocompleteGroup'; 13 | export * from './lib/classes/typing'; 14 | export * from './lib/utils/utils'; 15 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js/dist/zone'; 5 | import 'zone.js/dist/zone-testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting() 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "types": [], 15 | "lib": [ 16 | "dom", 17 | "es2015" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true, 26 | "enableResourceInlining": true, 27 | "enableIvy": false 28 | }, 29 | "exclude": [ 30 | "src/test.ts", 31 | "**/*.spec.ts" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/ng-auto-complete/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "ng", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "ng", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {Route1Component} from './routes/route1/route1.component'; 4 | import {Route2Component} from './routes/route2/route2.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | children: [ 10 | { 11 | path: '', 12 | component: Route1Component 13 | }, 14 | { 15 | path: 'route2', 16 | component: Route2Component 17 | } 18 | ] 19 | } 20 | ]; 21 | 22 | @NgModule({ 23 | imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })], 24 | exports: [RouterModule] 25 | }) 26 | export class AppRoutingModule { 27 | } 28 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | ROUTE1-ROUTE2 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | }) 7 | export class AppComponent { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 4 | 5 | import {AppComponent} from './app.component'; 6 | import {NgAutoCompleteModule} from 'ng-auto-complete'; 7 | import {NgViewComponent} from './ng-view/ng-view.component'; 8 | import {NgHolderComponent} from './ng-holder/ng-holder.component'; 9 | import {AppRoutingModule} from './app-routing.module'; 10 | import {Route1Component} from './routes/route1/route1.component'; 11 | import {Route2Component} from './routes/route2/route2.component'; 12 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | AppComponent, 17 | NgViewComponent, 18 | NgHolderComponent, 19 | Route1Component, 20 | Route2Component 21 | ], 22 | imports: [ 23 | BrowserModule, 24 | FormsModule, 25 | HttpClientModule, 26 | NgAutoCompleteModule, 27 | ReactiveFormsModule, 28 | AppRoutingModule 29 | ], 30 | providers: [HttpClient], 31 | bootstrap: [AppComponent] 32 | }) 33 | export class AppModule { 34 | } 35 | -------------------------------------------------------------------------------- /src/app/ng-holder/ng-holder.component.html: -------------------------------------------------------------------------------- 1 | 3 |
4 | -------------------------------------------------------------------------------- /src/app/ng-holder/ng-holder.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, ViewChild} from '@angular/core'; 2 | import {CreateNewAutocompleteGroup, NgAutoCompleteComponent} from 'ng-auto-complete'; 3 | 4 | @Component({ 5 | selector: 'app-ng-holder', 6 | templateUrl: './ng-holder.component.html' 7 | }) 8 | export class NgHolderComponent implements OnInit { 9 | @ViewChild(NgAutoCompleteComponent) public completer: NgAutoCompleteComponent; 10 | 11 | public group = [ 12 | CreateNewAutocompleteGroup( 13 | 'Search / choose in / from list', 14 | 'group', 15 | [ 16 | {title: 'Option 4', id: '1'}, 17 | {title: 'Option 5', id: 2}, 18 | {title: 'Option 6', id: '3'}, 19 | {title: 'Option 7', id: 4}, 20 | {title: 'Option 8', id: '5'}, 21 | {title: 'Option 9', id: 6}, 22 | ], 23 | {titleKey: 'title', childrenKey: null}, 24 | '' 25 | ) 26 | ]; 27 | 28 | constructor() { 29 | } 30 | 31 | /** 32 | * 33 | */ 34 | ngOnInit() { 35 | this.completer.SelectItem('group', 5); 36 | } 37 | 38 | /** 39 | * 40 | * @constructor 41 | */ 42 | Selected() { 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/ng-view/ng-view.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
-------------------------------------------------------------------------------- /src/app/ng-view/ng-view.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-ng-view', 5 | templateUrl: './ng-view.component.html' 6 | }) 7 | export class NgViewComponent implements OnInit { 8 | View: boolean = false; 9 | 10 | 11 | constructor() { 12 | } 13 | 14 | /** 15 | * 16 | */ 17 | ngOnInit() { 18 | } 19 | 20 | /** 21 | * 22 | * @constructor 23 | */ 24 | SetView() { 25 | this.View =! this.View; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/routes/route1/route1.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 | 39 | {{value.title}} 40 | 41 | 42 | 43 | Hey, you searched for: {{value}}. But there are no results! 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/app/routes/route1/route1.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; 2 | import {CreateNewAutocompleteGroup, GroupNoResult, NgAutoCompleteComponent, SelectedAutocompleteItem} from 'ng-auto-complete'; 3 | 4 | 5 | @Component({ 6 | selector: 'app-route1', 7 | templateUrl: './route1.component.html', 8 | }) 9 | export class Route1Component implements OnInit, AfterViewInit { 10 | // @ViewChildren(NgAutoCompleteComponent) public completers: QueryList; 11 | @ViewChild(NgAutoCompleteComponent, { static: true }) public completer: NgAutoCompleteComponent; 12 | 13 | @ViewChild('placeholderValue') placeholderValue: TemplateRef; 14 | @ViewChild('dropdownValue') dropdownValue: TemplateRef; 15 | @ViewChild('noResults') noResults: TemplateRef; 16 | 17 | _removables = []; 18 | 19 | // public group1 = [ 20 | // CreateNewAutocompleteGroup( 21 | // 'Search / choose in / from list', 22 | // 'parent', 23 | // [ 24 | // { 25 | // title: 'Option 1', id: '1', children: [ 26 | // {title: 'Option 4', id: '1'}, 27 | // {title: 'Option 5', id: '2'}, 28 | // {title: 'Option 6', id: '3'}, 29 | // ] 30 | // }, 31 | // { 32 | // title: 'Option 2', id: '2', children: [ 33 | // {title: 'Option 7', id: '1'}, 34 | // {title: 'Option 8', id: '2'}, 35 | // {title: 'Option 9', id: '3'}, 36 | // ] 37 | // }, 38 | // { 39 | // title: 'Option 3', id: '3', children: [ 40 | // {title: 'Option 10', id: '1'}, 41 | // {title: 'Option 11', id: '2'}, 42 | // {title: 'Option 12', id: '3'}, 43 | // ] 44 | // }, 45 | // ], 46 | // {titleKey: 'title', childrenKey: 'children'} 47 | // ), 48 | // CreateNewAutocompleteGroup( 49 | // 'Search / choose in / from list', 50 | // 'child', 51 | // [ 52 | // {title: 'Option 4', id: '1'}, 53 | // {title: 'Option 5', id: '2'}, 54 | // {title: 'Option 6', id: '3'}, 55 | // {title: 'Option 7', id: '4'}, 56 | // {title: 'Option 8', id: '5'}, 57 | // {title: 'Option 9', id: '6'}, 58 | // ], 59 | // {titleKey: 'title', childrenKey: null}, 60 | // 'parent', 61 | // ), 62 | // ]; 63 | 64 | // public group2 = [ 65 | // CreateNewAutocompleteGroup( 66 | // 'Search / choose in / from list', 67 | // 'normal', 68 | // [ 69 | // {title: 'Option 4', id: '1'}, 70 | // {title: 'Option 5', id: '2'}, 71 | // {title: 'Option 6', id: '3'}, 72 | // {title: 'Option 7', id: '4'}, 73 | // {title: 'Option 8', id: '5'}, 74 | // {title: 'Option 9', id: '6'}, 75 | // ], 76 | // {titleKey: 'title', childrenKey: null}, 77 | // '' 78 | // ), 79 | // CreateNewAutocompleteGroup( 80 | // 'Search / choose in / from list', 81 | // 'disabled.', 82 | // [ 83 | // {title: 'Option 4', id: '1'}, 84 | // {title: 'Option 5', id: '2'}, 85 | // {title: 'Option 6', id: '3'}, 86 | // {title: 'Option 7', id: '4'}, 87 | // {title: 'Option 8', id: '5'}, 88 | // {title: 'Option 9', id: '6'}, 89 | // ], 90 | // {titleKey: 'title', childrenKey: null}, 91 | // '', 92 | // false 93 | // ), 94 | // ]; 95 | 96 | // public group3 = [ 97 | // CreateNewAutocompleteGroup( 98 | // 'Search / choose in / from list', 99 | // 'late', 100 | // [], 101 | // {titleKey: 'title', childrenKey: null}, 102 | // '' 103 | // ) 104 | // ]; 105 | 106 | public groupStress1 = [ 107 | CreateNewAutocompleteGroup( 108 | 'Search / choose in / from list', 109 | 'items1', 110 | [], 111 | {titleKey: 'title', childrenKey: null}, 112 | '', 113 | true, 114 | ) 115 | ]; 116 | 117 | // public group4 = [ 118 | // CreateNewAutocompleteGroup( 119 | // 'Search / choose in / from list', 120 | // 'remove', 121 | // [ 122 | // {title: 'Option 4', id: '1'}, 123 | // {title: 'Option 5', id: 2}, 124 | // {title: 'Option 6', id: '3'}, 125 | // {title: 'Option 7', id: 4}, 126 | // {title: 'Option 8', id: '5'}, 127 | // {title: 'Option 9', id: 6}, 128 | // ], 129 | // {titleKey: 'title', childrenKey: null}, 130 | // '' 131 | // ) 132 | // ]; 133 | 134 | /** 135 | * 136 | * @returns {Array} 137 | * @constructor 138 | */ 139 | FillArray() { 140 | let arr = []; 141 | for (let i = 0; i < 50; i++) { 142 | arr.push({title: `Option ${i} Lorem ipsum dolor sit amet, consectetur adipiscing elit.`, id: i}); 143 | } 144 | 145 | return arr; 146 | } 147 | 148 | /** 149 | * 150 | * @param {GroupNoResult} group 151 | * @constructor 152 | */ 153 | NoResult(group: GroupNoResult) { 154 | console.log('NO RESULT', group); 155 | } 156 | 157 | constructor() { 158 | 159 | } 160 | 161 | ngOnInit() { 162 | const async = (str: string) => { 163 | return >new Promise((resolve, reject) => { 164 | setTimeout(() => { 165 | resolve([ 166 | { 167 | id: 0, 168 | title: `Test case 1 ${str}` 169 | }, 170 | { 171 | id: 1, 172 | title: `Test case 2 ${str}` 173 | }, 174 | { 175 | id: 2, 176 | title: `Test case 3 ${str}` 177 | } 178 | ]); 179 | }, 2000); 180 | }); 181 | }; 182 | 183 | this.completer.SetAsync('items1', async); 184 | 185 | this.completer.SelectItem('items1', '5'); 186 | 187 | this.completer.SetTemplate('items1', 'dropdownValue', this.dropdownValue); 188 | this.completer.SetTemplate('items1', 'noResults', this.noResults); 189 | this.completer.SetTemplate('items1', 'placeholderValue', this.placeholderValue); 190 | } 191 | 192 | /** 193 | * 194 | */ 195 | ngAfterViewInit() { 196 | console.log(); 197 | } 198 | 199 | /** 200 | * 201 | * @param item 202 | * @constructor 203 | */ 204 | Selected(item: SelectedAutocompleteItem) { 205 | console.log('its actually saying its selected', item); 206 | } 207 | 208 | /** 209 | * 210 | * @param item 211 | * @constructor 212 | */ 213 | RemoveSelected(item: SelectedAutocompleteItem) { 214 | if (item.item !== null) { 215 | this._removables.push(item.item); 216 | } 217 | 218 | this.completer.RemovableValues('items1', this._removables); 219 | } 220 | 221 | /** 222 | * 223 | * @constructor 224 | */ 225 | SetValues() { 226 | // const component = NgAutocompleteComponent.FindCompleter('group3', this.completers); 227 | 228 | this.completer.SetValues( 229 | 'late', 230 | [ 231 | {title: 'Option 4', id: '1'}, 232 | {title: 'Option 5', id: '2'}, 233 | {title: 'Option 6', id: '3'}, 234 | {title: 'Option 7', id: '4'}, 235 | {title: 'Option 8', id: '5'}, 236 | {title: 'Option 9', id: '6'}, 237 | ] 238 | ); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/app/routes/route2/route2.component.html: -------------------------------------------------------------------------------- 1 |

2 | route2 works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/routes/route2/route2.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-route2', 5 | templateUrl: './route2.component.html', 6 | }) 7 | export class Route2Component implements OnInit { 8 | 9 | constructor() { 10 | } 11 | 12 | ngOnInit() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sengirab/ngAutocomplete/9c26bc48196783bd0395ffe09070b32c13b29499/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /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 --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sengirab/ngAutocomplete/9c26bc48196783bd0395ffe09070b32c13b29499/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgAutoCompleteApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/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 | }; -------------------------------------------------------------------------------- /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 | 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in 9 | * IE on Windows Phone and in iOS. 10 | */ 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | html { 17 | line-height: 1.15; /* 1 */ 18 | -ms-text-size-adjust: 100%; /* 2 */ 19 | -webkit-text-size-adjust: 100%; /* 2 */ 20 | } 21 | 22 | /* Sections 23 | ========================================================================== */ 24 | 25 | /** 26 | * Remove the margin in all browsers (opinionated). 27 | */ 28 | 29 | body { 30 | margin: 0; 31 | } 32 | 33 | /** 34 | * Add the correct display in IE 9-. 35 | */ 36 | 37 | article, 38 | aside, 39 | footer, 40 | header, 41 | nav, 42 | section { 43 | display: block; 44 | } 45 | 46 | /** 47 | * Correct the font size and margin on `h1` elements within `section` and 48 | * `article` contexts in Chrome, Firefox, and Safari. 49 | */ 50 | 51 | h1 { 52 | font-size: 2em; 53 | margin: 0.67em 0; 54 | } 55 | 56 | /* Grouping content 57 | ========================================================================== */ 58 | 59 | /** 60 | * Add the correct display in IE 9-. 61 | * 1. Add the correct display in IE. 62 | */ 63 | 64 | figcaption, 65 | figure, 66 | main { /* 1 */ 67 | display: block; 68 | } 69 | 70 | /** 71 | * Add the correct margin in IE 8. 72 | */ 73 | 74 | figure { 75 | margin: 1em 40px; 76 | } 77 | 78 | /** 79 | * 1. Add the correct box sizing in Firefox. 80 | * 2. Show the overflow in Edge and IE. 81 | */ 82 | 83 | hr { 84 | box-sizing: content-box; /* 1 */ 85 | height: 0; /* 1 */ 86 | overflow: visible; /* 2 */ 87 | } 88 | 89 | /** 90 | * 1. Correct the inheritance and scaling of font size in all browsers. 91 | * 2. Correct the odd `em` font sizing in all browsers. 92 | */ 93 | 94 | pre { 95 | font-family: monospace, monospace; /* 1 */ 96 | font-size: 1em; /* 2 */ 97 | } 98 | 99 | /* Text-level semantics 100 | ========================================================================== */ 101 | 102 | /** 103 | * 1. Remove the gray background on active links in IE 10. 104 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 105 | */ 106 | 107 | a { 108 | background-color: transparent; /* 1 */ 109 | -webkit-text-decoration-skip: objects; /* 2 */ 110 | } 111 | 112 | /** 113 | * 1. Remove the bottom border in Chrome 57- and Firefox 39-. 114 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 115 | */ 116 | 117 | abbr[title] { 118 | border-bottom: none; /* 1 */ 119 | text-decoration: underline; /* 2 */ 120 | text-decoration: underline dotted; /* 2 */ 121 | } 122 | 123 | /** 124 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 125 | */ 126 | 127 | b, 128 | strong { 129 | font-weight: inherit; 130 | } 131 | 132 | /** 133 | * Add the correct font weight in Chrome, Edge, and Safari. 134 | */ 135 | 136 | b, 137 | strong { 138 | font-weight: bolder; 139 | } 140 | 141 | /** 142 | * 1. Correct the inheritance and scaling of font size in all browsers. 143 | * 2. Correct the odd `em` font sizing in all browsers. 144 | */ 145 | 146 | code, 147 | kbd, 148 | samp { 149 | font-family: monospace, monospace; /* 1 */ 150 | font-size: 1em; /* 2 */ 151 | } 152 | 153 | /** 154 | * Add the correct font style in Android 4.3-. 155 | */ 156 | 157 | dfn { 158 | font-style: italic; 159 | } 160 | 161 | /** 162 | * Add the correct background and color in IE 9-. 163 | */ 164 | 165 | mark { 166 | background-color: #ff0; 167 | color: #000; 168 | } 169 | 170 | /** 171 | * Add the correct font size in all browsers. 172 | */ 173 | 174 | small { 175 | font-size: 80%; 176 | } 177 | 178 | /** 179 | * Prevent `sub` and `sup` elements from affecting the line height in 180 | * all browsers. 181 | */ 182 | 183 | sub, 184 | sup { 185 | font-size: 75%; 186 | line-height: 0; 187 | position: relative; 188 | vertical-align: baseline; 189 | } 190 | 191 | sub { 192 | bottom: -0.25em; 193 | } 194 | 195 | sup { 196 | top: -0.5em; 197 | } 198 | 199 | /* Embedded content 200 | ========================================================================== */ 201 | 202 | /** 203 | * Add the correct display in IE 9-. 204 | */ 205 | 206 | audio, 207 | video { 208 | display: inline-block; 209 | } 210 | 211 | /** 212 | * Add the correct display in iOS 4-7. 213 | */ 214 | 215 | audio:not([controls]) { 216 | display: none; 217 | height: 0; 218 | } 219 | 220 | /** 221 | * Remove the border on images inside links in IE 10-. 222 | */ 223 | 224 | img { 225 | border-style: none; 226 | } 227 | 228 | /** 229 | * Hide the overflow in IE. 230 | */ 231 | 232 | svg:not(:root) { 233 | overflow: hidden; 234 | } 235 | 236 | /* Forms 237 | ========================================================================== */ 238 | 239 | /** 240 | * 1. Change the font styles in all browsers (opinionated). 241 | * 2. Remove the margin in Firefox and Safari. 242 | */ 243 | 244 | button, 245 | input, 246 | optgroup, 247 | select, 248 | textarea { 249 | font-family: sans-serif; /* 1 */ 250 | font-size: 100%; /* 1 */ 251 | line-height: 1.15; /* 1 */ 252 | margin: 0; /* 2 */ 253 | } 254 | 255 | /** 256 | * Show the overflow in IE. 257 | * 1. Show the overflow in Edge. 258 | */ 259 | 260 | button, 261 | input { /* 1 */ 262 | overflow: visible; 263 | } 264 | 265 | /** 266 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 267 | * 1. Remove the inheritance of text transform in Firefox. 268 | */ 269 | 270 | button, 271 | select { /* 1 */ 272 | text-transform: none; 273 | } 274 | 275 | /** 276 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 277 | * controls in Android 4. 278 | * 2. Correct the inability to style clickable types in iOS and Safari. 279 | */ 280 | 281 | button, 282 | html [type="button"], /* 1 */ 283 | [type="reset"], 284 | [type="submit"] { 285 | -webkit-appearance: button; /* 2 */ 286 | } 287 | 288 | /** 289 | * Remove the inner border and padding in Firefox. 290 | */ 291 | 292 | button::-moz-focus-inner, 293 | [type="button"]::-moz-focus-inner, 294 | [type="reset"]::-moz-focus-inner, 295 | [type="submit"]::-moz-focus-inner { 296 | border-style: none; 297 | padding: 0; 298 | } 299 | 300 | /** 301 | * Restore the focus styles unset by the previous rule. 302 | */ 303 | 304 | button:-moz-focusring, 305 | [type="button"]:-moz-focusring, 306 | [type="reset"]:-moz-focusring, 307 | [type="submit"]:-moz-focusring { 308 | outline: 1px dotted ButtonText; 309 | } 310 | 311 | /** 312 | * Correct the padding in Firefox. 313 | */ 314 | 315 | fieldset { 316 | padding: 0.35em 0.75em 0.625em; 317 | } 318 | 319 | /** 320 | * 1. Correct the text wrapping in Edge and IE. 321 | * 2. Correct the color inheritance from `fieldset` elements in IE. 322 | * 3. Remove the padding so developers are not caught out when they zero out 323 | * `fieldset` elements in all browsers. 324 | */ 325 | 326 | legend { 327 | box-sizing: border-box; /* 1 */ 328 | color: inherit; /* 2 */ 329 | display: table; /* 1 */ 330 | max-width: 100%; /* 1 */ 331 | padding: 0; /* 3 */ 332 | white-space: normal; /* 1 */ 333 | } 334 | 335 | /** 336 | * 1. Add the correct display in IE 9-. 337 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 338 | */ 339 | 340 | progress { 341 | display: inline-block; /* 1 */ 342 | vertical-align: baseline; /* 2 */ 343 | } 344 | 345 | /** 346 | * Remove the default vertical scrollbar in IE. 347 | */ 348 | 349 | textarea { 350 | overflow: auto; 351 | } 352 | 353 | /** 354 | * 1. Add the correct box sizing in IE 10-. 355 | * 2. Remove the padding in IE 10-. 356 | */ 357 | 358 | [type="checkbox"], 359 | [type="radio"] { 360 | box-sizing: border-box; /* 1 */ 361 | padding: 0; /* 2 */ 362 | } 363 | 364 | /** 365 | * Correct the cursor style of increment and decrement buttons in Chrome. 366 | */ 367 | 368 | [type="number"]::-webkit-inner-spin-button, 369 | [type="number"]::-webkit-outer-spin-button { 370 | height: auto; 371 | } 372 | 373 | /** 374 | * 1. Correct the odd appearance in Chrome and Safari. 375 | * 2. Correct the outline style in Safari. 376 | */ 377 | 378 | [type="search"] { 379 | -webkit-appearance: textfield; /* 1 */ 380 | outline-offset: -2px; /* 2 */ 381 | } 382 | 383 | /** 384 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 385 | */ 386 | 387 | [type="search"]::-webkit-search-cancel-button, 388 | [type="search"]::-webkit-search-decoration { 389 | -webkit-appearance: none; 390 | } 391 | 392 | /** 393 | * 1. Correct the inability to style clickable types in iOS and Safari. 394 | * 2. Change font properties to `inherit` in Safari. 395 | */ 396 | 397 | ::-webkit-file-upload-button { 398 | -webkit-appearance: button; /* 1 */ 399 | font: inherit; /* 2 */ 400 | } 401 | 402 | /* Interactive 403 | ========================================================================== */ 404 | 405 | /* 406 | * Add the correct display in IE 9-. 407 | * 1. Add the correct display in Edge, IE, and Firefox. 408 | */ 409 | 410 | details, /* 1 */ 411 | menu { 412 | display: block; 413 | } 414 | 415 | /* 416 | * Add the correct display in all browsers. 417 | */ 418 | 419 | summary { 420 | display: list-item; 421 | } 422 | 423 | /* Scripting 424 | ========================================================================== */ 425 | 426 | /** 427 | * Add the correct display in IE 9-. 428 | */ 429 | 430 | canvas { 431 | display: inline-block; 432 | } 433 | 434 | /** 435 | * Add the correct display in IE. 436 | */ 437 | 438 | template { 439 | display: none; 440 | } 441 | 442 | /* Hidden 443 | ========================================================================== */ 444 | 445 | /** 446 | * Add the correct display in IE 10-. 447 | */ 448 | 449 | body { 450 | height: 5000px; 451 | } 452 | 453 | [hidden] { 454 | display: none; 455 | } 456 | 457 | .fifty { 458 | width: 50%; 459 | float: left; 460 | } 461 | 462 | .ng-autocomplete-dropdown { 463 | position: relative; 464 | margin-top: 25px; 465 | height: 200px; 466 | } 467 | .ng-autocomplete-dropdown .ng-autocomplete-inputs { 468 | position: relative; 469 | } 470 | .ng-autocomplete-dropdown .ng-autocomplete-inputs input { 471 | width: 100%; 472 | padding: 6px 20px; 473 | font-family: Arial; 474 | font-weight: normal; 475 | outline: none !important; 476 | font-size: 15px; 477 | height: 56px; 478 | border: 1px solid #e0e0e0; 479 | } 480 | 481 | .ng-autocomplete-dropdown .ng-autocomplete-placeholder { 482 | position: absolute; 483 | margin: 3px; 484 | background-color: #fff; 485 | padding: 17px 18px; 486 | font-family: Arial; 487 | font-weight: normal; 488 | font-size: 15px; 489 | width: calc(100% - 4px); 490 | } 491 | .ng-autocomplete-dropdown .ng-dropdown { 492 | display: none; 493 | border: 1px solid #e0e0e0; 494 | z-index: 99999; 495 | max-height: 280px; 496 | overflow-x: hidden; 497 | position: absolute; 498 | width: 100%; 499 | } 500 | .ng-autocomplete-dropdown .ng-dropdown.open { 501 | display: block; 502 | } 503 | .ng-autocomplete-dropdown .ng-dropdown .dropdown-item { 504 | width: 100%; 505 | cursor: pointer; 506 | padding: 18px 20px; 507 | font-family: Arial; 508 | font-weight: normal; 509 | font-size: 15px; 510 | height: 56px; 511 | background-color: #ffffff; 512 | } 513 | .ng-autocomplete-dropdown .ng-dropdown .dropdown-item:nth-child(odd) { 514 | background-color: #efefef; 515 | } 516 | .ng-autocomplete-dropdown .ng-dropdown .dropdown-item.active { 517 | background-color: #0099cc; 518 | color: #fff !important; 519 | } 520 | 521 | .ng-autocomplete-dropdown .ng-dropdown .dropdown-item .dropdown-item-highlight { 522 | font-weight: bold; 523 | } 524 | 525 | .ng-autocomplete-dropdown-icon { 526 | display: block; 527 | width: 56px; 528 | text-align: center; 529 | position: absolute; 530 | top: 0; 531 | bottom: 0; 532 | right: 0; 533 | border-left: 1px solid #e0e0e0; 534 | cursor: pointer; 535 | z-index: 999; 536 | font-size: 12px; 537 | color: #758694; 538 | padding: 21px 0; 539 | 540 | } 541 | 542 | .ng-autocomplete-dropdown-icon:after { 543 | content: ''; 544 | display: block; 545 | width: 0; 546 | height: 0; 547 | border-left: 7px solid transparent; 548 | border-right: 7px solid transparent; 549 | border-top: 7px solid #000; 550 | position: absolute; 551 | right: 21px; 552 | z-index: 999; 553 | top: 24px; 554 | } 555 | 556 | .ng-autocomplete-dropdown-icon.open:after { 557 | transform: rotate(180deg); 558 | } 559 | -------------------------------------------------------------------------------- /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/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "importHelpers": true, 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "module": "es2015", 10 | "moduleResolution": "node", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2017", 19 | "dom" 20 | ], 21 | "paths": { 22 | "ng-auto-complete": [ 23 | "dist/ng-auto-complete" 24 | ], 25 | "ng-auto-complete/*": [ 26 | "dist/ng-auto-complete/*" 27 | ] 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": false, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "use-input-property-decorator": true, 122 | "use-output-property-decorator": true, 123 | "use-host-property-decorator": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-life-cycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "component-class-suffix": true, 129 | "directive-class-suffix": true 130 | } 131 | } 132 | --------------------------------------------------------------------------------