angular-material-npfixed
Version:
The Angular Material project is an implementation of Material Design in Angular.js. This project provides a set of reusable, well-tested, and accessible Material Design UI components. Angular Material is supported internally at Google by the Angular.js, M
418 lines (402 loc) • 18.9 kB
JavaScript
angular
.module('material.components.autocomplete')
.directive('mdAutocomplete', MdAutocomplete);
/**
* @ngdoc directive
* @name mdAutocomplete
* @module material.components.autocomplete
*
* @description
* `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a
* custom query. This component allows you to provide real-time suggestions as the user types
* in the input area.
*
* To start, you will need to specify the required parameters and provide a template for your
* results. The content inside `md-autocomplete` will be treated as a template.
*
* In more complex cases, you may want to include other content such as a message to display when
* no matches were found. You can do this by wrapping your template in `md-item-template` and
* adding a tag for `md-not-found`. An example of this is shown below.
*
* To reset the displayed value you must clear both values for `md-search-text` and `md-selected-item`.
*
* ### Validation
*
* You can use `ng-messages` to include validation the same way that you would normally validate;
* however, if you want to replicate a standard input with a floating label, you will have to
* do the following:
*
* - Make sure that your template is wrapped in `md-item-template`
* - Add your `ng-messages` code inside of `md-autocomplete`
* - Add your validation properties to `md-autocomplete` (ie. `required`)
* - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
*
* There is an example below of how this should look.
*
* ### Snapping Drop-Down
*
* You can cause the autocomplete drop-down to snap to an ancestor element by applying the
* `md-autocomplete-snap` attribute to that element. You can also snap to the width of
* the `md-autocomplete-snap` element by setting the attribute's value to `width`
* (ie. `md-autocomplete-snap="width"`).
*
* ### Notes
*
* **Autocomplete Dropdown Items Rendering**
*
* The `md-autocomplete` uses the the <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeat</a>
* directive for displaying the results inside of the dropdown.<br/>
*
* > When encountering issues regarding the item template please take a look at the
* <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation.
*
* **Autocomplete inside of a Virtual Repeat**
*
* When using the `md-autocomplete` directive inside of a
* <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> the dropdown items might
* not update properly, because caching of the results is enabled by default.
*
* The autocomplete will then show invalid dropdown items, because the VirtualRepeat only updates the
* scope bindings, rather than re-creating the `md-autocomplete` and the previous cached results will be used.
*
* > To avoid such problems ensure that the autocomplete does not cache any results.
*
* <hljs lang="html">
* <md-autocomplete
* md-no-cache="true"
* md-selected-item="selectedItem"
* md-items="item in items"
* md-search-text="searchText"
* md-item-text="item.display">
* <span>{{ item.display }}</span>
* </md-autocomplete>
* </hljs>
*
*
*
* @param {expression} md-items An expression in the format of `item in results` to iterate over
* matches for your search.<br/><br/>
* The `results` expression can be also a function, which returns the results synchronously
* or asynchronously (per Promise)
* @param {expression=} md-selected-item-change An expression to be run each time a new item is
* selected
* @param {expression=} md-search-text-change An expression to be run each time the search text
* updates
* @param {expression=} md-search-text A model to bind the search query text to
* @param {object=} md-selected-item A model to bind the selected item to
* @param {expression=} md-item-text An expression that will convert your object to a single string.
* @param {string=} placeholder Placeholder text that will be forwarded to the input.
* @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete
* @param {boolean=} ng-disabled Determines whether or not to disable the input field
* @param {boolean=} md-require-match When set to true, the autocomplete will add a validator,
* which will evaluate to false, when no item is currently selected.
* @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
* make suggestions
* @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
* for results
* @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show up or not.
* @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a `$mdDialog`,
* `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening. <br/><br/>
* Also the autocomplete will immediately focus the input element.
* @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating label
* @param {boolean=} md-autoselect If set to true, the first item will be automatically selected
* in the dropdown upon open.
* @param {string=} md-menu-class This will be applied to the dropdown menu for styling
* @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
* `md-input-container`
* @param {string=} md-input-name The name attribute given to the input element to be used with
* FormController
* @param {string=} md-select-on-focus When present the inputs text will be automatically selected
* on focus.
* @param {string=} md-input-id An ID to be added to the input element
* @param {number=} md-input-minlength The minimum length for the input's value for validation
* @param {number=} md-input-maxlength The maximum length for the input's value for validation
* @param {boolean=} md-select-on-match When set, autocomplete will automatically select exact
* the item if the search text is an exact match. <br/><br/>
* Exact match means that there is only one match showing up.
* @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete
* will select on case-insensitive match
* @param {string=} md-escape-options Override escape key logic. Default is `blur clear`.<br/>
* Options: `blur | clear`, `none`
* @param {string=} md-dropdown-items Specifies the maximum amount of items to be shown in
* the dropdown.<br/><br/>
* When the dropdown doesn't fit into the viewport, the dropdown will shrink
* as less as possible.
* @param {string=} md-dropdown-position Overrides the default dropdown position. Options: `top`, `bottom`.
* @param {string=} ng-trim If set to false, the search text will be not trimmed automatically.
* Defaults to true.
* @param {string=} ng-pattern Adds the pattern validator to the ngModel of the search text.
* [ngPattern Directive](https://docs.angularjs.org/api/ng/directive/ngPattern)
*
* @usage
* ### Basic Example
* <hljs lang="html">
* <md-autocomplete
* md-selected-item="selectedItem"
* md-search-text="searchText"
* md-items="item in getMatches(searchText)"
* md-item-text="item.display">
* <span md-highlight-text="searchText">{{item.display}}</span>
* </md-autocomplete>
* </hljs>
*
* ### Example with "not found" message
* <hljs lang="html">
* <md-autocomplete
* md-selected-item="selectedItem"
* md-search-text="searchText"
* md-items="item in getMatches(searchText)"
* md-item-text="item.display">
* <md-item-template>
* <span md-highlight-text="searchText">{{item.display}}</span>
* </md-item-template>
* <md-not-found>
* No matches found.
* </md-not-found>
* </md-autocomplete>
* </hljs>
*
* In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
* different parts that make up our component.
*
* ### Clear button for the input
* By default, for floating label autocomplete's the clear button is not showing up
* ([See specs](https://material.google.com/components/text-fields.html#text-fields-auto-complete-text-field))
*
* Nevertheless, developers are able to explicitly toggle the clear button for all types of autocomplete's.
*
* <hljs lang="html">
* <md-autocomplete ... md-clear-button="true"></md-autocomplete>
* <md-autocomplete ... md-clear-button="false"></md-autocomplete>
* </hljs>
*
* ### Example with validation
* <hljs lang="html">
* <form name="autocompleteForm">
* <md-autocomplete
* required
* md-input-name="autocomplete"
* md-selected-item="selectedItem"
* md-search-text="searchText"
* md-items="item in getMatches(searchText)"
* md-item-text="item.display">
* <md-item-template>
* <span md-highlight-text="searchText">{{item.display}}</span>
* </md-item-template>
* <div ng-messages="autocompleteForm.autocomplete.$error">
* <div ng-message="required">This field is required</div>
* </div>
* </md-autocomplete>
* </form>
* </hljs>
*
* In this example, our code utilizes `md-item-template` and `ng-messages` to specify
* input validation for the field.
*
* ### Asynchronous Results
* The autocomplete items expression also supports promises, which will resolve with the query results.
*
* <hljs lang="js">
* function AppController($scope, $http) {
* $scope.query = function(searchText) {
* return $http
* .get(BACKEND_URL + '/items/' + searchText)
* .then(function(data) {
* // Map the response object to the data object.
* return data;
* });
* };
* }
* </hljs>
*
* <hljs lang="html">
* <md-autocomplete
* md-selected-item="selectedItem"
* md-search-text="searchText"
* md-items="item in query(searchText)">
* <md-item-template>
* <span md-highlight-text="searchText">{{item}}</span>
* </md-item-template>
* </md-autocomplete>
* </hljs>
*
*/
function MdAutocomplete ($$mdSvgRegistry) {
return {
controller: 'MdAutocompleteCtrl',
controllerAs: '$mdAutocompleteCtrl',
scope: {
inputName: '@mdInputName',
inputMinlength: '@mdInputMinlength',
inputMaxlength: '@mdInputMaxlength',
searchText: '=?mdSearchText',
selectedItem: '=?mdSelectedItem',
itemsExpr: '@mdItems',
itemText: '&mdItemText',
placeholder: '@placeholder',
noCache: '=?mdNoCache',
requireMatch: '=?mdRequireMatch',
selectOnMatch: '=?mdSelectOnMatch',
matchInsensitive: '=?mdMatchCaseInsensitive',
itemChange: '&?mdSelectedItemChange',
textChange: '&?mdSearchTextChange',
minLength: '=?mdMinLength',
delay: '=?mdDelay',
autofocus: '=?mdAutofocus',
floatingLabel: '@?mdFloatingLabel',
autoselect: '=?mdAutoselect',
menuClass: '@?mdMenuClass',
inputId: '@?mdInputId',
escapeOptions: '@?mdEscapeOptions',
dropdownItems: '=?mdDropdownItems',
dropdownPosition: '@?mdDropdownPosition',
clearButton: '=?mdClearButton'
},
compile: function(tElement, tAttrs) {
var attributes = ['md-select-on-focus', 'md-no-asterisk', 'ng-trim', 'ng-pattern'];
var input = tElement.find('input');
attributes.forEach(function(attribute) {
var attrValue = tAttrs[tAttrs.$normalize(attribute)];
if (attrValue !== null) {
input.attr(attribute, attrValue);
}
});
return function(scope, element, attrs, ctrl) {
// Retrieve the state of using a md-not-found template by using our attribute, which will
// be added to the element in the template function.
ctrl.hasNotFound = !!element.attr('md-has-not-found');
// By default the inset autocomplete should show the clear button when not explicitly overwritten.
if (!angular.isDefined(attrs.mdClearButton) && !scope.floatingLabel) {
scope.clearButton = true;
}
}
},
template: function (element, attr) {
var noItemsTemplate = getNoItemsTemplate(),
itemTemplate = getItemTemplate(),
leftover = element.html(),
tabindex = attr.tabindex;
// Set our attribute for the link function above which runs later.
// We will set an attribute, because otherwise the stored variables will be trashed when
// removing the element is hidden while retrieving the template. For example when using ngIf.
if (noItemsTemplate) element.attr('md-has-not-found', true);
// Always set our tabindex of the autocomplete directive to -1, because our input
// will hold the actual tabindex.
element.attr('tabindex', '-1');
return '\
<md-autocomplete-wrap\
ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \
\'md-menu-showing\': !$mdAutocompleteCtrl.hidden, \
\'md-show-clear-button\': !!clearButton }">\
' + getInputElement() + '\
' + getClearButton() + '\
<md-progress-linear\
class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\
ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\
md-mode="indeterminate"></md-progress-linear>\
<md-virtual-repeat-container\
md-auto-shrink\
md-auto-shrink-min="1"\
ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\
ng-hide="$mdAutocompleteCtrl.hidden"\
class="md-autocomplete-suggestions-container md-whiteframe-z1"\
ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
role="presentation">\
<ul class="md-autocomplete-suggestions"\
ng-class="::menuClass"\
id="ul-{{$mdAutocompleteCtrl.id}}">\
<li md-virtual-repeat="item in $mdAutocompleteCtrl.matches"\
ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
ng-click="$mdAutocompleteCtrl.select($index)"\
md-extra-name="$mdAutocompleteCtrl.itemName">\
' + itemTemplate + '\
</li>' + noItemsTemplate + '\
</ul>\
</md-virtual-repeat-container>\
</md-autocomplete-wrap>';
function getItemTemplate() {
var templateTag = element.find('md-item-template').detach(),
html = templateTag.length ? templateTag.html() : element.html();
if (!templateTag.length) element.empty();
return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html + '</md-autocomplete-parent-scope>';
}
function getNoItemsTemplate() {
var templateTag = element.find('md-not-found').detach(),
template = templateTag.length ? templateTag.html() : '';
return template
? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()"\
md-autocomplete-parent-scope>' + template + '</li>'
: '';
}
function getInputElement () {
if (attr.mdFloatingLabel) {
return '\
<md-input-container ng-if="floatingLabel">\
<label>{{floatingLabel}}</label>\
<input type="search"\
' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
id="{{ inputId || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\
name="{{inputName}}"\
autocomplete="off"\
ng-required="$mdAutocompleteCtrl.isRequired"\
ng-readonly="$mdAutocompleteCtrl.isReadonly"\
ng-minlength="inputMinlength"\
ng-maxlength="inputMaxlength"\
ng-disabled="$mdAutocompleteCtrl.isDisabled"\
ng-model="$mdAutocompleteCtrl.scope.searchText"\
ng-model-options="{ allowInvalid: true }"\
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
ng-blur="$mdAutocompleteCtrl.blur($event)"\
ng-focus="$mdAutocompleteCtrl.focus($event)"\
aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
aria-label="{{floatingLabel}}"\
aria-autocomplete="list"\
role="combobox"\
aria-haspopup="true"\
aria-activedescendant=""\
aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
<div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
</md-input-container>';
} else {
return '\
<input type="search"\
' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
id="{{ inputId || \'input-\' + $mdAutocompleteCtrl.id }}"\
name="{{inputName}}"\
ng-if="!floatingLabel"\
autocomplete="off"\
ng-required="$mdAutocompleteCtrl.isRequired"\
ng-disabled="$mdAutocompleteCtrl.isDisabled"\
ng-readonly="$mdAutocompleteCtrl.isReadonly"\
ng-minlength="inputMinlength"\
ng-maxlength="inputMaxlength"\
ng-model="$mdAutocompleteCtrl.scope.searchText"\
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
ng-blur="$mdAutocompleteCtrl.blur($event)"\
ng-focus="$mdAutocompleteCtrl.focus($event)"\
placeholder="{{placeholder}}"\
aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
aria-label="{{placeholder}}"\
aria-autocomplete="list"\
role="combobox"\
aria-haspopup="true"\
aria-activedescendant=""\
aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>';
}
}
function getClearButton() {
return '' +
'<button ' +
'type="button" ' +
'aria-label="Clear Input" ' +
'tabindex="-1" ' +
'ng-if="clearButton && $mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled" ' +
'ng-click="$mdAutocompleteCtrl.clear($event)">' +
'<md-icon md-svg-src="' + $$mdSvgRegistry.mdClose + '"></md-icon>' +
'</button>';
}
}
};
}