AngularJS ng-change event is not defined in Firefox

I recently stumbled upon an interesting issue on a project using AngularJS v1.3.15. One day I was told that there were some issues with a drop-down list in one of our forms. Upon selecting another value in the drop-down list an error message would be thrown in the browser console with something along the lines of:

Error: event is not defined [...]

I thought that was quite interesting, as I had never observed this kind of behavior while developing. However after a few quick searches I found the following github issue ng-change doesn't get the $event argument #6370 in which a fellow github member, caitp, stated the following:

ng-change is not a directive for handling the change event (I realize that this is confusing given the name), but is actually instead notified when ngModelController.$setViewValue() is called and the value changes (because ng-change adds a listener to the $viewChangeListeners collection). So this is as expected.

I was quite confused at first and I thought to myself, that can't be right!
It has worked perfectly fine on my machine! heuheuheu

I headed over to the documentation for ngChange and sure enough I found the following.

Ehh?

And then I had a look at the docs for ngClick and I saw this.

Hmm..

It does not explicitly state that the event object is available for the ngChange directive. At this point I was thoroughly confused. Because it worked on my machine and it still does. However I shortly thereafter realized that the source to my confusion was due to the same piece of code behaving differently in Chrome and Firefox respectively.

Here below is a little example snippet of a drop down list..

<div ng-controller="MyController as mCtrl">
    <select type="dropdown" ng-model="mCtrl.country" ng-change="mCtrl.isForeignCountry($event)">
        <option value="DK">Denmark</option>
        <option value="SE">Sweden</option>
        <option value="NO">Norway</option>
    </select>
</div>

..and the following function would be called from the above ngChange directive placed on the select element.

// ...
var vm = this;

const COUNTRY = 'DK';
vm.country = 'DK';

vm.isForeignCountry = function($event) {
    console.log(event);

    if (event.target.value !== COUNTRY) {
        console.log(`Foreign country: ${vm.country}`);
    }
}

Here is a link to a working example of the code on Plunker. Simply open the console and choose another value from the drop down list, in Chrome and Firefox. The code works in Chrome without any errors whereas it in Firefox throws an undefined error for the event object. This lead me to think that Chrome was doing some magic behind the scenes.

After some more digging it turns out that Chrome and some other browsers including IE (tested with IE 11) provides an event object in the global scope whereas Firefox doesn't, which results in an undefined error. Here is a jsfiddle example to try out and verify that in Firefox the console throws an error but in Chrome or IE you get an alert popup.

Another interesting thing you can do is to view the events fired on an element which is pretty cool. Simply open the Plunker link and open the Chrome DevTools (F12).

  • Then goto the Sources tab.
  • In the right hand side expand the Event Listener Breakpoints tab.
  • Then expand the Control event listener and select change.

Now if you change the value in the drop down you will automatically trigger the debugger and end up in the following function.

// angular.js #2994
function createEventHandler(element, events) {
  var eventHandler = function(event, type) {
    // jQuery specific api
    event.isDefaultPrevented = function() {
      return event.defaultPrevented;
    };
    // for brevity..

If you step through the code a bit you will see that the event parameter has a target property which has a valueproperty with a value i.e. "DK". The same is possible in Firefox where you can examine event listeners and step through the code.

Essentially the key difference is that Chrome, IE and possibly other browsers provides an event object in the global scope whereas Firefox doesn't but that is another topic. Furthermore it doesn't change the fact that the ngChange directive doesn't accept an event object and in my case I was coincidentally using Chrome during development, which did provide its own event object. That is why I never noticed it myself and unfortunately it didn't get caught in tests either.

Solution

The solution to this problem is rather trivial, simply pass the model (ngModel) itself instead of the event object which once again according to the docs is not available for the ngChange directive.

<div ng-controller="MyController as mCtrl">
    <select type="dropdown" ng-model="mCtrl.country" ng-change="mCtrl.isForeignCountry(mCtrl.country)">
        <option value="DK">Denmark</option>
        <option value="SE">Sweden</option>
        <option value="NO">Norway</option>
    </select>
</div>

That is it!