Web App over the file:// Protocol using AngularJS

After reading a lot about AngularJS I decided to run through the Code School tutorials about it and learn more. After a few code snippets I got a glimpse of how powerful a framework it is.

At work I am mainly an embedded software enginner. Not a javascript/web type of software guy. We mostly work in C with a little assembly thrown in. However, I've developed a recent urge to learn more about the web.

One project I needed to work on was a simple user interface to view some content. The requirements though was that all the pages needed to work over the file:/// protocol because the user would not be running any sort of webserver. This task proved to be a challenge when getting certain things in AngularJS to work. The issues were not directly related to AngularJS but more to browser security policies and accessing the file system.

1. HTTP templates loading in ng-view

The first hurdle I hit was getting my *.html to load within the ng-view. After struggling with this for awhile I came to the conclusion that it was best to convert my html to javascript and store it in the $templateCache variable. This variable keeps a cache of all your templates so that a request doesn't need to be made if your browser already has the template. By converting all our html to js and loading it into the $templateCache it gives the browser access to all the templates without a request.

Because I was actively working on the html and didn't want to keep converting it to js I setup a grunt job to watch for html changes and run a grunt task to convert it to javascript.

    module.exports = function(grunt) {
      grunt.initConfig({
          pkg: grunt.file.readJSON('package.json'),
          ngtemplates: {
              myapp: {
                  options: {
                      base: "web",
                      module: "app",
                  },
                  src: "app/html/*.html",
                  dest: "app/js/templates.js"
              }
          },
          watch: {
            templates: {
              files: ['app/html/*.html'],
              tasks: ['ngtemplates'],
            }
          }
      });

      grunt.loadNpmTasks('grunt-angular-templates');
      grunt.loadNpmTasks('grunt-contrib-watch');

      grunt.registerTask('default', ["ngtemplates"]);
      };

This Gruntfile sets up a task to watch my app/html folder and when it sees an update runs the ngtemplates task. The ngtemplates task works by using the grunt-angular-templates module. When the task runs it generates a templates.js file that resembles the snippet below:

angular.module('app').run(['$templateCache', function($templateCache) {  
  'use strict';

  $templateCache.put('app/html/details.html',
   "<div class=\"row-fluid\">\n" +
   "  <div class=\"span9\">\n" +
   ...

Now that we have the html converted to js and loaded into our $templateCache all we need to do it include the templates.js file in our main index.html and AngularJS takes care of the rest. The grunt-angular-templates module takes care of all the javascript for you, all you need to do is write the html and point it to your source.

2. Requesting data over the file:/// protocol

Another part of my application was serveral JSON documents. These documents get shipped with the application and don't get updated by the user but I wanted an easy way to perform updates. This way I can just update the JSON file in the next release and the user sees the new content.

My solution to this wasn't as "clean" as to my first problem, but I have it working none the less. I started out using the $http AngularJS functions to try and load the JSON files from the hard drive. $http.get() was not working due to security issues once again so I moved on to $http.jsonp(). This also didn't work because without a backend server running you cannot name the callback appropriatly for AngularJS.

So I resorted to using a third party AngularJS plugin called AngularLoad. This plug adds javascript files to the DOM dynamically. So within your controller or serivice you can determine the data you need and add it accordingly. It did require some changes to my JSON documents though. It essentialy required my JSON documents to be converted into javascript files with a variable assignment.

So this:

 {
   "name": "Zack",
   "class": "Math"
 }

became

var student = {  
  "name": "Zack",
  "class": "Math"
};

This way once the "script" is loaded you have access to a variable that contains the data. You could do something similar with a callback function as well. To request the data my controller performed a simple call to

// Download student
angularLoad.loadScript('app/data/zack.js').then(function() {  
    // Script loaded succesfully
    $scope.student = student;
});

Once loading the script completes the above snippet takes the student variable from the script loaded and applies it to the $scope so it can be used within the template.

In order for the above code to work you also must inject the angularLoad serivce into your controller and add it do your app dependencies. This results in a controller that looks like:

app.controller('StudentController', function($scope, angularLoad) {  
  this.getStudent = function($scope, angularLoad) {
    // Download student
    angularLoad.loadScript('app/data/zack.js').then(function() {
      // Script loaded succesfully
      $scope.student = student;
    });
  };
});

Conclusion

In conclusion AngularJS is a very powerful framework you can use to create some very powerful apps with even if you are forced over the file:///. I'll update this post if I find anymore gotcha's along the way or come up with better ways for working around the issues I saw.

comments powered by Disqus