From 4dbfebefe93bf9c28e46bc1bec43590f83a7de44 Mon Sep 17 00:00:00 2001 From: Topher Fangio Date: Wed, 30 Sep 2015 23:29:30 -0500 Subject: [PATCH] fix(autocomplete): Fix many issues with showing/hiding. The logic behind showing and hiding the autocomplete's list of suggestions was difficult to understand and was causing some issues with displaying at the proper times. Refactor code to use a "show when criteria is met" approach instead of a "hide when criteria is met" approach and fix some issues with the md-not-found and suggestions list appearing when they should not. Fixes #4665. Fixes #4788. Fixes #4906. Fixes #4855. Fixes #4618. Fixes #4469. Fixes #4025. **References:** Refs #4309, #4678, #4673, #4518, #4503, #4358, #4905. --- src/components/autocomplete/autocomplete.scss | 48 +-- .../autocomplete/autocomplete.spec.js | 282 ++++++++++++------ .../autocomplete/demoBasicUsage/index.html | 3 +- .../autocomplete/demoBasicUsage/script.js | 6 + .../autocomplete/js/autocompleteController.js | 125 ++++++-- .../autocomplete/js/autocompleteDirective.js | 14 +- 6 files changed, 336 insertions(+), 142 deletions(-) diff --git a/src/components/autocomplete/autocomplete.scss b/src/components/autocomplete/autocomplete.scss index 618f04a86b6..97229a38dcf 100644 --- a/src/components/autocomplete/autocomplete.scss +++ b/src/components/autocomplete/autocomplete.scss @@ -16,6 +16,7 @@ $input-error-height: 24px !default; opacity: 0; } } + @keyframes md-autocomplete-list-in { 0% { opacity: 0; @@ -31,6 +32,7 @@ $input-error-height: 24px !default; height: 40px; } } + md-autocomplete { border-radius: 2px; display: block; @@ -78,26 +80,34 @@ md-autocomplete { &.md-menu-showing { z-index: $z-index-backdrop + 1; } - md-progress-linear .md-mode-indeterminate { + md-progress-linear { position: absolute; - top: 20px; left: 0; width: 100%; - height: 3px; - transition: none; + bottom: -2px; + left: 0; - .md-container { - transition: none; + .md-mode-indeterminate { + position: absolute; + top: 0; + left: 0; + width: 100%; height: 3px; - } - &.ng-enter { - transition: opacity 0.15s linear; - &.ng-enter-active { - opacity: 1; + transition: none; + + .md-container { + transition: none; + height: 3px; } - } - &.ng-leave { - transition: opacity 0.15s linear; - &.ng-leave-active { - opacity: 0; + &.ng-enter { + transition: opacity 0.15s linear; + &.ng-enter-active { + opacity: 1; + } + } + &.ng-leave { + transition: opacity 0.15s linear; + &.ng-leave-active { + opacity: 0; + } } } } @@ -184,12 +194,12 @@ md-autocomplete { max-height: 41px * 5.5; z-index: $z-index-tooltip; } + .md-autocomplete-suggestions { margin: 0; list-style: none; padding: 0; li { - cursor: pointer; font-size: 14px; overflow: hidden; padding: 0 15px; @@ -203,6 +213,10 @@ md-autocomplete { &:focus { outline: none; } + + &:not(.md-not-found-wrapper) { + cursor: pointer; + } } } diff --git a/src/components/autocomplete/autocomplete.spec.js b/src/components/autocomplete/autocomplete.spec.js index 85221acec26..8e78e43b71d 100644 --- a/src/components/autocomplete/autocomplete.spec.js +++ b/src/components/autocomplete/autocomplete.spec.js @@ -1,38 +1,40 @@ -describe('', function () { +describe('', function() { beforeEach(module('material.components.autocomplete')); - function compile (str, scope) { + function compile(str, scope) { var container; - inject(function ($compile) { + inject(function($compile) { container = $compile(str)(scope); scope.$apply(); }); return container; } - function createScope (items, obj) { + function createScope(items, obj) { var scope; - items = items || [ 'foo', 'bar', 'baz' ].map(function (item) { return { display: item }; }); - inject(function ($rootScope) { - scope = $rootScope.$new(); - scope.match = function (term) { - return items.filter(function (item) { + items = items || ['foo', 'bar', 'baz'].map(function(item) { + return {display: item}; + }); + inject(function($rootScope) { + scope = $rootScope.$new(); + scope.match = function(term) { + return items.filter(function(item) { return item.display.indexOf(term) === 0; }); }; - scope.searchText = ''; + scope.searchText = ''; scope.selectedItem = null; for (var key in obj) scope[key] = obj[key]; }); return scope; } - function keydownEvent (keyCode) { + function keydownEvent(keyCode) { return { keyCode: keyCode, stopPropagation: angular.noop, - preventDefault: angular.noop + preventDefault: angular.noop }; } @@ -44,15 +46,15 @@ describe('', function () { // Using md-item-size would reduce this to a single flush, but given that // autocomplete allows for custom row templates, it's better to measure // rather than assuming a given size. - inject(function ($material, $timeout) { + inject(function($material, $timeout) { $material.flushOutstandingAnimations(); $timeout.flush(); }); } - describe('basic functionality', function () { - it('should update selected item and search text', inject(function ($timeout, $mdConstant, $material) { - var scope = createScope(); + describe('basic functionality', function() { + it('should update selected item and search text', inject(function($timeout, $mdConstant, $material) { + var scope = createScope(); var template = '\ ', function () { placeholder="placeholder">\ {{item.display}}\ '; - var element = compile(template, scope); - var ctrl = element.controller('mdAutocomplete'); - var ul = element.find('ul'); + var element = compile(template, scope); + var ctrl = element.controller('mdAutocomplete'); + var ul = element.find('ul'); $material.flushInterimElement(); expect(scope.searchText).toBe(''); expect(scope.selectedItem).toBe(null); + // Focus the input + ctrl.focus(); + + // Update the scope element.scope().searchText = 'fo'; waitForVirtualRepeat(element); + // Check expectations expect(scope.searchText).toBe('fo'); expect(scope.match(scope.searchText).length).toBe(1); expect(ul.find('li').length).toBe(1); + // Run our key events ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.DOWN_ARROW)); ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.ENTER)); $timeout.flush(); + // Check expectations again expect(scope.searchText).toBe('foo'); - expect(scope.selectedItem).toBe(scope.match(scope.searchText)[ 0 ]); + expect(scope.selectedItem).toBe(scope.match(scope.searchText)[0]); element.remove(); })); - it('should allow you to set an input id without floating label', inject(function () { - var scope = createScope(null, { inputId: 'custom-input-id' }); + it('should allow you to set an input id without floating label', inject(function() { + var scope = createScope(null, {inputId: 'custom-input-id'}); var template = '\ ', function () { placeholder="placeholder">\ {{item.display}}\ '; - var element = compile(template, scope); - var input = element.find('input'); + var element = compile(template, scope); + var input = element.find('input'); expect(input.attr('id')).toBe(scope.inputId); element.remove(); })); - it('should allow you to set an input id with floating label', inject(function () { - var scope = createScope(null, { inputId: 'custom-input-id' }); + it('should allow you to set an input id with floating label', inject(function() { + var scope = createScope(null, {inputId: 'custom-input-id'}); var template = '\ ', function () { placeholder="placeholder">\ {{item.display}}\ '; - var element = compile(template, scope); - var input = element.find('input'); + var element = compile(template, scope); + var input = element.find('input'); expect(input.attr('id')).toBe(scope.inputId); element.remove(); })); - it('should clear value when hitting escape', inject(function ($mdConstant, $timeout) { - var scope = createScope(); + it('should clear value when hitting escape', inject(function($mdConstant, $timeout) { + var scope = createScope(); var template = '\ ', function () { placeholder="placeholder">\ {{item.display}}\ '; - var element = compile(template, scope); - var input = element.find('input'); - var ctrl = element.controller('mdAutocomplete'); + var element = compile(template, scope); + var input = element.find('input'); + var ctrl = element.controller('mdAutocomplete'); expect(scope.searchText).toBe(''); @@ -151,7 +160,9 @@ describe('', function () { expect(scope.searchText).toBe('test'); $timeout.flush(); - scope.$apply(function () { ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.ESCAPE)); }); + scope.$apply(function() { + ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.ESCAPE)); + }); expect(scope.searchText).toBe(''); @@ -159,9 +170,9 @@ describe('', function () { })); }); - describe('basic functionality with template', function () { - it('should update selected item and search text', inject(function ($timeout, $material, $mdConstant) { - var scope = createScope(); + describe('basic functionality with template', function() { + it('should update selected item and search text', inject(function($timeout, $material, $mdConstant) { + var scope = createScope(); var template = '\ ', function () { {{item.display}}\ \ '; - var element = compile(template, scope); - var ctrl = element.controller('mdAutocomplete'); - var ul = element.find('ul'); + var element = compile(template, scope); + var ctrl = element.controller('mdAutocomplete'); + var ul = element.find('ul'); expect(scope.searchText).toBe(''); expect(scope.selectedItem).toBe(null); $material.flushInterimElement(); + // Focus the input + ctrl.focus(); + element.scope().searchText = 'fo'; waitForVirtualRepeat(element); @@ -195,34 +209,37 @@ describe('', function () { $timeout.flush(); expect(scope.searchText).toBe('foo'); - expect(scope.selectedItem).toBe(scope.match(scope.searchText)[ 0 ]); + expect(scope.selectedItem).toBe(scope.match(scope.searchText)[0]); element.remove(); })); - it('should compile the template against the parent scope', inject(function ($timeout, $material) { - var scope = createScope(null, { bang: 'boom' }); - var template = '\ - \ - \ - {{bang}}\ - {{$index}}\ - {{item.display}}\ - \ - '; - var element = compile(template, scope); - var ctrl = element.controller('mdAutocomplete'); - var ul = element.find('ul'); + it('should compile the template against the parent scope', inject(function($timeout, $material) { + var scope = createScope(null, {bang: 'boom'}); + var template = + '' + + ' ' + + ' {{bang}}' + + ' {{$index}}' + + ' {{item.display}}' + + ' ' + + ''; + var element = compile(template, scope); + var ctrl = element.controller('mdAutocomplete'); + var ul = element.find('ul'); $material.flushOutstandingAnimations(); expect(scope.bang).toBe('boom'); + // Focus the input + ctrl.focus(); + element.scope().searchText = 'fo'; // Run our initial flush @@ -239,16 +256,91 @@ describe('', function () { expect(li.querySelector('.find-index').innerHTML).toBe('0'); expect(li.querySelector('.find-item').innerHTML).toBe('foo'); + // Make sure we wrap up anything and remove the element + $timeout.flush(); + element.remove(); + })); + + it('is hidden when no matches are found without an md-not-found template', inject(function($timeout, $material) { + var scope = createScope(); + var template = + '' + + ' {{item.display}}' + + ''; + var element = compile(template, scope); + var ctrl = element.controller('mdAutocomplete'); + + $material.flushOutstandingAnimations(); + + // Focus our input + ctrl.focus(); + + // Set our search text to a value that we know doesn't exist + scope.searchText = 'somethingthatdoesnotexist'; + + // Run our initial flush + $timeout.flush(); + waitForVirtualRepeat(element); + + // Wait for the next tick when the values will be updated $timeout.flush(); + // We should be hidden since no md-not-found template was provided + expect(ctrl.hidden).toBe(true); + + // Make sure we wrap up anything and remove the element + $timeout.flush(); + element.remove(); + })); + + it('is visible when no matches are found with an md-not-found template', inject(function($timeout, $material) { + var scope = createScope(); + var template = + '' + + ' {{item.display}}' + + ' Sorry, not found...' + + ''; + var element = compile(template, scope); + var ctrl = element.controller('mdAutocomplete'); + + $material.flushOutstandingAnimations(); + + // Focus our input + ctrl.focus(); + + // Set our search text to a value that we know doesn't exist + scope.searchText = 'somethingthatdoesnotexist'; + + // Run our initial flush + $timeout.flush(); + waitForVirtualRepeat(element); + + // Wait for the next tick when the values will be updated + $timeout.flush(); + + // We should be visible since an md-not-found template was provided + expect(ctrl.hidden).toBe(false); + + // Make sure we wrap up anything and remove the element + $timeout.flush(); element.remove(); })); }); - describe('xss prevention', function () { + describe('xss prevention', function() { it('should not allow html to slip through', inject(function($timeout, $material) { var html = 'foo '; - var scope = createScope([ { display: html } ]); + var scope = createScope([{display: html}]); var template = '\ ', function () { placeholder="placeholder">\ {{item.display}}\ '; - var element = compile(template, scope); - var ul = element.find('ul'); + var element = compile(template, scope); + var ctrl = element.controller('mdAutocomplete'); + var ul = element.find('ul'); $material.flushOutstandingAnimations(); expect(scope.searchText).toBe(''); expect(scope.selectedItem).toBe(null); + // Focus the input + ctrl.focus(); + scope.$apply('searchText = "fo"'); $timeout.flush(); waitForVirtualRepeat(element); @@ -280,9 +376,9 @@ describe('', function () { })); }); - describe('API access', function () { - it('should clear the selected item', inject(function ($timeout) { - var scope = createScope(); + describe('API access', function() { + it('should clear the selected item', inject(function($timeout) { + var scope = createScope(); var template = '\ ', function () { placeholder="placeholder">\ {{item.display}}\ '; - var element = compile(template, scope); - var ctrl = element.controller('mdAutocomplete'); + var element = compile(template, scope); + var ctrl = element.controller('mdAutocomplete'); element.scope().searchText = 'fo'; $timeout.flush(); @@ -315,8 +411,8 @@ describe('', function () { element.remove(); })); - it('should notify selected item watchers', inject(function ($timeout) { - var scope = createScope(); + it('should notify selected item watchers', inject(function($timeout) { + var scope = createScope(); scope.itemChanged = jasmine.createSpy('itemChanged'); var registeredWatcher = jasmine.createSpy('registeredWatcher'); @@ -331,8 +427,8 @@ describe('', function () { placeholder="placeholder">\ {{item.display}}\ '; - var element = compile(template, scope); - var ctrl = element.controller('mdAutocomplete'); + var element = compile(template, scope); + var ctrl = element.controller('mdAutocomplete'); ctrl.registerSelectedItemWatcher(registeredWatcher); @@ -343,10 +439,10 @@ describe('', function () { $timeout.flush(); expect(scope.itemChanged).toHaveBeenCalled(); - expect(scope.itemChanged.calls.mostRecent().args[ 0 ].display).toBe('foo'); + expect(scope.itemChanged.calls.mostRecent().args[0].display).toBe('foo'); expect(registeredWatcher).toHaveBeenCalled(); - expect(registeredWatcher.calls.mostRecent().args[ 0 ].display).toBe('foo'); - expect(registeredWatcher.calls.mostRecent().args[ 1 ]).toBeNull(); + expect(registeredWatcher.calls.mostRecent().args[0].display).toBe('foo'); + expect(registeredWatcher.calls.mostRecent().args[1]).toBeNull(); expect(scope.selectedItem).not.toBeNull(); expect(scope.selectedItem.display).toBe('foo'); @@ -358,15 +454,15 @@ describe('', function () { expect(registeredWatcher.calls.count()).toBe(1); expect(scope.itemChanged.calls.count()).toBe(2); - expect(scope.itemChanged.calls.mostRecent().args[ 0 ]).toBeNull(); + expect(scope.itemChanged.calls.mostRecent().args[0]).toBeNull(); expect(scope.selectedItem).toBeNull(); element.remove(); })); - it('should pass value to item watcher', inject(function ($timeout) { - var scope = createScope(); - var itemValue = null; - var template = '\ + it('should pass value to item watcher', inject(function($timeout) { + var scope = createScope(); + var itemValue = null; + var template = '\ ', function () { placeholder="placeholder">\ {{item.display}}\ '; - scope.itemChanged = function (item) { + scope.itemChanged = function(item) { itemValue = item; }; - var element = compile(template, scope); - var ctrl = element.controller('mdAutocomplete'); + var element = compile(template, scope); + var ctrl = element.controller('mdAutocomplete'); element.scope().searchText = 'fo'; $timeout.flush(); @@ -398,9 +494,9 @@ describe('', function () { })); }); - describe('md-select-on-match', function () { - it('should select matching item on exact match when `md-select-on-match` is toggled', inject(function ($timeout) { - var scope = createScope(); + describe('md-select-on-match', function() { + it('should select matching item on exact match when `md-select-on-match` is toggled', inject(function($timeout) { + var scope = createScope(); var template = '\ ', function () { placeholder="placeholder">\ {{item.display}}\ '; - var element = compile(template, scope); + var element = compile(template, scope); expect(scope.searchText).toBe(''); expect(scope.selectedItem).toBe(null); @@ -424,8 +520,8 @@ describe('', function () { element.remove(); })); - it('should not select matching item on exact match when `md-select-on-match` is NOT toggled', inject(function ($timeout) { - var scope = createScope(); + it('should not select matching item on exact match when `md-select-on-match` is NOT toggled', inject(function($timeout) { + var scope = createScope(); var template = '\ ', function () { placeholder="placeholder">\ {{item.display}}\ '; - var element = compile(template, scope); + var element = compile(template, scope); expect(scope.searchText).toBe(''); expect(scope.selectedItem).toBe(null); @@ -449,10 +545,10 @@ describe('', function () { })); }); - describe('md-highlight-text', function () { - it('should update when content is modified', inject(function () { + describe('md-highlight-text', function() { + it('should update when content is modified', inject(function() { var template = '
{{message}}
'; - var scope = createScope(null, { message: 'some text', query: 'some' }); + var scope = createScope(null, {message: 'some text', query: 'some'}); var element = compile(template, scope); expect(element.html()).toBe('some text'); diff --git a/src/components/autocomplete/demoBasicUsage/index.html b/src/components/autocomplete/demoBasicUsage/index.html index 4f45e37479d..c23b66cc0fa 100644 --- a/src/components/autocomplete/demoBasicUsage/index.html +++ b/src/components/autocomplete/demoBasicUsage/index.html @@ -17,7 +17,8 @@ {{item.display}} - No matches found for "{{ctrl.searchText}}". + No states matching "{{ctrl.searchText}}" were found. + Create a new one!

diff --git a/src/components/autocomplete/demoBasicUsage/script.js b/src/components/autocomplete/demoBasicUsage/script.js index 9aa449de314..6991ed5595b 100644 --- a/src/components/autocomplete/demoBasicUsage/script.js +++ b/src/components/autocomplete/demoBasicUsage/script.js @@ -16,6 +16,12 @@ self.selectedItemChange = selectedItemChange; self.searchTextChange = searchTextChange; + self.newState = newState; + + function newState(state) { + alert("Sorry! You'll need to create a Constituion for " + state + " first!"); + } + // ****************************** // Internal methods // ****************************** diff --git a/src/components/autocomplete/js/autocompleteController.js b/src/components/autocomplete/js/autocompleteController.js index 94923e8277f..f66a180b845 100644 --- a/src/components/autocomplete/js/autocompleteController.js +++ b/src/components/autocomplete/js/autocompleteController.js @@ -34,6 +34,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, ctrl.id = $mdUtil.nextUid(); ctrl.isDisabled = null; ctrl.isRequired = null; + ctrl.hasNotFound = false; //-- public methods ctrl.keydown = keydown; @@ -48,6 +49,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher; ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher; ctrl.notFoundVisible = notFoundVisible; + ctrl.loadingIsVisible = loadingIsVisible; return init(); @@ -209,19 +211,16 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, if (!hidden && oldHidden) { positionDropdown(); - if (elements) + if (elements) { $mdUtil.nextTick(function () { - $mdUtil.disableScrollAround(elements.ul); - - }, false, $scope); - } else if (hidden && !oldHidden) { - $mdUtil.nextTick(function () { - - $mdUtil.enableScrolling(); - }, false, $scope); } + } else if (hidden && !oldHidden) { + $mdUtil.nextTick(function () { + $mdUtil.enableScrolling(); + }, false, $scope); + } } /** @@ -236,7 +235,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, */ function onListLeave () { noBlur = false; - if (!hasFocus) ctrl.hidden = true; + ctrl.hidden = shouldHide(); } /** @@ -327,9 +326,8 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, // cancel results if search text is not long enough if (!isMinLengthMet()) { - ctrl.loading = false; ctrl.matches = []; - ctrl.hidden = shouldHide(); + setLoading(false); updateMessages(); } else { handleQuery(); @@ -343,8 +341,18 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, * Handles input blur event, determines if the dropdown should hide. */ function blur () { - hasFocus = false; - if (!noBlur) ctrl.hidden = true; + if (!noBlur) { + hasFocus = false; + ctrl.hidden = shouldHide(); + } + } + + function doBlur(forceBlur) { + if (forceBlur) { + noBlur = false; + } + + elements.input.blur(); } /** @@ -354,7 +362,6 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, hasFocus = true; //-- if searchText is null, let's force it to be a string if (!angular.isString($scope.searchText)) $scope.searchText = ''; - if ($scope.minLength > 0) return; ctrl.hidden = shouldHide(); if (!ctrl.hidden) handleQuery(); } @@ -392,9 +399,10 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, event.stopPropagation(); event.preventDefault(); clearValue(); - ctrl.matches = []; - ctrl.hidden = true; - ctrl.index = getDefaultIndex(); + + // Force the component to blur if they hit escape + doBlur(true); + break; default: } @@ -449,12 +457,61 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, return $scope.autoselect ? 0 : -1; } + /** + * Sets the loading parameter and updates the hidden state. + * @param value {boolean} Whether or not the component is currently loading. + */ + function setLoading(value) { + if (ctrl.loading != value) { + ctrl.loading = value; + } + + // Always refresh the hidden variable as something else might have changed + ctrl.hidden = shouldHide(); + } + /** * Determines if the menu should be hidden. * @returns {boolean} */ function shouldHide () { - if (!isMinLengthMet() || !ctrl.matches.length) return true; + if ((ctrl.loading && !hasMatches()) || hasSelection() || !hasFocus) { + return true; + } + + return !shouldShow(); + } + + /** + * Determines if the menu should be shown. + * @returns {boolean} + */ + function shouldShow() { + return (isMinLengthMet() && hasMatches()) || notFoundVisible(); + } + + /** + * Returns true if the search text has matches. + * @returns {boolean} + */ + function hasMatches() { + return ctrl.matches.length ? true : false; + } + + /** + * Returns true if the autocomplete has a valid selection. + * @returns {boolean} + */ + function hasSelection() { + return ctrl.scope.selectedItem ? true : false; + } + + /** + * Returns true if the loading indicator is, or should be, visible. + * @returns {boolean} + */ + function loadingIsVisible() { + return ctrl.loading && !hasSelection(); } /** @@ -470,7 +527,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, * @returns {*} */ function isMinLengthMet () { - return angular.isDefined($scope.searchText) && $scope.searchText.length >= getMinLength(); + return ($scope.searchText || '').length >= getMinLength(); } //-- actions @@ -505,10 +562,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, ngModel.$render(); }).finally(function () { $scope.selectedItem = ctrl.matches[ index ]; - ctrl.loading = false; - ctrl.hidden = true; - ctrl.index = 0; - ctrl.matches = []; + setLoading(false); }); }, false); } @@ -517,7 +571,15 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, * Clears the searchText value and selected item. */ function clearValue () { + // Set the loading to true so we don't see flashes of content + setLoading(true); + + // Reset our variables + ctrl.index = 0; + ctrl.matches = []; $scope.searchText = ''; + + // Tell the select to fire and select nothing select(-1); // Per http://www.w3schools.com/jsref/event_oninput.asp @@ -538,16 +600,18 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, if (angular.isArray(items)) { handleResults(items); } else if (items) { + setLoading(true); $mdUtil.nextTick(function () { - ctrl.loading = true; if (items.success) items.success(handleResults); if (items.then) items.then(handleResults); - if (items.finally) items.finally(function () { ctrl.loading = false; }); + if (items.finally) items.finally(function () { + setLoading(false); + }); },true, $scope); } function handleResults (matches) { cache[ term ] = matches; - if (searchText !== $scope.searchText) return; //-- just cache the results if old request + if ((searchText || '') !== ($scope.searchText || '')) return; //-- just cache the results if old request ctrl.matches = matches; ctrl.hidden = shouldHide(); if ($scope.selectOnMatch) selectItemOnMatch(); @@ -604,7 +668,9 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, } function notFoundVisible () { - return !ctrl.matches.length && !ctrl.loading && ctrl.scope.searchText >= getMinLength() && hasFocus && !ctrl.scope.selectedItem; + var textLength = (ctrl.scope.searchText || '').length; + + return ctrl.hasNotFound && !hasMatches() && !ctrl.loading && textLength >= getMinLength() && hasFocus && !hasSelection(); } /** @@ -621,7 +687,8 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, } else { fetchResults(searchText); } - if (hasFocus) ctrl.hidden = shouldHide(); + + ctrl.hidden = shouldHide(); } /** diff --git a/src/components/autocomplete/js/autocompleteDirective.js b/src/components/autocomplete/js/autocompleteDirective.js index 65838ed9834..cf532257827 100644 --- a/src/components/autocomplete/js/autocompleteDirective.js +++ b/src/components/autocomplete/js/autocompleteDirective.js @@ -118,6 +118,8 @@ angular */ function MdAutocomplete () { + var hasNotFoundTemplate = false; + return { controller: 'MdAutocompleteCtrl', controllerAs: '$mdAutocompleteCtrl', @@ -142,10 +144,18 @@ function MdAutocomplete () { menuClass: '@?mdMenuClass', inputId: '@?mdInputId' }, + link: function(scope, element, attrs, controller) { + controller.hasNotFound = hasNotFoundTemplate; + }, template: function (element, attr) { var noItemsTemplate = getNoItemsTemplate(), itemTemplate = getItemTemplate(), leftover = element.html(); + + if (noItemsTemplate) { + hasNotFoundTemplate = true; + } + return '\ \ ' + getInputElement() + '\ \ \