How To - Create a reusable autocomplete component in angular

By creating an autocomplete component you ensure flexibility and maintainability in your application. Changes and business rules are all concentrated in a single place making your views unchangeable if new features are needed or if perhaps your consumable service changes.

To create a re-usable autocomplete component in angular, you just need to understand how data-binding and observables work. 

Let’s create an autocomplete component that searches for galaxies, I know I’m super creative, and once we select the desire galaxy it gives us the galaxy id so we can save it else where.

Let’s start by defining our html template:

<md-content>
    <ng-form name="searchForm">
        <div layout="row">
            <md-autocomplete 
                             ng-required="isRequired" 
                             md-input-name="autocompleteField"
                             md-no-cache="ctrl.noCache"
                             md-selected-item="selectedItem"
                             md-search-text="searchText"
                             md-selected-item-change="ctrl.selectedItemChange(item)"
                             md-search-text-change="ctrl.searchTextChange(searchText)"
                             md-items="item in ctrl.search(searchText)"
                             md-item-text="item.name"
                             md-min-length="ctrl.minLength"
                             md-min-length="ctrl.maxlength"
                             md-require-match="ctrl.requireMatch"
                             md-floating-label="{{ctrl.labelName}}">
                <md-item-template>
                    <span md-highlight-text="searchText">{{item.name}}</span>
                </md-item-template>
                <md-not-found>
                    No galaxies matching "{{searchText}}" were found.
                </md-not-found>
                <div ng-messages="searchForm.autocompleteField.$error" ng-if="searchForm.autocompleteField.$touched">
                    <div ng-message="required">You <b>must</b> have a favorite galaxy</div>
                    <div ng-message="md-require-match">Please select an existing galaxy</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>
    </ng-form>
</md-content>

It’s better to have our autocomplete inside a form. Why? Because if we need to have validations, the

 ng-required=“isRequired" 

will set the required attribute if true, we need to have a way of refer to the input field. 

We can name it dynamically

md-input-name=“{{someValue}}“

  will create the input with the name passed but, we can’t reference it dynamically in a ng-message.

<div ng-messages=“searchForm.{{someValue}}.Field.$error”> 

won’t work, therefore, we can’t use the ng-message pattern to do angular validations. 

Finally, by doing it this way, if this autocomplete is part of a parent form and it’s in fact required, the parent scope will catch it. 

<div>
    <md-button ng-disabled=“parentForm.fieldForm.$invalid" ng-click="saveField()">Save</md-button
    <md-button ng-click="cancelFieldEdit()">Cancel</md-button>
</div>

parentForm will remain invalid until the autocomplete is valid depending of the parameters given.

Next, let's create the directive:

angular.module('GalaxyApp').directive('galaxy', function galaxyDirective() {
    return {
        restrict: 'E',
        templateUrl: 'galaxy-template.html',
        controller: 'GalaxyController',
        controllerAs: 'ctrl',
        scope:{
            identifier:'=ngModel',
            isRequired : '@isRequired',
            requireMatch : '@requireMatch',
            minLength : '@minLength',
            name: '@name'
        }
    }
});

identifier: the most important thing to mention is that identifier will have a two way data binding. This will be our variable to use in order to bind the galaxy id to the model needed.

isRequired: sets whether or not the autocomplete should be validate. The value required is set up by the ng-required used in the previous step.

requireMatch, minLenght, name: are variables use to make our component flexible. You can give more costumization to your component, for instance, maxLength. 

Last, let's create our controller:

angular.module('GalaxyApp').controller('GalaxyController',
    function galaxyController($scope, $timeout, $q) {
        var self = this;

        // Hardcoding the galaxies as I'm not using an external service
        var galaxies =  [{name:"Andromeda Galaxy", identifier: "M31", random: "dude"}, {name: "Triangulum Galaxy", identifier: "M33"}, 
                        {name: "Centaurus A", identifier: "NGC 5128"}, {name: "Bode's Galaxy", identifier:"M81"},
                        {name:"Sculptor Galaxy", identifier: "NGC 253"}, {name:"Messier 83", identifier:"NGC 5236"}];

        updateSelectedItem();

        // All these variables can be exposed in your directive so the developer can have more possible configurations
        self.noCache = false;
        self.simulateQuery = false;
        self.isDisabled = false;
        self.visible = false;

        self.searchText = null; 
        self.selectedItem  = null;

        self.labelName = ($scope.name) ? $scope.name : " "; // If you defined a floating label in your component, it has to be at least an empty string or it will not render.
        self.minLength = ($scope.minLength) ? $scope.minLength : 0;
        self.requireMatch = ($scope.requireMatch) ? $scope.requireMatch : false;

        self.search = search;
        self.selectedItemChange = selectedItemChange;
        self.searchTextChange = searchTextChange;

        // The $timeout and filtering is used to simulate an external call to a service.
        function search(galaxyName) {
            var results = galaxyName ? galaxies.filter( createFilterFor(galaxyName) ) : galaxies;
            var deferred = $q.defer();
            $timeout(function () { deferred.resolve( results ); }, Math.random() * 1000, false);
            return deferred.promise;
         }
        function createFilterFor(galaxyName) {
            return function filterFn(galaxy) {
                return (galaxy.name.indexOf(galaxyName) === 0);
            };

        }
         function selectedItemChange(item) {
            if (item) {
                $scope.identifier = item.identifier;
            }
         }
         // Right now, I don't need to do anything if the searchText is changed. But there will be times in which you have to.
         // This will depend on your business rules, requirements or features.
         function searchTextChange(text) {
         }
         function updateSelectedItem() {
            var galaxyName = "";
            for (var i = 0; i < galaxies.length; i ++) {
                if (galaxies[i].identifier === $scope.identifier) {
                    galaxyName = galaxies[i].name;
                    break;
                }
            }
            $scope.searchText = galaxyName;
         }
         $scope.$watch('identifier', updateSelectedItem, true); // Most important!
    });


The most important detail here is to $watch the variable that is binding your model. Why? Imagine you have a form that could be filled by selecting a row in a table. Unless you keep track of every change in the model, the autocomplete component won't be updated with the new value. For example, if you selected Andromeda Galaxy first, but then you select the row with Messier 83, unless you $watch the model, the autocomplete will stay with Andromeda Galaxy as the search text... resulting a bad costumer experience. For the sake of our example, the condition is true, but this could change depending on your requirements.

In conclusion, creating a reusable component in angular is not hard, that's the whole idea of data binding and scopes. But unless you fully understand how they function internally, and how you could keep your model updated if one changes, is going to be a nightmare!

I hope this will help you out creating your own autocomplete component!

 

Tags: 

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

PHP code

  • You may post PHP code. You should include <?php ?> tags.