Angular Material: md-autocomplete – how to hide md-autocomplete-suggestions on Enter event?

All we need is an easy explanation of the problem, so here it is.

I have md-autocomplete:

<md-autocomplete 
                         md-min-length="1"
                         ng-enter="presEnter();"
                         md-no-cache="true"                        
                         md-selected-item="selectedItem" 
                         md-search-text="searchText" 
                         md-items="item in querySearch(searchText)"
                         md-item-text="item.name" 
                         placeholder="Search for a vegetable">
          <span md-highlight-text="searchText">{{item.name}} :: {{item.type}}</span>
        </md-autocomplete>

with directive: ng-enter.

My goal: When user presses Enter I want to hide md-autocomplete-suggestions dropdown

I know from HTML I need somehow to call: $mdAutocompleteCtrl.hidden = true; but have no idea how to use $mdAutocompleteCtrl in Controller.

I googled and found:

$timeout( function() { $scope.$$childHead.$mdAutocompleteCtrl.hidden = true; },100);

but there is no $mdAutocompleteCtrl (at least in my JS, only in HTML and i don’t know its scope)

I play with this example: type ‘a’ and after dropdown press Enter.

Any ideas?

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

The $mdAutocompleteCtrl is placed as a property on the autocomplete’s scope.

First, you need access to the autocomplete element. One way to do that is to put an ID on the autocomplete:

<md-autocomplete id='Auto'
                 md-min-length="1"
                 ng-enter="presEnter();"
                 md-no-cache="true"
                 md-selected-item="selectedItem"
                 md-search-text="searchText"
                 md-items="item in querySearch(searchText)"
                 md-item-text="item.name"
                 placeholder="Search for a vegetable">

Then you can use that element to get the inner scope of the autocomplete. Because the autocomplete element itself is on the scope that you provided, you’ll want to get the scope of one of the autocomplete’s child elements.

$scope.presEnter = function(e){
    var autoChild = document.getElementById('Auto').firstElementChild;
    var el = angular.element(autoChild);
    el.scope().$mdAutocompleteCtrl.hidden = true;
};

Here is a working example: http://codepen.io/anon/pen/rVPZKN?editors=101

Method 2

TLDR: Example code that triggers hide http://codepen.io/anon/pen/mJvGzp?editors=101

The Problem(s):

First off, the “Angular Way” suggests that manipulating directives in your Controller should be avoided. The Controller should essentially just retrieve (via Services, etc.) and provide the data required to build a view; it generally should avoid caring about exactly what how those views are implemented (i.e. it should know not what directives will be used). There are various good reasons for this, one might be that it makes life much easier when you want to modify the view, such as swapping out directives.

If directives really need to be manually modified, it’s better to do so from another directive. This allows more flexibility, and simplifies refactoring later (same example: if swapping out for different autocomplete directives).

Also, although it seems to be the only way to solve the problem in this case, the $scope.$$childHead.$mdAutocompleteCtrl.hidden code seems fairly hacky – unless there is no other choice, one should avoid accessing properties starting with $$, and also avoid modifying sibling directives without doing so via shared scope properties.

Unfortunately, after digging into the source code ( on the master branch ), I could not find any nicer way to trigger the hide function than (as you suggested) to grab it’s scope and modify the hidden property.

Another problem with accessing it via the Controller is that it’s a bit more difficult because it’s nested through a few other scopes. You can pass the event, grab the DOM node, and pull up it’s scope, but that’s a lot of irrelevant stuff in a Controller.

The Solution:

So instead, we can add a sibling directive, similar to the ngEnter example directive that you’ve included in the Codepen example. Perhaps something a bit more explicit so that it’s a bit more obvious what it’s doing:

.directive('mdHideAutocompleteOnEnter', function () {
    return function (scope, element, attrs) {
        element.bind("keydown keypress", function (event) {
            if(event.which === 13) {
                scope.$apply(function (){
                    scope.$$childHead.$mdAutocompleteCtrl.hidden = true; // $scope  modified to scope
                });

                event.preventDefault();
            }
        });
    };
});

The HTML would simply include this directive when relevant:

<md-autocomplete 
               md-hide-autocomplete-on-enter
               md-items="item in querySearch(searchText)"
               md-item-text="item.name">
     <span md-highlight-text="searchText">{{item.name}} :: {{item.type}}</span>
</md-autocomplete>

Here’s the example, modified with it in action: http://codepen.io/anon/pen/mJvGzp?editors=101

Method 3

The better way to access the controller methods is to target the element and then use the jqLite object to gain access to the controller:

var $acElement = angular.element(document.getElementById('Auto'));
var acCtrl = $acElement.controller('mdAutocomplete');
acCtrl.hidden = true;

Whenever you access anything using the scope() method on an angular element, your implementation will break if you ever want to disable angular debug info.

Method 4

I think this solution is better because:

  • it uses a directive instead of the controller.

  • it is simpler than the other given directive solution.

Javascript

app.directive('closeOnEnter', function($compile) {
      return {
         restrict: 'A',
         require: 'mdAutocomplete',
         link: function(scope, element) {
            element.on('keydown keypress', function($event) {
               // 13: Enter
               if ($event.keyCode == 13) {
                  var eAcInput = this.getElementsByTagName('input')[0];
                  eAcInput.blur();
               }
            });
         },
      };
});

HTML

<md-autocomplete close-on-enter ... ...>

Method 5

If you don’t mind losing focus on the md-autocomplete input element on enter, you can close md-autocomplete suggestions without using hacky ways that involve messing with the internal $mdAutocompleteCtrl controller. This depends on md-autocomplete to automatically hide suggestions when the input element is no longer focused.

  1. Wrap your md-autocomplete element in a form (or use a directive like ng-enter) and add an ID to the input element using md-input-id

  <form ng-submit="vm.handleFormSubmit()">
    <md-autocomplete md-input-id="autocomplete" ...>
    </md-autocomplete>
  </form>

  1. Call blur() on the #autocomplete input element

handleFormSubmit() {
  angular.element(document.querySelector('#autocomplete')).blur();
}

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply