Angular Object Validator: Enforcing object types in an Angular.js application

As anyone with JavaScript experience will tell you, occasionally the Wild West of coding languages falls a little short in critical areas by not enforcing object types. On a small Angular application, where all the developers have intimate knowledge of the code base, this may not be an issue. However, as the project scales, and new people begin working in the solution, you may find yourself wishing you had a way to enforce object types within your more critical functions. At this point, you may be asking yourself “What about TypeScript?”. If you have the option to start fresh, and know that your application will require type enforcement, TypeScript is definitely the logical answer. However, there are times in which TypeScript might not be an option. (legacy code, etc.)

To help with this issue, here at Sigao we have created a solution specifically for Angular.js applications called Angular-Object-Validator. This simple service will provide a single entry point for loading and validating pre-defined objects against potentially dynamic objects within your application.

Installing Angular Object Validator

Via Bower:

bower install angular-object-validator --save

 

Once the package and its dependencies have been added to your project, your angular application should have access to a module called “ObjectValidator” which you will need to inject into your application.  Each controller that will use the validator needs to be injected with a variable called “objectValidator

 

Usage

Setting up the validator

Angular services behave as singletons, meaning once data has been changed inside the service, that data is accessible everywhere in the application that the service is used. Because of this, it is recommended to load all of your object models in one location via the command objectvalidator.addClass(“Class Name”, _yourModelObject).

 

Testing

Once the validator has ingested all of the classes, you can now validate objects at any point that you have injected the validator. To test a specific class, all you need to do is use objectValidator.test(“Class Name”, yourObjectVariable). This will return true for valid, and false for invalid

 

Configuration

Optionally, you may change the behavior of the validator by sending a configuration object to  objectValidator.config({}).  Object can contain a property called checkProto and matchPropertyCount, both of which are boolean values

 

Examples

1. Configuration property: checkProto

Default value: true. This configuration determines if prototyped properties are validated against the model.  Below is an example of 2 objects and how they compare with checkProto:

//Object 1
objectValidator.addclass("CheckProtoExample", {
     something:"",
     somethingElse:[]
});

//Object 2
function example() {
     this.something = "";
}
example.prototype = {somethingElse: []};
var testObject = new example();

objectvalidator.config({checkProto:true});
objectvalidator.test("CheckProtoExample", testObject); //Returns: true
objectvalidator.config({checkProto:false});
objectvalidator.test("CheckProtoExample", testObject); //Returns: false

2. Configuration property: matchPropertyCount

Default value: true.  This configuration decides if the validator matches the exact properties in the model, or if the tested object can have additional properties beyond those in the model.  Note that the tested object is always required to have at least all of the model objects properties.

objectValidator.addClass("matchPropertyCountExample", {
            test1: "",
            test2:[],
            test3:true,
            test4:{}
});

var testObject = {
            test1: "",
            test2:[],
            test3:true,
            test4:{},
            test5:"something",
            test6:{}
        }

objectValidator.config({matchPropertyCount: true});
objectValidator.test("matchPropertyCountExample", testObject);//Result: false
objectValidator.config({matchPropertyCount: false});
objectValidator.test("matchPropertyCountExample", testObject);//Result: true

 

3. Generic controller mock up

The following example is a mock up of a generic controller for interacting with a database. Keep in mind, because this is an example, it has not be tested and will likely need some extra work to be used. However, it illustrates how to effectively use the object validator in your application.

Module setup

(function () {
    "use strict";

    angular.module('YourModule', []).directive('YourDirective', yourDirective);
    yourDirective.$inject = ['$http', 'objectValidator'];

    function yourDirective($http, objectValidator) {
    
        //Your code goes here

    };
})();

Here we have the beginnings of our module, highlighting how to inject the object validator into the controller. All of the following code examples are placed inside the main controller function of this module, in the order that they are discussed.

 

Setting up the validator

//setting validator to match exact number of properties on objects as well as prototyped values
objectValidator.config({checkProto: true, matchPropertyCount: true});

//Load models into validator.  Because services are singletons, this can be done
//in one main controller for all of the critical objects in your site.  For the sake
//of time, we've defined them here
//Validator will use the type of the model property value for the validation
objectValidator.addClass("getDto",{
    status: 0,
    result: {
        property1: 0,
        property2: [],
        property3: ""
    }
});
objectValidator.addClass("postDto",{
    data: []
});

Now we are defining the models and configuring the validator. In larger applications it is recommended that loading the object models happens in one location for the whole application, rather than inside the controller. However, for this example we decided to simplify the process.  Keep in mind that the models must be defined using the type of variable you require.

Defining the functions

//Service model
var service = {
 getData: getData,
 setData: setDat
};
return service;

function getData() {
            return $http.get("http://yourURL.com/api/getData")
                .then(handleReturn)
                .catch(function (message) {
                    console.error(message);
                });
            function handleReturn(data, status, headers, config) {
                //make sure that inbound data matches structure of _dtoModel
                if (objectValidator.validateObject("getDto", data)) {
                    return data;
                } else {
                    console.error("YourDirective.getData received an invalid object format");
                    return null;
                }
            }
}
function setData(data) {
            if (objectValidator.validateObject("postDto",data)) {
                return $http.get("http://yourURL.com/api/setData")
                    .then(handleReturn)
                    .catch(function (message) {
                        console.error(message);
                    });
            } else {
                console.error("YourDirective.setData received an invalid object format");
                return null;
            
            function handleReturn(data, status, headers, config) {
                //optionally, you can validate the post return if you'd like
                return data;
            }
        }
}

Lastly, we define the functions for getting and setting the hypothetical data. We use the validator to compare the objects being sent and received against the models that we expect to send and receive. If they do no match, we handle the exception and alert the user.

 

Documentation

Name Description Argument Type(s) Returns
test() Tests an object against a pre loaded class String: Class Name,
Object: model
Bool: validation result
type() Returns the type of variable sent to it Var: input variable String: type of variable
addClass() Adds a class model into the validator to test data against String: Class Name,
Object: model
getClass() Gets a class that has been loaded into the validator String: model name Object: model object
removeClass()Gets a class that has been loaded into the validator Removes a class that has been loaded into the validator String: model name
getAllClasses() Gets all of the classes that have been loaded into the validator Object: object that contains all classes
removeAllClasses() Removes all classes in the validator
config() Sets configuration settings within the validator Object: Configuration object that contains 1-2 properties.  checkProto, and/or matchPropertyCount

 

The ultralight angular application setup package – Part 1: Four files to rule them all

In this tutorial I will be walking you through a means of rapid angular application setup. By the end you should be able to run a single command and have a bare bones angular application built and ready for experimenting. By eliminating the overhead in setting up a project, I hope to encourage rapid development of bad ideas.

We will be using Node.js, gulp.js, and bower.js to setup the initial structure for a project. The final product of this tutorial will a foundation for a project but will not contain the build processes needed compile project script. Because of this, you will not be able to official “start” the project until the end of Part 2.s

Step one: The package.json file

As a quick disclaimer, this tutorial will be using Node.js extensively. If you do not have any experience with Node, I would recommend downloading and installing it from NodeJS.org and familiarizing yourself with its functions before attempting this.

Navigate to the project folder you would like to use for your Angular application and create a file called package.json. Enter the following code into this file and save.

{
  "name": "PackageName",
  "private": true,
  "version": "0.0.0",
  "description": "",
  "repository": "",
  "license": "",
  "devDependencies": {
    "fs-path": "^0.0.22",
    "gulp": "^3.9.1",
    "gulp-load-plugins": "^1.2.4",
     "http-server": "^0.9.0",
    "mkdirp": "^0.5.1"
  },
  "scripts": {
    "preinstall": "npm install -g bower && npm install -g gulp",
    "postinstall": "bower install && gulp setup",
    "start": "http-server -a localhost -p 8000 -c-1",
    "pretest": "npm install"
  },
  "dependencies": {}
}

While the package.json file offers a large number of useful tools, the area we will be focusing on in this case is the devDependencies and scripts properties. Note: the name field is required by Node and will throw errors if empty.

  • devDependencies: This is what node will use to define what node development packages to install when you run the initial “npm install” command on the project folder. Keep in mind, packages placed here should only be regarding the development process, not libraries used by the application (Those would go in the dependencies object, but in this instance we’ll be managing those with Bower)
  • scripts: These are command line scripts that will be executed during various node processes. As you can see, before the installation we are running a command to install Bower and Gulp globally. After those development tools are installed, we’re running two tasks (both of which we will define later), one to install the website dependencies defined in the bower.json file and one to run the gulp process to build the site structure.

Step 2: The bower.json file

Next, we will create a file called bower.json in the folder and define it as follows.

{
  "name": "Setup package",
  "description": "",
  "main": "",
  "authors": ["" ],
  "license": "",
  "keywords": [""],
  "homepage": "",
  "private": true,
  "ignore": [""], 
  "dependencies": {
    "angular": "^1.5.7",
    "bootstrap": "^3.3.6",
    "angular-route": "^1.5.7"
  }
}

Once again, we will be skipping over some parts of the object that are not relevant to what we’re doing. It is worth reading about how bower manages packages, but for the time being we will focus on the dependencies object.

  • dependencies: These are actual packages used by your website that are managed by bower. These can be automatically updated with bower commands entered manually or from a gulp process. For now, we’ll be using this file to simply install the packages in our project folder.

Step 3: The custom file

Next comes our definition of how the Angular application project should be organized. In the same folder, create a file called projectSetup.json with the following code. This object will be consumed by our custom processes defined in step 4. With this, we will define the folder layout of the project, the .gitignore file, and any template files we would like to create.

{
    "fileTree": {
        "working": {
            "app": {
                "components": {
                    "main": {
                        "files":[
                        "main.html",
                        "main.js" ]
                    } 
                },
                "files":["app.module.js","app.route.js","app.controller.js"],
                "shared": null
            },
            "assets": {
                "css": null,
                "img": null,
                "js": null,
                "less": null
            },
            "files": ["index.html"]
        },
        "public": {
            "css": null,
            "img": null,
            "js": null,
            "html": {
                "files": ["main.html"]
            },
            "files": ["index.html"]
        }
    },
    "gitignore": [ "node_modules", "bower_components" ],
    "templates": {
        "index.html": [
            "<!DOCTYPE html>",
            "<html lang=\"en\" ng-app=\"app\">",
            "<head>",
            "<meta charset=\"utf-8\">",
            "<title></title>   ",
            "<link rel=\"stylesheet\" href=\"css/index.css\" />",
            "<!--Scripts-->",
            "<script src=\"js/vendor-min.js\"></script>",
            "<script src=\"js/app.js\"></script>",
            "</head>",
            "<body ng-controller=\"mainController\">",
            "<div ng-view></div> ",
            "</body> ",
            "</html>"
        ],
        "app.module.js": [ "var app = angular.module(\"app\", [\"ngRoute\"]);" ],
        "app.controller.js": [
            "app.controller(\"mainController\", function ($scope) {",
            "$scope.msg = \"Hello World\";",
            "});"
        ],
        "app.route.js": [
            "app.config(function($routeProvider) {",
            "$routeProvider.when(\"/\", {",
            "templateUrl : \"html/main.html\"",
            "});",
            "});"
        ],
        "main.html": ["<div>{{msg}}</div>"]
    }
}
  • fileTree: This is how we will lay out the folder structure for the project, as well as defining any placeholder files we may want. Folder names will be decided based on any property that contains an object or a null value, while file names will be defined by any property with an array of file names. While we chose to use “files” for all of our file arrays, this isn’t strictly necessary.
  • gitignore: This is a simple array of items you would like to be added to a .gitignore file that will be generated.
  • templates: The properties of this object should exactly match the names of placeholder files that you’ve defined in your file tree. The array of strings will be formatted and injected into the file that matches the property name, allowing you to define some basic default functionality in the javascript/html files. Note: this was written with the intention of keeping everything within a single file. While I will not be covering it in this tutorial, it would be fairly easy to convert the functions in step 4 to accept file paths that point to template files, rather than arrays of strings. You do you.

Step 4: The gulpfile.js file

This file will be the backbone of our setup process for our Angular application. For now, we will only use it for generating the folder/files structure, generating the .gitignore file, and injecting default code into our auto generated files. However, in the next tutorial we will also be defining build processes and ftp capabilities from this file.

//Includes
var gulp = require('gulp');
var fs = require('fs');
var fsPath = require('fs-path');
var mkdirp = require('mkdirp');


//Gulp plugin manager
 var plugins = require("gulp-load-plugins")({
     pattern: ['gulp-*', 'gulp.*', 'main-bower-files'],
     replaceString: /\bgulp[\-.]/
 });

//Define setup task
gulp.task('setup', function () {
    //get project layout object
    var json = JSON.parse(fs.readFileSync('projectSetup.json'));
    var paths = getPaths(json.fileTree || null);
    var templates = json.templates || null;
    var ignores = json.gitignore || null;

    //build file structure
    if (paths) {
        buildFileStructure(paths);
    }

    //build .gitignore
    if (ignores) {
        buildGitIgnore(ignores);
    }
   
    //populate base files
    if (templates) {
        buildBaseFiles(templates, paths);
    }
});


//Supporting Methods

/**
 * Loops through all template objects, finds the associated file path, and over writes the target file with the template
 * @param {object} templates 
 * @param {list} paths
 */
function buildBaseFiles(templates, paths) {
    for (var template in templates) {
        // skip loop if the property is from prototype
        if (!templates.hasOwnProperty(template)) continue;
      
        paths.forEach(function (path) {
            if (path.indexOf(template) > -1) {
                writeFileFromTemplate(path, templates[template]);
            }
        });
    }
}

/**
 * Creates files or folders according the paths passed in
 * @param {array} paths 
 */
function buildFileStructure(paths) {
    //Sort paths based on if they are files or folders
    paths.sort(function (a, b) {
        return (isFile(b) === isFile(a)) ? 0 : isFile(b) ? -1 : 1;
    });

    for (var path in paths) {
        var curPath = paths[path];

        //If the current path leads to a file, write the file path
        if (isFile(curPath)) {
            fsPath.writeFile(__dirname + '/' + curPath, "", function (err) {
                if (err) {
                    return console.log(err);
                }

                console.log("The file was saved!");
            })
        //if not, just make a folder
        } else {
            mkdirp(curPath, function (err) {
                if (err) { console.log(err) }
            });
        }
    }
}

/**
 * Finds the file defined in the path and overwrites it with strings in the content array
 * @param {string} path 
 * @param {array} contentArray 
 */
function writeFileFromTemplate(path, contentArray) {
    var content = "";
    for (var line in contentArray) {
        content += contentArray[line] + "\r\n";
    }
    fsPath.writeFile(__dirname + '/' + path, content, function (err) {
        if (err) {
            return console.log(err);
        }

        console.log("The file was saved!");
    });
}

/**
 * Builds the .gitignore file on the same file system level as this file
 * @param {array} ignores 
 */
function buildGitIgnore(ignores) {
    var content = "";
    ignores.forEach(function (ignore) {
        content += ignore + "\r\n";
    });
    fsPath.writeFile(__dirname + '/' + ".gitignore", content, function(err) {
        if (err) {
            return console.log(err);
        }

        console.log("The file was saved!");
    });
}

/**
 * Returns list of paths to be built in th file system
 * @param {Object} json 
 * @return {Array} pathArray
 */
function getPaths(json) {

    if (json == null) return null;
    var pathArray = [];
    for (var key in json) {
        // skip loop if the property is from prototype
        if (!json.hasOwnProperty(key)) continue;

        var obj = json[key];
        if (Array.isArray(obj)) {
            for (var x in obj) {
                pathArray.push(obj[x]);
            }

        } else if (obj == null) {
            console.log(key);
            pathArray.push(key);

        } else if (typeof obj == "object") {
            //console.log(key);
            var tempArray = getPaths(obj);
            for (var x = 0; x < tempArray.length; x++) {
                pathArray.push(key + "\\" + tempArray[x]);
            }
        }
    }
    return pathArray;
}

/**
 * Returns true if string contians a dot extension
 * @param {string} path 
 * @return {Bool} 
 */
function isFile(path) {
    return /\.[0-9a-z]+$/i.test(path);
}
  • Includes: Here you can see we are referencing the development packages we defined in step one. Require.js will automatically find them in the node_modules folder.
  • Gulp plugin manager: This is a handy tool that will be used in part 2 of this tutorial. In this step, we search for any node package that is prefixed with “gulp-”, strip the prefix off, and then load it into the plugins object. This keeps all our gulp plugins in one object.
  • Gulp setup: This is the main task that sets up the project structure. As you can see, we first read in the json object using the fs package and then send the various parts to our helper functions. In the first file when we defined gulp setup inside of the postInstall property, we were telling Node to run the setup command through Gulp.
  • Supporting Methods: We will not go into details about these as the names and purposes of them should be fairly intuitive.

Step 5: where do we stand now?

At this point, the final step is to watch it work. With your Angular application project folder select in your command window, enter the command npm install. At this point, the process will execute the following:

  1. Download the dev dependencies (defined in packages.json),
  2. Make sure gulp and bower are installed on your machine,
  3. Install bower packages (defined in bower.json),
  4. Run the gulp setup process (defined in gulpfile.js)
  5. Import the custom object
  6. Create the file structure
  7. Create the .gitignore file
  8. Overwrite blank files with templates

Once the process ends your project folder contents should look like this:

Angular_setup_file_structure

This is good start, but we’re not quite done getting everything ready to go. In part 2 we’ll cover compiling the bower package files into a usable script file for use by the index.html file, as well as how to automate that process during the installation.