AngularJS $watch doesn’t work for input type of email

I ran into an issue late last week working on a site for a client in which we’re using AngularJS.  I ended up burning a few hours on it and Google searches weren’t helping so hopefully this helps someone else.  I did find a bug report that seems related but it dates to September of 2012.  For reference, we’re using AngularJS version 1.2.26.

The application we are building has a form for registering for an event.  The UX/UI called for an introductory portion collecting first name, last name and email with a secondary portion for individual attendees that also includes name and email.  We had a request for the values in the introductory portion to get copied into the first entry of the secondary, operating under the assumption that the person doing the registration is most likely one of the attendees.  If this was a different JavaScript situation, I might try to introduce a button or other UI that the user had to engaged to run the copying.  I know in AngularJS I could just make the model for the first attendee and the registrant the same but that would make it so that they were always the same and that’s not what we’re after.  So without changing the UI and embracing a feature of AngularJS, I opted for leveraging the $watch function.

Here’s a snippet from my controller function:

        // init default values
        $scope.data = {
            firstName: '',
            lastName: '',
            email: '',
            attendees: [
                {
                    name: '',
                    email: ''
                },
                {
                    name: '',
                    email: ''
                }
            ]
        };

        $scope.$watch('data.firstName', function (newValue, oldValue) {
            updateFirstAttendeeName();
        })

        $scope.$watch('data.lastName', function (newValue, oldValue) {
            updateFirstAttendeeName();
        })

        $scope.$watch('data.email', function (newValue, oldValue) {
            $scope.data.attendees[0].email = $scope.data.email;
        })

        function updateFirstAttendeeName() {
            $scope.data.attendees[0].name = $scope.data.firstName + ' ' + $scope.data.lastName;
        }

This all seemed pretty easy and straightforward since $watch does all the heavy-lifting of tracking when there are changes.  That was the case for the name fields; it just magically worked.  However, the email hooks didn’t work, even since the code was virtually identical.  So I started looking closer at the markup for the UI.

<form-field invalid="eventRegistrationForm.firstName.$invalid">
  <label>@Html.Sitecore().Field("Lead Form First Name Prompt")</label>
  <input type="text" name="firstName" ng-model="data.firstName" ng-required="submitted" />

  <form-field-error>@Html.Sitecore().Field("Lead Form First Name Error Message")</form-field-error>
</form-field>

<form-field invalid="eventRegistrationForm.lastName.$invalid">
  <label>@Html.Sitecore().Field("Lead Form Last Name Prompt")</label>
  <input type="text" name="lastName" ng-model="data.lastName" ng-required="submitted" />

  <form-field-error>@Html.Sitecore().Field("Lead Form Last Name Error Message")</form-field-error>
</form-field>

<form-field invalid="emailIsInvalid()">
  <label>@Html.Sitecore().Field("Lead Form Email Address Prompt")</label>
  <input type="email" name="email" ng-model="data.email" required="" />

  <form-field-error>@Html.Sitecore().Field("Lead Form Email Address Error Message")</form-field-error>

  <label class="check">
    <input type="checkbox" name="emailPermission" ng-model="data.emailPermission" /> @Html.Sitecore().Field("Lead Form Permission to Email Opt-In Text")
  </label>
</form-field>

...

<div ng-repeat="n in ticketCountOptions" ng-show="n <= data.ticketCount">
  <h4>Ticket {{n}}</h4>

  <form-field>
    <label>@Html.Sitecore().Field("Event Registration Form Attendee Name Prompt")</label>
    <input type="text" name="attendeeName{{n}}" ng-model="data.attendees[$index].name" />
  </form-field>

  <form-field>
    <label>@Html.Sitecore().Field("Event Registration Form Attendee Email Prompt")</label>
    <input type="email" name="attendeeEmail{{n}}" ng-model="data.attendees[$index].email" />
  </form-field>

  <form-field>
    <label>@Html.Sitecore().Field("Event Registration Form Attendee Phone Prompt")</label>
    <input type="tel" name="attendeePhone{{n}}" ng-model="data.attendees[$index].phone" />
  </form-field>
</div>

The “form-field” tags are actually a custom directive we wrote that handles some stock markup and error handling.  I’m not sure if that is actually my problem but getting rid of that or digging into complicated directive scoping sounds like less fun.

Knowing that the only real difference between the name and email fields is the type parameter on the input tags of “text” vs. “email”.  I started with the first field and suddenly the $watch function started working.  However, the portion of my function that updates the first attendee model wasn’t making an impact as the UI wasn’t being updated.  So I changed the repeated attendee email field to type=text and it started to work as well.

I was relieved that I got this all working but a little disappointed that I had to “dumb down” the field to a basic text field.  The email input type has some important built-in validation and, for some mobile browsers, some modifications to keyboard to make entry faster.  If this is a genuine bug in AngularJS, then this seems like the best compromise.  Again, I wanted to post this in case someone else runs into it.  If someone has a suggestion as to why my code was working as expected, I’m happy to hear what I could do to fix it.  Feel free to leave a comment so we can have a discussion.

One thought on “AngularJS $watch doesn’t work for input type of email

Leave a Reply