├── src ├── index.ts ├── components │ └── web-complete │ │ ├── web-complete.css │ │ ├── web-complete.e2e.ts │ │ ├── web-complete.tsx │ │ └── readme.md ├── index.html └── components.d.ts ├── .npmignore ├── .travis.yml ├── .editorconfig ├── .prettierrc.json ├── .gitignore ├── stencil.config.ts ├── tsconfig.json ├── LICENSE ├── package.json ├── docs ├── index.html └── countries.json └── README.md /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src/ 3 | docs/ 4 | .stencil/ 5 | www/ 6 | .gitignore 7 | tsconfig.json 8 | .editorconfig 9 | stencil.config.ts -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | 4 | language: node_js 5 | node_js: 6 | - 12 7 | 8 | before_script: 9 | - npm i 10 | - npm run build 11 | 12 | script: 13 | - npm test -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "jsxBracketSameLine": false, 5 | "jsxSingleQuote": false, 6 | "quoteProps": "consistent", 7 | "printWidth": 180, 8 | "semi": true, 9 | "singleQuote": true, 10 | "tabWidth": 2, 11 | "trailingComma": "all", 12 | "useTabs": false 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | www/ 3 | loader/ 4 | 5 | *~ 6 | *.sw[mnpcod] 7 | *.log 8 | *.lock 9 | *.tmp 10 | *.tmp.* 11 | log.txt 12 | *.sublime-project 13 | *.sublime-workspace 14 | 15 | .stencil/ 16 | .idea/ 17 | .vscode/ 18 | .sass-cache/ 19 | .versions/ 20 | node_modules/ 21 | $RECYCLE.BIN/ 22 | 23 | .DS_Store 24 | Thumbs.db 25 | UserInterfaceState.xcuserstate 26 | .env 27 | -------------------------------------------------------------------------------- /stencil.config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@stencil/core'; 2 | 3 | export const config: Config = { 4 | namespace: 'web-complete', 5 | buildEs5: 'prod', 6 | outputTargets: [ 7 | { 8 | type: 'dist', 9 | esmLoaderPath: '../loader' 10 | }, 11 | { 12 | type: 'dist-custom-elements-bundle', 13 | }, 14 | { 15 | type: 'docs-readme' 16 | }, 17 | { 18 | type: 'www', 19 | serviceWorker: null // disable service workers 20 | } 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/web-complete/web-complete.css: -------------------------------------------------------------------------------- 1 | :host { 2 | --autocomplete-suggestion-focus-background: gray; 3 | } 4 | 5 | :host > div { 6 | position:relative; 7 | display:inline-block; 8 | } 9 | .suggestions { 10 | position:absolute; 11 | left:0; 12 | right:0; 13 | } 14 | .suggestion { 15 | display:block; 16 | width:100%; 17 | border:none; 18 | text-align:left; 19 | } 20 | .suggestion:hover, .suggestion.active { 21 | background:var(--autocomplete-suggestion-focus-background); 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "allowUnreachableCode": false, 5 | "declaration": false, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "dom", 9 | "es2017" 10 | ], 11 | "moduleResolution": "node", 12 | "module": "esnext", 13 | "target": "es2017", 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "jsx": "react", 17 | "jsxFactory": "h" 18 | }, 19 | "include": [ 20 | "src", 21 | "types/jsx.d.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, Stefan Huber 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-complete", 3 | "version": "1.3.2", 4 | "description": "A lightweight, dependency-free, styleable autocomplete web component", 5 | "keywords": [ 6 | "autocomplete", 7 | "web-component" 8 | ], 9 | "author": { 10 | "name": "Stefan Huber", 11 | "email": "mail@stefanhuber.at", 12 | "url": "https://www.stefanhuber.at" 13 | }, 14 | "main": "dist/index.cjs.js", 15 | "module": "dist/custom-elements/index.js", 16 | "es2015": "dist/esm/index.mjs", 17 | "es2017": "dist/esm/index.mjs", 18 | "types": "dist/types/index.d.ts", 19 | "collection": "dist/collection/collection-manifest.json", 20 | "collection:main": "dist/collection/index.js", 21 | "unpkg": "dist/web-complete/web-complete.esm.js", 22 | "files": [ 23 | "dist/", 24 | "loader/" 25 | ], 26 | "homepage": "https://github.com/stefanhuber/web-complete", 27 | "bugs": { 28 | "url": "https://github.com/stefanhuber/web-complete/issues" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/stefanhuber/web-complete.git" 33 | }, 34 | "scripts": { 35 | "build": "stencil build --docs", 36 | "start": "stencil build --dev --watch --serve", 37 | "test": "stencil test --spec --e2e", 38 | "test.watch": "stencil test --spec --e2e --watchAll", 39 | "generate": "stencil generate" 40 | }, 41 | "devDependencies": { 42 | "@stencil/core": "^2.3.0", 43 | "@types/jest": "^26.0.20", 44 | "@types/puppeteer": "^5.4.2", 45 | "jest": "^26.6.3", 46 | "jest-cli": "^26.6.3", 47 | "puppeteer": "^5.4.1" 48 | }, 49 | "license": "BSD-2-Clause" 50 | } 51 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Web Complete 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 | 34 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/components/web-complete/web-complete.e2e.ts: -------------------------------------------------------------------------------- 1 | import { newE2EPage } from '@stencil/core/testing'; 2 | 3 | const addDummySuggestionGenerator = (element: any) => { 4 | element.suggestionGenerator = (text) => { 5 | return ["abcdef", "xyz", "test abc", "test xyz"] 6 | .filter(suggestion => suggestion.indexOf(text) >= 0) 7 | .map((suggestion, index) => { 8 | return { 9 | text: suggestion, 10 | value: 1000 + index 11 | } 12 | }); 13 | }; 14 | }; 15 | 16 | describe('web-complete', () => { 17 | 18 | it('should set text and value on inner input field', async () => { 19 | const page = await newE2EPage(); 20 | await page.setContent(``); 21 | 22 | const complete = await page.find('web-complete'); 23 | 24 | const value = await complete.callMethod('getValue'); 25 | const text = await complete.callMethod('getText'); 26 | 27 | expect(value).toEqual('123'); 28 | expect(text).toEqual('some demo text'); 29 | }); 30 | 31 | it('should clear web-complete field', async () => { 32 | const page = await newE2EPage(); 33 | await page.setContent(``); 34 | 35 | const complete = await page.find('web-complete'); 36 | const unselected = await complete.spyOnEvent('unselected'); 37 | 38 | await complete.callMethod('clear'); 39 | 40 | const value = await complete.callMethod('getValue'); 41 | const text = await complete.callMethod('getText'); 42 | 43 | expect(value).toEqual(''); 44 | expect(text).toEqual(''); 45 | 46 | expect(unselected).toHaveReceivedEventDetail({ 47 | text: 'some demo text', 48 | value: '123' 49 | }); 50 | }); 51 | 52 | it('should generate suggestions on typing', async () => { 53 | const page = await newE2EPage(); 54 | await page.setContent(``); 55 | 56 | await page.$eval('web-complete', addDummySuggestionGenerator); 57 | await page.waitForChanges(); 58 | 59 | const input = await page.find('web-complete > div > input'); 60 | input.press('a'); 61 | input.press('b'); 62 | await page.waitForChanges(); 63 | 64 | const suggestions = await page.findAll('web-complete > div > div > button'); 65 | expect(suggestions.length).toEqual(2); 66 | expect(suggestions[0].textContent).toBe("abcdef"); 67 | expect(suggestions[1].textContent).toBe("test abc"); 68 | }); 69 | 70 | it('should select suggestion on click', async () => { 71 | const page = await newE2EPage(); 72 | await page.setContent(``); 73 | 74 | await page.$eval('web-complete', addDummySuggestionGenerator); 75 | await page.waitForChanges(); 76 | 77 | const input = await page.find('web-complete > div > input'); 78 | input.press('a'); 79 | input.press('b'); 80 | await page.waitForChanges(); 81 | 82 | const suggestions = await page.findAll('web-complete > div > div > button'); 83 | suggestions[1].click(); 84 | await page.waitForChanges(); 85 | 86 | const complete = await page.find('web-complete'); 87 | const value = await complete.callMethod('getValue'); 88 | const text = await complete.callMethod('getText'); 89 | expect(value).toEqual("1001"); 90 | expect(text).toEqual("test abc"); 91 | }); 92 | 93 | it('should keep text after blur when clearOnUnselectedClosing = false', async () => { 94 | const page = await newE2EPage(); 95 | await page.setContent(``); 96 | 97 | await page.$eval('web-complete', addDummySuggestionGenerator); 98 | await page.waitForChanges(); 99 | 100 | const input = await page.find('web-complete > div > input'); 101 | input.press('v'); 102 | input.press('w'); 103 | await page.waitForChanges(); 104 | 105 | const suggestions = await page.findAll('web-complete > div > div > button'); 106 | expect(suggestions.length).toEqual(0); 107 | 108 | page.$eval('web-complete > div > input', element => element['blur']()); 109 | await page.waitForTimeout(250); 110 | 111 | const complete = await page.find('web-complete'); 112 | const value = await complete.callMethod('getValue'); 113 | const text = await complete.callMethod('getText'); 114 | expect(value).toEqual(""); 115 | expect(text).toEqual("vw"); 116 | }); 117 | 118 | it('should remove text after blur when clearOnUnselectedClosing = true', async () => { 119 | const page = await newE2EPage(); 120 | await page.setContent(``); 121 | 122 | await page.$eval('web-complete', addDummySuggestionGenerator); 123 | await page.waitForChanges(); 124 | 125 | const input = await page.find('web-complete > div > input'); 126 | input.press('v'); 127 | input.press('w'); 128 | await page.waitForChanges(); 129 | 130 | const suggestions = await page.findAll('web-complete > div > div > button'); 131 | expect(suggestions.length).toEqual(0); 132 | 133 | page.$eval('web-complete > div > input', element => element['blur']()); 134 | await page.waitForTimeout(250); 135 | 136 | const complete = await page.find('web-complete'); 137 | const value = await complete.callMethod('getValue'); 138 | const text = await complete.callMethod('getText'); 139 | expect(value).toEqual(""); 140 | expect(text).toEqual(""); 141 | }); 142 | 143 | }); 144 | 145 | -------------------------------------------------------------------------------- /src/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** 4 | * This is an autogenerated file created by the Stencil compiler. 5 | * It contains typing information for all components that exist in this project. 6 | */ 7 | import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; 8 | export namespace Components { 9 | interface WebComplete { 10 | /** 11 | * Clears the form field (suggestions and selection) 12 | */ 13 | "clear": () => Promise; 14 | /** 15 | * If no value is selected, clear the input and emit unselected, if false, the value will not be cleared (usefull for suggesting values on a free text search) 16 | */ 17 | "clearOnUnselectedClosing": boolean; 18 | /** 19 | * The class names, which should be set on the rendered html elements 20 | */ 21 | "cssClasses": { wrapper: string; input: string; suggestions: string; suggestion: string; active: string; }; 22 | /** 23 | * Enable/Disable the input field 24 | */ 25 | "disabled": boolean; 26 | /** 27 | * Timing to suggest on empty (-1 to disable) 28 | */ 29 | "emptySuggestionTime": number; 30 | /** 31 | * Returns the `text` of the selected item 32 | */ 33 | "getText": () => Promise; 34 | /** 35 | * Returns the `value` of the selected item 36 | */ 37 | "getValue": () => Promise; 38 | /** 39 | * id of the input field 40 | */ 41 | "inputId": string; 42 | /** 43 | * A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. 44 | */ 45 | "inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; 46 | /** 47 | * The maximally shown suggestions in the list 48 | */ 49 | "maxSuggestions": number; 50 | /** 51 | * The minimum input size for generating suggestions 52 | */ 53 | "minInput": number; 54 | /** 55 | * The placeholder for the input field 56 | */ 57 | "placeholder": string; 58 | /** 59 | * Form validation: is the form input required 60 | */ 61 | "required": boolean; 62 | /** 63 | * Async suggestion generator: `text` is the displayed for users in the form after selection (if no `suggesion` also as suggesion) `value` is the actual value of the form field optional `suggesion` if the autocomplete suggestion item should be formatted differently than `text` 64 | */ 65 | "suggestionGenerator": (text: string) => Promise>; 66 | /** 67 | * The text is displayed by the form field for users 68 | */ 69 | "text": string; 70 | /** 71 | * The actual value of the form field 72 | */ 73 | "value": string; 74 | } 75 | } 76 | declare global { 77 | interface HTMLWebCompleteElement extends Components.WebComplete, HTMLStencilElement { 78 | } 79 | var HTMLWebCompleteElement: { 80 | prototype: HTMLWebCompleteElement; 81 | new (): HTMLWebCompleteElement; 82 | }; 83 | interface HTMLElementTagNameMap { 84 | "web-complete": HTMLWebCompleteElement; 85 | } 86 | } 87 | declare namespace LocalJSX { 88 | interface WebComplete { 89 | /** 90 | * If no value is selected, clear the input and emit unselected, if false, the value will not be cleared (usefull for suggesting values on a free text search) 91 | */ 92 | "clearOnUnselectedClosing"?: boolean; 93 | /** 94 | * The class names, which should be set on the rendered html elements 95 | */ 96 | "cssClasses"?: { wrapper: string; input: string; suggestions: string; suggestion: string; active: string; }; 97 | /** 98 | * Enable/Disable the input field 99 | */ 100 | "disabled"?: boolean; 101 | /** 102 | * Timing to suggest on empty (-1 to disable) 103 | */ 104 | "emptySuggestionTime"?: number; 105 | /** 106 | * id of the input field 107 | */ 108 | "inputId"?: string; 109 | /** 110 | * A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. 111 | */ 112 | "inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; 113 | /** 114 | * The maximally shown suggestions in the list 115 | */ 116 | "maxSuggestions"?: number; 117 | /** 118 | * The minimum input size for generating suggestions 119 | */ 120 | "minInput"?: number; 121 | /** 122 | * Emitted when an item from suggestions was selected 123 | */ 124 | "onSelected"?: (event: CustomEvent) => void; 125 | /** 126 | * Emitted when item was cleared/unselected 127 | */ 128 | "onUnselected"?: (event: CustomEvent) => void; 129 | /** 130 | * The placeholder for the input field 131 | */ 132 | "placeholder"?: string; 133 | /** 134 | * Form validation: is the form input required 135 | */ 136 | "required"?: boolean; 137 | /** 138 | * Async suggestion generator: `text` is the displayed for users in the form after selection (if no `suggesion` also as suggesion) `value` is the actual value of the form field optional `suggesion` if the autocomplete suggestion item should be formatted differently than `text` 139 | */ 140 | "suggestionGenerator"?: (text: string) => Promise>; 141 | /** 142 | * The text is displayed by the form field for users 143 | */ 144 | "text"?: string; 145 | /** 146 | * The actual value of the form field 147 | */ 148 | "value"?: string; 149 | } 150 | interface IntrinsicElements { 151 | "web-complete": WebComplete; 152 | } 153 | } 154 | export { LocalJSX as JSX }; 155 | declare module "@stencil/core" { 156 | export namespace JSX { 157 | interface IntrinsicElements { 158 | "web-complete": LocalJSX.WebComplete & JSXBase.HTMLAttributes; 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/components/web-complete/web-complete.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Prop, h, State, Event, EventEmitter, Method } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'web-complete', 5 | shadow: false 6 | }) 7 | export class Autocomplete implements ComponentInterface { 8 | 9 | @State() activeIndex = -1; // focused suggestion 10 | @State() data: Array<{ text: string, value: string, suggestion?: string }> = []; 11 | @State() active: boolean = false; // has focus 12 | 13 | /** 14 | * The text is displayed by the form field for users 15 | */ 16 | @Prop({ mutable: true }) text = ""; 17 | 18 | /** 19 | * The actual value of the form field 20 | */ 21 | @Prop({ mutable: true }) value = ""; 22 | 23 | /** 24 | * The placeholder for the input field 25 | */ 26 | @Prop() placeholder = ""; 27 | 28 | /** 29 | * If no value is selected, clear the input and emit unselected, if false, the value will not be cleared (usefull for suggesting values on a free text search) 30 | */ 31 | @Prop() clearOnUnselectedClosing: boolean = true; 32 | 33 | /** 34 | * Enable/Disable the input field 35 | */ 36 | @Prop() disabled = false; 37 | 38 | /** 39 | * The minimum input size for generating suggestions 40 | */ 41 | @Prop() minInput = 0; 42 | 43 | /** 44 | * The maximally shown suggestions in the list 45 | */ 46 | @Prop() maxSuggestions = 5; 47 | 48 | /** 49 | * Timing to suggest on empty (-1 to disable) 50 | */ 51 | @Prop() emptySuggestionTime = -1; 52 | 53 | /** 54 | * Form validation: is the form input required 55 | */ 56 | @Prop() required = false; 57 | 58 | /** 59 | * A hint to the browser for which keyboard to display. 60 | * Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, 61 | * `"email"`, `"numeric"`, `"decimal"`, and `"search"`. 62 | */ 63 | @Prop() inputmode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; 64 | 65 | /** 66 | * id of the input field 67 | */ 68 | @Prop() inputId = ""; 69 | 70 | /** 71 | * Async suggestion generator: 72 | * `text` is the displayed for users in the form after selection (if no `suggesion` also as suggesion) 73 | * `value` is the actual value of the form field 74 | * optional `suggesion` if the autocomplete suggestion item should be formatted differently than `text` 75 | */ 76 | @Prop() suggestionGenerator: (text: string) => Promise>; 77 | 78 | /** 79 | * The class names, which should be set on the rendered html elements 80 | */ 81 | @Prop() cssClasses = { 82 | wrapper: "", 83 | input: "", 84 | suggestions: "suggestions", 85 | suggestion: "suggestion", 86 | active: "active" 87 | }; 88 | 89 | /** 90 | * Emitted when an item from suggestions was selected 91 | */ 92 | @Event() selected: EventEmitter; 93 | 94 | /** 95 | * Emitted when item was cleared/unselected 96 | */ 97 | @Event() unselected: EventEmitter; 98 | 99 | /** 100 | * Returns the `value` of the selected item 101 | */ 102 | @Method() 103 | async getValue(): Promise { 104 | return this.value; 105 | } 106 | 107 | /** 108 | * Returns the `text` of the selected item 109 | */ 110 | @Method() 111 | async getText(): Promise { 112 | return this.text; 113 | } 114 | 115 | /** 116 | * Clears the form field (suggestions and selection) 117 | */ 118 | @Method() 119 | async clear() { 120 | this.handleClose(); 121 | } 122 | 123 | handleKeyDown(e: KeyboardEvent) { 124 | if (["ArrowDown", "ArrowUp", "Down", "Up"].indexOf(e.key) >= 0) { // some older browsers use Up/Down instead of ArrayUp/ArrowDown (https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) 125 | e.preventDefault(); 126 | this.handleActivation(e.key == "ArrowDown" || e.key == "Down") 127 | } else if (e.key == "Enter" || e.key == "Tab") { 128 | e.preventDefault(); 129 | this.handleSelection(this.activeIndex); 130 | } else if (e.key == "Escape") { 131 | this.handleClose(); 132 | } 133 | } 134 | 135 | handleKeyUp(key, text) { 136 | if (["ArrowDown", "ArrowUp", "Enter", "Tab", "Escape"].indexOf(key) < 0) { // IE doesn't have Array.includes (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes) 137 | this.clearSelection(true); 138 | this.prepareSuggestions(text); 139 | } 140 | this.active = true; 141 | this.text = text; 142 | } 143 | 144 | handleFocus(e: FocusEvent) { 145 | e.preventDefault(); 146 | this.active = true; 147 | if (this.emptySuggestionTime >= 0) { 148 | this.prepareSuggestions(this.text).then(() => { 149 | this.handleActivation(false); 150 | }) 151 | } 152 | } 153 | 154 | handleBlur(e: FocusEvent) { 155 | e.preventDefault(); 156 | 157 | setTimeout(() => { 158 | if (this.active) { 159 | if (this.value) { 160 | this.clearData(); 161 | } else { 162 | this.handleClose(); 163 | } 164 | } 165 | }, 250); 166 | } 167 | 168 | handleClose() { 169 | this.clearSelection(); 170 | this.clearData(); 171 | } 172 | 173 | handleActivation(next = true) { 174 | if (this.data.length > 0) { 175 | if (next && (this.activeIndex + 1) < this.data.length) { 176 | this.activeIndex += 1; 177 | } else if (next) { 178 | this.activeIndex = 0; 179 | } else if (!next && (this.activeIndex) > 0) { 180 | this.activeIndex -= 1; 181 | } else if (!next) { 182 | this.activeIndex = this.data.length - 1; 183 | } 184 | } 185 | } 186 | 187 | handleSelection(index) { 188 | if (index >= 0 && index < this.data.length) { 189 | this.text = this.data[index].text; 190 | this.value = this.data[index].value; 191 | this.selected.emit(this.data[index]); 192 | this.clearData(); 193 | } else if (!this.clearOnUnselectedClosing) { 194 | this.handleClose(); 195 | } 196 | } 197 | 198 | clearData() { 199 | this.data = []; 200 | this.activeIndex = -1; 201 | this.active = false; 202 | } 203 | 204 | clearSelection(clearOnlyValue = false) { 205 | if (this.value != "") { 206 | this.unselected.emit({ 207 | text: this.text, 208 | value: this.value 209 | }); 210 | if (this.clearOnUnselectedClosing) { 211 | this.value = ""; 212 | } 213 | } 214 | if (!clearOnlyValue && this.clearOnUnselectedClosing) { 215 | this.text = ""; 216 | } 217 | } 218 | 219 | async prepareSuggestions(text) { 220 | if (this.suggestionGenerator && text.length >= this.minInput) { 221 | let suggestions = await this.suggestionGenerator(text); 222 | suggestions.splice(this.maxSuggestions); 223 | this.data = suggestions; 224 | } else { 225 | this.data = []; 226 | } 227 | } 228 | 229 | render() { 230 | return ( 231 |
232 | this.handleKeyDown(e)} 235 | onKeyUp={(e) => this.handleKeyUp(e.key, e.target['value'])} 236 | onBlur={(e) => { this.handleBlur(e) }} 237 | onFocus={(e) => { this.handleFocus(e) }} 238 | type="text" 239 | inputMode={this.inputmode} 240 | id={this.inputId} 241 | required={this.required} 242 | autocomplete="off" 243 | disabled={this.disabled} 244 | placeholder={this.placeholder} 245 | value={this.text} 246 | /> 247 | { this.data && this.data.length > 0 248 | ?
{this.data.map((suggestion, index) => { 249 | return 253 | })}
254 | : "" 255 | } 256 |
257 | ); 258 | } 259 | } -------------------------------------------------------------------------------- /src/components/web-complete/readme.md: -------------------------------------------------------------------------------- 1 | # web-complete 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Properties 9 | 10 | | Property | Attribute | Description | Type | Default | 11 | | -------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | 12 | | `clearOnUnselectedClosing` | `clear-on-unselected-closing` | If no value is selected, clear the input and emit unselected, if false, the value will not be cleared (usefull for suggesting values on a free text search) | `boolean` | `true` | 13 | | `cssClasses` | -- | The class names, which should be set on the rendered html elements | `{ wrapper: string; input: string; suggestions: string; suggestion: string; active: string; }` | `{ wrapper: "", input: "", suggestions: "suggestions", suggestion: "suggestion", active: "active" }` | 14 | | `disabled` | `disabled` | Enable/Disable the input field | `boolean` | `false` | 15 | | `inputId` | `input-id` | id of the input field | `string` | `""` | 16 | | `inputmode` | `inputmode` | A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. | `"decimal" \| "email" \| "none" \| "numeric" \| "search" \| "tel" \| "text" \| "url"` | `undefined` | 17 | | `maxSuggestions` | `max-suggestions` | The maximally shown suggestions in the list | `number` | `5` | 18 | | `minInput` | `min-input` | The minimum input size for generating suggestions | `number` | `0` | 19 | | `placeholder` | `placeholder` | The placeholder for the input field | `string` | `""` | 20 | | `required` | `required` | Form validation: is the form input required | `boolean` | `false` | 21 | | `suggestionGenerator` | -- | Async suggestion generator: `text` is the displayed for users in the form after selection (if no `suggesion` also as suggesion) `value` is the actual value of the form field optional `suggesion` if the autocomplete suggestion item should be formatted differently than `text` | `(text: string) => Promise<{ text: string; value: string; suggestion?: string; }[]>` | `undefined` | 22 | | `text` | `text` | The text is displayed by the form field for users | `string` | `""` | 23 | | `value` | `value` | The actual value of the form field | `string` | `""` | 24 | | `emptySuggestionTime` | `empty-suggestion-time` | Milliseconds before diplaying autocomplete, even if it's empty or nothing is type in the input. It allow to inspire users for example. Use -1 to disable it. | `number` | `-1` | 25 | 26 | 27 | 28 | ## Events 29 | 30 | | Event | Description | Type | 31 | | ------------ | -------------------------------------------------- | ------------------ | 32 | | `selected` | Emitted when an item from suggestions was selected | `CustomEvent` | 33 | | `unselected` | Emitted when item was cleared/unselected | `CustomEvent` | 34 | 35 | 36 | ## Methods 37 | 38 | ### `clear() => Promise` 39 | 40 | Clears the form field (suggestions and selection) 41 | 42 | #### Returns 43 | 44 | Type: `Promise` 45 | 46 | 47 | 48 | ### `getText() => Promise` 49 | 50 | Returns the `text` of the selected item 51 | 52 | #### Returns 53 | 54 | Type: `Promise` 55 | 56 | 57 | 58 | ### `getValue() => Promise` 59 | 60 | Returns the `value` of the selected item 61 | 62 | #### Returns 63 | 64 | Type: `Promise` 65 | 66 | 67 | 68 | 69 | ---------------------------------------------- 70 | 71 | *Built with [StencilJS](https://stenciljs.com/)* 72 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Web Complete Demo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 |

web-complete

20 |

A lightweight, dependency-free, styleable autocomplete web component

21 |
22 |

You can find some usage examples below. Check out the source code on Github or install via npm.

23 | 24 | Github 25 | 26 | npm 27 | 28 |
29 | 30 | 31 |
32 |

Basic example

33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 |
42 |
43 | 77 |
78 | 79 |
80 |

Add a clear button and custom suggestions

81 |
82 |
83 |
84 |
85 | 86 |
87 |
88 | 89 |
90 |
91 | 92 |
93 |
94 |
95 |
96 |
97 |
98 | 138 |
139 | 140 |
141 |

Form validation

142 |
143 |
144 |
145 |
146 | 147 |
148 |
149 | 150 |
151 |
152 | 153 |
154 |
155 |
156 |
157 |
158 |
159 | 200 |
201 | 202 |
203 |

Keep text also if no suggestion fits

204 |
205 |
206 |
207 |
208 | 209 |
210 |
211 | 212 |
213 |
214 |
215 |
216 |
217 |
218 | 244 |
245 | 246 |
247 | 248 | 249 | -------------------------------------------------------------------------------- /docs/countries.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"name": "Afghanistan", "code": "AF"}, 3 | {"name": "Åland Islands", "code": "AX"}, 4 | {"name": "Albania", "code": "AL"}, 5 | {"name": "Algeria", "code": "DZ"}, 6 | {"name": "American Samoa", "code": "AS"}, 7 | {"name": "AndorrA", "code": "AD"}, 8 | {"name": "Angola", "code": "AO"}, 9 | {"name": "Anguilla", "code": "AI"}, 10 | {"name": "Antarctica", "code": "AQ"}, 11 | {"name": "Antigua and Barbuda", "code": "AG"}, 12 | {"name": "Argentina", "code": "AR"}, 13 | {"name": "Armenia", "code": "AM"}, 14 | {"name": "Aruba", "code": "AW"}, 15 | {"name": "Australia", "code": "AU"}, 16 | {"name": "Austria", "code": "AT"}, 17 | {"name": "Azerbaijan", "code": "AZ"}, 18 | {"name": "Bahamas", "code": "BS"}, 19 | {"name": "Bahrain", "code": "BH"}, 20 | {"name": "Bangladesh", "code": "BD"}, 21 | {"name": "Barbados", "code": "BB"}, 22 | {"name": "Belarus", "code": "BY"}, 23 | {"name": "Belgium", "code": "BE"}, 24 | {"name": "Belize", "code": "BZ"}, 25 | {"name": "Benin", "code": "BJ"}, 26 | {"name": "Bermuda", "code": "BM"}, 27 | {"name": "Bhutan", "code": "BT"}, 28 | {"name": "Bolivia", "code": "BO"}, 29 | {"name": "Bosnia and Herzegovina", "code": "BA"}, 30 | {"name": "Botswana", "code": "BW"}, 31 | {"name": "Bouvet Island", "code": "BV"}, 32 | {"name": "Brazil", "code": "BR"}, 33 | {"name": "British Indian Ocean Territory", "code": "IO"}, 34 | {"name": "Brunei Darussalam", "code": "BN"}, 35 | {"name": "Bulgaria", "code": "BG"}, 36 | {"name": "Burkina Faso", "code": "BF"}, 37 | {"name": "Burundi", "code": "BI"}, 38 | {"name": "Cambodia", "code": "KH"}, 39 | {"name": "Cameroon", "code": "CM"}, 40 | {"name": "Canada", "code": "CA"}, 41 | {"name": "Cape Verde", "code": "CV"}, 42 | {"name": "Cayman Islands", "code": "KY"}, 43 | {"name": "Central African Republic", "code": "CF"}, 44 | {"name": "Chad", "code": "TD"}, 45 | {"name": "Chile", "code": "CL"}, 46 | {"name": "China", "code": "CN"}, 47 | {"name": "Christmas Island", "code": "CX"}, 48 | {"name": "Cocos (Keeling) Islands", "code": "CC"}, 49 | {"name": "Colombia", "code": "CO"}, 50 | {"name": "Comoros", "code": "KM"}, 51 | {"name": "Congo", "code": "CG"}, 52 | {"name": "Congo, The Democratic Republic of the", "code": "CD"}, 53 | {"name": "Cook Islands", "code": "CK"}, 54 | {"name": "Costa Rica", "code": "CR"}, 55 | {"name": "Cote D'Ivoire", "code": "CI"}, 56 | {"name": "Croatia", "code": "HR"}, 57 | {"name": "Cuba", "code": "CU"}, 58 | {"name": "Cyprus", "code": "CY"}, 59 | {"name": "Czech Republic", "code": "CZ"}, 60 | {"name": "Denmark", "code": "DK"}, 61 | {"name": "Djibouti", "code": "DJ"}, 62 | {"name": "Dominica", "code": "DM"}, 63 | {"name": "Dominican Republic", "code": "DO"}, 64 | {"name": "Ecuador", "code": "EC"}, 65 | {"name": "Egypt", "code": "EG"}, 66 | {"name": "El Salvador", "code": "SV"}, 67 | {"name": "Equatorial Guinea", "code": "GQ"}, 68 | {"name": "Eritrea", "code": "ER"}, 69 | {"name": "Estonia", "code": "EE"}, 70 | {"name": "Ethiopia", "code": "ET"}, 71 | {"name": "Falkland Islands (Malvinas)", "code": "FK"}, 72 | {"name": "Faroe Islands", "code": "FO"}, 73 | {"name": "Fiji", "code": "FJ"}, 74 | {"name": "Finland", "code": "FI"}, 75 | {"name": "France", "code": "FR"}, 76 | {"name": "French Guiana", "code": "GF"}, 77 | {"name": "French Polynesia", "code": "PF"}, 78 | {"name": "French Southern Territories", "code": "TF"}, 79 | {"name": "Gabon", "code": "GA"}, 80 | {"name": "Gambia", "code": "GM"}, 81 | {"name": "Georgia", "code": "GE"}, 82 | {"name": "Germany", "code": "DE"}, 83 | {"name": "Ghana", "code": "GH"}, 84 | {"name": "Gibraltar", "code": "GI"}, 85 | {"name": "Greece", "code": "GR"}, 86 | {"name": "Greenland", "code": "GL"}, 87 | {"name": "Grenada", "code": "GD"}, 88 | {"name": "Guadeloupe", "code": "GP"}, 89 | {"name": "Guam", "code": "GU"}, 90 | {"name": "Guatemala", "code": "GT"}, 91 | {"name": "Guernsey", "code": "GG"}, 92 | {"name": "Guinea", "code": "GN"}, 93 | {"name": "Guinea-Bissau", "code": "GW"}, 94 | {"name": "Guyana", "code": "GY"}, 95 | {"name": "Haiti", "code": "HT"}, 96 | {"name": "Heard Island and Mcdonald Islands", "code": "HM"}, 97 | {"name": "Holy See (Vatican City State)", "code": "VA"}, 98 | {"name": "Honduras", "code": "HN"}, 99 | {"name": "Hong Kong", "code": "HK"}, 100 | {"name": "Hungary", "code": "HU"}, 101 | {"name": "Iceland", "code": "IS"}, 102 | {"name": "India", "code": "IN"}, 103 | {"name": "Indonesia", "code": "ID"}, 104 | {"name": "Iran, Islamic Republic Of", "code": "IR"}, 105 | {"name": "Iraq", "code": "IQ"}, 106 | {"name": "Ireland", "code": "IE"}, 107 | {"name": "Isle of Man", "code": "IM"}, 108 | {"name": "Israel", "code": "IL"}, 109 | {"name": "Italy", "code": "IT"}, 110 | {"name": "Jamaica", "code": "JM"}, 111 | {"name": "Japan", "code": "JP"}, 112 | {"name": "Jersey", "code": "JE"}, 113 | {"name": "Jordan", "code": "JO"}, 114 | {"name": "Kazakhstan", "code": "KZ"}, 115 | {"name": "Kenya", "code": "KE"}, 116 | {"name": "Kiribati", "code": "KI"}, 117 | {"name": "Korea, Democratic People'S Republic of", "code": "KP"}, 118 | {"name": "Korea, Republic of", "code": "KR"}, 119 | {"name": "Kuwait", "code": "KW"}, 120 | {"name": "Kyrgyzstan", "code": "KG"}, 121 | {"name": "Lao People'S Democratic Republic", "code": "LA"}, 122 | {"name": "Latvia", "code": "LV"}, 123 | {"name": "Lebanon", "code": "LB"}, 124 | {"name": "Lesotho", "code": "LS"}, 125 | {"name": "Liberia", "code": "LR"}, 126 | {"name": "Libyan Arab Jamahiriya", "code": "LY"}, 127 | {"name": "Liechtenstein", "code": "LI"}, 128 | {"name": "Lithuania", "code": "LT"}, 129 | {"name": "Luxembourg", "code": "LU"}, 130 | {"name": "Macao", "code": "MO"}, 131 | {"name": "Macedonia, The Former Yugoslav Republic of", "code": "MK"}, 132 | {"name": "Madagascar", "code": "MG"}, 133 | {"name": "Malawi", "code": "MW"}, 134 | {"name": "Malaysia", "code": "MY"}, 135 | {"name": "Maldives", "code": "MV"}, 136 | {"name": "Mali", "code": "ML"}, 137 | {"name": "Malta", "code": "MT"}, 138 | {"name": "Marshall Islands", "code": "MH"}, 139 | {"name": "Martinique", "code": "MQ"}, 140 | {"name": "Mauritania", "code": "MR"}, 141 | {"name": "Mauritius", "code": "MU"}, 142 | {"name": "Mayotte", "code": "YT"}, 143 | {"name": "Mexico", "code": "MX"}, 144 | {"name": "Micronesia, Federated States of", "code": "FM"}, 145 | {"name": "Moldova, Republic of", "code": "MD"}, 146 | {"name": "Monaco", "code": "MC"}, 147 | {"name": "Mongolia", "code": "MN"}, 148 | {"name": "Montserrat", "code": "MS"}, 149 | {"name": "Morocco", "code": "MA"}, 150 | {"name": "Mozambique", "code": "MZ"}, 151 | {"name": "Myanmar", "code": "MM"}, 152 | {"name": "Namibia", "code": "NA"}, 153 | {"name": "Nauru", "code": "NR"}, 154 | {"name": "Nepal", "code": "NP"}, 155 | {"name": "Netherlands", "code": "NL"}, 156 | {"name": "Netherlands Antilles", "code": "AN"}, 157 | {"name": "New Caledonia", "code": "NC"}, 158 | {"name": "New Zealand", "code": "NZ"}, 159 | {"name": "Nicaragua", "code": "NI"}, 160 | {"name": "Niger", "code": "NE"}, 161 | {"name": "Nigeria", "code": "NG"}, 162 | {"name": "Niue", "code": "NU"}, 163 | {"name": "Norfolk Island", "code": "NF"}, 164 | {"name": "Northern Mariana Islands", "code": "MP"}, 165 | {"name": "Norway", "code": "NO"}, 166 | {"name": "Oman", "code": "OM"}, 167 | {"name": "Pakistan", "code": "PK"}, 168 | {"name": "Palau", "code": "PW"}, 169 | {"name": "Palestinian Territory, Occupied", "code": "PS"}, 170 | {"name": "Panama", "code": "PA"}, 171 | {"name": "Papua New Guinea", "code": "PG"}, 172 | {"name": "Paraguay", "code": "PY"}, 173 | {"name": "Peru", "code": "PE"}, 174 | {"name": "Philippines", "code": "PH"}, 175 | {"name": "Pitcairn", "code": "PN"}, 176 | {"name": "Poland", "code": "PL"}, 177 | {"name": "Portugal", "code": "PT"}, 178 | {"name": "Puerto Rico", "code": "PR"}, 179 | {"name": "Qatar", "code": "QA"}, 180 | {"name": "Reunion", "code": "RE"}, 181 | {"name": "Romania", "code": "RO"}, 182 | {"name": "Russian Federation", "code": "RU"}, 183 | {"name": "RWANDA", "code": "RW"}, 184 | {"name": "Saint Helena", "code": "SH"}, 185 | {"name": "Saint Kitts and Nevis", "code": "KN"}, 186 | {"name": "Saint Lucia", "code": "LC"}, 187 | {"name": "Saint Pierre and Miquelon", "code": "PM"}, 188 | {"name": "Saint Vincent and the Grenadines", "code": "VC"}, 189 | {"name": "Samoa", "code": "WS"}, 190 | {"name": "San Marino", "code": "SM"}, 191 | {"name": "Sao Tome and Principe", "code": "ST"}, 192 | {"name": "Saudi Arabia", "code": "SA"}, 193 | {"name": "Senegal", "code": "SN"}, 194 | {"name": "Serbia and Montenegro", "code": "CS"}, 195 | {"name": "Seychelles", "code": "SC"}, 196 | {"name": "Sierra Leone", "code": "SL"}, 197 | {"name": "Singapore", "code": "SG"}, 198 | {"name": "Slovakia", "code": "SK"}, 199 | {"name": "Slovenia", "code": "SI"}, 200 | {"name": "Solomon Islands", "code": "SB"}, 201 | {"name": "Somalia", "code": "SO"}, 202 | {"name": "South Africa", "code": "ZA"}, 203 | {"name": "South Georgia and the South Sandwich Islands", "code": "GS"}, 204 | {"name": "Spain", "code": "ES"}, 205 | {"name": "Sri Lanka", "code": "LK"}, 206 | {"name": "Sudan", "code": "SD"}, 207 | {"name": "Suriname", "code": "SR"}, 208 | {"name": "Svalbard and Jan Mayen", "code": "SJ"}, 209 | {"name": "Swaziland", "code": "SZ"}, 210 | {"name": "Sweden", "code": "SE"}, 211 | {"name": "Switzerland", "code": "CH"}, 212 | {"name": "Syrian Arab Republic", "code": "SY"}, 213 | {"name": "Taiwan, Province of China", "code": "TW"}, 214 | {"name": "Tajikistan", "code": "TJ"}, 215 | {"name": "Tanzania, United Republic of", "code": "TZ"}, 216 | {"name": "Thailand", "code": "TH"}, 217 | {"name": "Timor-Leste", "code": "TL"}, 218 | {"name": "Togo", "code": "TG"}, 219 | {"name": "Tokelau", "code": "TK"}, 220 | {"name": "Tonga", "code": "TO"}, 221 | {"name": "Trinidad and Tobago", "code": "TT"}, 222 | {"name": "Tunisia", "code": "TN"}, 223 | {"name": "Turkey", "code": "TR"}, 224 | {"name": "Turkmenistan", "code": "TM"}, 225 | {"name": "Turks and Caicos Islands", "code": "TC"}, 226 | {"name": "Tuvalu", "code": "TV"}, 227 | {"name": "Uganda", "code": "UG"}, 228 | {"name": "Ukraine", "code": "UA"}, 229 | {"name": "United Arab Emirates", "code": "AE"}, 230 | {"name": "United Kingdom", "code": "GB"}, 231 | {"name": "United States", "code": "US"}, 232 | {"name": "United States Minor Outlying Islands", "code": "UM"}, 233 | {"name": "Uruguay", "code": "UY"}, 234 | {"name": "Uzbekistan", "code": "UZ"}, 235 | {"name": "Vanuatu", "code": "VU"}, 236 | {"name": "Venezuela", "code": "VE"}, 237 | {"name": "Viet Nam", "code": "VN"}, 238 | {"name": "Virgin Islands, British", "code": "VG"}, 239 | {"name": "Virgin Islands, U.S.", "code": "VI"}, 240 | {"name": "Wallis and Futuna", "code": "WF"}, 241 | {"name": "Western Sahara", "code": "EH"}, 242 | {"name": "Yemen", "code": "YE"}, 243 | {"name": "Zambia", "code": "ZM"}, 244 | {"name": "Zimbabwe", "code": "ZW"} 245 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Built With Stencil](https://img.shields.io/badge/-Built%20With%20Stencil-16161d.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI%2BCjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI%2BCgkuc3Qwe2ZpbGw6I0ZGRkZGRjt9Cjwvc3R5bGU%2BCjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik00MjQuNywzNzMuOWMwLDM3LjYtNTUuMSw2OC42LTkyLjcsNjguNkgxODAuNGMtMzcuOSwwLTkyLjctMzAuNy05Mi43LTY4LjZ2LTMuNmgzMzYuOVYzNzMuOXoiLz4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTQyNC43LDI5Mi4xSDE4MC40Yy0zNy42LDAtOTIuNy0zMS05Mi43LTY4LjZ2LTMuNkgzMzJjMzcuNiwwLDkyLjcsMzEsOTIuNyw2OC42VjI5Mi4xeiIvPgo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNDI0LjcsMTQxLjdIODcuN3YtMy42YzAtMzcuNiw1NC44LTY4LjYsOTIuNy02OC42SDMzMmMzNy45LDAsOTIuNywzMC43LDkyLjcsNjguNlYxNDEuN3oiLz4KPC9zdmc%2BCg%3D%3D&colorA=16161d&style=flat-square) [![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/web-complete) [![Build Status](https://travis-ci.com/stefanhuber/web-complete.svg?branch=master)](https://travis-ci.com/stefanhuber/web-complete) 2 | 3 | > `web-complete` is a lightweight, dependency-free, styleable autocomplete web component. 4 | 5 | # Installation 6 | 7 | ## Script tag 8 | 9 | ```html 10 | 11 | 12 | ``` 13 | 14 | ## Node Module 15 | 16 | - Install via npm: `npm install web-complete --save` 17 | - Add script to html: `` 18 | - Or import as JS module: `import 'web-complete';` 19 | 20 | ## Framework integration 21 | 22 | For integration with different frameworks the [stencil docs](https://stenciljs.com/docs/overview) should be consulted. 23 | 24 | # Using this component 25 | 26 | Add the component to your html: 27 | ```html 28 | 29 | ``` 30 | 31 | Add some javascript for additional configuration: 32 | ```javascript 33 | const webcomplete = document.querySelector('#my-web-complete'); 34 | 35 | // change css classes for styling 36 | webcomplete.cssClasses = { 37 | "wrapper": "dropdown", 38 | "input": "form-control", 39 | "suggestions": "dropdown-menu show", 40 | "suggestion": "dropdown-item", 41 | "active": "active" 42 | }; 43 | 44 | // add an async suggestion generator 45 | webcomplete.suggestionGenerator = function(text) { 46 | return new Promise(function(resolve, reject) { 47 | // generate suggestions with input text 48 | // e.g. by using http fetch 49 | }); 50 | }; 51 | 52 | // listen to selected/unselected events 53 | webcomplete.addEventListener('selected', function(e) { 54 | // suggestion selected (e.detail) 55 | }); 56 | webcomplete.addEventListener('unselected', function(e) { 57 | // suggestion unselected (e.detail) 58 | }); 59 | ``` 60 | 61 | A full example with [Bootstrap 4 Dropdown](https://getbootstrap.com/docs/4.3/components/dropdowns/) theming can be found [here](https://github.com/stefanhuber/web-complete/blob/master/docs/index.html). 62 | 63 | # Component API 64 | 65 | ## Properties 66 | 67 | | Property | Attribute | Description | Type | Default | 68 | | -------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | 69 | | `clearOnUnselectedClosing` | `clear-on-unselected-closing` | If no value is selected, clear the input and emit unselected, if false, the value will not be cleared (usefull for suggesting values on a free text search) | `boolean` | `true` | 70 | | `cssClasses` | -- | The class names, which should be set on the rendered html elements | `{ wrapper: string; input: string; suggestions: string; suggestion: string; active: string; }` | `{ wrapper: "", input: "", suggestions: "suggestions", suggestion: "suggestion", active: "active" }` | 71 | | `disabled` | `disabled` | Enable/Disable the input field | `boolean` | `false` | 72 | | `inputId` | `input-id` | id of the input field | `string` | `""` | 73 | | `inputmode` | `inputmode` | A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. | `"decimal" \| "email" \| "none" \| "numeric" \| "search" \| "tel" \| "text" \| "url"` | `undefined` | 74 | | `maxSuggestions` | `max-suggestions` | The maximally shown suggestions in the list | `number` | `5` | 75 | | `minInput` | `min-input` | The minimum input size for generating suggestions | `number` | `0` | 76 | | `placeholder` | `placeholder` | The placeholder for the input field | `string` | `""` | 77 | | `required` | `required` | Form validation: is the form input required | `boolean` | `false` | 78 | | `suggestionGenerator` | -- | Async suggestion generator: `text` is the displayed for users in the form after selection (if no `suggesion` also as suggesion) `value` is the actual value of the form field optional `suggesion` if the autocomplete suggestion item should be formatted differently than `text` | `(text: string) => Promise<{ text: string; value: string; suggestion?: string; }[]>` | `undefined` | 79 | | `text` | `text` | The text is displayed by the form field for users | `string` | `""` | 80 | | `value` | `value` | The actual value of the form field | `string` | `""` | 81 | | `emptySuggestionTime` | `empty-suggestion-time` | Milliseconds before diplaying autocomplete, even if it's empty or nothing is type in the input. It allow to inspire users for example. Use -1 to disable it. | `number` | `-1` | 82 | 83 | 84 | ## Events 85 | 86 | | Event | Description | Type | 87 | | ------------ | -------------------------------------------------- | ------------------ | 88 | | `selected` | Emitted when an item from suggestions was selected | `CustomEvent` | 89 | | `unselected` | Emitted when item was cleared/unselected | `CustomEvent` | 90 | 91 | 92 | ## Methods 93 | 94 | ### `clear() => Promise` 95 | 96 | Clears the form field (suggestions and selection) 97 | 98 | #### Returns 99 | 100 | Type: `Promise` 101 | 102 | 103 | 104 | ### `getText() => Promise` 105 | 106 | Returns the `text` of the selected item 107 | 108 | #### Returns 109 | 110 | Type: `Promise` 111 | 112 | 113 | 114 | ### `getValue() => Promise` 115 | 116 | Returns the `value` of the selected item 117 | 118 | #### Returns 119 | 120 | Type: `Promise` 121 | 122 | 123 | # Developer 124 | 125 | ``` 126 | npm i install dependencies 127 | npm start start local development 128 | npm run build build component for production 129 | npm test run e2e tests 130 | ``` 131 | --------------------------------------------------------------------------------