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. 


Comments
gravatar James Monday, March 17, 2014
This is an interesting idea, but I can't help but feel you're working against Angular, rather than with it. Plus, when most projects are going to be split into their respective files for readability, you'll be repeating immediately invoked function expressions. Could you elaborate on the benefits of slimming down the $scope? Is it a performance thing?
gravatar scott Monday, March 17, 2014
@James: I'm not sure what you mean by working against Angular and repeating IIFEs. Each of the IFFEs represent a distinct file to keep the concepts separated, and each file is only included once. We might have different ideas on how the code is consumed. This example is relatively simple, but many other frameworks find value in making models more than just simple data containers. My idea is to encapsulate certain types of logic in the model itself and outside of $scope to make it easier to use and test.
gravatar Bart van den Burg Monday, March 17, 2014
You might find our RRM interesting. It takes care if the transformations but also handles model relations: https://github.com/SamsonIT/RRM
gravatar Ismael Monday, March 17, 2014
@Scott, what's the reasoning for registering the Movie constructor function as a VALUE instead of a SERVICE? I'm still trying to wrap my head around the different types of services in angular.
gravatar Francisco Gileno Monday, March 17, 2014
I liked the idea of adding behaviors for models when these behaviors only matters to the model itself. We could also use a service for adding more complex behaviors. What is your opinion? I think I learnt from you in a pluralsight course to pass the angular module as a parameter to the anonymous function. Is there any reason to not use this approach this time? Thanks for sharing this. Cheers.
gravatar Scott Monday, March 17, 2014
@Ismael: The service API is terribly confusing. The .service method expects you to give it a constructor function, which is exactly what Movie is, but .service also expects to use the new operator on this function to create an instance of a service - I don't want that to happen. I want the function to be the service and inject the raw function into other components and allow them to use the new operator to create an instance of the model. The .value method allows me to do this, because it simply takes the value you give it and treat that as the service instance. Don't worry, it takes a while for this to sink in :) @Francisco: Yes, you can always use a service to add more complex behaviors. I've gotten away from passing the module as a param only because I want to keep the code compatible with the ng-min tool. https://github.com/btford/ngmin
gravatar Kny Tuesday, March 18, 2014
Would you say its bad to keep the class Movie but skip the ModuleTransformer and the registrering and "new up" Movies in out service directly?
gravatar logongas Tuesday, March 18, 2014
My solution is to send function code from the server as a String and in the client to transform it into a JS function. The advantage is to keep server and client funcionality together as they are almost identical.
gravatar scott Tuesday, March 18, 2014
@kny - no I do not think it is bad. I'm keeping them separate and hoping to use a decorator at some point, but what's right is what is right for your app.
gravatar Dean Wednesday, March 19, 2014
This looks eerily familiar: http://www.youtube.com/watch?v=Iw-3qgG_ipU
gravatar Alex Wednesday, March 19, 2014
I find it easier to work with plain JSON object returned from the server (let's say Survey) and have some service that relates to this object (SurveyServices). This way I don't have any problem changing the object on the server (like adding new fields). The working logic in a service anyway.
gravatar simalexan Saturday, March 22, 2014
First of all, a very nice blog post. I like it very much. This is actually one of the proper ways to use the Angular.js Provider and its derivatives: Factory & Service. And it is functional side of JavaScript. The structure you represented is the JavaScript "function object". Also the part of Angular.js Best practices is to make your controller as thin as possible. Your post is completely aligned with that. Keep up the good work!
gravatar Phillip Tuesday, March 25, 2014
I've just started getting into angular.. Coming from the MS MVC world, I can tell you that I'm really not that big a fan (based on what I've seen so far). I do like your post though! Seems very nicely done.
gravatar Cameron Tuesday, March 25, 2014
Mind blowing. I'd be forever grateful if you made a simple github project or something demonstrating how this approach fits within an overall application.
gravatar Mohamed Meligy Tuesday, March 25, 2014
What this introduces is non-Plain_Old_JavaScript_Objects. It's quite like how knockout has observables, and you need to do exactly the same trick to convert from JSON-parsed-objects to knockout models. Not that it's better or worse. Just very similar. It so front to me as now I had to work on an old project using knockout again after several months of completely converting to angular.
gravatar Scot Tuesday, March 25, 2014
@Mohamed: Respectfully disagree. KO observables completely destroy an object by rewriting the properties. A constructor function + prototype object is about as plain as possible.
gravatar Tim Stearn Tuesday, March 25, 2014
I like the idea generally. Saw this proposed on a ng-conf video a couple of months ago and it would help. We have 1000+ line controllers, shamefully. I might want to use Crockford's module pattern and do this with a Closure though. JavaScript prototypes are smelly...
gravatar Gert Wednesday, March 26, 2014
Wow, just yesterday I posted a very similar article, only to find this in my inbox today (in the ng-newsletter). I have a slightly different approach you might like: https://medium.com/opinionated-angularjs/2e6a067c73bc
gravatar Yngve B. Nilsen Wednesday, March 26, 2014
Great little trick. I stole this code snippet, and slightly modified it in order to keep everything wrapped in the angular-scope by using IIFE in the .value itself: angular.value('Movie', (function() { function Movie() { ... } return Movie }())); I'd love to see a followup post with regards to your comment: "I'm keeping them separate and hoping to use a decorator at some point". Haven't gotten around to decorators yet.
gravatar Paul Sweeney Wednesday, March 26, 2014
Angular definitely wants you to bring your own model layer, and seems to be pretty mute on what that should look like. The point that you haven't mentioned, but has chewed more cycles of my time than I'd like to admit is `ng-model` pointing to primitive values. My own similar approach was to rebuild Backbone's models and collections whole-hog as Angular factories: https://gist.github.com/8bitDesigner/015b407102ff1b780287
gravatar Adrian Hara Friday, April 4, 2014
We actually use .factory for our models (in fact we use view models and models both). The controller then takes a dependency on the view model which is subsequently injected and the controller news it up. The advantage is the model/viewmodel can have any dependencies injected into its factory and can use them afterwards, like so: .factory('name', [[dependencies declaration]], function([[dependencies params]]) { return function([[view model params from controller]]) { // this is the model/view model, it can use both injected params into the factory as well as dynamic params passed to it from the controller } }); .controller('name', ['Someviewmodel', function(Someviewmodel) { new Someviewmodel([[whatever dynamic params]]); })]);
gravatar Alan Plum Friday, April 4, 2014
I'm using a document database and wanted to use the same models on the client and server and be able to serialize and deserialize them at any point to be able to send them across the wire, so I guess I had a similar problem to what you're trying to solve. But using regular prototype-based constructors isn't necessarily the best approach. The transformations are extremely model specific so it felt wrong to have them separate from the model -- you're creating an implicit dependency between every FooModel and FooModelTransformer. If you try to generalize the ModelTransformer service you end up having to bake in all kinds of assumptions and may have to invent your own data format on top of JSON to handle things like nested objects. Because I used a document database rather than a relational one, I don't have many references between the different objects and instead nest objects that don't have a reason to exist on their own (e.g. a user might have multiple shipping addresses or a blog article may have multiple resources, but it may not make sense to have these exist on their own from the business domain POV). In cases where I do want a reference to a distinct object, I often prefer having a stub copy in addition to the object's ID so I don't have to look the entire object up every time I want to show the reference -- these references might get stale, but in many cases that is a feature rather than a bug (e.g. if you store an invoice, you might want to know the exact address that was used rather than what the address object looks like now). Long story short, I found revalidator (https://github.com/flatiron/revalidator) was a great solution for defining the schema of my models. Plus, it gave me validation for free so I could make sure a model was well-formed at any point in the process. I extended the functionality a bit and wrote revalidator-model (https://github.com/pluma/revalidator-model) which lets you define a proto object which will be used as the model's prototype and provide defaults as well as hydration (i.e. deserialization) and dehydration (i.e. serialization) methods that can take care of building nested object hierarchies out of the flat JSON you're sending back and forth. It's not at all exclusive to Angular, but it is easy to use in your project if you already use browserify (and if you don't, you should). I just make sure all API calls use the hydrate and dehydrate methods every model has and add any functions I want to use to modify the object or to get info about it to the model's proto. I eventually noticed I often want different "views" of the model (e.g. a more limited version of the model, like the subset of user data a user can update in their profile) and I was able to avoid a lot of duplication by just creating separate models for these and adding conversion methods to the canonical model's proto.
gravatar LaBrina Loving Wednesday, April 9, 2014
Hi thanks for sharing this. As a ASP.Net MVC developer, trying to live in the new client-side world, I love Angular. But I have been struggling to figure out where my model should go. Thanks for sharing this.
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!