Autocomplete for Angular.js applications with a lot to complete

Details

Dependencies Angular, Angular-Sanitize Size 12.2kb, 4kb minified Bower bower install angular-mass-autocomplete NPM npm install angular-mass-autocomplete

Directive

mass-autocomplete - Auto complete container. Maintains the suggestion box.

mass-autocomplete-item - Attached to an input. Requires ng-model and mass-autocomplete.

Usage

{{ $index }}: {{ cb }}

HTML

<html> <head> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-sanitize.js"></script> <script src="massautocomplete.js"></script> <!-- Optional --> <link href="massautocomplete.theme.css" rel="stylesheet" type="text/css"> </head> <body ng-app=app> <div ng-controller=mainCtrl> <div mass-autocomplete> <input ng-model="dirty.value" mass-autocomplete-item="autocomplete_options"> </div> </div> </body> </html>

Javascript

var app = angular.module('app', ['ngSanitize', 'MassAutoComplete']); app.controller('mainCtrl', function ($scope, $sce, $q) { $scope.dirty = {}; var states = ['Alabama', 'Alaska', 'California', /* ... */ ]; function suggest_state(term) { var q = term.toLowerCase().trim(); var results = []; // Find first 10 states that start with `term`. for (var i = 0; i < states.length && results.length < 10; i++) { var state = states[i]; if (state.toLowerCase().indexOf(q) === 0) results.push({ label: state, value: state }); } return results; } $scope.autocomplete_options = { suggest: suggest_state }; });

Motivation

MassAutocomplete was created as part of a new project that required auto completing a lot of input fields.

Most existing auto complete implementations (1, 2) are wrapping the input field and appending a suggestion box along with several watches. This is fine as long as you want to auto-complete only one or two inputs. But, when there are several dozen inputs, the memory, DOM and watch count will start to bloat and that might impact performance.

A different approach

MassAutocomplete was implemented with a different approach in mind. Instead of attaching the suggestion box to each input field, we use transclusion to maintain only one box for the entire document. This approach guarantees that additional input fields will not require additional DOM or watches for the purpose of the auto complete.

MassAutocomplete does not provide filtering, linking, ranking or sorting. Generating the suggestions is left to the application.

Comparing implementations

We performed a comparison between mass-autocomplete and the popular ui-typehead.

The results are here.

Examples

All examples in this section are using just one mass-autocomplete container.

Kitchen Sink

Complete example including highlighting and fuzzy matching. Searching and ranking using Fuse.js.

Try inserting sauth or rod iland or gorgia.

var fuzzySearch = new Fuse(states, { shouldSort: true, caseSensitive: false, threshold: 0.4, }); function fuzzy_suggest(term) { if (!term) return []; return fuzzySearch .search(term) .slice(0, 10) .map(function (i) { var val = states[i]; return { value: val, label: $sce.trustAsHtml(highlight(val, term)) }; }); } $scope.ac_fuse_options = { suggest: fuzzy_suggest };

Highlighting

function highlight(str, term) { var highlight_regex = new RegExp('(' + term + ')', 'gi'); return str.replace(highlight_regex, '<span class="highlight">$1</span>'); }; function suggest_state_with_highlight(term) { if (term.length < 2) return; var suggestions = suggest_state(term); suggestions.forEach(function (s) { // In real life you should reuse the regexp object. s.label = $sce.trustAsHtml(highlight(s.label, term)); }); return suggestions; }; $scope.ac_option_highlight = { suggest: suggest_state_with_highlight };

Delimited

function suggest_state_delimited(term) { var ix = term.lastIndexOf(','), lhs = term.substring(0, ix + 1), rhs = term.substring(ix + 1), suggestions = suggest_state(rhs); suggestions.forEach(function (s) { s.value = lhs + s.value; }); return suggestions; }; $scope.ac_option_delimited = { suggest: suggest_state_delimited };

Custom formatting

function suggest_state_as_tag(term) { var suggestions = suggest_state_delimited(term); suggestions.forEach(function (s) { s.label = $sce.trustAsHtml( '<span class="badge">' + s.label + '</span>' ); }); return suggestions; }; $scope.ac_option_tag = { suggest: suggest_state_as_tag };

Using the selected object



No tags selected.



Select a tag from the auto complete menu using the keyboard or mouse. {{tag}} $scope.tags = []; function add_tag(selected) { $scope.tags.push(selected.value); // Clear model $scope.dirty.selected_tag = undefined; }; $scope.ac_option_tag_select = { suggest: suggest_state_as_tag, on_select: add_tag };

Remote source

function suggest_state_remote(term) { var deferred = $q.defer(); // Fake remote source using timeout $timeout(function () { deferred.resolve(suggest_state(term)); }, 500); return deferred.promise; } $scope.ac_option_remote = { suggest: suggest_state_remote, on_error: console.log };

Passing objects

Selected Object {{ selected_user | json }} var users = [ {name: 'Haki', joined: '2 month ago', email: 'Haki@email.com'}, {name: 'Ran', joined: '2 days ago', email: 'Ran123@ac.org'}, {name: 'John', joined: 'a week ago', email: 'JJ@gmail.com'}, {name: 'Mary', joined: 'Yesterday', email: 'Mary@yahoo.com'}, {name: 'Charlie', joined: 'Just now', email: 'Charlie@msn.com'}, {name: 'Rebecca', joined: 'Yesterday', email: 'Becky@mail.com'}, {name: 'James', joined: '3 month ago', email: 'James@inbox.com'} ]; function suggest_users(term) { var q = term.toLowerCase().trim(), results = []; for (var i = 0; i < users.length; i++) { var user = users[i]; if (user.name.toLowerCase().indexOf(q) !== -1 || user.email.toLowerCase().indexOf(q) !== -1) results.push({ value: user.name, // Pass the object as well. Can be any property name. obj: user, label: $sce.trustAsHtml( '<div class="row">' + ' <div class="col-xs-5">' + ' <i class="fa fa-user"></i>' + ' <strong>' + highlight(user.name,term) + '</strong>'+ ' </div>' + ' <div class="col-xs-7 text-right text-muted">' + ' <small>' + highlight(user.email,term) + '</small>' + ' </div>' + ' <div class="col-xs-12">' + ' <span class="text-muted">Joined</span>' + user.joined + ' </div>' + '</div>' ) }); } return results; }; $scope.ac_options_users = { suggest: suggest_users, on_select: function (selected) { $scope.selected_user = selected.obj; } };

Reusing options



Enter states:

<div ng-repeat="i in n_array(n) track by $index"/> <input type="text" placeholder="Select State" ng-model="dirty.state[$index]" calc-autocomplete-item="ac_option_highlight"> </div>

Options

mass-autocomplete=options debounce_position(150) Debounce in ms for repositioning suggestion box on window resize. debounce_attach(300) Debounce in ms for attaching input after focus.

Prevents unnecessary positioning when quickly jumping between inputs using tab . debounce_suggest(200) Debounce in ms for calling suggest.

Useful for remote sources and to suspend suggestions while user is typing. debounce_blur(150) Debounce in ms for detach on blur.

Determines the amount of time in milliseconds before losing focus as a result of selecting from the menu until detach is invoked. mass-autocomplete-item=options_name suggest(term) Receive a string and return an array of suggestions. Each suggestion must contain value - Text to display in the textbox.

- Text to display in the textbox. label - String or trusted HTML to display in the suggestion box. The label is bound using ng-bind-html - to bind raw html use $sce.trustAsHtml() . The return value is an array of objects or a promise that resolves to an array of objects. The object is passed to the on_select callback so additional properties can be used. on_attach() Callback fired when the user focus a field. on_select(selected_item) Callback fired when the user select an item from the suggestion box. on_detach(current_value) Callback fired when the input field is blurred. Useful for purging caches. on_error() Callback fired in case suggest() fails. auto_select_first (false) Auto select the first option in the suggestion box. massAutoCompleteConfigProvider position_autocomplete(container, target) Called on attach to position the ac container relative to the target input. Some use common use cases when a custom positioning function might be desired: The AC container should be fixed positioned. You wish to use external libraries to position the element (such as jquery). The default positioning function is not sufficient and adjustments cannot be made using only css. generate_random_id(prefix) Used to generate random Id's starting at `prefix`. Id's are generated mostly for accessibility needs.

It's very unlikely you will need to change this function. DEBOUNCE Set default debounce globally.

Default values are: position = 150

= 150 attach = 300

= 300 suggest = 200

= 200 blur = 150

CSS

The suggestion box template

<div class="ac-container" ng-show="show_autocomplete && results.length" style="position:absolute;"> <ul class="ac-menu"> <li ng-repeat="result in results" ng-if="$index > 0" class="ac-menu-item" ng-class="$index == selected_index ? 'ac-state-focus' : ''"> <a href ng-click="apply_selection($index, $event)" ng-bind-html=result.label> </a> </li> </ul> </div>

We recommend adding backface-visibility: hidden; and transform: translate3d(0, 0, 0); to .ac-container to limit the paint area to that of the suggestion box. See massautocomplete.theme.css for the complete CSS.

Accessibility

The implementation was inspired by the dojo project implementation of autocomplete. It is also recommended to include ngAria for the built-in aria support (mainly for ng-show/ng-hide).

The following markup is used to accompany assisting technology:

<input id="ac_element_XXXXXX" ng-model="dirty.value" mass-autocomplete-item /> .... <div class="ac-container" aria-autocomplete="list" role="listbox" aria-labelledby="ac_element_XXXXXX" aria-activedescendant="ac_item_XXXXX" ng-show="show_autocomplete"> <ul class="ac-menu"> <li ng-repeat="result in results" class="ac-menu-item" role="option" id="ac_item_XXXXX" ng-class="..."> <a href ng-click="..."></a> </li> </ul> </div>

Keyboard