├── .gitignore
├── .travis.yml
├── Gruntfile.js
├── LICENSE
├── README.md
├── RELEASENOTES.md
├── bower.json
├── demo.gif
├── dist
├── ion-autocomplete.css
├── ion-autocomplete.js
├── ion-autocomplete.min.css
└── ion-autocomplete.min.js
├── karma.conf.js
├── package.json
├── protractor-conf.js
├── src
├── ion-autocomplete.css
└── ion-autocomplete.js
└── test
├── e2e
├── ion-autocomplete.multiple-select.e2e.html
├── ion-autocomplete.multiple-select.e2e.spec.js
├── ion-autocomplete.prepopulated.e2e.html
├── ion-autocomplete.prepopulated.e2e.spec.js
├── ion-autocomplete.single-select.e2e.html
└── ion-autocomplete.single-select.e2e.spec.js
├── ion-autocomplete.multiple-select.spec.js
├── ion-autocomplete.single-select.spec.js
└── templates
├── test-template-data.html
├── test-template-dynamic.html
└── test-template.html
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | bower_components
3 | npm-debug.log
4 | .idea
5 | *.iml
6 | build
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: 5.5.0
3 | before_install:
4 | - export CHROME_BIN=chromium-browser
5 | - export DISPLAY=:99.0
6 | - sh -e /etc/init.d/xvfb start
7 | - npm install -g grunt-cli
8 | install: npm install
9 | env:
10 | global:
11 | - secure: P11PFMRl2C6GGp22pSsSntUjXYN4blW0Q4hXGKWirVoxoie0f+2b0zy0f3rb9z/nmtOHKTwxfJgmarFV5UAV2BgvyNN97TGHc8BufCcBhLZyZL+KNNHatxDnAuFXn1Xrofl/sFUygySN6KgUcEbhpP+wUn5QO5EAzIW/embbUQY=
12 | - secure: OtdVVdeoOgyUijyo0px2VlPb++6CY2NIZuyvNUiHTn3UTsjWxvcJmumGGXeyTYWebO71xxLssmOiVjRN8FoLWMgaMl7dBJCNL3E5dnnYS75ek5JBVlJ+AlY4mI5NkA+WAPKKwJCWrcNthHat+lZHyUstFhqxMndbgT/C3V98kUo=
13 | addons:
14 | sauce_connect: true
15 | hosts: ion-autocomplete
16 | after_script: cat ./build/coverage/**/lcov.info | ./node_modules/coveralls/bin/coveralls.js
17 | sudo: false
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function (grunt) {
4 |
5 | // load grunt tasks automatically
6 | require('load-grunt-tasks')(grunt);
7 |
8 | // add time used for tasks statistics
9 | require('time-grunt')(grunt);
10 |
11 | grunt.initConfig({
12 | pkg: grunt.file.readJSON("package.json"),
13 | "bower-install-simple": {
14 | options: {
15 | color: true
16 | },
17 | dev: {
18 | options: {
19 | production: false
20 | }
21 | }
22 | },
23 | concat: {
24 | js: {
25 | options: {
26 | banner: "/*\n * <%= pkg.name %> <%= pkg.version %>\n * Copyright <%= grunt.template.today('yyyy') %> Danny Povolotski \n * Copyright modifications <%= grunt.template.today('yyyy') %> Guy Brand \n * https://github.com/guylabs/ion-autocomplete\n */\n(function() {\n\n'use strict';\n\n",
27 | footer: '\n})();',
28 | separator: '\n',
29 | process: true
30 | },
31 | src: 'src/ion-autocomplete.js',
32 | dest: 'dist/<%= pkg.name %>.js'
33 |
34 | },
35 | css: {
36 | src: 'src/ion-autocomplete.css',
37 | dest: 'dist/<%= pkg.name %>.css'
38 | }
39 | },
40 | uglify: {
41 | options: {
42 | banner: "/*\n * <%= pkg.name %> <%= pkg.version %>\n * Copyright <%= grunt.template.today('yyyy') %> Danny Povolotski \n * Copyright modifications <%= grunt.template.today('yyyy') %> Guy Brand \n * https://github.com/guylabs/ion-autocomplete\n */\n"
43 | },
44 | dist: {
45 | files: {
46 | 'dist/<%= pkg.name %>.min.js': ['<%= concat.js.dest %>']
47 | }
48 | }
49 | },
50 | cssmin: {
51 | target: {
52 | files: [{
53 | expand: true,
54 | cwd: 'src',
55 | src: ['ion-autocomplete.css'],
56 | dest: 'dist',
57 | ext: '.min.css'
58 | }]
59 | }
60 | },
61 | karma: {
62 | unit: {
63 | configFile: 'karma.conf.js'
64 | },
65 | continuous: {
66 | configFile: 'karma.conf.js',
67 | singleRun: true
68 | }
69 | },
70 | 'http-server': {
71 | dev: {
72 | runInBackground: true
73 | },
74 | debug: {
75 | runInBackground: false
76 | }
77 | },
78 | protractor: {
79 | options: {
80 | configFile: "protractor-conf.js"
81 | },
82 | run: {}
83 | },
84 | coveralls: {
85 | options: {
86 | src: 'coverage-results/lcov.info',
87 | force: true
88 | },
89 | your_target: {
90 | src: 'coverage-results/extra-results-*.info'
91 | }
92 | }
93 | });
94 |
95 | grunt.registerTask('build', ['bower-install-simple:dev', 'test', 'concat', 'uglify', 'cssmin']);
96 | grunt.registerTask('test', ['karma:continuous', 'http-server:dev', 'protractor:run']);
97 | grunt.registerTask('default', ['build']);
98 |
99 | // cannot use sauce labs with a pull requests
100 | if (parseInt(process.env.TRAVIS_PULL_REQUEST, 10) > 0) {
101 | grunt.registerTask('test', ['karma:continuous']);
102 | }
103 |
104 | };
105 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Danny Povolotski
4 | Copyright (c) 2015-2017 Modifications by Guy Brand
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ion-autocomplete
2 | ================
3 | [](https://travis-ci.org/guylabs/ion-autocomplete)
4 | [](https://coveralls.io/r/guylabs/ion-autocomplete)
5 | [](http://badge.fury.io/bo/ion-autocomplete)
6 | [](http://badge.fury.io/js/ion-autocomplete)
7 |
8 | > Configurable Ionic directive for an autocomplete dropdown.
9 |
10 | :warning: Please follow the [Guidelines to report an issue](#guidelines-to-report-an-issue)
11 |
12 | #Table of contents
13 |
14 | - [Demo](#demo)
15 | - [Introduction](#introduction)
16 | - [Features](#features)
17 | - [Installation](#installation)
18 | - [Ionic compatibility](#ionic-compatibility)
19 | - [Usage](#usage)
20 | - [Configurable options](#configurable-options)
21 | - [The `items-method`](#the-items-method)
22 | - [The `items-method-value-key`](#the-items-method-value-key)
23 | - [The `item-value-key`](#the-item-value-key)
24 | - [The `item-view-value-key`](#the-item-view-value-key)
25 | - [The `max-selected-items`](#the-max-selected-items)
26 | - [The `items-clicked-method`](#the-items-clicked-method)
27 | - [The `items-removed-method`](#the-items-removed-method)
28 | - [External model](#external-model)
29 | - [The `model-to-item-method`](#the-model-to-item-method)
30 | - [The `cancel-button-clicked-method` (same as done button)](#the-cancel-button-clicked-method-same-as-done-button)
31 | - [ComponentId](#component-id)
32 | - [Placeholder](#placeholder)
33 | - [Cancel button label](#cancel-button-label)
34 | - [Select items label](#select-items-label)
35 | - [Selected items label](#selected-items-label)
36 | - [Template url](#template-url)
37 | - [Template data](#template-data)
38 | - [Loading icon](#loading-icon)
39 | - [Manage externally](#manage-externally)
40 | - [Selected items](#selected-items)
41 | - [Clear on select](#clear-on-select)
42 | - [Open and close CSS class ](#open-and-close-css-class)
43 | - [Using expressions in value keys](#using-expressions-in-value-keys)
44 | - [Debouncing](#debouncing)
45 | - [Usage inside an Ionic modal](#usage-inside-an-ionic-modal)
46 | - [Guidelines to report an issue](#guidelines-to-report-an-issue)
47 | - [Release notes](#release-notes)
48 | - [Acknowledgements](#acknowledgements)
49 | - [License](#license)
50 |
51 | # Demo
52 |
53 | You can find a live demo on [Codepen](http://codepen.io/guylabs/pen/GJmwMw) or see it in action in the following image:
54 |
55 | 
56 |
57 | # Introduction
58 |
59 | For one of my private projects I needed an autocomplete component in Ionic. I searched a lot and found some plain Angular autocompletes, but these had too much other dependencies and mostly didn't look that good within Ionic. Then one day I stumbled upon the [ion-google-place](https://github.com/israelidanny/ion-google-place) project which was exactly what I was looking for, except that it was just working with the Google Places API. So I forked the project and made it configurable such that you can add the service you need. The differences between the ion-google-place project and the ion-autocomplete are listed in the features.
60 |
61 | # Features
62 |
63 | The ion-autocomplete component has the following features:
64 | - Multiple selection support
65 | - Configurable service which provides the items to list
66 | - Allow to define the maximum number of selected items
67 | - Configure what is stored in the model and what is seen in the list
68 | - Configure the template used to show the autocomplete component
69 | - Configure a callback when an item is clicked/removed
70 | - Configure a callback when the cancel/done button is clicked
71 | - Configure all labels used in the component
72 |
73 | # Installation
74 |
75 | 1. Use bower to install the new module:
76 | ```bash
77 | bower install ion-autocomplete --save
78 | ```
79 | 2. Import the `ion-autocomplete` javascript and css file into your HTML file:
80 | ```html
81 |
82 |
83 | ```
84 | 3. Add `ion-autocomplete` as a dependency on your Ionic app:
85 | ```javascript
86 | angular.module('myApp', [
87 | 'ionic',
88 | 'ion-autocomplete'
89 | ]);
90 | ```
91 |
92 | # Ionic compatibility
93 |
94 | The ion-autocomplete component is running with the following Ionic versions:
95 |
96 | ion-autocomplete version | Ionic version
97 | ------------------------ | -------------
98 | 0.0.2 - 0.1.2 | 1.0.0-beta.14
99 | 0.2.0 - 0.2.1 | 1.0.0-rc.3
100 | 0.2.2 - 0.2.3 | 1.0.0
101 | 0.3.0 - 0.3.1 | 1.1.0
102 | 0.3.2 - 0.3.3 | 1.1.1
103 | 0.4.0 - latest | 1.3.2
104 |
105 | # Usage
106 |
107 | To use the `ion-autocomplete` directive in single select mode you need set the `max-selected-items` attribute and add the following snippet to your template:
108 | ```html
109 | //usage with the attribute restriction
110 |
111 | ```
112 |
113 | If you want to use it in multiple select mode you do not need to add anything special, just the following snippet to your template:
114 | ```html
115 | //usage with the attribute restriction
116 |
117 | ```
118 |
119 | Check out the next chapter on how to configure the directive.
120 |
121 | ## Configurable options
122 |
123 | ### The `items-method`
124 |
125 | You are able to pass in a callback method which gets called when the user changes the value of the search input field. This is
126 | normally a call to the back end which retrieves the items for the specified query. Here is a small sample which will
127 | return a static item of the query:
128 |
129 | Define the callback in your scope:
130 | ```javascript
131 | $scope.callbackMethod = function (query, isInitializing) {
132 | return [query];
133 | }
134 | ```
135 |
136 | And set the items method on the directive:
137 | ```html
138 |
139 | ```
140 |
141 | You are also able to return a promise from this callback method. For example:
142 | ```javascript
143 | $scope.callbackMethod = function (query, isInitializing) {
144 | return $http.get(endpoint);
145 | }
146 | ```
147 |
148 | Note that the parameter for the `callbackMethod` needs to be named `query`. Otherwise the callback will not get called properly.
149 | If you want to also retrieve the [ComponentId](#component-id) then you need to add a second parameter called `componentId`:
150 | ```javascript
151 | $scope.callbackMethod = function (query, isInitializing, componentId) {
152 | if(componentId == "component1") {
153 | return $http.get(endpoint1);
154 | }
155 | return [query];
156 | }
157 | ```
158 |
159 | If you want to pre populate the items which are shown when the modal is visible before the user enters a query then you can check the `isInitializing` flag of
160 | the `items-method` as this is set to true if it is called for the initial items. Here is an example which shows the `test` item as an initial item:
161 | ```javascript
162 | $scope.callbackMethod = function (query, isInitializing) {
163 | if(isInitializing) {
164 | // depends on the configuration of the `items-method-value-key` (items) and the `item-value-key` (name) and `item-view-value-key` (name)
165 | return { items: [ { name: "test" } ] }
166 | } else {
167 | return $http.get(endpoint);
168 | }
169 | }
170 | ```
171 |
172 | If you want to clear the list each time the user opens the modal then just return an empty array like in the following example:
173 | ```javascript
174 | $scope.callbackMethod = function (query, isInitializing) {
175 | if(isInitializing) {
176 | // depends on the configuration of the `items-method-value-key` (items) and the `item-value-key` (name) and `item-view-value-key` (name)
177 | return { items: [] }
178 | } else {
179 | return $http.get(endpoint);
180 | }
181 | }
182 | ```
183 |
184 | And if you do not want that the searched items list gets modified then just return nothing as in this example:
185 | ```javascript
186 | $scope.callbackMethod = function (query, isInitializing) {
187 | if(!isInitializing) {
188 | return $http.get(endpoint);
189 | }
190 | }
191 | ```
192 |
193 | A common usage for the `items-method` is to use the [Google Map Geocode API](https://developers.google.com/maps/documentation/geocoding/intro?hl=de#Geocoding) for address suggestions.
194 |
195 | To use Googles API you need to link the required library in your `index.html` file:
196 | ```html
197 |
198 | ```
199 |
200 | In the `ion-autocomplete` input field you set the `items-method` to the below shown method `getAddressSuggestions` and set the value key to `formatted_address` to display the formatted address:
201 | ```html
202 |
205 | ```
206 |
207 | To query Googles API you have to create a `Geocoder` instance and use the `queryString` as input and return the result object in a promise.
208 | ```javascript
209 | var geocoder = new google.maps.Geocoder();
210 |
211 | $scope.getAddressSuggestions(queryString){
212 | var defer = $q.defer();
213 | geocoder.geocode(
214 | {address: queryString},
215 | function (results, status) {
216 | if (status == google.maps.GeocoderStatus.OK) { defer.resolve(results); }
217 | else { defer.reject(results); }
218 | }
219 | );
220 | return defer.promise;
221 | }
222 | ```
223 |
224 |
225 | ### The `items-method-value-key`
226 |
227 | You are able to set the `items-method-value-key` attribute which maps to a value of the returned data of the `items-method`. If for
228 | example your callback method returns the following object:
229 | ```json
230 | {
231 | "items" : [ {
232 | "name" : "item1"
233 | },{
234 | "name" : "item2"
235 | },
236 | ...
237 | ]
238 | }
239 | ```
240 | Then when you do not specify the `items-method-value-key` there will be no list displayed when you search for items in
241 | the search input field. You need to set the `items-method-value-key` to `items` such that the items are shown. If you right
242 | away return an array of items then you do not need to set the `items-method-value-key`.
243 |
244 | ### The `item-value-key`
245 |
246 | You are able to set the `item-value-key` attribute which maps to a value of the returned object from the `items-method`. The value
247 | is then saved in the defined `ng-model`. Here an example:
248 |
249 | The items method returns the following object:
250 | ```javascript
251 | [
252 | {
253 | "id": "1",
254 | "name": "Item 1",
255 | ...
256 | }
257 | ...
258 | ]
259 | ```
260 |
261 | And now you set the following `item-value-key`:
262 | ```html
263 |
264 | ```
265 |
266 | Now when the user selects the `Item 1` from the list, then the value of the objects `id` is stored in the `ng-model`. If
267 | no `item-value-key` is passed into the directive, the whole item object will be stored in the `ng-model`.
268 |
269 | ### The `item-view-value-key`
270 |
271 | You are able to set the `item-view-value-key` attribute which maps to a value of the returned object from the `items-method`. The
272 | value is then showed in both input fields. Here an example:
273 |
274 | The `items-method` returns the following object:
275 | ```javascript
276 | [
277 | {
278 | "id": "1",
279 | "name": "Item 1",
280 | ...
281 | }
282 | ...
283 | ]
284 | ```
285 |
286 | And now you set the following `item-view-value-key`:
287 | ```html
288 |
289 | ```
290 |
291 | Now when the user selects the `Item 1` from the list, then the value of the objects `name` is showed in both input fields. If
292 | no `item-view-value-key` is passed into the directive, the whole item object will be showed in both input fields.
293 |
294 | ### The `max-selected-items`
295 |
296 | You are able to set the `max-selected-items` attribute to any number to set the maximum selectable items inside the component. Here an example:
297 | ```html
298 |
299 | ```
300 |
301 | Then the user is just able to select three items out of the returned items and also delete them again. The given `ng-model` is an
302 | array if multiple items are selected.
303 |
304 | You can also set a scope variable instead of a fixed value such that you can dynamically change the `max-selected-items` property according to your
305 | requirements.
306 |
307 | ### The `items-clicked-method`
308 |
309 | You are able to pass a function to the `items-clicked-method` attribute to be notified when an item is clicked. The name of the
310 | parameter of the function must be `callback`. Here is an example:
311 |
312 | Define the callback in your scope:
313 | ```javascript
314 | $scope.clickedMethod = function (callback) {
315 | // print out the selected item
316 | console.log(callback.item);
317 |
318 | // print out the component id
319 | console.log(callback.componentId);
320 |
321 | // print out the selected items if the multiple select flag is set to true and multiple elements are selected
322 | console.log(callback.selectedItems);
323 |
324 | // print out the selected items as an array
325 | console.log(callback.selectedItemsArray);
326 | }
327 | ```
328 |
329 | And pass in the callback method in the directive:
330 | ```html
331 |
332 | ```
333 |
334 | Then you get a callback object with the clicked/selected item and the selected items if you have multiple selected items (see [The `multiple-select`](#the-multiple-select)).
335 |
336 | ### The `items-removed-method`
337 |
338 | You are able to pass a function to the `items-removed-method` attribute to be notified when an item is removed from a multi-select list. The name of the
339 | parameter of the function must be `callback`. It is similar to items-clicked-method. This attribute has no defined behaviour for a single select list.
340 |
341 | Here is an example:
342 |
343 | Define the callback in your scope:
344 | ```javascript
345 | $scope.removedMethod = function (callback) {
346 | // print out the removed item
347 | console.log(callback.item);
348 |
349 | // print out the component id
350 | console.log(callback.componentId);
351 |
352 | // print out the selected items
353 | console.log(callback.selectedItems);
354 |
355 | // print out the selected items as an array
356 | console.log(callback.selectedItemsArray);
357 | }
358 | ```
359 |
360 | And pass in the callback method in the directive:
361 | ```html
362 |
363 | ```
364 |
365 | Then you get a callback object with the removed item and the selected items.
366 |
367 | ### External model
368 |
369 | The two way binded external model (`external-model` attribute on the component) is used to prepopulate the selected items with the model value. The [`model-to-item-method`](#the-model-to-item-method) is used to get the view item to the model and then the item is selected in the
370 | component. Be aware that the `external-model` is not updated by the component when an item is selected. It is just used to prepopulate or clear the selected items. If you need to get the current selected items you are able
371 | to read the value of the `ng-model`. For an example have a look at the [`model-to-item-method`](#the-model-to-item-method) documentation.
372 |
373 | If you need to clear the selected items then you are able to set the `external-model` to an empty array (another value is not clearing the selected items).
374 |
375 | ### The `model-to-item-method`
376 |
377 | This method is used if you want to prepopulate the model of the `ion-autocomplete` component. The [external model](#external-model) needs
378 | to have the same data as it would have when you select the items by hand. The component then takes the model values
379 | and calls the specified `model-to-item-method` to resolve the item from the back end and select it such that it is preselected.
380 |
381 | Here a small example:
382 |
383 | Define the `model-to-item-method` and `external-model` in your scope:
384 | ```javascript
385 | $scope.modelToItemMethod = function (modelValue) {
386 |
387 | // get the full model item from the model value and return it. You need to implement the `getModelItem` method by yourself
388 | // as this is just a sample. The method needs to retrieve the whole item (like the `items-method`) from just the model value.
389 | var modelItem = getModelItem(modelValue);
390 | return modelItem;
391 | }
392 | $scope.externalModel = ['test1', 'test2', 'test3'];
393 | ```
394 |
395 | And set the `model-to-item-method` on the directive:
396 | ```html
397 |
398 | ```
399 |
400 | You are also able to return a promise from this callback method. For example:
401 | ```javascript
402 | $scope.modelToItemMethod = function (modelValue) {
403 | return $http.get(endpoint + '?q=' + modelValue);
404 | }
405 | ```
406 |
407 | Note that the parameter for the `model-to-item-method` needs to be named `modelValue`. Otherwise the callback will not get called properly.
408 |
409 | ### The `cancel-button-clicked-method` (same as done button)
410 |
411 | You are able to pass a function to the `cancel-button-clicked-method` attribute to be notified when the cancel/done button is clicked to close the modal. The name of the
412 | parameter of the function must be `callback`. Here is an example:
413 |
414 | Define the callback in your scope:
415 | ```javascript
416 | $scope.cancelButtonClickedMethod = function (callback) {
417 | // print out the component id
418 | console.log(callback.componentId);
419 |
420 | // print out the selected items
421 | console.log(callback.selectedItems);
422 |
423 | // print out the selected items as an array
424 | console.log(callback.selectedItemsArray);
425 | }
426 | ```
427 |
428 | And pass in the callback method in the directive:
429 | ```html
430 |
431 | ```
432 |
433 | Then you get a callback object with the selected items and the component id.
434 |
435 | ### Component Id
436 |
437 | The component id is an attribute on the `ion-autocomplete` component which sets a given id to the component. This id is then returned in
438 | the callback object of the [`items-clicked-method`](#the-items-clicked-method) and as a second parameter of the [`items-method`](#the-items-method).
439 | Here an example:
440 | ```html
441 | `
442 | ```
443 |
444 | You are able to set this is on each component if you have multiple components built up in a ng-repeat where you do not want to have multiple `items-method`
445 | for each component because you want to display other items in each component. You will also get it in the `items-clicked-method` callback object such that you just
446 | need to define one callback method and you can distinguish the calls with the `componentId` attribute right inside the method.
447 |
448 | ### Placeholder
449 |
450 | You are also able to set the placeholder on the input field and on the search input field if you add the `placeholder`
451 | attribute to the directive:
452 | ```html
453 | `
454 | ```
455 |
456 | ### Cancel button label
457 |
458 | You are also able to set the cancel button label (defaults to `Cancel`) if you add the `cancel-label` attribute to the directive:
459 | ```html
460 | `
461 | ```
462 |
463 | ### Select items label
464 |
465 | You are also able to set the select items label (defaults to `Select an item...`) if you add the `select-items-label` attribute to the directive:
466 | ```html
467 | `
468 | ```
469 |
470 | ### Selected items label
471 |
472 | You are also able to set the selected items label (defaults to `Selected items:`) if you add the `selected-items-label` attribute to the directive:
473 | ```html
474 | `
475 | ```
476 |
477 | ### Template url
478 |
479 | You are also able to set an own template for the autocomplete component (defaults to `''`) if you add the `template-url` attribute to the directive:
480 | ```html
481 | `
482 | ```
483 |
484 | This way you are able to override the default template (the `template` variable [here](https://github.com/guylabs/ion-autocomplete/blob/master/src/ion-autocomplete.js#L68))
485 | and use your own template. The component will use the default template if the `template-url` is not defined.
486 |
487 | You are able to use all the configurable attributes as expressions in your template. I would advise to use the default template as base template
488 | and then add your custom additions to it.
489 |
490 | > Please also take care when you change how the items are shown or what method is called if an item is clicked,
491 | > because changing this could make the component unusable.
492 |
493 | You will need to set the proper `randomCssClass` for the outer most div container in your template and you can get the value by using the `{{viewModel.randomCssClass}}` expression
494 | like in the following example:
495 |
496 | ```html
497 |
498 | ```
499 |
500 | ### Template data
501 |
502 | If you change the template with the `template-url` and want to pass in additional data then you are able to set
503 | the `template-data` attribute on the directive. If you for example have a `templateData.testData` expression in your own
504 | template like this:
505 | ```html
506 | ...
507 |
{{templateData.testData}}
508 | ...
509 | ```
510 | Then you need to set the proper object on your Angular scope the following way:
511 | ```javascript
512 | $scope.templateData = {
513 | testData: "test-data"
514 | };
515 | ```
516 | And now you just need to add the `templateData` attribute on the directive:
517 | ```html
518 | `
519 | ```
520 |
521 | Then the expression in your template gets resolved properly.
522 |
523 | ### Loading icon
524 |
525 | If you want to display a loading icon when the `items-method` promise gets resolved then you need to set the `loading-icon`
526 | attribute to a value given by the Ionic spinner: http://ionicframework.com/docs/api/directive/ionSpinner. Then the spinner should
527 | be shown at the right side of the search input field.
528 |
529 | ### Manage externally
530 |
531 | To manage the `ion-autocomplete` component externally means that you need to handle when the search modal is shown. To enable this functionality
532 | you need to set the `manage-externally` attribute to `true` and then you can call the `showModal()` method on the controller. Here an example:
533 |
534 | ```javascript
535 | // create the externally managed component and a button which has a click handler to a scope method
536 |
537 |
538 |
539 | // inside your controller you can define the 'clickButton()' method the following way
540 | this.clickButton = function () {
541 | var ionAutocompleteElement = document.getElementsByClassName("ion-autocomplete");
542 | angular.element(ionAutocompleteElement).controller('ionAutocomplete').fetchSearchQuery("", true);
543 | angular.element(ionAutocompleteElement).controller('ionAutocomplete').showModal();
544 | }
545 | ```
546 |
547 | Then you will need to click on the button to open the search modal. This functionality is useful if the user wants to edit the selected item inside the
548 | input field after she/he selected the item/s.
549 |
550 | ### Selected items
551 |
552 | If you want to clear the selected items programmatically, then you are able to set the `selected-items` attribute with a two way binded model value which then gets updated
553 | when the items get selected. If you want to clear them just set the given model value to an empty array.
554 |
555 | Please *do not* use it for pre populating the selected items. For this use the standard `ng-model` value and [the `model-to-item-method`](#the-model-to-item-method).
556 |
557 | ### Clear on select
558 |
559 | This option is to clear the search input when an item is selected. You need to set it to `true` as in the following example to enable this functionality:
560 |
561 | ```javascript
562 |
563 | ```
564 |
565 | ### Open and close CSS class
566 |
567 | By default two CSS classes are used to display and hide the modal, namely the ion-autocomplete-open and the ion-autocomplete-close CSS class. These are used to show and hide the modal. When you need to override these classes, you can define the following two properties with your CSS class.
568 |
569 | ```javascript
570 |
571 | ```
572 |
573 | ## Using expressions in value keys
574 |
575 | All value keys are parsed with the Angular `$parse` service such that you are able to use expressions like in the following
576 | example:
577 |
578 | ```javascript
579 | [
580 | {
581 | "id": "1",
582 | "name": "Item 1",
583 | "child": {
584 | "name": "Child Item 1",
585 | }
586 | ...
587 | }
588 | ...
589 | ]
590 | ```
591 |
592 | This would be the JSON model returned by the `items-method` and in the next snippet we define that we want to show the
593 | name attribute of the child object:
594 |
595 | ```html
596 |
597 | ```
598 |
599 | ## Debouncing
600 |
601 | If you want to debounce the search input field request, then you are able to set the `ng-model-options` attribute on the input field where you define the `ion-autocomplete`
602 | directive. These options will then be added to the search input field. Be aware that when you add a debounce the update of the model value will also be debounced the
603 | same amount as the request to the `items-method`. Here a small example:
604 |
605 | ```html
606 |
607 | ```
608 |
609 | ## Usage inside an Ionic modal
610 |
611 | When you add the `ion-autocomplete` component to a separate Ionic modal, then you need to remove the modal when you switch the view. You can achieve this by adding the following scope `$destroy` listener where the separate modal is removed:
612 |
613 | ```javascript
614 | $scope.$on('$destroy', function () {
615 | $scope.modal.remove();
616 | });
617 | ```
618 |
619 | # Guidelines to report an issue
620 |
621 | Please follow these rules when you create an issue here in Github:
622 |
623 | 1. Have a meaningful title of the issue.
624 | 2. Describe exactly how to reproduce the issue and create a Codepen based on the [demo](#demo) Codepen which reproduces the issue.
625 | 3. Show how you configured the directive with all the options.
626 | 4. Write down the Ionic version you use and which version of the directive.
627 |
628 | These steps are needed to be able to analyze the issue properly without asking much questions. It is also useful for others when the issues
629 | exactly describe what the problem is and in which environment it happened.
630 |
631 | For feature request please add a proper title and describe it as much as possible and also tell about the requirement you have.
632 |
633 | # Release notes
634 |
635 | Check them here: [Release notes](https://github.com/guylabs/ion-autocomplete/blob/master/RELEASENOTES.md)
636 |
637 | # Acknowledgements
638 |
639 | When I first searched for an Ionic autocomplete component I just found the project from Danny. So please have a look at
640 | his [ion-google-place](https://github.com/israelidanny/ion-google-place) project as this project here is a fork of it.
641 | At this point I want to thank him for his nice work.
642 |
643 | # License
644 |
645 | This Ionic autocomplete directive is available under the MIT license.
646 |
647 | (c) Danny Povolotski
648 |
649 | (c) Modifications by Guy Brand
650 |
--------------------------------------------------------------------------------
/RELEASENOTES.md:
--------------------------------------------------------------------------------
1 | # Release notes of ion-autocomplete
2 |
3 | ## Version 0.4.0
4 |
5 | * Tag: [0.4.0](https://github.com/guylabs/ion-autocomplete/tree/v0.4.0)
6 | * Release: [ion-autocomplete-0.4.0.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.4.0.zip)
7 |
8 | ### Changes
9 |
10 | * Upgraded to Ionic `1.3.2` and Angular `1.5.3` - [#189](https://github.com/guylabs/ion-autocomplete/issues/189)
11 | * Added open and close CSS class feature. Thanks to [@nlarche](https://github.com/nlarche) for the pull request. - [#221](https://github.com/guylabs/ion-autocomplete/pull/221)
12 |
13 | ## Version 0.3.3
14 |
15 | * Tag: [0.3.3](https://github.com/guylabs/ion-autocomplete/tree/v0.3.3)
16 | * Release: [ion-autocomplete-0.3.3.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.3.3.zip)
17 |
18 | ### Changes
19 |
20 | * Several bugs are fixed.
21 | * New `clearOnSelect` option - [#186](https://github.com/guylabs/ion-autocomplete/pull/186)
22 | * The `maxSelectedItems` option is now two way binded allowing for dynamic values - [#140](https://github.com/guylabs/ion-autocomplete/issues/140)
23 | * Added a new property `selectedItemsArray` to the callback responses - [#115](https://github.com/guylabs/ion-autocomplete/issues/115)
24 |
25 | ## Version 0.3.2
26 |
27 | * Tag: [0.3.2](https://github.com/guylabs/ion-autocomplete/tree/v0.3.2)
28 | * Release: [ion-autocomplete-0.3.2.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.3.2.zip)
29 |
30 | ### Changes
31 |
32 | * A single selected item is not an array anymore with a single value - [#115](https://github.com/guylabs/ion-autocomplete/issues/115)
33 | * The item repeat has been switched from `collection-repeat` to `ng-repeat` as there were some issues - [#126](https://github.com/guylabs/ion-autocomplete/issues/126)
34 |
35 | ### Migration notes
36 |
37 | * As part of the [#115](https://github.com/guylabs/ion-autocomplete/issues/115) issue when you now select a single value (`maxSelectedItems=1`) then the item
38 | is now returned as object and not as an array with one element. If you use a custom template, please also check the changes in the default template.
39 |
40 | ## Version 0.3.1
41 |
42 | * Tag: [0.3.1](https://github.com/guylabs/ion-autocomplete/tree/v0.3.1)
43 | * Release: [ion-autocomplete-0.3.1.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.3.1.zip)
44 |
45 | ### Changes
46 |
47 | * Added the `max-selected-items` attribute to restrict the selected search items - [#58](https://github.com/guylabs/ion-autocomplete/issues/58).
48 | * Added the `cancel-button-clicked-method` attribute to be able to get notified when the cancel button is clicked - [#63](https://github.com/guylabs/ion-autocomplete/issues/63).
49 | * Added the `external-model` attribute to be able to prepopulate the selected items and to clear them programmatically - [#66](https://github.com/guylabs/ion-autocomplete/issues/66), [#89](https://github.com/guylabs/ion-autocomplete/issues/89).
50 | * Added the ability to pass the `ng-model-options` to the inner search input field - [#91](https://github.com/guylabs/ion-autocomplete/issues/91)
51 | * Added the ability to initialize the search items within the `items-method` - [#57](https://github.com/guylabs/ion-autocomplete/issues/57)
52 |
53 | ### Migration notes
54 |
55 | * As of version `0.3.1` the `multiple-select` attribute has been dropped in favor of the `max-selected-items` attribute.
56 | Please have a look at the documentation here https://github.com/guylabs/ion-autocomplete#the-max-selected-items on how to migrate this.
57 | * The `search-items` attribute has been removed as now the initialization of the `search-items` is done in the `items-method`. See the
58 | new documentation here https://github.com/guylabs/ion-autocomplete#the-items-method.
59 |
60 | ## Version 0.3.0
61 |
62 | * Tag: [0.3.0](https://github.com/guylabs/ion-autocomplete/tree/v0.3.0)
63 | * Release: [ion-autocomplete-0.3.0.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.3.0.zip)
64 |
65 | ### Changes
66 |
67 | * Upgraded to Ionic 1.1.0 and Angular 1.4.3.
68 | * Fixed an issue with multiple `ion-autocomplete` directives on one page.
69 | * Prepared for Angular 2.0.
70 | * Upgraded project to use newest libraries.
71 |
72 | ### Migration notes
73 |
74 | * As of version `0.3.0` the component does not support the element restriction anymore, such that you are just able to
75 | use the attribute restriction on all your elements. This means that you need to convert all ``
76 | tags to the following tag: ``
77 |
78 | ## Version 0.2.3
79 |
80 | * Tag: [0.2.3](https://github.com/guylabs/ion-autocomplete/tree/v0.2.3)
81 | * Release: [ion-autocomplete-0.2.2.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.2.3.zip)
82 |
83 | ### Changes
84 |
85 | * Add new `model-to-item-method` to be able to prepopulate the model. (See issue [#25](https://github.com/guylabs/ion-autocomplete/issues/25))
86 | * Fixed issue 'Feature: Loading icon' - [#12](https://github.com/guylabs/ion-autocomplete/issues/12).
87 | * Fixed issue 'Bug: Cannot pass in pre-populated model' - [#25](https://github.com/guylabs/ion-autocomplete/issues/25).
88 | * Fixed issue 'Bug: $http promise not working' - [#27](https://github.com/guylabs/ion-autocomplete/issues/27).
89 | * Fixed issue 'Bug: Item list from variable won't show first time you start typing' - [#30](https://github.com/guylabs/ion-autocomplete/issues/30).
90 | * Fixed issue 'Feature: No callback when items removed' - [#32](https://github.com/guylabs/ion-autocomplete/issues/32).
91 | * Fixed issue 'Bug: JS error when using with jQuery' - [#34](https://github.com/guylabs/ion-autocomplete/issues/34).
92 | * Fixed issue 'Feature: Display all items if query is empty' - [#38](https://github.com/guylabs/ion-autocomplete/issues/38).
93 | * Fixed issue 'Feature: Ability to pass in arbitrary data to pass to the template' - [#39](https://github.com/guylabs/ion-autocomplete/issues/39).
94 | * Fixed issue 'Feature: Autocomplete box should open on as well as click' - [#43](https://github.com/guylabs/ion-autocomplete/issues/43).
95 |
96 | ### Migration notes
97 |
98 | * The query can now also be empty in the `items-method` and this could change the logic in your `items-method`. Please check [#38](https://github.com/guylabs/ion-autocomplete/issues/38) for more information.
99 |
100 | ## Version 0.2.2
101 |
102 | * Tag: [0.2.2](https://github.com/guylabs/ion-autocomplete/tree/v0.2.2)
103 | * Release: [ion-autocomplete-0.2.2.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.2.2.zip)
104 |
105 | ### Changes
106 |
107 | * Fixed issue 'Model binding issue' - [#14](https://github.com/guylabs/ion-autocomplete/issues/14).
108 | * Fixed issue 'Upgrade to Ionic 1.0.0' - [#15](https://github.com/guylabs/ion-autocomplete/issues/15).
109 | * Fixed issue 'Cant display list in from query' - [#16](https://github.com/guylabs/ion-autocomplete/issues/16).
110 | * Fixed issue 'How can I call the auto complete form in the ng-click' - [#18](https://github.com/guylabs/ion-autocomplete/issues/18).
111 | * Fixed issue 'I cant get autocomplete to work in my project' - [#21](https://github.com/guylabs/ion-autocomplete/issues/21).
112 | * Fixed issue 'Add version table to documentation' - [#22](https://github.com/guylabs/ion-autocomplete/issues/22).
113 | * Fixed issue 'Ability to pass sort of id to the item-method' - [#23](https://github.com/guylabs/ion-autocomplete/issues/23).
114 |
115 | ## Version 0.2.1
116 |
117 | * Tag: [0.2.1](https://github.com/guylabs/ion-autocomplete/tree/v0.2.1)
118 | * Release: [ion-autocomplete-0.2.1.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.2.1.zip)
119 |
120 | ### Changes
121 |
122 | * Fixed issue [#5](https://github.com/guylabs/ion-autocomplete/issues/5).
123 | * Fixed issue [#6](https://github.com/guylabs/ion-autocomplete/issues/6).
124 | * Fixed issue [#8](https://github.com/guylabs/ion-autocomplete/issues/8).
125 | * Fixed issue [#10](https://github.com/guylabs/ion-autocomplete/issues/10).
126 |
127 | ## Version 0.2.0
128 |
129 | * Tag: [0.2.0](https://github.com/guylabs/ion-autocomplete/tree/v0.2.0)
130 | * Release: [ion-autocomplete-0.2.0.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.2.0.zip)
131 |
132 | ### Changes
133 |
134 | * Add the ability to select multiple items
135 | * Add ability to pass in a callback function when an item is clicked
136 | * Use `collection-repeat` instead of `ng-repeat` to improve the performance
137 | * Add the ability to use an own external template
138 |
139 | ## Version 0.1.2
140 |
141 | * Tag: [0.1.2](https://github.com/guylabs/ion-autocomplete/tree/v0.1.2)
142 | * Release: [ion-autocomplete-0.1.2.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.1.2.zip)
143 |
144 | ### Changes
145 |
146 | * Fixed issue that the model was not shown if it was already populated
147 | * Add ability to use expressions in the value keys
148 |
149 | ## Version 0.1.1
150 |
151 | * Tag: [0.1.1](https://github.com/guylabs/ion-autocomplete/tree/v0.1.1)
152 | * Release: [ion-autocomplete-0.1.0.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.1.1.zip)
153 |
154 | ### Changes
155 |
156 | * Fix release version
157 |
158 | ## Version 0.1.0
159 |
160 | * Tag: [0.1.0](https://github.com/guylabs/ion-autocomplete/tree/v0.1.0)
161 | * Release: [ion-autocomplete-0.1.0.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.1.0.zip)
162 |
163 | ### Changes
164 |
165 | * Add promise support for the items method
166 | * Add `items-method-value-key` property to specify the value key of the data returned by the items method
167 |
168 | ## Version 0.0.2
169 |
170 | * Tag: [0.1.0](https://github.com/guylabs/ion-autocomplete/tree/v0.0.2)
171 | * Release: [ion-autocomplete-0.0.2.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.0.2.zip)
172 |
173 | ### Changes
174 |
175 | * Initial release of `ion-autocomplete`
176 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ion-autocomplete",
3 | "version": "0.4.0",
4 | "description": "A configurable Ionic directive for an autocomplete dropdown",
5 | "main": [
6 | "./dist/ion-autocomplete.js",
7 | "./dist/ion-autocomplete.css"
8 | ],
9 | "license": "MIT",
10 | "ignore": [
11 | "**/.*",
12 | ".gitignore",
13 | "Gruntfile.js",
14 | "karma.conf.js",
15 | "lib",
16 | "src",
17 | "test",
18 | "node_modules",
19 | "bower_components",
20 | "demo.gif",
21 | "protractor-conf.js"
22 | ],
23 | "keywords": [
24 | "AngularJS",
25 | "angular",
26 | "Ionic",
27 | "ion",
28 | "autocomplete"
29 | ],
30 | "authors": [
31 | "Danny Povolotski ",
32 | "Guy Brand "
33 | ],
34 | "homepage": "https://github.com/guylabs/ion-autocomplete",
35 | "repository": {
36 | "type": "git",
37 | "url": "https://github.com/guylabs/ion-autocomplete.git"
38 | },
39 | "devDependencies": {
40 | "ionic": "driftyco/ionic-bower#1.3.2",
41 | "angular-mocks": "1.5.3"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guylabs/ion-autocomplete/801719947cdc44181852b5c2c0ff0e0a1f95e5df/demo.gif
--------------------------------------------------------------------------------
/dist/ion-autocomplete.css:
--------------------------------------------------------------------------------
1 | .ion-autocomplete-container {
2 | position: fixed;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | z-index: 20;
8 | display: none;
9 | margin: auto;
10 | }
11 |
12 | input.ion-autocomplete[readonly] {
13 | background-color: transparent;
14 | cursor: text;
15 | }
16 |
17 | .ion-autocomplete-loading-icon {
18 | padding-left: 10px;
19 | }
20 |
21 | .ion-autocomplete-open {
22 | display: block;
23 | }
24 |
25 | .ion-autocomplete-close {
26 | display: none;
27 | }
28 |
--------------------------------------------------------------------------------
/dist/ion-autocomplete.js:
--------------------------------------------------------------------------------
1 | /*
2 | * ion-autocomplete 0.4.0
3 | * Copyright 2017 Danny Povolotski
4 | * Copyright modifications 2017 Guy Brand
5 | * https://github.com/guylabs/ion-autocomplete
6 | */
7 | (function() {
8 |
9 | 'use strict';
10 |
11 | angular.module('ion-autocomplete', []).directive('ionAutocomplete', [
12 | '$ionicBackdrop', '$ionicScrollDelegate', '$document', '$q', '$parse', '$interpolate', '$ionicPlatform', '$compile', '$templateRequest',
13 | function ($ionicBackdrop, $ionicScrollDelegate, $document, $q, $parse, $interpolate, $ionicPlatform, $compile, $templateRequest) {
14 | return {
15 | require: ['ngModel', 'ionAutocomplete'],
16 | restrict: 'A',
17 | scope: {},
18 | bindToController: {
19 | ngModel: '=',
20 | externalModel: '=',
21 | templateData: '=',
22 | maxSelectedItems: '=',
23 | itemsMethod: '&',
24 | itemsClickedMethod: '&',
25 | itemsRemovedMethod: '&',
26 | modelToItemMethod: '&',
27 | cancelButtonClickedMethod: '&',
28 | placeholder: '@',
29 | cancelLabel: '@',
30 | selectItemsLabel: '@',
31 | selectedItemsLabel: '@',
32 | templateUrl: '@',
33 | itemValueKey: '@',
34 | itemViewValueKey: '@'
35 | },
36 | controllerAs: 'viewModel',
37 | controller: ['$attrs', '$timeout', '$scope', function ($attrs, $timeout, $scope) {
38 |
39 | var valueOrDefault = function (value, defaultValue) {
40 | return !value ? defaultValue : value;
41 | };
42 |
43 | var controller = this;
44 |
45 | // set the default values of the one way binded attributes
46 | $timeout(function () {
47 | controller.placeholder = valueOrDefault(controller.placeholder, 'Click to enter a value...');
48 | controller.cancelLabel = valueOrDefault(controller.cancelLabel, 'Done');
49 | controller.selectItemsLabel = valueOrDefault(controller.selectItemsLabel, "Select an item...");
50 | controller.selectedItemsLabel = valueOrDefault(controller.selectedItemsLabel, $interpolate("Selected items{{maxSelectedItems ? ' (max. ' + maxSelectedItems + ')' : ''}}:")(controller));
51 | controller.templateUrl = valueOrDefault(controller.templateUrl, undefined);
52 | controller.itemValueKey = valueOrDefault(controller.itemValueKey, undefined);
53 | controller.itemViewValueKey = valueOrDefault(controller.itemViewValueKey, undefined);
54 | });
55 |
56 | // set the default values of the passed in attributes
57 | this.itemsMethodValueKey = valueOrDefault($attrs.itemsMethodValueKey, undefined);
58 | this.componentId = valueOrDefault($attrs.componentId, undefined);
59 | this.loadingIcon = valueOrDefault($attrs.loadingIcon, undefined);
60 | this.manageExternally = valueOrDefault($attrs.manageExternally, "false");
61 | this.clearOnSelect = valueOrDefault($attrs.clearOnSelect, "true");
62 | this.ngModelOptions = valueOrDefault($scope.$eval($attrs.ngModelOptions), {});
63 | this.openClass = valueOrDefault($attrs.openClass, 'ion-autocomplete-open');
64 | this.closeClass = valueOrDefault($attrs.closeClass, 'ion-autocomplete-close');
65 |
66 | // loading flag if the items-method is a function
67 | this.showLoadingIcon = false;
68 |
69 | // the items, selected items and the query for the list
70 | this.searchItems = [];
71 | this.selectedItems = [];
72 | this.searchQuery = undefined;
73 |
74 | this.isArray = function (array) {
75 | return angular.isArray(array);
76 | };
77 | }],
78 | link: function (scope, element, attrs, controllers) {
79 |
80 | // get the two needed controllers
81 | var ngModelController = controllers[0];
82 | var ionAutocompleteController = controllers[1];
83 |
84 | // use a random css class to bind the modal to the component
85 | ionAutocompleteController.randomCssClass = "ion-autocomplete-random-" + Math.floor((Math.random() * 1000) + 1);
86 |
87 | var template = [
88 | '
'
115 | ].join('');
116 |
117 | // load the template synchronously or asynchronously
118 | $q.when().then(function () {
119 |
120 | // first check if a template url is set and use this as template
121 | if (ionAutocompleteController.templateUrl) {
122 | return $templateRequest(ionAutocompleteController.templateUrl);
123 | } else {
124 | return template;
125 | }
126 | }).then(function (template) {
127 |
128 | // compile the template
129 | var searchInputElement = $compile(angular.element(template))(scope);
130 |
131 | // append the template to body
132 | $document.find('body').append(searchInputElement);
133 |
134 |
135 | // returns the value of an item
136 | ionAutocompleteController.getItemValue = function (item, key) {
137 |
138 | // if it's an array, go through all items and add the values to a new array and return it
139 | if (angular.isArray(item)) {
140 | var items = [];
141 | angular.forEach(item, function (itemValue) {
142 | if (key && angular.isObject(item)) {
143 | items.push($parse(key)(itemValue));
144 | } else {
145 | items.push(itemValue);
146 | }
147 | });
148 | return items;
149 | } else {
150 | if (key && angular.isObject(item)) {
151 | return $parse(key)(item);
152 | }
153 | }
154 | return item;
155 | };
156 |
157 | // function which selects the item, hides the search container and the ionic backdrop if it has not maximum selected items attribute set
158 | ionAutocompleteController.selectItem = function (item) {
159 |
160 | // if the clear on select is true, clear the search query when an item is selected
161 | if (ionAutocompleteController.clearOnSelect == "true") {
162 | ionAutocompleteController.searchQuery = undefined;
163 | }
164 |
165 | // return if the max selected items is not equal to 1 and the maximum amount of selected items is reached
166 | if (ionAutocompleteController.maxSelectedItems != "1" &&
167 | angular.isArray(ionAutocompleteController.selectedItems) &&
168 | ionAutocompleteController.maxSelectedItems <= ionAutocompleteController.selectedItems.length) {
169 | return;
170 | }
171 |
172 | // store the selected items
173 | if (!isKeyValueInObjectArray(ionAutocompleteController.selectedItems,
174 | ionAutocompleteController.itemValueKey, ionAutocompleteController.getItemValue(item, ionAutocompleteController.itemValueKey))) {
175 |
176 | // if it is a single select set the item directly
177 | if (ionAutocompleteController.maxSelectedItems == "1") {
178 | ionAutocompleteController.selectedItems = item;
179 | } else {
180 | // create a new array to update the model. See https://github.com/angular-ui/ui-select/issues/191#issuecomment-55471732
181 | ionAutocompleteController.selectedItems = ionAutocompleteController.selectedItems.concat([item]);
182 | }
183 | }
184 |
185 | // set the view value and render it
186 | ngModelController.$setViewValue(ionAutocompleteController.selectedItems);
187 | ngModelController.$render();
188 |
189 | // hide the container and the ionic backdrop if it is a single select to enhance usability
190 | if (ionAutocompleteController.maxSelectedItems == 1) {
191 | ionAutocompleteController.hideModal();
192 | }
193 |
194 | // call items clicked callback
195 | if (angular.isDefined(attrs.itemsClickedMethod)) {
196 | ionAutocompleteController.itemsClickedMethod({
197 | callback: {
198 | item: item,
199 | selectedItems: angular.isArray(ionAutocompleteController.selectedItems) ? ionAutocompleteController.selectedItems.slice() : ionAutocompleteController.selectedItems,
200 | selectedItemsArray: angular.isArray(ionAutocompleteController.selectedItems) ? ionAutocompleteController.selectedItems.slice() : [ionAutocompleteController.selectedItems],
201 | componentId: ionAutocompleteController.componentId
202 | }
203 | });
204 | }
205 | };
206 |
207 | // function which removes the item from the selected items.
208 | ionAutocompleteController.removeItem = function (index) {
209 |
210 | // clear the selected items if just one item is selected
211 | if (!angular.isArray(ionAutocompleteController.selectedItems)) {
212 | ionAutocompleteController.selectedItems = [];
213 | } else {
214 | // remove the item from the selected items and create a copy of the array to update the model.
215 | // See https://github.com/angular-ui/ui-select/issues/191#issuecomment-55471732
216 | var removed = ionAutocompleteController.selectedItems.splice(index, 1)[0];
217 | ionAutocompleteController.selectedItems = ionAutocompleteController.selectedItems.slice();
218 | }
219 |
220 | // set the view value and render it
221 | ngModelController.$setViewValue(ionAutocompleteController.selectedItems);
222 | ngModelController.$render();
223 |
224 | // call items clicked callback
225 | if (angular.isDefined(attrs.itemsRemovedMethod)) {
226 | ionAutocompleteController.itemsRemovedMethod({
227 | callback: {
228 | item: removed,
229 | selectedItems: angular.isArray(ionAutocompleteController.selectedItems) ? ionAutocompleteController.selectedItems.slice() : ionAutocompleteController.selectedItems,
230 | selectedItemsArray: angular.isArray(ionAutocompleteController.selectedItems) ? ionAutocompleteController.selectedItems.slice() : [ionAutocompleteController.selectedItems],
231 | componentId: ionAutocompleteController.componentId
232 | }
233 | });
234 | }
235 | };
236 |
237 | // watcher on the search field model to update the list according to the input
238 | scope.$watch('viewModel.searchQuery', function (query) {
239 | ionAutocompleteController.fetchSearchQuery(query, false);
240 | });
241 |
242 | // watcher on the max selected items to update the selected items label
243 | scope.$watch('viewModel.maxSelectedItems', function (maxSelectedItems) {
244 |
245 | // only update the label if the value really changed
246 | if (ionAutocompleteController.maxSelectedItems != maxSelectedItems) {
247 | ionAutocompleteController.selectedItemsLabel = $interpolate("Selected items{{maxSelectedItems ? ' (max. ' + maxSelectedItems + ')' : ''}}:")(ionAutocompleteController);
248 | }
249 | });
250 |
251 | // update the search items based on the returned value of the items-method
252 | ionAutocompleteController.fetchSearchQuery = function (query, isInitializing) {
253 |
254 | // right away return if the query is undefined to not call the items method for nothing
255 | if (query === undefined) {
256 | return;
257 | }
258 |
259 | if (angular.isDefined(attrs.itemsMethod)) {
260 |
261 | // show the loading icon
262 | ionAutocompleteController.showLoadingIcon = true;
263 |
264 | var queryObject = {query: query, isInitializing: isInitializing};
265 |
266 | // if the component id is set, then add it to the query object
267 | if (ionAutocompleteController.componentId) {
268 | queryObject = {
269 | query: query,
270 | isInitializing: isInitializing,
271 | componentId: ionAutocompleteController.componentId
272 | }
273 | }
274 |
275 | // convert the given function to a $q promise to support promises too
276 | var promise = $q.when(ionAutocompleteController.itemsMethod(queryObject));
277 |
278 | promise.then(function (promiseData) {
279 |
280 | // if the promise data is not set do nothing
281 | if (!promiseData) {
282 | return;
283 | }
284 |
285 | // if the given promise data object has a data property use this for the further processing as the
286 | // standard httpPromises from the $http functions store the response data in a data property
287 | if (promiseData && promiseData.data) {
288 | promiseData = promiseData.data;
289 | }
290 |
291 | // set the items which are returned by the items method
292 | ionAutocompleteController.searchItems = ionAutocompleteController.getItemValue(promiseData,
293 | ionAutocompleteController.itemsMethodValueKey);
294 |
295 | // force the collection repeat to redraw itself as there were issues when the first items were added
296 | $ionicScrollDelegate.resize();
297 | }, function (error) {
298 | // reject the error because we do not handle the error here
299 | return $q.reject(error);
300 | }).finally(function () {
301 | // hide the loading icon
302 | ionAutocompleteController.showLoadingIcon = false;
303 | });
304 | }
305 | };
306 |
307 | var searchContainerDisplayed = false;
308 |
309 | ionAutocompleteController.showModal = function () {
310 | if (searchContainerDisplayed) {
311 | return;
312 | }
313 |
314 | // show the backdrop and the search container
315 | $ionicBackdrop.retain();
316 | var modal = angular.element($document[0].querySelector('div.ion-autocomplete-container.' + ionAutocompleteController.randomCssClass));
317 | modal.addClass(this.openClass);
318 | modal.removeClass(this.closeClass);
319 |
320 | // hide the container if the back button is pressed
321 | scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(function () {
322 | ionAutocompleteController.hideModal();
323 | }, 300);
324 |
325 | // get the compiled search field
326 | var searchInputElement = angular.element($document[0].querySelector('div.ion-autocomplete-container.' + ionAutocompleteController.randomCssClass + ' input:not(.no-autofocus)'));
327 |
328 | // focus on the search input field
329 | if (searchInputElement.length > 0) {
330 | searchInputElement[0].focus();
331 | setTimeout(function () {
332 | searchInputElement[0].focus();
333 | }, 100);
334 | }
335 |
336 | // force the collection repeat to redraw itself as there were issues when the first items were added
337 | $ionicScrollDelegate.resize();
338 |
339 | searchContainerDisplayed = true;
340 | };
341 |
342 | ionAutocompleteController.hideModal = function () {
343 | var modal = angular.element($document[0].querySelector('div.ion-autocomplete-container.' + ionAutocompleteController.randomCssClass));
344 | modal.addClass(this.closeClass);
345 | modal.removeClass(this.openClass);
346 | ionAutocompleteController.searchQuery = undefined;
347 | $ionicBackdrop.release();
348 | scope.$deregisterBackButton && scope.$deregisterBackButton();
349 | searchContainerDisplayed = false;
350 | };
351 |
352 | // object to store if the user moved the finger to prevent opening the modal
353 | var scrolling = {
354 | moved: false,
355 | startX: 0,
356 | startY: 0
357 | };
358 |
359 | // store the start coordinates of the touch start event
360 | var onTouchStart = function (e) {
361 | scrolling.moved = false;
362 | // Use originalEvent when available, fix compatibility with jQuery
363 | if (typeof(e.originalEvent) !== 'undefined') {
364 | e = e.originalEvent;
365 | }
366 | scrolling.startX = e.touches[0].clientX;
367 | scrolling.startY = e.touches[0].clientY;
368 | };
369 |
370 | // check if the finger moves more than 10px and set the moved flag to true
371 | var onTouchMove = function (e) {
372 | // Use originalEvent when available, fix compatibility with jQuery
373 | if (typeof(e.originalEvent) !== 'undefined') {
374 | e = e.originalEvent;
375 | }
376 | if (Math.abs(e.touches[0].clientX - scrolling.startX) > 10 ||
377 | Math.abs(e.touches[0].clientY - scrolling.startY) > 10) {
378 | scrolling.moved = true;
379 | }
380 | };
381 |
382 | // click handler on the input field to show the search container
383 | var onClick = function (event) {
384 | // only open the dialog if was not touched at the beginning of a legitimate scroll event
385 | if (scrolling.moved) {
386 | return;
387 | }
388 |
389 | // prevent the default event and the propagation
390 | event.preventDefault();
391 | event.stopPropagation();
392 |
393 | // call the fetch search query method once to be able to initialize it when the modal is shown
394 | // use an empty string to signal that there is no change in the search query
395 | ionAutocompleteController.fetchSearchQuery("", true);
396 |
397 | // show the ionic backdrop and the search container
398 | ionAutocompleteController.showModal();
399 | };
400 |
401 | var isKeyValueInObjectArray = function (objectArray, key, value) {
402 | if (angular.isArray(objectArray)) {
403 | for (var i = 0; i < objectArray.length; i++) {
404 | if (ionAutocompleteController.getItemValue(objectArray[i], key) === value) {
405 | return true;
406 | }
407 | }
408 | }
409 | return false;
410 | };
411 |
412 | // function to call the model to item method and select the item
413 | var resolveAndSelectModelItem = function (modelValue) {
414 | // convert the given function to a $q promise to support promises too
415 | var promise = $q.when(ionAutocompleteController.modelToItemMethod({modelValue: modelValue}));
416 |
417 | promise.then(function (promiseData) {
418 | // select the item which are returned by the model to item method
419 | ionAutocompleteController.selectItem(promiseData);
420 | }, function (error) {
421 | // reject the error because we do not handle the error here
422 | return $q.reject(error);
423 | });
424 | };
425 |
426 | // if the click is not handled externally, bind the handlers to the click and touch events of the input field
427 | if (ionAutocompleteController.manageExternally == "false") {
428 | element.bind('touchstart', onTouchStart);
429 | element.bind('touchmove', onTouchMove);
430 | element.bind('touchend click focus', onClick);
431 | }
432 |
433 | // cancel handler for the cancel button which clears the search input field model and hides the
434 | // search container and the ionic backdrop and calls the cancel button clicked callback
435 | ionAutocompleteController.cancelClick = function () {
436 | ionAutocompleteController.hideModal();
437 |
438 | // call cancel button clicked callback
439 | if (angular.isDefined(attrs.cancelButtonClickedMethod)) {
440 | ionAutocompleteController.cancelButtonClickedMethod({
441 | callback: {
442 | selectedItems: angular.isArray(ionAutocompleteController.selectedItems) ? ionAutocompleteController.selectedItems.slice() : ionAutocompleteController.selectedItems,
443 | selectedItemsArray: angular.isArray(ionAutocompleteController.selectedItems) ? ionAutocompleteController.selectedItems.slice() : [ionAutocompleteController.selectedItems],
444 | componentId: ionAutocompleteController.componentId
445 | }
446 | });
447 | }
448 | };
449 |
450 | // watch the external model for changes and select the items inside the model
451 | scope.$watch("viewModel.externalModel", function (newModel) {
452 |
453 | if (angular.isArray(newModel) && newModel.length == 0) {
454 | // clear the selected items and set the view value and render it
455 | ionAutocompleteController.selectedItems = [];
456 | ngModelController.$setViewValue(ionAutocompleteController.selectedItems);
457 | ngModelController.$render();
458 | return;
459 | }
460 |
461 | // prepopulate view and selected items if external model is already set
462 | if (newModel && angular.isDefined(attrs.modelToItemMethod)) {
463 | if (angular.isArray(newModel)) {
464 | ionAutocompleteController.selectedItems = [];
465 | angular.forEach(newModel, function (modelValue) {
466 | resolveAndSelectModelItem(modelValue);
467 | })
468 | } else {
469 | resolveAndSelectModelItem(newModel);
470 | }
471 | }
472 | });
473 |
474 | // remove the component from the dom when scope is getting destroyed
475 | scope.$on('$destroy', function () {
476 | $ionicBackdrop.release();
477 |
478 | // angular takes care of cleaning all $watch's and listeners, but we still need to remove the modal
479 | searchInputElement.remove();
480 | });
481 |
482 | // render the view value of the model
483 | ngModelController.$render = function () {
484 | element.val(ionAutocompleteController.getItemValue(ngModelController.$viewValue, ionAutocompleteController.itemViewValueKey));
485 | };
486 |
487 | // set the view value of the model
488 | ngModelController.$formatters.push(function (modelValue) {
489 | var viewValue = ionAutocompleteController.getItemValue(modelValue, ionAutocompleteController.itemViewValueKey);
490 | return viewValue == undefined ? "" : viewValue;
491 | });
492 |
493 | // set the model value of the model
494 | ngModelController.$parsers.push(function (viewValue) {
495 | return ionAutocompleteController.getItemValue(viewValue, ionAutocompleteController.itemValueKey);
496 | });
497 |
498 | });
499 |
500 | }
501 | };
502 | }
503 | ]);
504 |
505 | })();
506 |
--------------------------------------------------------------------------------
/dist/ion-autocomplete.min.css:
--------------------------------------------------------------------------------
1 | .ion-autocomplete-container{position:fixed;top:0;right:0;bottom:0;left:0;z-index:20;display:none;margin:auto}input.ion-autocomplete[readonly]{background-color:transparent;cursor:text}.ion-autocomplete-loading-icon{padding-left:10px}.ion-autocomplete-open{display:block}.ion-autocomplete-close{display:none}
--------------------------------------------------------------------------------
/dist/ion-autocomplete.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | * ion-autocomplete 0.4.0
3 | * Copyright 2017 Danny Povolotski
4 | * Copyright modifications 2017 Guy Brand
5 | * https://github.com/guylabs/ion-autocomplete
6 | */
7 | !function(){"use strict";angular.module("ion-autocomplete",[]).directive("ionAutocomplete",["$ionicBackdrop","$ionicScrollDelegate","$document","$q","$parse","$interpolate","$ionicPlatform","$compile","$templateRequest",function(a,b,c,d,e,f,g,h,i){return{require:["ngModel","ionAutocomplete"],restrict:"A",scope:{},bindToController:{ngModel:"=",externalModel:"=",templateData:"=",maxSelectedItems:"=",itemsMethod:"&",itemsClickedMethod:"&",itemsRemovedMethod:"&",modelToItemMethod:"&",cancelButtonClickedMethod:"&",placeholder:"@",cancelLabel:"@",selectItemsLabel:"@",selectedItemsLabel:"@",templateUrl:"@",itemValueKey:"@",itemViewValueKey:"@"},controllerAs:"viewModel",controller:["$attrs","$timeout","$scope",function(a,b,c){var d=function(a,b){return a?a:b},e=this;b(function(){e.placeholder=d(e.placeholder,"Click to enter a value..."),e.cancelLabel=d(e.cancelLabel,"Done"),e.selectItemsLabel=d(e.selectItemsLabel,"Select an item..."),e.selectedItemsLabel=d(e.selectedItemsLabel,f("Selected items{{maxSelectedItems ? ' (max. ' + maxSelectedItems + ')' : ''}}:")(e)),e.templateUrl=d(e.templateUrl,void 0),e.itemValueKey=d(e.itemValueKey,void 0),e.itemViewValueKey=d(e.itemViewValueKey,void 0)}),this.itemsMethodValueKey=d(a.itemsMethodValueKey,void 0),this.componentId=d(a.componentId,void 0),this.loadingIcon=d(a.loadingIcon,void 0),this.manageExternally=d(a.manageExternally,"false"),this.clearOnSelect=d(a.clearOnSelect,"true"),this.ngModelOptions=d(c.$eval(a.ngModelOptions),{}),this.openClass=d(a.openClass,"ion-autocomplete-open"),this.closeClass=d(a.closeClass,"ion-autocomplete-close"),this.showLoadingIcon=!1,this.searchItems=[],this.selectedItems=[],this.searchQuery=void 0,this.isArray=function(a){return angular.isArray(a)}}],link:function(j,k,l,m){var n=m[0],o=m[1];o.randomCssClass="ion-autocomplete-random-"+Math.floor(1e3*Math.random()+1);var p=['