Wednesday, January 15, 2014

So here's the scenario: you've got a form enabled with AngularJS. The form is populated with data from a data model object retrieved from a REST call. You need to know at a certain point (perhaps at the end of every user action, or perhaps at the moment of submission) whether the form data is different from when it was originally retrieved. How would you do that?

If you wrap your form elements within a set of HTML form tags and name the form, Angular automatically (via the ngFormController) monitors the overall state of the form and provides some status flags, one of which is the $dirty property. Problem solved, right? Well, not quite.

Take a look at this example form I've set up with AngularJS 1.2.6 and Bootstrap 3:

I threw a bit of a twist into my form. The tasks at the end of the form (stored as an array of objects in the main project object model) can be reordered by dragging: that functionality is accomplished with the addition of jQuery, jQuery UI, and the Angular UI Sortable project. As you might guess, if you did nothing to the form but reorder the tasks, the $dirty status of the form (displayed in the diagnostic area beneath the form) would remain "false": that status evaluates the data in the form fields but doesn't recognize other changes to the underlying data model.

But even if you didn't have a UI element like that in your form, $dirty isn't going to tell you what you need to know. Go into the form and change "Form" in the Name field to "Forms". Note the $dirty status changes to "true". Now change it back to "Form." $dirty still evaluates to "true". That's because $dirty isn't evaluating differences between the original data and the new data, it's just tracking interactions. Any form field the user interacts with is considered to be dirty, and that propagates up to the form.

So what you need then is a means of recording the original state of the data when the page/form is loaded and a means to compare that original data with the current model data.

Fortunately, Angular provides two convenience functions that can answer those needs. The angular.copy() function can create a clone of an object, while angular.equals() will do a deep comparison between two objects (comparing all property names and values, rather than telling you if one object is a reference/pointer to the other).

Here's another version of that same form (open up the page but don't do anything with it yet):

With this version of the form, I added code to the controller function to create an "original" clone object of the starting "project" model object as soon as the data was made available, then used angular.equals() to verify that both objects were identical. I also added a $watch function that would monitor changes in the "project" model object and re-compare the "original" and "project" objects.

Now go to the form and reorder the tasks, and you'll see that the first block of diagnostic data indicates that the data has changed, even though $dirty is still false. Go into the form and change "Form" in the Name field to "Forms," and then back again, and you'll see that the change status updates accordingly.

There's still a catch, though. Note the second second of diagnotic information, where (in both the "original" and "project" objects) it denotes the result of adding 1 to the estimated hours, and whether the Meeting Notes property is a null value or an empty string. They start off the same, but you can probably guess where this is going. Change the hours from 40 to 41, and then back to 40. Type a character or two in the Meeting Notes field, and then delete them.

The form inputs set the corresponding model data as strings, so once you edit the data in the inputs, even if you put it back to the way it was, anything that was not a string before (like a number or a null value) is now a string, and now the "property" object doesn't equal the "original" object anymore.

The workaround is straightforward enough: before you create the clone of the original data, parse it and "stringify" the appropriate properties.

...the code could probably be more elegant, but it gets the job done. So now if you change the Estimated Hours and Meeting Notes fields in the final example but then change them back, the code will correctly denote that "original" and "project" are back in sync after you remove your changes.

Since the demos consists solely of HTML, CSS, and JavaScript, the entirety of the code is there in the GitHub repo that powers the demo for you to look at if you wish. I created separate controller functions for each demo to make clear what was happening in each one.