Smart Tech for a better Web
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:
For the model validation you can choose between three different approaches. The difference is how you make the validation happen.
First, lets look at the form. Its dead simple:
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.
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:
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:
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:
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.
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:
Therefore we need to extend our controller with another minor method which switches this property.
This is so easy, I will not explain it further. If you are curious how validateModel() is called, read on.
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.:
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:
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.
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:
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.
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!