Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

feat(autocomplete): adds support for ng-messages #2664

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions src/components/autocomplete/autocomplete.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
$autocomplete-option-height: 48px;
$input-container-padding: 2px !default;
$input-error-height: 24px !default;

@keyframes md-autocomplete-list-out {
0% {
Expand Down Expand Up @@ -40,21 +38,13 @@ md-autocomplete {
overflow: visible;
min-width: 190px;
&[md-floating-label] {
padding-bottom: $input-container-padding + $input-error-height;
box-shadow: none;
border-radius: 0;
background: transparent;
height: auto;
md-input-container {
padding-bottom: 0;
}
md-autocomplete-wrap {
height: auto;
}
button {
top: auto;
bottom: 5px;
}
}
md-autocomplete-wrap {
display: block;
Expand Down Expand Up @@ -105,6 +95,12 @@ md-autocomplete {
display: none;
}
}
.md-autocomplete-button-wrapper {
position: relative;
flex: 1 1 auto;
order: 1;
transform: translateY(-10px);
}
button {
position: absolute;
top: 10px;
Expand Down
40 changes: 39 additions & 1 deletion src/components/autocomplete/autocomplete.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('<md-autocomplete>', function() {
}

describe('basic functionality', function () {
it('should fail', inject(function($timeout, $mdConstant, $rootElement) {
it('should update selected item and search text', inject(function($timeout, $mdConstant, $rootElement) {
var scope = createScope();
var template = '\
<md-autocomplete\
Expand Down Expand Up @@ -63,6 +63,44 @@ describe('<md-autocomplete>', function() {
}));
});

describe('basic functionality with template', function () {
it('should update selected item and search text', inject(function($timeout, $mdConstant, $rootElement) {
var scope = createScope();
var template = '\
<md-autocomplete\
md-selected-item="selectedItem"\
md-search-text="searchText"\
md-items="item in match(searchText)"\
md-item-text="item.display"\
placeholder="placeholder">\
<md-item-template>\
<span md-highlight-text="searchText">{{item.display}}</span>\
</md-item-template>\
</md-autocomplete>';
var element = compile(template, scope);
var ctrl = element.controller('mdAutocomplete');
var ul = element.find('ul');

expect(scope.searchText).toBe('');
expect(scope.selectedItem).toBe(null);

element.scope().searchText = 'fo';
ctrl.keydown({});
element.scope().$apply();
$timeout.flush();

expect(scope.searchText).toBe('fo');
expect(scope.match(scope.searchText).length).toBe(1);
expect(ul.find('li').length).toBe(1);

ctrl.keydown({ keyCode: $mdConstant.KEY_CODE.DOWN_ARROW, preventDefault: angular.noop });
ctrl.keydown({ keyCode: $mdConstant.KEY_CODE.ENTER, preventDefault: angular.noop });
scope.$apply();
expect(scope.searchText).toBe('foo');
expect(scope.selectedItem).toBe(scope.match(scope.searchText)[0]);
}));
});

describe('API access', function() {
it('should clear the selected item', inject(function($timeout, $mdConstant) {
var scope = createScope();
Expand Down
22 changes: 0 additions & 22 deletions src/components/autocomplete/demoFloatingLabel/index.html

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<div ng-app="autocompleteFloatingLabelDemo" ng-controller="DemoCtrl as ctrl" layout="column">
<md-content class="md-padding" layout="column">
<form ng-submit="$event.preventDefault()" name="searchForm">
<p>The following example demonstrates floating labels being used as a normal form element.</p>
<div layout-gt-sm="row">
<md-input-container flex>
<label>Name</label>
<input type="text"/>
</md-input-container>
<md-autocomplete
flex
required
input-name="autocompleteField"
ng-disabled="ctrl.isDisabled"
md-no-cache="ctrl.noCache"
md-selected-item="ctrl.selectedItem"
md-search-text="ctrl.searchText"
md-items="item in ctrl.querySearch(ctrl.searchText)"
md-item-text="item.display"
md-floating-label="Favorite state">
<md-item-template>
<span md-highlight-text="ctrl.searchText">{{item.display}}</span>
</md-item-template>
<div ng-messages="searchForm.autocompleteField.$error" ng-if="searchForm.autocompleteField.$touched">
<div ng-message="required">You <b>must</b> have a favorite state.</div>
<div ng-message="minlength">Your entry is not long enough.</div>
<div ng-message="maxlength">Your entry is too long.</div>
</div>
</md-autocomplete>
</div>
</form>
</md-content>
</div>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(function () {
'use strict';
angular
.module('autocompleteFloatingLabelDemo', ['ngMaterial'])
.module('autocompleteFloatingLabelDemo', ['ngMaterial', 'ngMessages'])
.controller('DemoCtrl', DemoCtrl);

function DemoCtrl ($timeout, $q) {
Expand Down
4 changes: 3 additions & 1 deletion src/components/autocomplete/js/autocompleteController.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,9 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
}

function isMinLengthMet () {
return $scope.searchText.length >= getMinLength();
var minLength = getMinLength();
if (!minLength) return true;
return $scope.searchText && $scope.searchText.length >= minLength;
}

//-- actions
Expand Down
134 changes: 98 additions & 36 deletions src/components/autocomplete/js/autocompleteDirective.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ angular
* 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.
*
* ### 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.
*
* @param {expression} md-items An expression in the format of `item in items` to iterate over matches for your search.
* @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
Expand Down Expand Up @@ -63,6 +76,29 @@ angular
*
* In this example, our code utilizes `md-item-template` and `md-not-found` to specify the different
* parts that make up our component.
*
* ### Example with validation
* <hljs lang="html">
* <form name="autocompleteForm">
* <md-autocomplete
* required
* 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 `md-not-found` to specify the different
* parts that make up our component.
*/

function MdAutocomplete ($mdTheming, $mdUtil) {
Expand All @@ -71,33 +107,51 @@ function MdAutocomplete ($mdTheming, $mdUtil) {
controllerAs: '$mdAutocompleteCtrl',
link: link,
scope: {
name: '@',
searchText: '=?mdSearchText',
selectedItem: '=?mdSelectedItem',
itemsExpr: '@mdItems',
itemText: '&mdItemText',
placeholder: '@placeholder',
noCache: '=?mdNoCache',
itemChange: '&?mdSelectedItemChange',
textChange: '&?mdSearchTextChange',
minLength: '=?mdMinLength',
delay: '=?mdDelay',
autofocus: '=?mdAutofocus',
floatingLabel: '@?mdFloatingLabel',
autoselect: '=?mdAutoselect',
menuClass: '@?mdMenuClass'
inputName: '@',
inputMinlength: '@',
inputMaxlength: '@',
searchText: '=?mdSearchText',
selectedItem: '=?mdSelectedItem',
itemsExpr: '@mdItems',
itemText: '&mdItemText',
placeholder: '@placeholder',
noCache: '=?mdNoCache',
itemChange: '&?mdSelectedItemChange',
textChange: '&?mdSearchTextChange',
minLength: '=?mdMinLength',
delay: '=?mdDelay',
autofocus: '=?mdAutofocus',
floatingLabel: '@?mdFloatingLabel',
autoselect: '=?mdAutoselect',
menuClass: '@?mdMenuClass'
},
template: function (element, attr) {
var itemTemplate = getItemTemplate(),
noItemsTemplate = getNoItemsTemplate();
var noItemsTemplate = getNoItemsTemplate(),
itemTemplate = getItemTemplate(),
leftover = element.html();
element.empty();

return '\
<md-autocomplete-wrap role="listbox">\
<md-input-container ng-if="floatingLabel">\
<label>{{floatingLabel}}</label>\
<div class="md-autocomplete-button-wrapper">\
<button\
type="button"\
tabindex="-1"\
ng-if="$mdAutocompleteCtrl.scope.searchText && !isDisabled"\
ng-click="$mdAutocompleteCtrl.clear()">\
<md-icon md-svg-icon="md-cancel"></md-icon>\
<span class="md-visually-hidden">Clear</span>\
</button>\
</div>\
<input type="text"\
id="fl-input-{{$mdAutocompleteCtrl.id}}"\
name="{{name}}"\
name="{{inputName}}"\
autocomplete="off"\
ng-required="isRequired"\
ng-minlength="inputMinlength"\
ng-maxlength="inputMaxlength"\
ng-disabled="isDisabled"\
ng-model="$mdAutocompleteCtrl.scope.searchText"\
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
Expand All @@ -109,11 +163,23 @@ function MdAutocomplete ($mdTheming, $mdUtil) {
aria-haspopup="true"\
aria-activedescendant=""\
aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
<div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
</md-input-container>\
<div ng-if="!floatingLabel" class="md-autocomplete-button-wrapper">\
<button\
type="button"\
tabindex="-1"\
ng-if="$mdAutocompleteCtrl.scope.searchText && !isDisabled"\
ng-click="$mdAutocompleteCtrl.clear()">\
<md-icon md-svg-icon="md-cancel"></md-icon>\
<span class="md-visually-hidden">Clear</span>\
</button>\
</div>\
<input type="text"\
id="input-{{$mdAutocompleteCtrl.id}}"\
name="{{name}}"\
name="{{inputName}}"\
ng-if="!floatingLabel"\
ng-required="isRequired"\
autocomplete="off"\
ng-disabled="isDisabled"\
ng-model="$mdAutocompleteCtrl.scope.searchText"\
Expand All @@ -127,14 +193,6 @@ function MdAutocomplete ($mdTheming, $mdUtil) {
aria-haspopup="true"\
aria-activedescendant=""\
aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
<button\
type="button"\
tabindex="-1"\
ng-if="$mdAutocompleteCtrl.scope.searchText && !isDisabled"\
ng-click="$mdAutocompleteCtrl.clear()">\
<md-icon md-svg-icon="md-cancel"></md-icon>\
<span class="md-visually-hidden">Clear</span>\
</button>\
<md-progress-linear\
ng-if="$mdAutocompleteCtrl.loading"\
md-mode="indeterminate"></md-progress-linear>\
Expand All @@ -151,13 +209,14 @@ function MdAutocomplete ($mdTheming, $mdUtil) {
md-autocomplete-list-item="$mdAutocompleteCtrl.itemName">\
' + itemTemplate + '\
</li>\
' + (function () {
return noItemsTemplate
? '<li ng-if="!$mdAutocompleteCtrl.matches.length"\
ng-hide="$mdAutocompleteCtrl.hidden"\
md-autocomplete-parent-scope>' + noItemsTemplate + '</li>'
: '';
})() + '\
' +
//-- Add "not found" template if available
(noItemsTemplate
? '<li ng-if="!$mdAutocompleteCtrl.matches.length"\
ng-hide="$mdAutocompleteCtrl.hidden"\
md-autocomplete-parent-scope>' + noItemsTemplate + '</li>'
: '' )
+ '\
</ul>\
</md-autocomplete-wrap>\
<aria-status\
Expand All @@ -168,8 +227,10 @@ function MdAutocomplete ($mdTheming, $mdUtil) {
</aria-status>';

function getItemTemplate () {
var templateTag = element.find('md-item-template').remove();
return templateTag.length ? templateTag.html() : element.html();
var templateTag = element.find('md-item-template').remove(),
html = templateTag.length ? templateTag.html() : element.html();
if (!templateTag.length) element.empty();
return html;
}

function getNoItemsTemplate () {
Expand All @@ -181,6 +242,7 @@ function MdAutocomplete ($mdTheming, $mdUtil) {

function link (scope, element, attr) {
attr.$observe('disabled', function (value) { scope.isDisabled = value; });
attr.$observe('required', function (value) { scope.isRequired = value !== null; });

$mdUtil.initOptionalProperties(scope, attr, {searchText:null, selectedItem:null} );

Expand Down
6 changes: 5 additions & 1 deletion src/components/autocomplete/js/parentScope.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ function MdAutocompleteParentScope ($compile, $mdUtil) {
scope: false
};
function postLink (scope, element, attr) {
var ctrl = scope.$parent.$mdAutocompleteCtrl;
var ctrl = scope.$parent.$mdAutocompleteCtrl;
$compile(element.contents())(ctrl.parent);
if (attr.hasOwnProperty('mdAutocompleteReplace')) {
element.after(element.contents());
element.remove();
}
}
}