├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── build └── build.js ├── dist ├── style │ └── vue2-autocomplete.css └── vue2-autocomplete.js ├── index.html ├── package.json ├── src ├── img │ └── demo.gif └── js │ ├── components │ ├── app.vue │ └── vue-autocomplete.vue │ ├── main.js │ └── plugin.js ├── webpack ├── webpack-bundle.config.js ├── webpack-prod.config.js └── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["es2015", { "modules": false }], "stage-1"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Naufal Rabbani 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 | # Vue 2 Autocomplete 2 | Autocomplete Component For [Vue 2](http://vuejs.org). It's based on [vue-autocomplete](https://github.com/BosNaufal/vue-autocomplete). [LIVE DEMO HERE!](https://rawgit.com/BosNaufal/vue2-autocomplete/master/index.html) 3 | 4 |

5 | 6 | vue Autocomplete component 7 | 8 |

9 | 10 | ## Install 11 | You can import [vue2-autocomplete.vue](./src/js/components/vue-autocomplete.vue) to your vue component file like [this](./src/js/components/app.vue) and process it with your preprocessor. 12 | 13 | You can install it via NPM 14 | ```bash 15 | npm install vue2-autocomplete-js 16 | ``` 17 | 18 | Or Just put it after Vue JS~ 19 | ```html 20 | 21 | 22 | 30 | ``` 31 | 32 | ## Import Style 33 | Don't forget to import vue 2 css. You can link it via html 34 | ```html 35 | 36 | ``` 37 | 38 | Or You can import it using commonJS 39 | 40 | ```javascript 41 | require('vue2-autocomplete-js/dist/style/vue2-autocomplete.css') 42 | ``` 43 | 44 | Its style is very customizable. You can put any CSS over it. And You can add custom class via its prop. 45 | 46 | 47 | ## Import Module 48 | ```javascript 49 | import Autocomplete from 'vue2-autocomplete-js' 50 | // Or 51 | var Autocomplete = require('vue2-autocomplete-js'); 52 | ``` 53 | 54 | ## Usage 55 | ```html 56 | 66 | 67 | 68 | 84 | ``` 85 | 86 | Available Props 87 | ```html 88 | 128 | ``` 129 | 130 | ## Props 131 | #### url* (String) 132 | the URL must be active (not from file). the component will fetch JSON from this URL and passing one params (default : `q`) query. like: 133 | ``` 134 | http://some-url.com/API/list?q= 135 | ``` 136 | There are no filter and limit action inside the component. So, do it in your API logic. 137 | 138 | #### param (String: "q") 139 | name of the search parameter to query in Ajax call. default is `q` 140 | 141 | #### min (Number: 0) 142 | Minimum input typed chars before performing the search query. default is `0` 143 | 144 | #### anchor* (String) 145 | It's a object property path that used for Anchor in suggestions list. Example `anchor="name"` will get the name property of your JSON object. Like ("Bambang", "Sukijan", "Bejo") in the demo above. Or you can reach the deep value of your object. Like `anchor="geometry.location.lat"` 146 | 147 | 148 | #### label (String) 149 | Same as anchor but it's used for subtitle or description of list 150 | 151 | #### options (Array) 152 | Manual pass an Array of list options to the autocomplete. 153 | 154 | #### filterByAnchor (Boolean: true) 155 | When you're using options props, you can have autocomplete to filter your data. Or you can just show your data directly without any filter from autocomplete. The options will be filtered by anchor and it according to the user input. 156 | 157 | #### encodeParams (Boolean: true) 158 | Autocomplete will ```encodeURIComponent``` all your params before ajax send, When this props sets to ```true```. Default is ```true``` [#35](https://github.com/BosNaufal/vue2-autocomplete/pull/35) 159 | 160 | #### debounce (Number) 161 | Delay time before do the ajax for the data 162 | 163 | 164 | #### required (Boolean) 165 | Required attribute for input 166 | 167 | #### placeholder (String) 168 | Placeholder for input 169 | 170 | #### className (String) 171 | Custom class name for autocomplete component 172 | 173 | #### classes (Object) 174 | Spesific custom class for each part. available: wrapper, input, list, and item 175 | 176 | #### id (String) 177 | Custom id name for autocomplete component 178 | 179 | #### debounce (number) 180 | Number of milliseconds the user should stop typing for before the request is sent. Default is 0, meaning all requests are sent immediately. 181 | 182 | #### process (Function) 183 | Function to process the API result with. Should return an array of entries or an object whose properties can be enumerated. 184 | 185 | #### template (Function) 186 | Function to process each result with. Takes the type of an API reply element and should return HTML data. 187 | 188 | 189 | ## Callback Events 190 | You can make a callback event via props. 191 | 192 | #### onInput (Function) 193 | On Input event in autocomplete 194 | 195 | #### onShow (Function) 196 | On Show event in autocomplete list 197 | 198 | #### onBlur (Function) 199 | When autocomplete is blured 200 | 201 | #### onHide (Function) 202 | When autocomplete list is hidden 203 | 204 | #### onFocus (Function) 205 | When autocomplete input in focus mode 206 | 207 | #### onSelect (Function) 208 | When user has selected one item in the list 209 | 210 | #### onBeforeAjax (Function) 211 | Before the ajax send 212 | 213 | #### onAjaxProgress (Function) 214 | While ajax is fetching the data 215 | 216 | #### onAjaxLoaded (Function) 217 | When ajax process is totally loaded 218 | 219 | #### onShouldGetData (Function) 220 | Manually Process the whole ajax process. If it's a Promise, it should resolve the options for the list of autocomplete. If it isn't a Promise, you can manually pass the options to the props of autocomplete 221 | ```html 222 | 227 | 228 | ``` 229 | ```javascript 230 | methods: { 231 | promise(value) { 232 | return new Promise((resolve, reject) => { 233 | let ajax = new XMLHttpRequest(); 234 | ajax.open('GET', `https://maps.googleapis.com/maps/api/geocode/json?address=${value}`, true); 235 | // On Done 236 | ajax.addEventListener('loadend', (e) => { 237 | const { responseText } = e.target 238 | let response = JSON.parse(responseText); 239 | // The options to pass in the autocomplete 240 | resolve(response.results) 241 | }); 242 | ajax.send(); 243 | }) 244 | }, 245 | 246 | nonPromise() { 247 | getData(value) { 248 | let ajax = new XMLHttpRequest(); 249 | ajax.open('GET', `https://maps.googleapis.com/maps/api/geocode/json?address=${value}`, true); 250 | // On Done 251 | ajax.addEventListener('loadend', (e) => { 252 | const { responseText } = e.target 253 | let response = JSON.parse(responseText); 254 | // The options to pass in the autocomplete props 255 | this.options = response.results; 256 | }); 257 | ajax.send(); 258 | }, 259 | } 260 | } 261 | ``` 262 | 263 | #### process (Function) 264 | Process the result before retrieveng the result array. You can shape your data here before it's passed to the autocomplete 265 | 266 | #### onShouldRenderChild (Function) 267 | Wanna use custom template for the list? Now, you can do it! 268 | ```html 269 | 274 | 275 | ``` 276 | ```javascript 277 | methods: { 278 | renderChild(data) { 279 | return ` 280 | 281 | ${data.something} 282 | ` 283 | }, 284 | } 285 | ``` 286 | 287 | 288 | 289 | ## Methods 290 | You can do some methods by accessing the component via javascript. 291 | ```javascript 292 | this.$refs.autocomplete.someMethod() 293 | ``` 294 | 295 | #### setValue (String) 296 | To set the value of the autocomplete input 297 | 298 | 299 | 300 | ## Thank You for Making this useful~ 301 | 302 | ## Let's talk about some projects with me 303 | Just Contact Me At: 304 | - Email: [bosnaufalemail@gmail.com](mailto:bosnaufalemail@gmail.com) 305 | - Skype Id: bosnaufal254 306 | - twitter: [@BosNaufal](https://twitter.com/BosNaufal) 307 | 308 | ## License 309 | [MIT](http://opensource.org/licenses/MIT) 310 | Copyright (c) 2016 - forever Naufal Rabbani 311 | -------------------------------------------------------------------------------- /dist/style/vue2-autocomplete.css: -------------------------------------------------------------------------------- 1 | 2 | .transition, .autocomplete, .showAll-transition, .autocomplete ul, .autocomplete ul li a{ 3 | transition:all 0.3s ease-out; 4 | -moz-transition:all 0.3s ease-out; 5 | -webkit-transition:all 0.3s ease-out; 6 | -o-transition:all 0.3s ease-out; 7 | } 8 | 9 | .autocomplete ul{ 10 | font-family: sans-serif; 11 | position: absolute; 12 | list-style: none; 13 | background: #f8f8f8; 14 | padding: 10px 0; 15 | margin: 0; 16 | display: inline-block; 17 | min-width: 15%; 18 | margin-top: 10px; 19 | } 20 | 21 | .autocomplete ul:before{ 22 | content: ""; 23 | display: block; 24 | position: absolute; 25 | height: 0; 26 | width: 0; 27 | border: 10px solid transparent; 28 | border-bottom: 10px solid #f8f8f8; 29 | left: 46%; 30 | top: -20px 31 | } 32 | 33 | .autocomplete ul li a{ 34 | text-decoration: none; 35 | display: block; 36 | background: #f8f8f8; 37 | color: #2b2b2b; 38 | padding: 5px; 39 | padding-left: 10px; 40 | } 41 | 42 | .autocomplete ul li a:hover, .autocomplete ul li.focus-list a{ 43 | color: white; 44 | background: #2F9AF7; 45 | } 46 | 47 | .autocomplete ul li a span, /*backwards compat*/ 48 | .autocomplete ul li a .autocomplete-anchor-label{ 49 | display: block; 50 | margin-top: 3px; 51 | color: grey; 52 | font-size: 13px; 53 | } 54 | 55 | .autocomplete ul li a:hover .autocomplete-anchor-label, 56 | .autocomplete ul li.focus-list a span, /*backwards compat*/ 57 | .autocomplete ul li a:hover .autocomplete-anchor-label, 58 | .autocomplete ul li.focus-list a span{ /*backwards compat*/ 59 | color: white; 60 | } 61 | 62 | /*.showAll-transition{ 63 | opacity: 1; 64 | height: 50px; 65 | overflow: hidden; 66 | } 67 | 68 | .showAll-enter{ 69 | opacity: 0.3; 70 | height: 0; 71 | } 72 | 73 | .showAll-leave{ 74 | display: none; 75 | }*/ 76 | -------------------------------------------------------------------------------- /dist/vue2-autocomplete.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2016 Naufal Rabbani (http://github.com/BosNaufal), 3 | * ,Licensed Under MIT (http://opensource.org/licenses/MIT), 4 | * , 5 | * ,Vue 2 Autocomplete @ Version 0.2.1, 6 | * 7 | */ 8 | (function webpackUniversalModuleDefinition(root, factory) { 9 | if(typeof exports === 'object' && typeof module === 'object') 10 | module.exports = factory(); 11 | else if(typeof define === 'function' && define.amd) 12 | define([], factory); 13 | else if(typeof exports === 'object') 14 | exports["Vue2Autocomplete"] = factory(); 15 | else 16 | root["Vue2Autocomplete"] = factory(); 17 | })(this, function() { 18 | return /******/ (function(modules) { // webpackBootstrap 19 | /******/ // The module cache 20 | /******/ var installedModules = {}; 21 | /******/ 22 | /******/ // The require function 23 | /******/ function __webpack_require__(moduleId) { 24 | /******/ 25 | /******/ // Check if module is in cache 26 | /******/ if(installedModules[moduleId]) { 27 | /******/ return installedModules[moduleId].exports; 28 | /******/ } 29 | /******/ // Create a new module (and put it into the cache) 30 | /******/ var module = installedModules[moduleId] = { 31 | /******/ i: moduleId, 32 | /******/ l: false, 33 | /******/ exports: {} 34 | /******/ }; 35 | /******/ 36 | /******/ // Execute the module function 37 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 38 | /******/ 39 | /******/ // Flag the module as loaded 40 | /******/ module.l = true; 41 | /******/ 42 | /******/ // Return the exports of the module 43 | /******/ return module.exports; 44 | /******/ } 45 | /******/ 46 | /******/ 47 | /******/ // expose the modules object (__webpack_modules__) 48 | /******/ __webpack_require__.m = modules; 49 | /******/ 50 | /******/ // expose the module cache 51 | /******/ __webpack_require__.c = installedModules; 52 | /******/ 53 | /******/ // identity function for calling harmony imports with the correct context 54 | /******/ __webpack_require__.i = function(value) { return value; }; 55 | /******/ 56 | /******/ // define getter function for harmony exports 57 | /******/ __webpack_require__.d = function(exports, name, getter) { 58 | /******/ if(!__webpack_require__.o(exports, name)) { 59 | /******/ Object.defineProperty(exports, name, { 60 | /******/ configurable: false, 61 | /******/ enumerable: true, 62 | /******/ get: getter 63 | /******/ }); 64 | /******/ } 65 | /******/ }; 66 | /******/ 67 | /******/ // getDefaultExport function for compatibility with non-harmony modules 68 | /******/ __webpack_require__.n = function(module) { 69 | /******/ var getter = module && module.__esModule ? 70 | /******/ function getDefault() { return module['default']; } : 71 | /******/ function getModuleExports() { return module; }; 72 | /******/ __webpack_require__.d(getter, 'a', getter); 73 | /******/ return getter; 74 | /******/ }; 75 | /******/ 76 | /******/ // Object.prototype.hasOwnProperty.call 77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 78 | /******/ 79 | /******/ // __webpack_public_path__ 80 | /******/ __webpack_require__.p = "../dist/"; 81 | /******/ 82 | /******/ // Load entry module and return exports 83 | /******/ return __webpack_require__(__webpack_require__.s = 2); 84 | /******/ }) 85 | /************************************************************************/ 86 | /******/ ([ 87 | /* 0 */ 88 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 89 | 90 | "use strict"; 91 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__babel_loader_node_modules_vue_loader_lib_selector_type_script_index_0_vue_autocomplete_vue__ = __webpack_require__(1); 92 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__node_modules_vue_loader_lib_template_compiler_index_id_data_v_e47ae2be_hasScoped_false_node_modules_vue_loader_lib_selector_type_template_index_0_vue_autocomplete_vue__ = __webpack_require__(4); 93 | var disposed = false 94 | var normalizeComponent = __webpack_require__(3) 95 | /* script */ 96 | 97 | /* template */ 98 | 99 | /* styles */ 100 | var __vue_styles__ = null 101 | /* scopeId */ 102 | var __vue_scopeId__ = null 103 | /* moduleIdentifier (server only) */ 104 | var __vue_module_identifier__ = null 105 | var Component = normalizeComponent( 106 | __WEBPACK_IMPORTED_MODULE_0__babel_loader_node_modules_vue_loader_lib_selector_type_script_index_0_vue_autocomplete_vue__["a" /* default */], 107 | __WEBPACK_IMPORTED_MODULE_1__node_modules_vue_loader_lib_template_compiler_index_id_data_v_e47ae2be_hasScoped_false_node_modules_vue_loader_lib_selector_type_template_index_0_vue_autocomplete_vue__["a" /* default */], 108 | __vue_styles__, 109 | __vue_scopeId__, 110 | __vue_module_identifier__ 111 | ) 112 | Component.options.__file = "src/js/components/vue-autocomplete.vue" 113 | if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key.substr(0, 2) !== "__"})) {console.error("named exports are not supported in *.vue files.")} 114 | if (Component.options.functional) {console.error("[vue-loader] vue-autocomplete.vue: functional components are not supported with templates, they should use render functions.")} 115 | 116 | /* hot reload */ 117 | if (false) {(function () { 118 | var hotAPI = require("vue-hot-reload-api") 119 | hotAPI.install(require("vue"), false) 120 | if (!hotAPI.compatible) return 121 | module.hot.accept() 122 | if (!module.hot.data) { 123 | hotAPI.createRecord("data-v-e47ae2be", Component.options) 124 | } else { 125 | hotAPI.reload("data-v-e47ae2be", Component.options) 126 | } 127 | module.hot.dispose(function (data) { 128 | disposed = true 129 | }) 130 | })()} 131 | 132 | /* harmony default export */ __webpack_exports__["a"] = (Component.exports); 133 | 134 | 135 | /***/ }), 136 | /* 1 */ 137 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 138 | 139 | "use strict"; 140 | // 141 | // 142 | // 143 | // 144 | // 145 | // 146 | // 147 | // 148 | // 149 | // 150 | // 151 | // 152 | // 153 | // 154 | // 155 | // 156 | // 157 | // 158 | // 159 | // 160 | // 161 | // 162 | // 163 | // 164 | // 165 | // 166 | // 167 | // 168 | // 169 | // 170 | // 171 | // 172 | // 173 | // 174 | // 175 | // 176 | // 177 | // 178 | // 179 | // 180 | // 181 | // 182 | // 183 | // 184 | // 185 | // 186 | // 187 | // 188 | 189 | 190 | /*! Copyright (c) 2016 Naufal Rabbani (http://github.com/BosNaufal) 191 | * Licensed Under MIT (http://opensource.org/licenses/MIT) 192 | * 193 | * Vue 2 Autocomplete @ Version 0.0.1 194 | * 195 | */ 196 | 197 | /* harmony default export */ __webpack_exports__["a"] = ({ 198 | 199 | props: { 200 | id: String, 201 | name: String, 202 | className: String, 203 | classes: { 204 | type: Object, 205 | default: function _default() { 206 | return { 207 | wrapper: false, 208 | input: false, 209 | list: false, 210 | item: false 211 | }; 212 | } 213 | }, 214 | placeholder: String, 215 | required: Boolean, 216 | 217 | // Intial Value 218 | initValue: { 219 | type: String, 220 | default: "" 221 | }, 222 | 223 | // Manual List 224 | options: Array, 225 | 226 | // Filter After Get the data 227 | filterByAnchor: { 228 | type: Boolean, 229 | default: true 230 | }, 231 | 232 | // Anchor of list 233 | anchor: { 234 | type: String, 235 | required: true 236 | }, 237 | 238 | // Label of list 239 | label: String, 240 | 241 | // Debounce time 242 | debounce: Number, 243 | 244 | // ajax URL will be fetched 245 | url: { 246 | type: String, 247 | required: true 248 | }, 249 | 250 | // query param 251 | param: { 252 | type: String, 253 | default: 'q' 254 | }, 255 | 256 | encodeParams: { 257 | type: Boolean, 258 | default: true 259 | }, 260 | 261 | // Custom Params 262 | customParams: Object, 263 | 264 | // Custom Headers 265 | customHeaders: Object, 266 | 267 | // minimum length 268 | min: { 269 | type: Number, 270 | default: 0 271 | }, 272 | 273 | // Create a custom template from data. 274 | onShouldRenderChild: Function, 275 | 276 | // Process the result before retrieveng the result array. 277 | process: Function, 278 | 279 | // Callback 280 | onInput: Function, 281 | onShow: Function, 282 | onBlur: Function, 283 | onHide: Function, 284 | onFocus: Function, 285 | onSelect: Function, 286 | onBeforeAjax: Function, 287 | onAjaxProgress: Function, 288 | onAjaxLoaded: Function, 289 | onShouldGetData: Function 290 | }, 291 | 292 | data: function data() { 293 | return { 294 | showList: false, 295 | type: "", 296 | json: [], 297 | focusList: "", 298 | debounceTask: undefined 299 | }; 300 | }, 301 | 302 | 303 | watch: { 304 | options: function options(newVal, oldVal) { 305 | if (this.filterByAnchor) { 306 | var type = this.type, 307 | anchor = this.anchor; 308 | 309 | var regex = new RegExp("" + type, 'i'); 310 | var filtered = newVal.filter(function (item) { 311 | var found = item[anchor].search(regex) !== -1; 312 | return found; 313 | }); 314 | this.json = filtered; 315 | } else { 316 | this.json = newVal; 317 | } 318 | } 319 | }, 320 | 321 | methods: { 322 | getClassName: function getClassName(part) { 323 | var classes = this.classes, 324 | className = this.className; 325 | 326 | if (classes[part]) return "" + classes[part]; 327 | return className ? className + "-" + part : ''; 328 | }, 329 | 330 | 331 | // Netralize Autocomplete 332 | clearInput: function clearInput() { 333 | this.showList = false; 334 | this.type = ""; 335 | this.json = []; 336 | this.focusList = ""; 337 | }, 338 | 339 | 340 | // Get the original data 341 | cleanUp: function cleanUp(data) { 342 | return JSON.parse(JSON.stringify(data)); 343 | }, 344 | 345 | 346 | /*============================== 347 | INPUT EVENTS 348 | =============================*/ 349 | handleInput: function handleInput(e) { 350 | var _this = this; 351 | 352 | var value = e.target.value; 353 | 354 | this.showList = true; 355 | // Callback Event 356 | if (this.onInput) this.onInput(value); 357 | // If Debounce 358 | if (this.debounce) { 359 | if (this.debounceTask !== undefined) clearTimeout(this.debounceTask); 360 | this.debounceTask = setTimeout(function () { 361 | return _this.getData(value); 362 | }, this.debounce); 363 | } else { 364 | return this.getData(value); 365 | } 366 | }, 367 | handleKeyDown: function handleKeyDown(e) { 368 | var key = e.keyCode; 369 | 370 | // Disable when list isn't showing up 371 | if (!this.showList) return; 372 | 373 | // Key List 374 | var DOWN = 40; 375 | var UP = 38; 376 | var ENTER = 13; 377 | var ESC = 27; 378 | 379 | // Prevent Default for Prevent Cursor Move & Form Submit 380 | switch (key) { 381 | case DOWN: 382 | e.preventDefault(); 383 | this.focusList++; 384 | break; 385 | case UP: 386 | e.preventDefault(); 387 | this.focusList--; 388 | break; 389 | case ENTER: 390 | e.preventDefault(); 391 | this.selectList(this.json[this.focusList]); 392 | this.showList = false; 393 | break; 394 | case ESC: 395 | this.showList = false; 396 | break; 397 | } 398 | 399 | var listLength = this.json.length - 1; 400 | var outOfRangeBottom = this.focusList > listLength; 401 | var outOfRangeTop = this.focusList < 0; 402 | var topItemIndex = 0; 403 | var bottomItemIndex = listLength; 404 | 405 | var nextFocusList = this.focusList; 406 | if (outOfRangeBottom) nextFocusList = topItemIndex; 407 | if (outOfRangeTop) nextFocusList = bottomItemIndex; 408 | this.focusList = nextFocusList; 409 | }, 410 | setValue: function setValue(val) { 411 | this.type = val; 412 | }, 413 | 414 | 415 | /*============================== 416 | LIST EVENTS 417 | =============================*/ 418 | 419 | handleDoubleClick: function handleDoubleClick() { 420 | this.json = []; 421 | this.getData(""); 422 | // Callback Event 423 | this.onShow ? this.onShow() : null; 424 | this.showList = true; 425 | }, 426 | handleBlur: function handleBlur(e) { 427 | var _this2 = this; 428 | 429 | // Callback Event 430 | this.onBlur ? this.onBlur(e) : null; 431 | setTimeout(function () { 432 | // Callback Event 433 | _this2.onHide ? _this2.onHide() : null; 434 | _this2.showList = false; 435 | }, 250); 436 | }, 437 | handleFocus: function handleFocus(e) { 438 | this.focusList = 0; 439 | // Callback Event 440 | this.onFocus ? this.onFocus(e) : null; 441 | }, 442 | mousemove: function mousemove(i) { 443 | this.focusList = i; 444 | }, 445 | activeClass: function activeClass(i) { 446 | var focusClass = i === this.focusList ? 'focus-list' : ''; 447 | return "" + focusClass; 448 | }, 449 | selectList: function selectList(data) { 450 | // Deep clone of the original object 451 | var clean = this.cleanUp(data); 452 | // Put the selected data to type (model) 453 | this.type = clean[this.anchor]; 454 | // Hide List 455 | this.showList = false; 456 | // Callback Event 457 | this.onSelect ? this.onSelect(clean) : null; 458 | }, 459 | deepValue: function deepValue(obj, path) { 460 | var arrayPath = path.split('.'); 461 | for (var i = 0; i < arrayPath.length; i++) { 462 | obj = obj[arrayPath[i]]; 463 | } 464 | return obj; 465 | }, 466 | 467 | 468 | /*============================== 469 | AJAX EVENTS 470 | =============================*/ 471 | 472 | composeParams: function composeParams(val) { 473 | var _this3 = this; 474 | 475 | var encode = function encode(val) { 476 | return _this3.encodeParams ? encodeURIComponent(val) : val; 477 | }; 478 | var params = this.param + "=" + encode(val); 479 | if (this.customParams) { 480 | Object.keys(this.customParams).forEach(function (key) { 481 | params += "&" + key + "=" + encode(_this3.customParams[key]); 482 | }); 483 | } 484 | return params; 485 | }, 486 | composeHeader: function composeHeader(ajax) { 487 | var _this4 = this; 488 | 489 | if (this.customHeaders) { 490 | Object.keys(this.customHeaders).forEach(function (key) { 491 | ajax.setRequestHeader(key, _this4.customHeaders[key]); 492 | }); 493 | } 494 | }, 495 | doAjax: function doAjax(val) { 496 | var _this5 = this; 497 | 498 | // Callback Event 499 | this.onBeforeAjax ? this.onBeforeAjax(val) : null; 500 | // Compose Params 501 | var params = this.composeParams(val); 502 | // Init Ajax 503 | var ajax = new XMLHttpRequest(); 504 | ajax.open('GET', this.url + "?" + params, true); 505 | this.composeHeader(ajax); 506 | // Callback Event 507 | ajax.addEventListener('progress', function (data) { 508 | if (data.lengthComputable && _this5.onAjaxProgress) _this5.onAjaxProgress(data); 509 | }); 510 | // On Done 511 | ajax.addEventListener('loadend', function (e) { 512 | var responseText = e.target.responseText; 513 | 514 | var json = JSON.parse(responseText); 515 | // Callback Event 516 | _this5.onAjaxLoaded ? _this5.onAjaxLoaded(json) : null; 517 | _this5.json = _this5.process ? _this5.process(json) : json; 518 | }); 519 | // Send Ajax 520 | ajax.send(); 521 | }, 522 | getData: function getData(value) { 523 | if (value.length < this.min || !this.url) return; 524 | if (this.onShouldGetData) this.manualGetData(value);else this.doAjax(value); 525 | }, 526 | 527 | 528 | // Do Ajax Manually, so user can do whatever he want 529 | manualGetData: function manualGetData(val) { 530 | var _this6 = this; 531 | 532 | var task = this.onShouldGetData(val); 533 | if (task && task.then) { 534 | return task.then(function (options) { 535 | _this6.json = options; 536 | }); 537 | } 538 | } 539 | }, 540 | 541 | created: function created() { 542 | // Sync parent model with initValue Props 543 | this.type = this.initValue ? this.initValue : null; 544 | }, 545 | mounted: function mounted() { 546 | if (this.required) this.$refs.input.setAttribute("required", this.required); 547 | } 548 | }); 549 | 550 | /***/ }), 551 | /* 2 */ 552 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 553 | 554 | "use strict"; 555 | Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); 556 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__components_vue_autocomplete_vue__ = __webpack_require__(0); 557 | 558 | /* harmony default export */ __webpack_exports__["default"] = (__WEBPACK_IMPORTED_MODULE_0__components_vue_autocomplete_vue__["a" /* default */]); 559 | 560 | /***/ }), 561 | /* 3 */ 562 | /***/ (function(module, exports) { 563 | 564 | /* globals __VUE_SSR_CONTEXT__ */ 565 | 566 | // this module is a runtime utility for cleaner component module output and will 567 | // be included in the final webpack user bundle 568 | 569 | module.exports = function normalizeComponent ( 570 | rawScriptExports, 571 | compiledTemplate, 572 | injectStyles, 573 | scopeId, 574 | moduleIdentifier /* server only */ 575 | ) { 576 | var esModule 577 | var scriptExports = rawScriptExports = rawScriptExports || {} 578 | 579 | // ES6 modules interop 580 | var type = typeof rawScriptExports.default 581 | if (type === 'object' || type === 'function') { 582 | esModule = rawScriptExports 583 | scriptExports = rawScriptExports.default 584 | } 585 | 586 | // Vue.extend constructor export interop 587 | var options = typeof scriptExports === 'function' 588 | ? scriptExports.options 589 | : scriptExports 590 | 591 | // render functions 592 | if (compiledTemplate) { 593 | options.render = compiledTemplate.render 594 | options.staticRenderFns = compiledTemplate.staticRenderFns 595 | } 596 | 597 | // scopedId 598 | if (scopeId) { 599 | options._scopeId = scopeId 600 | } 601 | 602 | var hook 603 | if (moduleIdentifier) { // server build 604 | hook = function (context) { 605 | // 2.3 injection 606 | context = 607 | context || // cached call 608 | (this.$vnode && this.$vnode.ssrContext) || // stateful 609 | (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional 610 | // 2.2 with runInNewContext: true 611 | if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { 612 | context = __VUE_SSR_CONTEXT__ 613 | } 614 | // inject component styles 615 | if (injectStyles) { 616 | injectStyles.call(this, context) 617 | } 618 | // register component module identifier for async chunk inferrence 619 | if (context && context._registeredComponents) { 620 | context._registeredComponents.add(moduleIdentifier) 621 | } 622 | } 623 | // used by ssr in case component is cached and beforeCreate 624 | // never gets called 625 | options._ssrRegister = hook 626 | } else if (injectStyles) { 627 | hook = injectStyles 628 | } 629 | 630 | if (hook) { 631 | var functional = options.functional 632 | var existing = functional 633 | ? options.render 634 | : options.beforeCreate 635 | if (!functional) { 636 | // inject component registration as beforeCreate hook 637 | options.beforeCreate = existing 638 | ? [].concat(existing, hook) 639 | : [hook] 640 | } else { 641 | // register for functioal component in vue file 642 | options.render = function renderWithStyleInjection (h, context) { 643 | hook.call(context) 644 | return existing(h, context) 645 | } 646 | } 647 | } 648 | 649 | return { 650 | esModule: esModule, 651 | exports: scriptExports, 652 | options: options 653 | } 654 | } 655 | 656 | 657 | /***/ }), 658 | /* 4 */ 659 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 660 | 661 | "use strict"; 662 | var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h; 663 | return _c('div', { 664 | class: ((_vm.getClassName('wrapper')) + " autocomplete-wrapper") 665 | }, [_c('input', { 666 | directives: [{ 667 | name: "model", 668 | rawName: "v-model", 669 | value: (_vm.type), 670 | expression: "type" 671 | }], 672 | ref: "input", 673 | class: ((_vm.getClassName('input')) + " autocomplete-input"), 674 | attrs: { 675 | "type": "text", 676 | "id": _vm.id, 677 | "placeholder": _vm.placeholder, 678 | "name": _vm.name, 679 | "autocomplete": "off" 680 | }, 681 | domProps: { 682 | "value": (_vm.type) 683 | }, 684 | on: { 685 | "input": [function($event) { 686 | if ($event.target.composing) { return; } 687 | _vm.type = $event.target.value 688 | }, _vm.handleInput], 689 | "dblclick": _vm.handleDoubleClick, 690 | "blur": _vm.handleBlur, 691 | "keydown": _vm.handleKeyDown, 692 | "focus": _vm.handleFocus 693 | } 694 | }), _vm._v(" "), _c('div', { 695 | directives: [{ 696 | name: "show", 697 | rawName: "v-show", 698 | value: (_vm.showList && _vm.json.length), 699 | expression: "showList && json.length" 700 | }], 701 | class: ((_vm.getClassName('list')) + " autocomplete autocomplete-list") 702 | }, [_c('ul', _vm._l((_vm.json), function(data, i) { 703 | return _c('li', { 704 | class: _vm.activeClass(i) 705 | }, [_c('a', { 706 | attrs: { 707 | "href": "#" 708 | }, 709 | on: { 710 | "click": function($event) { 711 | $event.preventDefault(); 712 | _vm.selectList(data) 713 | }, 714 | "mousemove": function($event) { 715 | _vm.mousemove(i) 716 | } 717 | } 718 | }, [(_vm.onShouldRenderChild) ? _c('div', { 719 | domProps: { 720 | "innerHTML": _vm._s(_vm.onShouldRenderChild(data)) 721 | } 722 | }) : _vm._e(), _vm._v(" "), (!_vm.onShouldRenderChild) ? _c('div', [_c('b', { 723 | staticClass: "autocomplete-anchor-text" 724 | }, [_vm._v(_vm._s(_vm.deepValue(data, _vm.anchor)))]), _vm._v(" "), _c('span', { 725 | staticClass: "autocomplete-anchor-label" 726 | }, [_vm._v(_vm._s(_vm.deepValue(data, _vm.label)))])]) : _vm._e()])]) 727 | }))])]) 728 | } 729 | var staticRenderFns = [] 730 | render._withStripped = true 731 | var esExports = { render: render, staticRenderFns: staticRenderFns } 732 | /* harmony default export */ __webpack_exports__["a"] = (esExports); 733 | if (false) { 734 | module.hot.accept() 735 | if (module.hot.data) { 736 | require("vue-hot-reload-api").rerender("data-v-e47ae2be", esExports) 737 | } 738 | } 739 | 740 | /***/ }) 741 | /******/ ]); 742 | }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue 2 Autocomplete 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue2-autocomplete-js", 3 | "version": "0.2.1", 4 | "description": "Autocomplete Component for Vue 2", 5 | "main": "./dist/vue2-autocomplete.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=development webpack-dev-server --config ./webpack/webpack.config.js --inline --hot", 8 | "build": "cross-env NODE_ENV=production webpack --config ./webpack/webpack-prod.config.js -p", 9 | "build:prod": "cross-env NODE_ENV=production webpack --config ./webpack/webpack-bundle.config.js -p", 10 | "build:dist": "cross-env NODE_ENV=development webpack --config ./webpack/webpack-bundle.config.js", 11 | "prepublish": "npm run build && npm run build:prod && npm run build:dist" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/BosNaufal/vue2-autocomplete.git" 16 | }, 17 | "keywords": [ 18 | "vue-js", 19 | "autocomplete", 20 | "remote-list", 21 | "form", 22 | "component", 23 | "vue2-autocomplete", 24 | "form-helper", 25 | "vue-2" 26 | ], 27 | "author": "Naufal Rabbani ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/BosNaufal/vue2-autocomplete/issues" 31 | }, 32 | "homepage": "https://github.com/BosNaufal/vue2-autocomplete#readme", 33 | "devDependencies": { 34 | "babel-core": "^6.24.1", 35 | "babel-loader": "^6.4.1", 36 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 37 | "babel-preset-es2015": "^6.24.1", 38 | "babel-preset-stage-1": "^6.24.1", 39 | "cross-env": "^5.0.3", 40 | "css-loader": "^0.28.0", 41 | "es6-promise": "^4.1.0", 42 | "extract-text-webpack-plugin": "^2.1.2", 43 | "gulp": "^3.9.1", 44 | "gulp-autoprefixer": "^4.0.0", 45 | "gulp-cssmin": "^0.2.0", 46 | "gulp-cssnano": "^2.1.2", 47 | "gulp-group-css-media-queries": "^1.2.0", 48 | "gulp-rename": "^1.2.2", 49 | "gulp-sourcemaps": "^2.6.0", 50 | "gulp-string-replace": "^0.4.0", 51 | "jsx-control-statements": "^3.2.5", 52 | "postcss-loader": "^2.0.6", 53 | "style-loader": "^0.16.1", 54 | "vue-loader": "^13.0.2", 55 | "vue-template-compiler": "^2.4.2", 56 | "webpack": "^2.3.3", 57 | "webpack-dev-server": "^2.4.2" 58 | }, 59 | "dependencies": { 60 | "vue": "^2.4.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BosNaufal/vue2-autocomplete/c4b81a931fb182490e6f4b1c8fc6f37c6c80868f/src/img/demo.gif -------------------------------------------------------------------------------- /src/js/components/app.vue: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 56 | -------------------------------------------------------------------------------- /src/js/components/vue-autocomplete.vue: -------------------------------------------------------------------------------- 1 | 2 | 47 | 48 | 49 | 406 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './components/app.vue'; 3 | 4 | Vue.config.debug = true 5 | Vue.config.devtools = true 6 | 7 | new Vue(Vue.util.extend(App)).$mount('app') 8 | -------------------------------------------------------------------------------- /src/js/plugin.js: -------------------------------------------------------------------------------- 1 | import plugin from './components/vue-autocomplete.vue' 2 | export default plugin; 3 | -------------------------------------------------------------------------------- /webpack/webpack-bundle.config.js: -------------------------------------------------------------------------------- 1 | 2 | var webpack = require('webpack'); 3 | require('es6-promise').polyfill(); 4 | 5 | var npm = require("../package.json"); 6 | 7 | module.exports = { 8 | 9 | entry: __dirname + '/../src/js/plugin.js', 10 | 11 | output: { 12 | path: __dirname + '/../dist/', 13 | publicPath: '../dist/', 14 | filename: 'vue2-autocomplete.js', 15 | libraryTarget: "umd", 16 | library: "Vue2Autocomplete" 17 | }, 18 | 19 | externals: { 20 | "vue": "Vue" 21 | }, 22 | 23 | 24 | module: { 25 | 26 | loaders: [ 27 | { 28 | test: /\.vue$/, 29 | loader: 'vue-loader' 30 | }, 31 | 32 | { 33 | test: /\.js$/, 34 | exclude: /(node_modules|bower_components)/, 35 | loader: 'babel-loader', 36 | }, 37 | 38 | { 39 | test: /\.css$/, 40 | use: ['style-loader','css-loader'] 41 | } 42 | ] 43 | }, 44 | 45 | plugins: [ 46 | 47 | new webpack.BannerPlugin(( 48 | [ 49 | "Copyright (c) 2016 Naufal Rabbani (http://github.com/BosNaufal)", 50 | "\n", 51 | "Licensed Under MIT (http://opensource.org/licenses/MIT)", 52 | "\n", 53 | "\n", 54 | "Vue 2 Autocomplete @ Version "+ npm.version, 55 | "\n" 56 | ]) 57 | .join(",")), 58 | 59 | new webpack.DefinePlugin({ 60 | 'process.env': { 61 | 'NODE_ENV': '"production"' 62 | } 63 | }), 64 | 65 | ] 66 | 67 | }; 68 | -------------------------------------------------------------------------------- /webpack/webpack-prod.config.js: -------------------------------------------------------------------------------- 1 | 2 | var webpack = require('webpack'); 3 | require('es6-promise').polyfill(); 4 | 5 | module.exports = { 6 | 7 | devtool: 'eval', 8 | 9 | entry: __dirname + '/../src/js/main.js', 10 | 11 | output: { 12 | path: __dirname + '/../build', 13 | publicPath: '/build/', 14 | filename: 'build.js', 15 | chunkFilename: '[name].js' 16 | }, 17 | 18 | 19 | module: { 20 | 21 | loaders: [ 22 | { 23 | test: /\.vue$/, 24 | loader: 'vue-loader' 25 | }, 26 | 27 | { 28 | test: /\.js$/, 29 | exclude: /(node_modules|bower_components)/, 30 | loader: 'babel-loader', 31 | }, 32 | 33 | { 34 | test: /\.css$/, 35 | use: ['style-loader','css-loader'] 36 | } 37 | ] 38 | }, 39 | 40 | plugins: [ 41 | new webpack.DefinePlugin({ 42 | 'process.env': { 43 | 'NODE_ENV': '"production"' 44 | } 45 | }), 46 | ] 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | require('es6-promise').polyfill(); 3 | 4 | module.exports = { 5 | 6 | entry: __dirname + '/../src/js/main.js', 7 | 8 | output: { 9 | path: __dirname + '/../build', 10 | publicPath: '/build/', 11 | filename: 'build.js', 12 | chunkFilename: '[name].js' 13 | }, 14 | 15 | module: { 16 | loaders: [ 17 | { 18 | test: /\.vue$/, 19 | loader: 'vue-loader' 20 | }, 21 | { 22 | test: /\.js$/, 23 | exclude: /(node_modules|bower_components)/, 24 | loader: 'babel-loader', 25 | }, 26 | { 27 | test: /\.css$/, 28 | use: ['style-loader','css-loader'] 29 | } 30 | ] 31 | } 32 | 33 | }; 34 | --------------------------------------------------------------------------------