OdeToCode IC Logo

Building Better Models For AngularJS

Monday, March 17, 2014

Retrieving JSON data and binding the data to a template is easy with Angular, so easy that quite a bit of Angular code appears like the following.

<button title="Make movie longer" class="btn" 
        ng-click="makeLonger(movie)">
      <span class="glyphicon glyphicon-plus"></span>
</button>

What’s of interest is the ng-click directive, which is using an expression to invoke behavior directly against $scope.

ng-click="makeLonger(movie)"

The approach works well for simple applications and demos, but in the face of more complexity it would nice to have a proper model object that knows nothing about scopes or templates and contains straight-up logic related to a business concept, in which case ng-click might look like the following.

ng-click="movie.makeLonger()"

Subtle difference, but in my experience it is small changes like this that can make a code base easier and enjoyable to work with because responsibilities are well thought and separated. Even if the model only encapsulates a couple lines of code, it is a couple lines of code that don’t have to appear in a controller, or exploded in a complex if statement, or duplicated in multiple areas because all models are only data transfer objects brought to life by JSON deserialization.

Starting Over

Instead of allowing an HTTP API to define a model, we could start by defining a model with the following code.

(function() {

    var Movie = function() {
        this.length = 0;
        this.title = "";
        this.rating = 1;
    };

    Movie.minLength = 0;
    Movie.maxLength = 300;
    Movie.minRating = 1;
    Movie.maxRating = 5;

    Movie.prototype = {

        setRating: function(newRating) {
            if (newRating <= Movie.maxRating &&
                newRating >= Movie.minRating) {
                this.rating = newRating;
            } else {
                throw "Invalid rating value: " + newRating;
            }
        },

        makeLonger: function() {
            if (this.length < Movie.maxLength) {
                this.length += 1;
            }
        },

        makeShorter: function() {
            if (this.length > 0) {
                this.length -= 1;
            }
        }

    };

    var module = angular.module("movieModels");
    module.value("Movie", Movie);

}());

This approach allows a model to provide both state and behavior with unlimited functionality. The last few lines of code give the model definition a dependency on Angular, but it would be easy to factor out the registration of the constructor function and rely on something like a proper module API or global export. The code above is demonstrating what should eventually happen, which is that the constructor function is registered with Angular as a value service, and this allows the constructor to be decorated and injected into other services at run time.

Next we’d need the ability to take an object deserialzed from JSON and transform the data-only object into a proper model. The transformation is generic and could become the responsibility of another service.

(function() {
   
    var transformObject = function(jsonResult, constructor) {
        var model = new constructor();
        angular.extend(model, jsonResult);
        return model;
    };

    var transformResult = function(jsonResult, constructor) {
        if (angular.isArray(jsonResult)) {
            var models = [];
            angular.forEach(jsonResult, function(object) {
                models.push(transformObject(object, constructor));
            });
            return models;
        } else {
            return transformObject(jsonResult, constructor);
        }
    };

    var modelTransformer = function() {
        return {
            transform: transformResult
        };
    };

    var module = angular.module("dataServices");
    module.factory("modelTransformer", modelTransformer);

}());

Now any service can ask for the transformer and a constructor function to turn JSON into rich models.

(function() {

    var movieDataService = function ($http, modelTransformer, Movie) {

        var movies = [];

        var get = function () {

            return $http
                .get(movieUrl)
                .then(function (response) {
                    movies = modelTransformer.transform(response.data, Movie);
                    return movies;
                });
        };

        // ... more implementation

        return {
            get: get
            // ... more API
        };
    };

    var module = angular.module("dataServices", ["movieModels"]);
    module.factory("movieDataService", movieDataService);

}());

 

The end result is a set of richer models that make it easier to keep functionality of out $scope objects. This might seem like a lot of code, but other than the transformer, this is all code that would still be written but scattered around in $scopes.

What's The Downside?

Here are just a few of the reasons you might not like this approach. 

1. It's not functional JavaScript, it's classes with prototypes. Not everyone likes classes and prototypes. An alternative (and more common) approach to slimming down $scope would be to group interesting functions into a movie service.  

2. Adding additional state to a model might result in additional and unexpected values arriving at the server, if the model is serialized and sent back in an HTTP call.

3. If the service caches the model, it might require some code using instanceof to keep track of what objects have been transformed, and which have not. It also makes it more difficult to decorate the service.