Monday, February 2, 2015

Using Grunt to Concatenate Only the JavaScript/CSS Files Used in Index.html

One of the most common uses of the Grunt task runner is to build a deployment package out of your development code for your website or web application, and part of that build process is usually a task that concatenates the CSS and JavaScript files into singular (or at least fewer) files for optimal download.

The grunt-contrib-concat Grunt plugin allows you to configure a concatenation task to target individual files or entire directories, like so:

concat: {
            js: {
                src: [ 'dev/jquery/jquery.js', 'dev/angular/services/*.js', 'dev/angular/directives/*.js' ],
                dest: '../build/combined.js',
                options: {
                    separator: ';'
                }
            },
        }

The only drawback is that you have to update the task's "src" property as you add or remove CSS and JavaScript assets from your web application.

As I was playing around with Grunt on a personal project, I came to wonder: could I create a Grunt task or set of tasks that could figure out which files to concatenate based on the <link> and <script> tags in my code?  Here's what I came up with.

My project is a single page web application powered by AngularJS.  It has an index.html file that serves as the "single page" that displays the appropriate view fragment (HTML files stored in a "views" directory) for a given page/route in the application.  None of those view fragments require additional CSS or JavaScript resources, so my index.html page pulls down all of the necessary CSS and JavaScript files.

In my index.html file, I wrapped my blocks of <link> and <script> tags with special HTML "build" comments, a set for my CSS block and a set for my JavaScript block:


<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>My HTML Page</title>

    <!--build-css-start-->
    <link rel="stylesheet" media="screen" href="assets/app.css" />
    <link rel="stylesheet" media="screen" href="packages/bootstrap/dist/css/bootstrap.css" />
    <!--build-css-end-->

</head>
<body ng-app="demoApp">

    <!--Div where my Angular views will be displayed-->
    <div ng-view></div>

    <!--build-js-start-->
    <script type="text/javascript" src="packages/angular/angular.js" ></script>
    <script type="text/javascript" src="common/app.js" ></script>
    <script type="text/javascript" src="controllers/loginController.js" ></script>
    <script type="text/javascript" src="controllers/logoutController.js" ></script>
    <script type="text/javascript" src="services/authService.js" ></script>
    <script type="text/javascript" src="services/session.js" ></script>
    <!--build-js-end-->

</body>
</html>

I then created a Grunt task powered by the grunt-replace plugin that finds and parses those blocks via regex, extracting and modifying the file path within each "src" and "href" attribute and appending them to arrays stored as Grunt configuration properities. Each block is replaced by a single <link> and <script> tag pointed at the combined CSS and JavaScript file. The overall build task then executes the concat task, which concatenates all of the files in the respective configuration arrays into the combined files.

So when I run the master build task against my original set of development files:

...I end up with a build directory containing the index.html file, the views folders with the view HTML files, and a single CSS and JavaScript file: 

...and the index.html file itself looks like this:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>My HTML Page</title>

    <link rel="stylesheet" media="screen" href="combined.css"/>

</head>
<body ng-app="demoApp">

    <!--Div where my Angular views will be displayed-->
    <div ng-view></div>

    <script type="text/javascript" src="combined.js"></script>

</body>
</html>

Cool, right?

If you want to try it out for yourself, the entire Gruntfile used to power the build task is below, with comments:

Monday, January 19, 2015

Angular 1.3x Makes ARIA Enhancement Simple With ngAria Module

One of the JavaScript podcasts I listen to mentioned that the one of the new features in Angular 1.3x was the ngAria module.  I knew just from the name that the module had something to do with ARIA (Accessible Rich Internet Applications) but I wasn't sure what it did or how to implement it, so I decided to check it out.

Turns out using this new module is drop-dead simple.  You don't have to add any code to your markup in order to use it:  once you include the ngAria module in your Angular application, it'll automatically add and manage "aria-" attributes on your DOM elements, attributes that help screen readers understand what's going on in your application.

The folks over at Egghead.io have an excellent five minute video that demonstrates ngAria in action:  https://egghead.io/lessons/angularjs-using-ng-aria-to-automatically-improve-your-angularjs-accessibility

You can also read more about ngAria on the AngularJS documentation page on accessibility:  https://docs.angularjs.org/guide/accessibility

Frankly, I can't see a reason for not including this module in your AngularJS application if you're using Angular 1.3x.  It won't solve every accessibility issue, but it's a good starting point.

 

 

Friday, January 16, 2015

Using the "Controller As" Syntax With AngularJS Controllers

I recently had a chance to try out the "controller as" alternative to storing model data and controller functions on the $scope of an Angular controller, and I was rather taken with the elegance of it.

An example of the familar method of attaching a controller to a view via the regular Angular router and exposing data and methods via the $scope:

Route:

...
when('/auth/sales/salesPage', {
  templateUrl: 'views/sales.html',
  controller: 'salesController'
})
...

 

Controller:


.controller( 'salesController', [ '$scope', 'demoService',  function( $scope, demoService ) {
   $scope.uiState = { pageLoaded: false };

   $scope.performAction = function() {
     console.log( "Action!" );
   }
 }]);

 

View:


<h1>Sales Prospects</h1>
<div ng-show="uiState.pageLoaded"></div>

 

...now the same code but using the "controller as" methodology:

 

Route:


...
when('/auth/sales/salesPage', {
  templateUrl: 'views/sales.html',
  controller: 'salesController as vm' //'vm' is now a reference to the controller object
})
...

 

Controller:


.controller( 'salesController', [ 'demoService',  function( demoService ) {
   var vm = this; //The controller object takes the place of the scope.
   vm.uiState = { pageLoaded: false };

   vm.performAction = function() {
     console.log( "Action!" );
   }
 }]);

 

View:


<h1>Sales Prospects</h1>
<-- The route provides the controller object to the view as 'vm' -->
<div ng-show="vm.uiState.pageLoaded"></div>

 

Some of the benefits of this "controller as" methodology:

  • You don't have to inject the $scope into the controller.
  • If you have multiple views with multiple controllers on the same page, you can now have unique references/namespaces for each controller and not worry about name collisions between each controller's $scope.
  • It looks cleaner.
  • Less typing of "$scope" means you won't wear out the 4/$ key on your keyboard as quickly. :)

 

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.

Thursday, September 12, 2013

Introducing validORM: A Development Tool for Creating ORM CFC And ValidateThis Files

In my previous post about relating different data constructs in AngularJS to each other via property names, I mentioned that I figured out that technique while working on an internal development tool.  This blog post is about that tool.

The AngularJS-powered validORM tool lets you design the ID and column properties of your ColdFusion/CFML ORM CFCs while at the same time creating a matching set of validation rules using the ValidateThis (VT) library (a great library for handling both client and server-side validation).

The first view of the tool is a menu view.  You can choose to generate an updated version of your CFC and VT rules file based on a previous session or create a new set of files from scratch.  Either choice takes you to the generator view.

The first two sections of the in the generator view are pretty obvious:  that's where you choose the name and database table for your ORM object and configue the ID property.

The third section, where you define your column properties and create your validation rules, is where AngularJS really comes into play.  The property drop-down selection, the form controls and the hover hints for each property attribute you add, and the validation rule options and parameters:  those are all AngularJS manifestations of JavaScript data constructs.  Using the "Add Attribute" button to add a property attibute simply adds a JavaScript object of that name to the dataset; the creation of the form elements on the page for that attribute is handled automatically by AngularJS based on the Angular directives in the HTML.  That's Angular's strength: driving page behavior purely through data manipulation. 

The data constructs created by your choice of property attributes and validation rules are separate from the JavaScript objects and arrays of objects that represent all of the attribute and rule options, but they reference each other via the attribute and rule key names.

The last two sections allow you to further define any ValidateThis conditions and contexts referenced in your column property rules.  When you're done, clicking the submission button will trigger the file generation process.  Your ORM and VT configuration data will be parsed by a CFC which will then generate three files in the output folder of the tool:

  • An ORM CFC written in script format.
  • A ValidateThis rules definition file matching that CFC written in JSON format.
  • A JSON-formatted snapshot of the configuration data, with a filename reflecting the name of the object and the timestamp when the file was generated.  Said file will then appear in the menu view of the tool.

Some caveats:  the tool only allows for one ID property, doesn't include absolutely every type of property attribute (just the majority of them), and it doesn't let you define ORM relationship properties.  And as my first foray with using AngularJS, I'm sure it violates one or two Angular best practices (if you're an Angular guru, feel free to chime in with any suggestions for improvement).

But it works and can provide a kickstart to creating any ORM files and ValidateThis rules for your projects.  It comes with an example "Employee" configuration set that you can play with and then delete once you start using the tool.

The tool is available for download (and modification) via GitHub:  https://github.com/bcswartz/validORM. I also created a short video of the tool in action and posted it on YouTube at: http://www.youtube.com/watch?v=-fnt_n65NWg

Monday, August 12, 2013

Quick AngularJS Tip: Relating User Input Data to Metadata

I started learning AngularJS a few weeks back because I thought it would work well for an internal tool I was building (more on that in a later post).

Many AngularJS examples, including those in the current AngularJS tutorial (like this one), illustrate how to use the ng-repeat and ng-options directives using a typical Javascript array of objects.  So given such an array in the model:

$scope.plans= [
  {"name": "Basic", "cost": "$250.00"},
  {"name": "Deluxe","cost": "$325.00"},
  {"name": "Premium","cost": "$335.00"}    	
];

...you can output a list of the data like so:

<ul ng-repeat="plan in plans">
  <li>
    {{plan.name}}: {{plan.cost}}
  </li>
</ul>

But, as noted in the AngularJS API, you can also use ng-repeat and ng-options to loop through a set of properties/keys in an object, and that works even if each property references another object.  So you can accomplish the same thing with this model object:

$scope.plans= {
  "Basic": {"cost": "$250.00"},
  "Deluxe": {"cost":"$325.00"},
  "Premium": {"cost": "$335.00"}    	
};

...and this HTML:

<ul ng-repeat="(planKey,planObject) in plans">
  <li>
    {{planKey}}: {{planObject.cost}}
  </li>
</ul>

The advantage to using the latter technique comes into play when you want to relate something in the part of the model you want to preserve / process with a part of the model that provides metadata.

Taking the above examples a step further, say you wanted to create a short conference registration form. The user has to select a conference package, and you want to provide information about each package (what it entails and the cost) but you don't want that information to be part of the form submission. You can do this by having the user's conference selection pull the related data from the plan metadata.

So with the following AngularJS controller function:

function ConfReg($scope, $http) {
  $scope.reg= {};
  $scope.reg.plan= "Basic";
  
  $scope.plans= {
    "Basic": {
      "description": "Access to all first-run conference sessions on Thursday and Friday.",
      "cost": "$250.00"
    },
    "Deluxe": {
      "description": "Access to all first-run conference sessions on Thursday and Friday, access to repeat sessions on Saturday.  Complimentary breakfast included all three days.",
      "cost": "$325.00"
    },
    "Premium": {
      "description": "Access to all first-run conference sessions on Thursday and Friday, access to repeat sessions on Saturday.  Complimentary breakfast included all three days.  Also includes conference t-shirt and kazoo.",
      "cost": "$335.00"
    }

  };
}

...and the corresponding HTML:

<h3>Conference Registration</h3>
  <form name="regForm" id="regForm" ng-controller="ConfReg">
    <p>
      <label>First Name:</label>
      <input type="text" name="firstName" id="firstName" ng-model="reg.firstName" />
    </p>
			
    <p>
      <label class="control-label" for="lastName" id="lblLastName">Last Name:</label>   
      <input type="text" name="lastName" id="lastName" ng-model="reg.lastName" />
    </p>
				
    <p>
      <label class="control-label" for="plan" id="lblPlan">Conference plan:</label>   
      <select name="plan" id="plan" ng-model="reg.plan" ng-options="planName as planName for (planName,planObject) in plans"></select>
    </p>
    <div>
      <p>{{plans[reg.plan].description}}</p>
      <strong>Price:</strong> {{plans[reg.plan].cost}}
    </div>
				
</form>

...The user's selection of plan determines which plan object data is pulled from the "plans" part of the model for display in that div block, yet the data sent as the form submission (the "reg" object) only includes the plan name.