Thursday, October 23, 2014

Task-based web browsing: Grunt and the grunt-open plugin

Lately I've been playing around with Grunt, which is a JavaScript-based task runner similar to Ant and Gradle. Generally, these tools are used to automate software builds, but they can be utilized in other ways.

While browsing through the large collection of Grunt plugins, I came across one called grunt-open. The grunt-open plugin lets you create a task that opens a URL in your web browser. It looks like it was created with the idea of opening a web page at the end of a chain of Grunt tasks to check the result of a software build. But I thought it might be useful in automating everyday web browsing tasks.

I like to start off my work day with a bit of Internet humor and I have certains sites I visit for that. Dilbert has a new strip every day, while JoyOfTech and XKCD get published every Monday, Wednesday, and Friday, and the XKCD "What-If" article is published once a week, usually by Thursday. Rather than manually opening the appropriate bookmarks for the appropriate day of the week, I figured I'd program a task to take care of opening the appropriate sites.

I won't reinvent the wheel here trying to explain the basics of Grunt: the Grunt "Getting Started" guide does a good job of explaining how to install Grunt and generate the files needed (the package.json and the Gruntfile.js files). Here's what my Gruntfile looks like:

So in the directory where this Gruntfile.js lives, I just have to type "grunt webhumor' in the command, and Grunt will open the appropriate sites. Not a huge timesaver, but kinda cool.

Wednesday, January 15, 2014

Determining if Data Has Changed in Your AngularJS-Powered Form

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:

https://bcswartz.github.io/AngularJS-formDataChangeDetection-demo/index.html#/demo1/1

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):

https://bcswartz.github.io/AngularJS-formDataChangeDetection-demo/index.html#/demo1/2

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.

projectResource.get().$promise.then(function(project) {
  $scope.project= project;
  $scope.original= angular.copy(project);
			
  $scope.initialComparison= angular.equals($scope.project,$scope.original);
  $scope.dataHasChanged= angular.copy($scope.initialComparison);
});

...

$scope.$watch('project',function(newValue, oldValue) {
  if(newValue != oldValue) {
    $scope.dataHasChanged= angular.equals($scope.project,$scope.original);
  }
},true);

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.

Here's the final example:

https://bcswartz.github.io/AngularJS-formDataChangeDetection-demo/index.html#/demo1/3

This time, before using angular.copy(), I processed the "project" data model via a service function, which in turn executed a "stringification" function on each of the relevant properties:

Controller code:

$scope.project= service.stringifyProjectData(project);

 

Service code:

.factory("demo1_service", [function() {
  var stringifyProperty= function(property) {
    if(property== null) {
      return "";
    } else {
      return property.toString();
    }
  };
		
  return {
    stringifyProjectData: function(project) {
      var simpleProperties= ["projectName","description","hours","meetingNotes"];
      for(var p= 0; p < simpleProperties.length; p++) {
        project[simpleProperties[p]]= stringifyProperty(project[simpleProperties[p]]);
      }

      for(var t= 0; t < project.tasks.length; t++) {
        project.tasks[t].description= stringifyProperty(project.tasks[t].description);
      }

      return project;
    }
  };
		
}]);

 

...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.