and contributors.
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | this software and associated documentation files (the "Software"), to deal in
6 | the Software without restriction, including without limitation the rights to
7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8 | of the Software, and to permit persons to whom the Software is furnished to do
9 | so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/test-amd.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /* jshint browser: true */
3 | /* globals require: true */
4 | var allTestFiles = [];
5 | var TEST_REGEXP = /(spec|test)\.js$/i;
6 |
7 | // Get a list of all the test files to include
8 | Object.keys(window.__karma__.files).forEach(function (file) {
9 | if (TEST_REGEXP.test(file)) {
10 | // Normalize paths to RequireJS module names.
11 | // If you require sub-dependencies of test files to be loaded as-is (requiring file extension)
12 | // then do not normalize the paths
13 | var normalizedTestModule = file.replace(/^\/base\/|\.js$/g, '');
14 | allTestFiles.push(normalizedTestModule);
15 | }
16 | });
17 |
18 | require.config({
19 | // Karma serves files under /base, which is the basePath from your config file
20 | baseUrl: '/base',
21 |
22 | // dynamically load all test files
23 | deps: allTestFiles,
24 |
25 | paths: {
26 | 'angular-typeahead': 'build/angular-typeahead',
27 | 'typeahead': 'node_modules/typeahead.js/dist/typeahead.bundle',
28 | 'angular': 'node_modules/angular/angular',
29 | 'angular-mocks': 'node_modules/angular-mocks/angular-mocks',
30 | 'jquery': 'node_modules/jquery/dist/jquery'
31 | },
32 |
33 | shim: {
34 | 'angular': {
35 | exports: 'angular'
36 | },
37 | 'angular-mocks': {
38 | deps: [ 'angular' ]
39 | },
40 | 'jquery': {
41 | exports: '$'
42 | }
43 | },
44 |
45 | // we have to kickoff jasmine, as it is asynchronous
46 | callback: function() {
47 | /* globals $: true */
48 | require.s.contexts._.registry['typeahead.js'].factory( $ );
49 | window.__karma__.start();
50 | }
51 | });
52 |
--------------------------------------------------------------------------------
/test/karma.shared.conf.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /* jshint node:true */
3 | module.exports = function() {
4 | return {
5 | // base path that will be used to resolve all patterns (eg. files, exclude)
6 | basePath: '..',
7 |
8 | // frameworks to use
9 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
10 | frameworks: ['jasmine'],
11 |
12 | // list of files to exclude
13 | exclude: [],
14 |
15 | files: [ ],
16 |
17 |
18 | // preprocess matching files before serving them to the browser
19 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
20 | preprocessors: { },
21 |
22 | // test results reporter to use
23 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
24 | reporters: ['spec'],
25 |
26 | // web server port
27 | port: 9876,
28 |
29 | // enable / disable colors in the output (reporters and logs)
30 | colors: true,
31 |
32 | // level of logging
33 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
34 | logLevel: 'LOG_INFO',
35 |
36 | // enable / disable watching file and executing tests whenever any file changes
37 | autoWatch: false,
38 |
39 | // start these browsers
40 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
41 | browsers: ['Chrome'],
42 |
43 | // Continuous Integration mode
44 | // if true, Karma captures browsers, runs the tests and exits
45 | singleRun: true,
46 |
47 | // Concurrency level
48 | // how many browser should be started simultaneous
49 | concurrency: Infinity
50 | };
51 | };
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-typeahead",
3 | "version": "1.0.2",
4 | "subdomain": "Siyfion",
5 | "main": "dist/angular-typeahead.js",
6 | "author": "siyfion@gmail.com",
7 | "license": "MIT",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/Siyfion/angular-typeahead.git"
11 | },
12 | "description": "An Angular.js wrapper around the Twitter Typeahead library.",
13 | "scripts": {
14 | "test": "grunt test",
15 | "watch": "grunt watch",
16 | "dist": "grunt dist"
17 | },
18 | "files": [
19 | "dist"
20 | ],
21 | "contributors": [
22 | {
23 | "name": "Simon Mansfield",
24 | "email": "siyfion@gmail.com"
25 | },
26 | {
27 | "name": "Hadrien Milano",
28 | "email": "hadrien.milano@gmail.com"
29 | }
30 | ],
31 | "keywords": [
32 | "angular",
33 | "angularjs",
34 | "typeahead"
35 | ],
36 | "devDependencies": {
37 | "angular": "1.5.x",
38 | "angular-mocks": "^1.5.8",
39 | "browserify": "^13.1.0",
40 | "grunt": "1.0.1",
41 | "grunt-contrib-clean": "~1.0.0",
42 | "grunt-contrib-copy": "^1.0.0",
43 | "grunt-contrib-jshint": "^1.0.0",
44 | "grunt-contrib-uglify": "~2.0.0",
45 | "grunt-contrib-watch": "^1.0.0",
46 | "grunt-karma": "^2.0.0",
47 | "grunt-umd": "^2.3.6",
48 | "jquery": "^3.1.1",
49 | "karma": "^1.3.0",
50 | "karma-browserify": "^5.1.0",
51 | "karma-chrome-launcher": "^2.0.0",
52 | "karma-coverage": "^1.1.1",
53 | "karma-firefox-launcher": "^1.0.0",
54 | "karma-jasmine": "^1.0.2",
55 | "karma-phantomjs-launcher": "^1.0.2",
56 | "karma-requirejs": "^1.1.0",
57 | "karma-spec-reporter": "0.0.26",
58 | "karma-threshold-reporter": "^0.1.15",
59 | "requirejs": "^2.3.2",
60 | "typeahead.js": "^0.11.1",
61 | "watchify": "^3.7.0"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/dist/angular-typeahead.min.js:
--------------------------------------------------------------------------------
1 | !function(a,b){"function"==typeof define&&define.amd?define("angular-typeahead",["angular"],function(a){return b(a)}):"object"==typeof exports?module.exports=b(require("angular"),require("typeahead.js")):b(angular)}(this,function(a){"use strict";a.module("siyfion.sfTypeahead",[]).value("$typeahead",function(a){a.typeahead.apply(a,Array.prototype.slice.call(arguments,1))}).directive("sfTypeahead",["$typeahead",function(b){return{restrict:"AC",require:"ngModel",scope:{datasets:"=",options:"=",allowCustom:"="},link:function(c,d,e,f){function g(){r=p?c.$watchCollection("datasets",h):c.$watch("datasets",h)}function h(b,c){if(!a.equals(b,c)){var d=p;i(),p!==d&&(r(),g())}}function i(){if(null==c.datasets)throw new Error("The datasets parameter is mandatory!");n=c.options||{},p=a.isArray(c.datasets),o=p?c.datasets:[c.datasets],q?(b(d,"destroy"),b(d,n,o)):(b(d,n,o),c.$watch("options",i),q=!0)}function j(b){for(var c in o){var d=o[c],e=d.displayKey||"value",f=(a.isFunction(e)?e(b):b[e])||"";return f}}function k(){return void 0===c.allowCustom||!!c.allowCustom}function l(a){c.$apply(function(){f.$setViewValue(a)})}function m(a){d.bind(a,function(){c.$emit(a,Array.prototype.slice.call(arguments,1))})}var n,o,p,q=!1,r=null;i(),g(),f.$parsers.push(function(b){return a.isObject(f.$modelValue)&&b===j(f.$modelValue)?f.$modelValue:k()||"string"!=typeof b?b:f.$modelValue}),f.$formatters.push(function(c){return a.isObject(c)&&(c=j(c)),c?b(d,"val",c):b(d,"val",""),c}),d.bind("typeahead:select",function(a,b,d){l(b),c.$emit("typeahead:select",b,d)}),d.bind("typeahead:autocomplete",function(a,b,d){l(b),c.$emit("typeahead:autocomplete",b,d)}),m("typeahead:active"),m("typeahead:idle"),m("typeahead:open"),m("typeahead:close"),m("typeahead:change"),m("typeahead:render"),m("typeahead:cursorchange"),m("typeahead:asyncrequest"),m("typeahead:asynccancel"),m("typeahead:asyncreceive")}}}])});
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /* jshint node: true */
3 | module.exports = function (grunt) {
4 |
5 | var karma_browser = process.env.KARMA_BROWSER || 'Chrome';
6 |
7 | // Project configuration.
8 | grunt.initConfig({
9 | pkg: grunt.file.readJSON('package.json'),
10 | clean: {
11 | files: {
12 | src: [
13 | 'build/'
14 | ]
15 | }
16 | },
17 | copy: {
18 | files: {
19 | expand: true,
20 | src: ['angular-typeahead.js', 'angular-typeahead.min.js'],
21 | cwd: 'build',
22 | dest: 'dist/'
23 | }
24 | },
25 | uglify: {
26 | build: {
27 | src: 'build/angular-typeahead.js',
28 | dest: 'build/angular-typeahead.min.js'
29 | }
30 | },
31 | karma: {
32 | global: {
33 | configFile: 'test/karma.global.conf.js',
34 | browsers: [ karma_browser ]
35 | },
36 | amd: {
37 | configFile: 'test/karma.amd.conf.js',
38 | browsers: [ karma_browser ]
39 | },
40 | cjs: {
41 | configFile: 'test/karma.cjs.conf.js',
42 | browsers: [ karma_browser ]
43 | }
44 | },
45 | jshint: {
46 | default: {
47 | options: {
48 | jshintrc: true,
49 | },
50 | files: {
51 | src: [
52 | 'angular-typeahead.js',
53 | 'test/*.js']
54 | }
55 | }
56 | },
57 | umd: {
58 | src: {
59 | options: {
60 | src: 'angular-typeahead.js',
61 | dest: 'build/angular-typeahead.js',
62 | amdModuleId: 'angular-typeahead',
63 | deps: {
64 | default: ['angular'],
65 | global: ['angular'],
66 | amd: ['angular'],
67 | cjs: ['angular', 'typeahead.js']
68 | }
69 | }
70 | },
71 | test: {
72 | src: 'test/angular-typeahead.spec.js',
73 | dest: 'build/angular-typeahead.spec.js',
74 | amdModuleId: 'build/angular-typeahead.spec',
75 | deps: {
76 | default: ['angular'],
77 | global: ['angular'],
78 | amd: ['angular', 'angular-typeahead', 'angular-mocks'],
79 | cjs: ['angular', 'angular-typeahead', 'angular-mocks']
80 | }
81 | }
82 | },
83 | watch: {
84 | default: {
85 | files: [
86 | 'angular-typeahead.js',
87 | 'test/angular-typeahead.spec.js'],
88 | tasks: ['test:lite'],
89 | options: {
90 | spawn: false,
91 | },
92 | },
93 | },
94 | });
95 |
96 | // Load the plugins that provide the tasks.
97 | grunt.loadNpmTasks('grunt-contrib-clean');
98 | grunt.loadNpmTasks('grunt-contrib-copy');
99 | grunt.loadNpmTasks('grunt-contrib-uglify');
100 | grunt.loadNpmTasks('grunt-contrib-jshint');
101 | grunt.loadNpmTasks('grunt-karma');
102 | grunt.loadNpmTasks('grunt-contrib-watch');
103 | grunt.loadNpmTasks('grunt-umd');
104 |
105 | grunt.registerTask('require-self', 'Fixes require calls to self for tests', require('./tasks/require-self'));
106 |
107 | // Utility Tasks
108 | grunt.registerTask('_build', ['require-self', 'umd']);
109 | grunt.registerTask('_test', ['karma', 'jshint']);
110 |
111 |
112 | // Tasks
113 | grunt.registerTask('test:lite', ['_build', 'karma:global', 'jshint']);
114 | grunt.registerTask('test', ['_build', '_test']);
115 | grunt.registerTask('dist', ['_build', 'uglify', 'copy']);
116 | };
117 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## v1.0.2
4 |
5 | - Prevent $dirty flag on a form from being set to false on initialization
6 |
7 | ## v1.0.0 - Major refactoring
8 |
9 | The code has been rewritten almost from scratch with a focus on code quality and testing. This will make future development easier and limit the number of bugs on the long term. Beware that regressions are likely!
10 |
11 | - Built files are now wrapped in [UMD](https://github.com/umdjs/umd)
12 | - Add unit tests, jshint and continuous integration
13 | - Moved built files to `dist/` folder
14 | - Values set directly to $scope.model are no longer validated against the suggestions.
15 | - Remove ambiguous "suggestionKey" parameter
16 | - moved options not related to tt to top level
17 | - renamed "editable" option to "allow-custom"
18 | - Renamed events to match typeahead event names (see [#82](https://github.com/Siyfion/angular-typeahead/pull/82))
19 |
20 | ## v0.3.2
21 | * Adds render event
22 |
23 | ## v0.3.1
24 | * Removed the "all-events" binding, as the code didn't work.
25 |
26 | ## v0.3.0
27 | * Fixes \#71, \#72 and \#79, binds ALL typeahead events adds `require()` support.
28 |
29 | ## v0.2.4
30 | * Adds typeahead.js v0.11.1 dependency
31 |
32 | ## v0.2.3
33 | * Adds async event propagation (Thanks @powange)
34 | * Fixes the Firefox "focus bug" (Thanks @CyborgMaster)
35 |
36 | ## v0.2.2
37 | * Adds a watcher to the datsets and options and reinitializes if any changes are detected.
38 |
39 | ## v0.2.1
40 | * Removes the event bindings as they should no-longer be needed due to the new way the ng-model is being updated.
41 |
42 | ## v0.2.0
43 | * This is a *potentially* **BREAKING CHANGE** release.
44 | * This adds initial support for two-way data binding.
45 |
46 | ## v0.1.4
47 | * Adds a sanity check to the options object for the 'editable' property.
48 |
49 | ## v0.1.3
50 | * Adds 'editable' option, to only allow model values from datum objects. (Thanks to @raphahardt)
51 |
52 | ## v0.1.2
53 | * Retains cursor position, thanks to @skakri.
54 |
55 | ## v0.1.1
56 | * Adds event propagation for all the other typeahead.js events.
57 |
58 | ## v0.1.0
59 | * This is a **BREAKING CHANGE** release.
60 | * Major new release that adds initial support for the new Twitter Typeahead v0.10.x release.
61 | * As a direct result of Twitter's changes, two-way binding is no longer possible at this time (the directive cannot access any local datum lists & never could access remote lists).
62 | * You can now update local datasets using [`Bloodhound#add`](https://github.com/twitter/typeahead.js/blob/master/src/bloodhound/bloodhound.js#L151) and I have questioned whether a similar method could be added for datum removal.
63 |
64 | ## v0.0.12
65 | * Merged in Jakob Lahmer's changes, which ensures that the typeahead events are propagated to the scope.
66 |
67 | ## v0.0.11
68 | * Merged in slobo's changes, including a refactoring of the event methods and a slight hack around the [object Object] issue.
69 |
70 | ## v0.0.10
71 | * Reverting previous change in v0.0.9 (return the datum object, not the string!)
72 |
73 | ## v0.0.9
74 | * Added type check to the datum object, in-case the datum is a string that is implicitly converted.
75 |
76 | ## v0.0.8
77 | * Bugfix for issue #10
78 | * The fix should also support datums with non-default value keys.
79 |
80 | ## v0.0.7
81 | * Now updates the ngModel with the raw user input.
82 | * Optimization on the ngModel watch.
83 |
84 | ## v0.0.6
85 | * Merged in @jmaynier's PR for supporting multiple datasets. (Thanks!)
86 |
87 | ## v0.0.5
88 | * Renamed the angular module to `siyfion.sfTypeahead`.
89 | * Renamed the angular directive to `sfTypeahead`.
90 | * Added two-way binding support to ng-model.
91 |
92 | ## v0.0.4
93 | * Added one-way binding support to ng-model.
94 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | sfTypeahead: A Twitter Typeahead directive
2 | =================
3 |
4 | [](https://travis-ci.org/Siyfion/angular-typeahead)
5 | 
6 | [](https://badge.fury.io/gh/Siyfion%2Fangular-typeahead)
7 | [](https://david-dm.org/Siyfion/angular-typeahead)
8 |
9 | A simple Angular.js directive wrapper around the Twitter Typeahead library.
10 |
11 | Getting Started
12 | ---------------
13 |
14 | Get angular-typeahead from your favorite source:
15 |
16 | * Install with [Bower][bower]: `$ bower install angular-typeahead`
17 | * Install with [npm][npm]: `$ npm install angular-typeahead`
18 | * Download latest *[angular-typeahead.js][angular-typeahead.js]* or *[angular-typeahead.min.js][angular-typeahead.min.js]*.
19 |
20 | **Note:** angular-typeahead supports [Angular.js][angularjs] v1.2.x through v1.5.x and depends on [typeahead.js][typeahead.js] v0.11.x. Make sure dependencies are met in your setup:
21 |
22 | * **global**: include jQuery, angularjs and typeahead.js before *angular-typeahead.js*.
23 | * **commonJS** (node, browserify): angular-typeahead explicitly *requires* `angular` and `typeahead.js`. (note: with browserify, include jquery.js and typeahead.js externally, because angular does not define a dependency on jquery)
24 | * **amd** (require.js): angular-typeahead explicitly *requires* `angular` and declares itself as `angular-typeahead`. Note that `typeahead.js` does not work well with AMD.js, you may find [this workaround](https://github.com/twitter/typeahead.js/issues/1211#issuecomment-129189829) useful.
25 |
26 | Demo
27 | ---------------
28 |
29 | Please feel free to play with the Plnkr: [LIVE DEMO][plnkr]
30 |
31 | Usage
32 | ---------------
33 |
34 | ```html
35 |
36 | ```
37 |
38 | See the Plnkr [LIVE DEMO][plnkr] for a complete integrated example.
39 |
40 | ### Parameters
41 |
42 | | Parameter | Default | Description |
43 | |---------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
44 | | datasets | {} | One or an array of twitter typeahead [datasets][twitter datasets]. |
45 | | options | {} | [Options][twitter options] parameter passed directly to twitter typeahead. |
46 | | allow-custom | true | Boolean. If false, the model value can not take custom values as text is typed in the input field. |
47 |
48 | Contributing
49 | ---------------
50 |
51 | Please feel free to add any issues to the GitHub issue tracker.
52 |
53 | Contributions are welcome but please try to adhere to the folowing guidelines:
54 |
55 | ### Testing
56 |
57 | Any code you write should be tested. Test the "happy path" as well as corner cases.
58 | Code cannot be merged in master unless it achieves 100% coverage on everything.
59 | To run tests automatically when a file changes, run `npm run watch`.
60 |
61 | Tests run in Chrome by default, but you can override this by setting the `KARMA_BROWSER`
62 | environment variable.
63 | Example:
64 | ```sh
65 | KARMA_BROWSER=Firefox npm run watch
66 | KARMA_BROWSER=PhantomJS npm run watch
67 | ```
68 |
69 | If you are not sure how to test something, ask about it in your pull request description.
70 |
71 | ### JSHint
72 |
73 | I recommend you use a jshint plugin in your editor, this will help you spot errors
74 | faster and make it easier to write clean code that is going to pass QA.
75 | In any case, `npm run watch` runs jshint on the code whenever you save.
76 |
77 |
78 |
79 | [angular-typeahead.js]: https://raw.github.com/Siyfion/angular-typeahead/master/dist/angular-typeahead.js
80 | [angular-typeahead.min.js]: https://raw.github.com/Siyfion/angular-typeahead/master/dist/angular-typeahead.min.js
81 |
82 |
83 | [bower]: http://twitter.github.com/bower/
84 | [npm]: https://www.npmjs.com/
85 | [jQuery]: http://jquery.com/
86 | [angularjs]: http://angularjs.org/
87 | [typeahead.js]: http://twitter.github.io/typeahead.js/
88 | [plnkr]: http://plnkr.co/edit/k2JWu6tZMXwkB8Oi9CSv?p=preview
89 | [twitter datasets]: https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#datasets
90 | [twitter options]: https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#options
91 |
--------------------------------------------------------------------------------
/angular-typeahead.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | angular.module('siyfion.sfTypeahead', [])
3 |
4 | // Inject the typeahead jquery plugin through angular to make it easier to unit
5 | // test the library.
6 | // Usage:
7 | // instead of `$element.typeahead(foo, bar)`
8 | // do `$typeahead($element, foo, bar)`
9 | .value('$typeahead', function(subject) {
10 | subject.typeahead.apply(subject, Array.prototype.slice.call(arguments, 1));
11 | })
12 |
13 | // The actual directive
14 | .directive('sfTypeahead', ['$typeahead', function ($typeahead) {
15 |
16 | return {
17 | restrict: 'AC', // Only apply on an attribute or class
18 | require: 'ngModel', // The two-way data bound value that is returned by the directive
19 | scope: {
20 | datasets: '=',
21 | options: '=',
22 | allowCustom: '=' // We cannot use '<' if we want to support angular 1.2.x
23 | },
24 | link: function(scope, element, attrs, ngModel) {
25 | var initialized = false;
26 | var options;
27 | var datasets;
28 | // Unsubscribe handle for the scope watcher
29 | var unsubscribe = null;
30 | // Remembers whether the `datasets` parameter provided was an array or an object.
31 | var datasetsIsArray;
32 |
33 | // Create the typeahead on the element
34 | initialize();
35 |
36 | // Watch for changes on datasets
37 | watchDatasets();
38 |
39 | // Parses and validates what is going to be set to model (called when: ngModel.$setViewValue(value))
40 | ngModel.$parsers.push(function(fromView) {
41 | // In Firefox, when the typeahead field loses focus, it fires an extra
42 | // angular input update event. This causes the stored model to be
43 | // replaced with the search string. If the typeahead search string
44 | // hasn't changed at all (the 'val' property doesn't update until
45 | // after the event loop finishes), then we can bail out early and keep
46 | // the current model value.
47 | if (angular.isObject(ngModel.$modelValue) && fromView === getDatumValue(ngModel.$modelValue)) {
48 | return ngModel.$modelValue;
49 | }
50 |
51 | if (!isCustomAllowed() && typeof fromView === 'string') {
52 | return ngModel.$modelValue;
53 | }
54 |
55 | return fromView;
56 | });
57 |
58 | // Formats what is going to be displayed (called when: $scope.model = { object })
59 | ngModel.$formatters.push(function(fromModel) {
60 | if (angular.isObject(fromModel)) {
61 | fromModel = getDatumValue(fromModel);
62 | }
63 |
64 | if (!fromModel) {
65 | $typeahead(element, 'val', '');
66 | } else {
67 | $typeahead(element, 'val', fromModel);
68 | }
69 | return fromModel;
70 | });
71 |
72 | function watchDatasets() {
73 | unsubscribe = datasetsIsArray ?
74 | scope.$watchCollection('datasets', datasetsChangeHandler) :
75 | scope.$watch('datasets', datasetsChangeHandler);
76 | }
77 |
78 | function datasetsChangeHandler(newValue, oldValue) {
79 | if (angular.equals(newValue, oldValue)) {
80 | return;
81 | }
82 | var oldDatasetsIsArray = datasetsIsArray;
83 | initialize();
84 | if (datasetsIsArray !== oldDatasetsIsArray) {
85 | unsubscribe();
86 | watchDatasets();
87 | }
88 | }
89 |
90 | function initialize() {
91 | if (scope.datasets == null) {
92 | throw new Error('The datasets parameter is mandatory!');
93 | }
94 |
95 | options = scope.options || {};
96 | datasetsIsArray = angular.isArray(scope.datasets);
97 | datasets = datasetsIsArray ? scope.datasets : [scope.datasets];
98 |
99 | if (!initialized) {
100 | $typeahead(element, options, datasets);
101 | scope.$watch('options', initialize);
102 | initialized = true;
103 | } else {
104 | $typeahead(element, 'destroy');
105 | $typeahead(element, options, datasets);
106 | }
107 | }
108 |
109 | // Returns the string to be displayed given some datum
110 | function getDatumValue(datum) {
111 | for (var i in datasets) {
112 | var dataset = datasets[i];
113 | var displayKey = dataset.displayKey || 'value';
114 | var value = (angular.isFunction(displayKey) ? displayKey(datum) : datum[displayKey]) || '';
115 | return value;
116 | }
117 | }
118 |
119 | function isCustomAllowed() {
120 | return scope.allowCustom === undefined || !!scope.allowCustom;
121 | }
122 |
123 | function updateScope (suggestion) {
124 | scope.$apply(function () {
125 | ngModel.$setViewValue(suggestion);
126 | });
127 | }
128 |
129 | function forwardEvent(name) {
130 | element.bind(name, function() {
131 | scope.$emit(name, Array.prototype.slice. call(arguments, 1));
132 | });
133 | }
134 |
135 | // Update the value binding when a value is manually selected from the dropdown.
136 | element.bind('typeahead:select', function(evt, suggestion, dataset) {
137 | updateScope(suggestion);
138 | scope.$emit('typeahead:select', suggestion, dataset);
139 | });
140 |
141 | // Update the value binding when a query is autocompleted.
142 | element.bind('typeahead:autocomplete', function(evt, suggestion, dataset) {
143 | updateScope(suggestion);
144 | scope.$emit('typeahead:autocomplete', suggestion, dataset);
145 | });
146 |
147 | forwardEvent('typeahead:active');
148 | forwardEvent('typeahead:idle');
149 | forwardEvent('typeahead:open');
150 | forwardEvent('typeahead:close');
151 | forwardEvent('typeahead:change');
152 | forwardEvent('typeahead:render');
153 | forwardEvent('typeahead:cursorchange');
154 | forwardEvent('typeahead:asyncrequest');
155 | forwardEvent('typeahead:asynccancel');
156 | forwardEvent('typeahead:asyncreceive');
157 | }
158 | };
159 | }]);
160 |
--------------------------------------------------------------------------------
/dist/angular-typeahead.js:
--------------------------------------------------------------------------------
1 | (function (root, factory) {
2 | if (typeof define === 'function' && define.amd) {
3 | // AMD. Register as an anonymous module unless amdModuleId is set
4 | define('angular-typeahead', ["angular"], function (a0) {
5 | return (factory(a0));
6 | });
7 | } else if (typeof exports === 'object') {
8 | // Node. Does not work with strict CommonJS, but
9 | // only CommonJS-like environments that support module.exports,
10 | // like Node.
11 | module.exports = factory(require("angular"),require("typeahead.js"));
12 | } else {
13 | factory(angular);
14 | }
15 | }(this, function (angular) {
16 |
17 | "use strict";
18 | angular.module('siyfion.sfTypeahead', [])
19 |
20 | // Inject the typeahead jquery plugin through angular to make it easier to unit
21 | // test the library.
22 | // Usage:
23 | // instead of `$element.typeahead(foo, bar)`
24 | // do `$typeahead($element, foo, bar)`
25 | .value('$typeahead', function(subject) {
26 | subject.typeahead.apply(subject, Array.prototype.slice.call(arguments, 1));
27 | })
28 |
29 | // The actual directive
30 | .directive('sfTypeahead', ['$typeahead', function ($typeahead) {
31 |
32 | return {
33 | restrict: 'AC', // Only apply on an attribute or class
34 | require: 'ngModel', // The two-way data bound value that is returned by the directive
35 | scope: {
36 | datasets: '=',
37 | options: '=',
38 | allowCustom: '=' // We cannot use '<' if we want to support angular 1.2.x
39 | },
40 | link: function(scope, element, attrs, ngModel) {
41 | var initialized = false;
42 | var options;
43 | var datasets;
44 | // Unsubscribe handle for the scope watcher
45 | var unsubscribe = null;
46 | // Remembers whether the `datasets` parameter provided was an array or an object.
47 | var datasetsIsArray;
48 |
49 | // Create the typeahead on the element
50 | initialize();
51 |
52 | // Watch for changes on datasets
53 | watchDatasets();
54 |
55 | // Parses and validates what is going to be set to model (called when: ngModel.$setViewValue(value))
56 | ngModel.$parsers.push(function(fromView) {
57 | // In Firefox, when the typeahead field loses focus, it fires an extra
58 | // angular input update event. This causes the stored model to be
59 | // replaced with the search string. If the typeahead search string
60 | // hasn't changed at all (the 'val' property doesn't update until
61 | // after the event loop finishes), then we can bail out early and keep
62 | // the current model value.
63 | if (angular.isObject(ngModel.$modelValue) && fromView === getDatumValue(ngModel.$modelValue)) {
64 | return ngModel.$modelValue;
65 | }
66 |
67 | if (!isCustomAllowed() && typeof fromView === 'string') {
68 | return ngModel.$modelValue;
69 | }
70 |
71 | return fromView;
72 | });
73 |
74 | // Formats what is going to be displayed (called when: $scope.model = { object })
75 | ngModel.$formatters.push(function(fromModel) {
76 | if (angular.isObject(fromModel)) {
77 | fromModel = getDatumValue(fromModel);
78 | }
79 |
80 | if (!fromModel) {
81 | $typeahead(element, 'val', '');
82 | } else {
83 | $typeahead(element, 'val', fromModel);
84 | }
85 | return fromModel;
86 | });
87 |
88 | function watchDatasets() {
89 | unsubscribe = datasetsIsArray ?
90 | scope.$watchCollection('datasets', datasetsChangeHandler) :
91 | scope.$watch('datasets', datasetsChangeHandler);
92 | }
93 |
94 | function datasetsChangeHandler(newValue, oldValue) {
95 | if (angular.equals(newValue, oldValue)) {
96 | return;
97 | }
98 | var oldDatasetsIsArray = datasetsIsArray;
99 | initialize();
100 | if (datasetsIsArray !== oldDatasetsIsArray) {
101 | unsubscribe();
102 | watchDatasets();
103 | }
104 | }
105 |
106 | function initialize() {
107 | if (scope.datasets == null) {
108 | throw new Error('The datasets parameter is mandatory!');
109 | }
110 |
111 | options = scope.options || {};
112 | datasetsIsArray = angular.isArray(scope.datasets);
113 | datasets = datasetsIsArray ? scope.datasets : [scope.datasets];
114 |
115 | if (!initialized) {
116 | $typeahead(element, options, datasets);
117 | scope.$watch('options', initialize);
118 | initialized = true;
119 | } else {
120 | $typeahead(element, 'destroy');
121 | $typeahead(element, options, datasets);
122 | }
123 | }
124 |
125 | // Returns the string to be displayed given some datum
126 | function getDatumValue(datum) {
127 | for (var i in datasets) {
128 | var dataset = datasets[i];
129 | var displayKey = dataset.displayKey || 'value';
130 | var value = (angular.isFunction(displayKey) ? displayKey(datum) : datum[displayKey]) || '';
131 | return value;
132 | }
133 | }
134 |
135 | function isCustomAllowed() {
136 | return scope.allowCustom === undefined || !!scope.allowCustom;
137 | }
138 |
139 | function updateScope (suggestion) {
140 | scope.$apply(function () {
141 | ngModel.$setViewValue(suggestion);
142 | });
143 | }
144 |
145 | function forwardEvent(name) {
146 | element.bind(name, function() {
147 | scope.$emit(name, Array.prototype.slice. call(arguments, 1));
148 | });
149 | }
150 |
151 | // Update the value binding when a value is manually selected from the dropdown.
152 | element.bind('typeahead:select', function(evt, suggestion, dataset) {
153 | updateScope(suggestion);
154 | scope.$emit('typeahead:select', suggestion, dataset);
155 | });
156 |
157 | // Update the value binding when a query is autocompleted.
158 | element.bind('typeahead:autocomplete', function(evt, suggestion, dataset) {
159 | updateScope(suggestion);
160 | scope.$emit('typeahead:autocomplete', suggestion, dataset);
161 | });
162 |
163 | forwardEvent('typeahead:active');
164 | forwardEvent('typeahead:idle');
165 | forwardEvent('typeahead:open');
166 | forwardEvent('typeahead:close');
167 | forwardEvent('typeahead:change');
168 | forwardEvent('typeahead:render');
169 | forwardEvent('typeahead:cursorchange');
170 | forwardEvent('typeahead:asyncrequest');
171 | forwardEvent('typeahead:asynccancel');
172 | forwardEvent('typeahead:asyncreceive');
173 | }
174 | };
175 | }]);
176 |
177 |
178 | }));
179 |
--------------------------------------------------------------------------------
/test/angular-typeahead.spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var module = angular.mock.module;
4 | var inject = angular.mock.inject;
5 |
6 | describe('dependencies', function() {
7 | describe('typeahead.js', function() {
8 | it('is correctly loaded', inject(function($compile, $rootScope) {
9 | var elem = $compile('')($rootScope.$new());
10 | expect(angular.isFunction(elem.typeahead)).toBe(true);
11 | }));
12 | });
13 | });
14 |
15 | describe('$typeahead', function() {
16 | beforeEach(module('siyfion.sfTypeahead'));
17 | it('provides a proxy to the jquery function `typeahead` to let tests hook it',
18 | inject(function($typeahead) {
19 | var subject = {
20 | typeahead: jasmine.createSpy('typeahead')
21 | };
22 | $typeahead(subject, 'jasmine', 'test');
23 | expect(subject.typeahead).toHaveBeenCalledWith('jasmine', 'test');
24 | }));
25 | });
26 |
27 | describe('sfTypeahead', function() {
28 | beforeEach(module('siyfion.sfTypeahead'));
29 |
30 | var $scope;
31 | var $element;
32 | var $typeahead;
33 |
34 | var createScope = function($rootScope) {
35 | var $scope = $rootScope.$new();
36 | $scope.datasets = {
37 | source: jasmine.createSpy('dataset source'),
38 | };
39 | $scope.options = {
40 | highlight: true
41 | };
42 | $scope.model = 'simple value';
43 | return $scope;
44 | };
45 |
46 | beforeEach(module(function($provide) {
47 | $typeahead = jasmine.createSpy('typeahead').and.callFake(function(subject) {
48 | subject.typeahead.apply(subject, Array.prototype.slice.call(arguments, 1));
49 | });
50 | $provide.value('$typeahead', $typeahead);
51 | }));
52 |
53 | describe('Behaviour in a form', function() {
54 | it('sets the proper form flags when there is data', inject(function($rootScope, $compile) {
55 | $scope = createScope($rootScope);
56 | $compile('')($scope);
59 | $scope.$digest();
60 | expect($scope.theForm.$pristine).toEqual(true);
61 | expect($scope.theForm.$dirty).toEqual(false);
62 | expect($scope.theForm.$valid).toEqual(true);
63 | expect($scope.theForm.$invalid).toEqual(false);
64 | }));
65 | it('sets the proper form flags when there is no data', inject(function($rootScope, $compile) {
66 | $scope = createScope($rootScope);
67 | $scope.model = undefined;
68 | $compile('')($scope);
71 | $scope.$digest();
72 | expect($scope.theForm.$pristine).toEqual(true);
73 | expect($scope.theForm.$dirty).toEqual(false);
74 | expect($scope.theForm.$valid).toEqual(false);
75 | expect($scope.theForm.$invalid).toEqual(true);
76 | }));
77 | });
78 | describe('Directive syntax', function() {
79 | it('is compiled on class name', inject(function($rootScope, $compile) {
80 | $scope = createScope($rootScope);
81 | $element = $compile('')($scope);
82 | $scope.$digest();
83 | expect($element.hasClass("tt-input")).toBe(true);
84 | }));
85 | it('is compiled on attribute name', inject(function($rootScope, $compile) {
86 | $scope = createScope($rootScope);
87 | $element = $compile('')($scope);
88 | $scope.$digest();
89 | expect($element.hasClass("tt-input")).toBe(true);
90 | }));
91 | it('is not compiled on tag name', inject(function($rootScope, $compile) {
92 | $scope = createScope($rootScope);
93 | $element = $compile('')($scope);
94 | $scope.$digest();
95 | expect($element.hasClass("tt-input")).not.toBe(true);
96 | }));
97 | it('requires the datasets parameter', inject(function($rootScope, $compile) {
98 | $scope = createScope($rootScope);
99 | try {
100 | $element = $compile('')($scope);
101 | $scope.$digest();
102 | fail('expected an exception');
103 | } catch(e) {
104 | expect(e.message).toEqual('The datasets parameter is mandatory!');
105 | // success
106 | }
107 | }));
108 | it('requires the ng-model parameter', inject(function($rootScope, $compile) {
109 | $scope = createScope($rootScope);
110 | try {
111 | $element = $compile('')($scope);
112 | $scope.$digest();
113 | fail('expected an exception');
114 | } catch(e) {
115 | // success
116 | }
117 | }));
118 | it('forwards `options` to tt', inject(function($rootScope, $compile) {
119 | $scope = createScope($rootScope);
120 | $element = $compile('')($scope);
121 | $scope.$digest();
122 | expect($typeahead.calls.first().args[1]).toEqual($scope.options);
123 | }));
124 | });
125 | describe('initialize', function() {
126 | it('recreates the typeahead when options attribute changes',
127 | inject(function($rootScope, $compile) {
128 | $scope = createScope($rootScope);
129 | $element = $compile('')($scope);
130 | $scope.$digest();
131 | $scope.options = {
132 | highlight: false
133 | };
134 | $scope.$digest();
135 | expect($element.hasClass('tt-input')).toBe(true);
136 | expect($typeahead).toHaveBeenCalledWith(jasmine.anything(), 'destroy');
137 | expect($typeahead).toHaveBeenCalledWith(jasmine.anything(), $scope.options, [$scope.datasets]);
138 | expect($element.val()).toEqual('simple value');
139 | expect($scope.model).toEqual('simple value');
140 | }));
141 | it('recreates the typeahead when datasets attribute changes',
142 | inject(function($rootScope, $compile) {
143 | $scope = createScope($rootScope);
144 | $element = $compile('')($scope);
145 | $scope.$digest();
146 | $scope.datasets = {
147 | source: function() {}
148 | };
149 | $scope.$digest();
150 | expect($element.hasClass('tt-input')).toBe(true);
151 | expect($typeahead).toHaveBeenCalledWith(jasmine.anything(), 'destroy');
152 | expect($typeahead).toHaveBeenCalledWith(jasmine.anything(), $scope.options, [$scope.datasets]);
153 | expect($element.val()).toEqual('simple value');
154 | expect($scope.model).toEqual('simple value');
155 | }));
156 | it('recreates the typeahead when datasets array attribute changes',
157 | inject(function($rootScope, $compile) {
158 | $scope = createScope($rootScope);
159 | $scope.datasets = [$scope.datasets];
160 | $element = $compile('')($scope);
161 | $scope.$digest();
162 | $scope.datasets.push({
163 | source: function() {}
164 | });
165 | $scope.$digest();
166 | expect($element.hasClass('tt-input')).toBe(true);
167 | expect($typeahead).toHaveBeenCalledWith(jasmine.anything(), 'destroy');
168 | expect($typeahead).toHaveBeenCalledWith(jasmine.anything(), $scope.options, $scope.datasets);
169 | expect($element.val()).toEqual('simple value');
170 | expect($scope.model).toEqual('simple value');
171 | }));
172 | it('recreates the typeahead when datasets array becomes a single dataset',
173 | inject(function($rootScope, $compile) {
174 | $scope = createScope($rootScope);
175 | $scope.datasets = [$scope.datasets];
176 | $element = $compile('')($scope);
177 | $scope.$digest();
178 | $scope.datasets = {
179 | source: function() {}
180 | };
181 | $scope.$digest();
182 | expect($element.hasClass('tt-input')).toBe(true);
183 | expect($typeahead).toHaveBeenCalledWith(jasmine.anything(), 'destroy');
184 | expect($typeahead).toHaveBeenCalledWith(jasmine.anything(), $scope.options, [$scope.datasets]);
185 | expect($element.val()).toEqual('simple value');
186 | expect($scope.model).toEqual('simple value');
187 | }));
188 | });
189 | describe('model', function() {
190 | var $typeahead;
191 | var $replicated;
192 | beforeEach(inject(function($rootScope, $compile) {
193 | $scope = createScope($rootScope);
194 | $element = $compile('' +
195 | '' +
196 | '' +
197 | '
')($scope);
198 | $typeahead = $element.find('#typeahead');
199 | $replicated = $element.find('#replicated');
200 | $scope.$digest();
201 | }));
202 | it('is shown on initialization', function() {
203 | $scope.$digest();
204 | expect($typeahead.val()).toEqual('simple value');
205 | });
206 | it('is bound [scope -> UI]', function() {
207 | expect($typeahead.val()).toEqual('simple value');
208 | $scope.model = 'other value';
209 | $scope.$digest();
210 | expect($typeahead.val()).toEqual('other value');
211 | });
212 | it('is bound [UI -> scope]', function() {
213 | expect($typeahead.val()).toEqual('simple value');
214 | $typeahead.val('other value').trigger('input');
215 | expect($scope.model).toEqual('other value');
216 | expect($replicated.val()).toEqual('other value');
217 | });
218 | it('is updated when on typeahead:select', inject(function() {
219 | $typeahead.val('other value').trigger('typeahead:select', 'other value');
220 | expect($scope.model).toEqual('other value');
221 | expect($replicated.val()).toEqual('other value');
222 | }));
223 | it('is updated when on typeahead:autocomplete', function() {
224 | $typeahead.val('other value').trigger('typeahead:autocomplete', 'other value');
225 | expect($scope.model).toEqual('other value');
226 | expect($replicated.val()).toEqual('other value');
227 | });
228 | });
229 | describe('formatter', function() {
230 | it('sets simple string values', inject(function($rootScope, $compile) {
231 | $scope = createScope($rootScope);
232 | $element = $compile('')($scope);
233 | $scope.$digest();
234 | expect($element.val()).toEqual('simple value');
235 | expect($element.typeahead('val')).toEqual('simple value');
236 | }));
237 | it('sets objects with key', inject(function($rootScope, $compile) {
238 | $scope = createScope($rootScope);
239 | $scope.model = {
240 | value: 'simple value'
241 | };
242 | $element = $compile('')($scope);
243 | $scope.$digest();
244 | expect($element.val()).toEqual('simple value');
245 | expect($element.typeahead('val')).toEqual('simple value');
246 | }));
247 | it('sets the empty string for falsy values', inject(function($rootScope, $compile) {
248 | $scope = createScope($rootScope);
249 | $scope.model = {
250 | value: false
251 | };
252 | $element = $compile('')($scope);
253 | $scope.$digest();
254 | expect($element.val()).toEqual('');
255 | expect($element.typeahead('val')).toEqual('');
256 | }));
257 | });
258 | describe('allowCustom', function() {
259 | beforeEach(inject(function($rootScope, $compile) {
260 | $scope = createScope($rootScope);
261 | $scope.allowCustom = false;
262 | $element = $compile('')($scope);
263 | $scope.$digest();
264 | }));
265 | it('forbids modification of the model from user input', function() {
266 | expect($element.val()).toEqual('simple value');
267 | $element.val('other value').trigger('input');
268 | expect($scope.model).toEqual('simple value');
269 | });
270 | it('can be changed at runtime', function() {
271 | $scope.allowCustom = true;
272 | $scope.$digest();
273 | expect($element.val()).toEqual('simple value');
274 | $element.val('other value').trigger('input');
275 | expect($scope.model).toEqual('other value');
276 | });
277 | });
278 | describe('displayKey', function() {
279 | it('equals "value" by default', inject(function($rootScope, $compile) {
280 | $scope = createScope($rootScope);
281 | $scope.model = {
282 | value: 'simple value'
283 | };
284 | $element = $compile('')($scope);
285 | $scope.$digest();
286 | expect($element.val()).toEqual('simple value');
287 | }));
288 | it('can be a function which returns the value', inject(function($rootScope, $compile) {
289 | $scope = createScope($rootScope);
290 | $scope.model = {
291 | my_value: 'simple value'
292 | };
293 | $scope.datasets.displayKey = function(model) { return model.my_value; };
294 | spyOn($scope.datasets, 'displayKey').and.callThrough();
295 | $element = $compile('')($scope);
296 | $scope.$digest();
297 | expect($element.val()).toEqual('simple value');
298 | expect($scope.datasets.displayKey).toHaveBeenCalledWith($scope.model);
299 | }));
300 | it('Is an empty string if the key is falsy', inject(function($rootScope, $compile) {
301 | $scope = createScope($rootScope);
302 | $scope.model = {
303 | value: null
304 | };
305 | $element = $compile('')($scope);
306 | $scope.$digest();
307 | expect($element.val()).toEqual('');
308 | }));
309 | });
310 | describe('object model', function() {
311 | var $typeahead;
312 | var $replicated;
313 | beforeEach(inject(function($rootScope, $compile) {
314 | $scope = createScope($rootScope);
315 | $scope.model = {
316 | my_value: 'simple value'
317 | };
318 | $scope.datasets.displayKey = 'my_value';
319 | $element = $compile('' +
320 | '' +
321 | '' +
322 | '
'
323 | )($scope);
324 | $typeahead = $element.find('#typeahead');
325 | $replicated = $element.find('#replicated');
326 | $scope.$digest();
327 | }));
328 | it('is shown on initialization', function() {
329 | $scope.$digest();
330 | expect($typeahead.val()).toEqual('simple value');
331 | });
332 | it('is bound [scope -> UI]', function() {
333 | expect($typeahead.val()).toEqual('simple value');
334 | $scope.model = {
335 | my_value: 'other value'
336 | };
337 | $scope.$digest();
338 | expect($typeahead.val()).toEqual('other value');
339 | });
340 | it('is bound [UI -> scope] and takes the string value in that case', function() {
341 | expect($typeahead.val()).toEqual('simple value');
342 | $typeahead.val('other value').trigger('input');
343 | expect($scope.model).toEqual('other value');
344 | expect($replicated.val()).toEqual('');
345 | });
346 | it('is updated on typeahead:select', inject(function() {
347 | $typeahead.val('other value').trigger('typeahead:select', { my_value: 'other value' });
348 | expect($scope.model).toEqual({ my_value: 'other value' });
349 | expect($replicated.val()).toEqual('other value');
350 | }));
351 | it('is updated on typeahead:autocomplete', function() {
352 | $typeahead.val('other value').trigger('typeahead:autocomplete', { my_value: 'other value' });
353 | expect($scope.model).toEqual({ my_value: 'other value' });
354 | expect($replicated.val()).toEqual('other value');
355 | });
356 | it('prevents unintended model updates (#63)',
357 | function() {
358 | $typeahead.val('other value').trigger('typeahead:autocomplete', { my_value: 'other value' });
359 | expect($scope.model).toEqual({ my_value: 'other value' });
360 | $typeahead.trigger('input');
361 | expect($scope.model).toEqual({ my_value: 'other value' });
362 | });
363 | });
364 | describe('events', function() {
365 | var handler;
366 | beforeEach(inject(function($rootScope, $compile) {
367 | handler = jasmine.createSpy('event handler');
368 | $scope = createScope($rootScope);
369 | $element = $compile('')($scope);
370 | $scope.$digest();
371 | }));
372 | it('forwards typeahead:active', function() {
373 | $scope.$on('typeahead:active', handler);
374 | $element.trigger('typeahead:active', 'some value');
375 | expect(handler).toHaveBeenCalled();
376 | });
377 | it('forwards typeahead:idle', function() {
378 | $scope.$on('typeahead:idle', handler);
379 | $element.trigger('typeahead:idle', 'some value');
380 | expect(handler).toHaveBeenCalled();
381 | });
382 | it('forwards typeahead:open', function() {
383 | $scope.$on('typeahead:open', handler);
384 | $element.trigger('typeahead:open');
385 | expect(handler).toHaveBeenCalled();
386 | });
387 | it('forwards typeahead:close', function() {
388 | $scope.$on('typeahead:close', handler);
389 | $element.trigger('typeahead:close', 'some value');
390 | expect(handler).toHaveBeenCalled();
391 | });
392 | it('forwards typeahead:change', function() {
393 | $scope.$on('typeahead:change', handler);
394 | $element.trigger('typeahead:change', 'some value');
395 | expect(handler).toHaveBeenCalled();
396 | });
397 | it('forwards typeahead:render', function() {
398 | $scope.$on('typeahead:render', handler);
399 | $element.trigger('typeahead:render');
400 | expect(handler).toHaveBeenCalled();
401 | });
402 | it('forwards typeahead:select', function() {
403 | $scope.$on('typeahead:select', handler);
404 | $element.trigger('typeahead:select', 'some value');
405 | expect(handler).toHaveBeenCalled();
406 | });
407 | it('forwards typeahead:autocomplete', function() {
408 | $scope.$on('typeahead:autocomplete', handler);
409 | $element.trigger('typeahead:autocomplete', 'some value');
410 | expect(handler).toHaveBeenCalled();
411 | });
412 | it('forwards typeahead:cursorchange', function() {
413 | $scope.$on('typeahead:cursorchange', handler);
414 | $element.trigger('typeahead:cursorchange');
415 | expect(handler).toHaveBeenCalled();
416 | });
417 | it('forwards typeahead:asyncrequest', function() {
418 | $scope.$on('typeahead:asyncrequest', handler);
419 | $element.trigger('typeahead:asyncrequest');
420 | expect(handler).toHaveBeenCalled();
421 | });
422 | it('forwards typeahead:asynccancel', function() {
423 | $scope.$on('typeahead:asynccancel', handler);
424 | $element.trigger('typeahead:asynccancel');
425 | expect(handler).toHaveBeenCalled();
426 | });
427 | it('forwards typeahead:asyncreceive', function() {
428 | $scope.$on('typeahead:asyncreceive', handler);
429 | $element.trigger('typeahead:asyncreceive');
430 | expect(handler).toHaveBeenCalled();
431 | });
432 | });
433 | });
434 |
--------------------------------------------------------------------------------