Angular.js: Autocomplete and enabling a form with $watch and blur

By on

I have that small form, consisting of an jQuery autocomplete and a submit button. When the user selects something from the autocompleter, I want to store an ID in the model. Only when this ID is set the submit button should be enabled.

It sounds easy in first glance, and it is, if you know how. But first you face these problems:

  • Autocomplete is from jQuery UI. One needs to wrap it into a Directive
  • You cannot use the standard form validation to enable your button, because the ID is not set into an input field. By default form validation does only look at the actual form fields

For the model validation you can choose between three different approaches. The difference is how you make the validation happen.

  1. You could use ng-change. This directive does fire the validation when you type into the field. Of course it does happen with each letter you type and I found it not very efficient. As this is very easy to implement, I dropped it from this example
  2. You could listen to the blur event (when you leave the input field). Problem: there is no ready-to-use directive, we have to write it ourselves.
  3. You could use $watch to observe your model and catch an event, when it changes Let's shed some light on this use case.

The form and the autocompleter

First, lets look at the form. Its dead simple:

<div ng-controller="MyController">
<form ng-submit="submit()">
   <input type="text" autocomplete ng-model="myModel" />
   <input type="submit" ng-disabled="isDisabled"/>
</form>
</div>

For now, I just added a new attribute named "autocomplete". This is the name of my directive with which I will wrap my jQuery UI Autocomplete code. I disabled my submit button with ng-disabled. This directive inspects my controller for the property "isDisabled". If true, it is disabled.

The plan is to create another model named "myModelId" to store the ID which comes from the autocomplete.

Here comes my basic controller.

function MyController ($scope) {
    $scope.myModel = null;
    $scope.myModelId = null;
    $scope.isDisabled = true;
}

Now it is interesting to look at what my server would deliver in response to a successful request of the kind http://localhost/words.php?keyword=hello

It looks like this:

  
{
    "results": [
        {
            "value": "108",
            "label": "Hello World"
        },
        {
            "value": "109",
            "label": "Hello Reader"
        },
        {
            "value": "110",
            "label": "Hello Mike"
        }
    ]
}

It's JSON. The value contains my ID, the label is what I want my user to see. So far so good - its going away from the standard JSON which is expected by the autocompleter, so we have to do some more stuff. Here is the directive:

var directives = angular.module('directives');

directives.directive('autocomplete', ['$http', function($http) {
    return function (scope, element, attrs) {
        element.autocomplete({
            minLength:3,
            source:function (request, response) {
                var url = "http://localhost/words.php?keyword=" + request.term;
                $http.get(url).success( function(data) {
                    response(data.results);
                });
            },
            focus:function (event, ui) {
                element.val(ui.item.label);
                return false;
            },
            select:function (event, ui) {
                scope.myModelId.selected = ui.item.value;
                scope.$apply;
                return false;
            },
            change:function (event, ui) {
                if (ui.item === null) {
                    scope.myModelId.selected = null;
                }
            }
        }).data("autocomplete")._renderItem = function (ul, item) {
            return $("<li></li>")
                .data("item.autocomplete", item)
                .append("<a>" + item.label + "</a>")
                .appendTo(ul);
        };
    }
}]);

Ok, I agree, this looks horrible at first glance. But it's not so hard actually. First we create a new directives module which should contain our little beast. In line 3 we define it is dependent to $http, a service, which will make the call to our backend server. Then we create the usual creational function, which gets the execution scope, the element which is defining our autocomplete and its attributes.

The "element" in this case is where we want to create the autocomplete. So we are doing plain jQuery UI in line 5: this is the way how you make an input field to an autocomplete field.

There are 5 properties defined in my Autocompleter:

  1. minLength: this says search only when you got 3 letters. Its up to you what you choose here, and its optional
  2. source: this is a function which gives you a request and a response. Its doing the actual job of requesting the backend. You get your keyword by "request.term" and can create your url with that. Then we can already perform the AJAX request. In my case it is done with using a GET request. The returning data will be put in my callback. The only thing to know here is to put your JSON response to the response. From there the plugin can work with your data.
  3. focus: when you focus an element in your list, it needs to be put into the value field. We need to define it like that because we have a custom json structure. The autocomplete plugin doesn't know what to put. If we leave that out, the final selection is not shown in the textfield, just what we typed. What we have in focus is stored in ui.item. The name "label" matches what we got from our JSON.
  4. select: this should happen when we select something. No magic here: the value (compare to our JSON) is stored into our scope and thus we have it in our model.
  5. change: you need to reset your model if you change it and have nothing chosen (or typed away without caring on the results from the backend). With the next "select" your model will be filled again.
Last but not least we have line 26 to 31 which render our item in our custom way. I made my own list elements which contain a simple link with my label. This is necessary because of my custom JSON structure. Anyway nice to know and you can do some fancy stuff with overriding the rendering.

So far so good. If we select something from the autocomplete we fill the "myModel" and the "myModelId" properties of our controller. Now we need to enable the submit button if myModelId is filled.

Check if there is enough data to submit

We need a method which "validates" the model and enables the button or not. The button is enabled, when we set the controllers isDisabled property to true as defined here:

<input type="submit" ng-disabled="isDisabled"/>

Therefore we need to extend our controller with another minor method which switches this property.

function MyController ($scope) {
    $scope.myModel = null;
    $scope.myModelId = null;
    $scope.isDisabled = true;

    $scope.validateModel = function () {
        $scope.isDisabled = ($scope.myModelId === null);
    };
}

This is so easy, I will not explain it further. If you are curious how validateModel() is called, read on.

Angular.js does not have on blur events

Yes, it's true. Angular.js supports many events, but not blur and focus. There is an issue for that and actually my blur directive is looking like that one from this issue.:

directives.directive('blur', function () {
    return function (scope, elem, attrs) {
        elem.bind('blur', function () {
            scope.$apply(attrs.blur);
        });
    };
});

This directive binds the "blur" event to the element which contains the "blur" attribute. In line 4 we simply take the expression from the blur attribute and execute it. That's it already. In HTML it looks like this:

<input type="text" autocomplete blur="validateModel()" ng-model="myModel" />

The problem with blur in this case is that the button does not enable when you selected it but only when you left the field. In other terms, you need to click something outside the field that its loosing focus and only then directive will execute. But this is not really nice for the user, so we should look at the $watch method.

Observing with $watch

Scope has $watch. It does fire when $digest is called, and this is called when the model changes. In other terms: we have a nice little observer which does do things when the model changes.

In my code I have removed the blur-Code and intestead I just need to write a watch expression into my controller:

function MyController ($scope) {
    $scope.myModel = null;
    $scope.myModelId = null;
    $scope.isDisabled = true;

    $scope.validateModel = function () {
        $scope.isDisabled = !($scope.myModelId !== null);
    };

    $scope.$watch('myModelId', function() {
        $scope.validateModel();
    }
}

Isn't it nice? When the "myModelId" changes, the $watch is executing the validateModel. If it doesn't in your code, check if your directive needs an $apply when it sets the ID to the model.

While it is pretty nice to do something like that - no HTML extensions required, easy to understand - it can give you some hell. $watch can change the model itself. And of course this will cause other $watch'ers to run too. If you do you use that too much, your listeners will fire around events and in worst case they will never stop. So be carefully if you do that. In my case it's pretty much OK: I operate in a limited scope only and I am not doing extraordinary stuff here. That said, this problem is not an Angular.js problem, it is a problem which resides to the Observer pattern itself.

Conclusion

With Angular.js it is pretty easy to wrap existing functionality into own directives. With $watch we have a powerful observer to react on model changes. And blur - well, it is missing in the official API (like focus) but it is dead easy to implement. So far I have not found too much drawbacks on Angular.js. Have fun!

Tags: Angular.js, jQuery, jQuery UI, Open Source