AngularJS Data Model


As we already know, AngularJS doesn’t come with an out of the box solution for data modeling. In the most abstract way, AngularJS lets us use JSON data as a model in the controller. As the time passed and my application grew, I realized that this modeling method isn’t powerful enough to fit my application’s needs. In this article I will present the way I dealt with data modeling in my AngularJS application.

Defining a model for controller

Let’s start with a simple example. I would like to display a book view. This is the controller:

BookController
app.controller('BookController', ['$scope', function($scope) {  
    $scope.book = {
        id: 1,
        name: 'Harry Potter',
        author: 'J. K. Rowling',
        stores: [
            { id: 1, name: 'Barnes & Noble', quantity: 3},
            { id: 2, name: 'Waterstones', quantity: 2},
            { id: 3, name: 'Book Depository', quantity: 5}
        ]
    };
}]);

This controller creates a model of book which can be later used in our template:

A template for displaying a book
<div ng-controller="BookController">  
    Id: <span ng-bind="book.id"></span>
    <br/>
    Name:<input type="text" ng-model="book.name" />
    <br/>
    Author: <input type="text" ng-model="book.author" />
</div>

In case we would like to get the book data from a backend api, we can also use $http:

BookController with $http
app.controller('BookController', ['$scope', '$http', function($scope, $http) {  
    var bookId = 1;

    $http.get('ourserver/books/' + bookId).success(function(bookData) {
        $scope.book = bookData;
    });
}]);

Notice that bookData is still a JSON object.
Later on we would like to do something with this data. For example, update the book, delete it or even do other operations that are not dealing with the backend, like generate a book image url according to requested size or determining whether the book is available. Those methods can be declared on our controller:

BookController with several book actions
app.controller('BookController', ['$scope', '$http', function($scope, $http) {  
    var bookId = 1;

    $http.get('ourserver/books/' + bookId).success(function(bookData) {
        $scope.book = bookData;
    });

    $scope.deleteBook = function() {
        $http.delete('ourserver/books/' + bookId);
    };

    $scope.updateBook = function() {
        $http.put('ourserver/books/' + bookId, $scope.book);
    };

    $scope.getBookImageUrl = function(width, height) {
        return 'our/image/service/' + bookId + '/width/height';
    };

    $scope.isAvailable = function() {
        if (!$scope.book.stores || $scope.book.stores.length === 0) {
            return false;
        }
        return $scope.book.stores.some(function(store) {
            return store.quantity > 0;
        });
    };
}]);

And later in our template:

A template for displaying a complete book
<div ng-controller="BookController">  
    <div ng-style="{ backgroundImage: 'url(' + getBookImageUrl(100, 100) + ')' }"></div>
    Id: <span ng-bind="book.id"></span>
    <br/>
    Name:<input type="text" ng-model="book.name" />
    <br/>
    Author: <input type="text" ng-model="book.author" />
    <br/>
    Is Available: <span ng-bind="isAvailable() ? 'Yes' : 'No' "></span>
    <br/>
    <button ng-click="deleteBook()">Delete</button>
    <br/>
    <button ng-click="updateBook()">Update</button>
</div>

Sharing a model between controllers

As long as the book’s structure and methods are relevant only to one controller, all is fine and our work here is done. But as the application grows, there might be other controllers that will deal with books. Those controllers will sometimes need to fetch a book, update it, delete it or get it’s image url or availability. Therefore we have to share the behaviors of a book between controllers. In order to do this we will use a factory that returns the book’s behavior. Before writing this factory, I would like to mention here that we could make the factory return an object that contains helper methods for book (i.e. functions that get a book JSON and do what asked), but I prefer to use prototype for constructing a Book class, which I believe is the right choice:

Book model service
app.factory('Book', ['$http', function($http) {  
    function Book(bookData) {
        if (bookData) {
            this.setData(bookData):
        }
        // Some other initializations related to book
    };
    Book.prototype = {
        setData: function(bookData) {
            angular.extend(this, bookData);
        },
        load: function(id) {
            var scope = this;
            $http.get('ourserver/books/' + bookId).success(function(bookData) {
                scope.setData(bookData);
            });
        },
        delete: function() {
            $http.delete('ourserver/books/' + bookId);
        },
        update: function() {
            $http.put('ourserver/books/' + bookId, this);
        },
        getImageUrl: function(width, height) {
            return 'our/image/service/' + this.book.id + '/' + width + '/' + height;
        },
        isAvailable: function() {
            if (!this.book.stores || this.book.stores.length === 0) {
                return false;
            }
            return this.book.stores.some(function(store) {
                return store.quantity > 0;
            });
        }
    };
    return Book;
}]);

This way all book’s behavior is encapsulated in Book service. Now, let’s use our shiny Book service in our BookController:

BookController that uses Book model
app.controller('BookController', ['$scope', 'Book', function($scope, Book) {  
    $scope.book = new Book();
    $scope.book.load(1);
}]);

As you can see, the controller became very thin. It now creates a Book instance, assigns it to the scope and loads it from the backend. When the book will be loaded, it’s properties will be changed and so the template. Keep in mind that other controllers that interact with a book, simply inject the Book service. We have to change the template to use book’s methods as well:

A template that uses book instance
<div ng-controller="BookController">  
    <div ng-style="{ backgroundImage: 'url(' + book.getImageUrl(100, 100) + ')' }"></div>
    Id: <span ng-bind="book.id"></span>
    <br/>
    Name:<input type="text" ng-model="book.name" />
    <br/>
    Author: <input type="text" ng-model="book.author" />
    <br/>
    Is Available: <span ng-bind="book.isAvailable() ? 'Yes' : 'No' "></span>
    <br/>
    <button ng-click="book.delete()">Delete</button>
    <br/>
    <button ng-click="book.update()">Update</button>
</div>

Up to here we saw how to model a data, encapsulate all its methods in one class and share this class between controllers without code duplication.

Model of the same book in several controllers

So we have a book model definition and several controllers that work with books. After using this modeling architecture you will notice that there is a big problem.
Up to now we supported several controllers that do operations with books. But what will happen if two controllers will deal with the same book?
Assume that we have a section with a list of names of all our books and another section with an editable view of a book. We have two controllers, one for each section. The first controller loads the books list and the second controller loads a single book. Our user sees the second section, edit the name of the book and then presses on the “update” button. The update process will succeed and the book name will be changed. But in the books list section the user still sees the old name! What happened actually is that there were two different instances of the same book – one for the books list and one for the editable view. When the user edited the book’s name, he actually changed the name property of the book instance that was binded to the editable view. Whereas the book instance that was binded to the books list view didn’t changed.
The solution for this problem is to share the same books instances with any controller that needs them. This way both the books list controller and the editable view controller will hold the same book instance and whenever this instance is changed, the changes will be reflected in all the views. Translating words to actions, we have to create a booksManager service (the letter b is not capital because it is an object and not a Class) that will manage books instances pool and will be responsible for returning instances of books. If the required instance doesn’t exist in the pool, the service will create it. If the required instance already exists in the pool, the service will only return it. Keep in mind that all the functions that load instances of books will be defined eventually only in our booksManager service since it has to be the only component that provide books instances.

booksManager service
app.factory('booksManager', ['$http', '$q', 'Book', function($http, $q, Book) {  
    var booksManager = {
        _pool: {},
        _retrieveInstance: function(bookId, bookData) {
            var instance = this._pool[bookId];

            if (instance) {
                instance.setData(bookData);
            } else {
                instance = new Book(bookData);
                this._pool[bookId] = instance;
            }

            return instance;
        },
        _search: function(bookId) {
            return this._pool[bookId];
        },
        _load: function(bookId, deferred) {
            var scope = this;

            $http.get('ourserver/books/' + bookId)
                .success(function(bookData) {
                    var book = scope._retrieveInstance(bookData.id, bookData);
                    deferred.resolve(book);
                })
                .error(function() {
                    deferred.reject();
                });
        },
        /* Public Methods */
        /* Use this function in order to get a book instance by it's id */
        getBook: function(bookId) {
            var deferred = $q.defer();
            var book = this._search(bookId);
            if (book) {
                deferred.resolve(book);
            } else {
                this._load(bookId, deferred);
            }
            return deferred.promise;
        },
        /* Use this function in order to get instances of all the books */
        loadAllBooks: function() {
            var deferred = $q.defer();
            var scope = this;
            $http.get('ourserver/books)
                .success(function(booksArray) {
                    var books = [];
                    booksArray.forEach(function(bookData) {
                        var book = scope._retrieveInstance(bookData.id, bookData);
                        books.push(book);
                    });

                    deferred.resolve(books);
                })
                .error(function() {
                    deferred.reject();
                });
            return deferred.promise;
        },
        /*  This function is useful when we got somehow the book data and we wish to store it or update the pool and get a book instance in return */
        setBook: function(bookData) {
            var scope = this;
            var book = this._search(bookData.id);
            if (book) {
                book.setData(bookData);
            } else {
                book = scope._retrieveInstance(bookData);
            }
            return book;
        },

    };
    return booksManager;
}]);

Our Book service is now without the load method:

Book model without the load method
app.factory('Book', ['$http', function($http) {  
    function Book(bookData) {
        if (bookData) {
            this.setData(bookData):
        }
        // Some other initializations related to book
    };
    Book.prototype = {
        setData: function(bookData) {
            angular.extend(this, bookData);
        },
        delete: function() {
            $http.delete('ourserver/books/' + bookId);
        },
        update: function() {
            $http.put('ourserver/books/' + bookId, this);
        },
        getImageUrl: function(width, height) {
            return 'our/image/service/' + this.book.id + '/width/height';
        },
        isAvailable: function() {
            if (!this.book.stores || this.book.stores.length === 0) {
                return false;
            }
            return this.book.stores.some(function(store) {
                return store.quantity > 0;
            });
        }
    };
    return Book;
}]);

Our EditableBookController and BooksListController controllers looks like:

EditableBookController and BooksListController that uses booksManager
app  
    .controller('EditableBookController', ['$scope', 'booksManager', function($scope, booksManager) {
        booksManager.getBook(1).then(function(book) {
            $scope.book = book
        });
    }])
    .controller('BooksListController', ['$scope', 'booksManager', function($scope, booksManager) {
        booksManager.loadAllBooks().then(function(books) {
            $scope.books = books
        });
    }]);

Notice that the templates remain the same as they still use instances. Now the application will hold only one book instance with id equals to 1 and any change on it will be reflected on all views that use it.

Summary

On this article I suggested an architecture for modeling data in AngularJS. First, I presented the default model binding of AngularJS, then I showed how to encapsulate the model’s methods and operations so we can share it between different controllers, and finally I explained how to manage our models instances so all the changes will be reflected on all the application views.

I hope this article gave you ideas how to implement your data models. If you have any question, don’t hesitate to ask!

NaorYe

  • David Stanley

    Great article. I’ve used it as a reference for a while now, thanks! One question, though. I have several different entities (think Books, Magazines, Journals, etc) that should be handled with this method. How can I abstract this into something re-usable by those different entities without simply doing a copy/paste? Any suggestions about where to start?

    • Hi David,
      In my project (Spot.IM) I am also using this method for several entities. Each entity has different id property. Moreover, some of them has several primary ids. I can create a base manager class and inherit it for creating other managers, but for simplicity I’ve created several entity managers. If you are going to create an abstraction of this – let me know.
      Good luck!
      NaorYe

      • David Stanley

        I went ahead and created a gist to show you what I’m doing. The entityManager.js file is the base object that very closely follows your example. Many of the entities in my app will use this and they don’t need anything else. Some, however, need a few more methods. The nodeEntityManager.js file shows how I’ve extended the initial object so I can add some methods. The last file, listingProvider.js, is how I’m using these items, now extended and ready to go. I’d love to hear your thoughts.

        DISCLAIMER: I don’t claim to know what I’m doing with Angular or JS, in general (I come from a PHP background). Javascript inheritance has been a challenge for me, but I’m slowly getting a stronger understanding of it. I’m sure there are much better ways of doing this, but this is what I have for now.

        Thanks for your time!

        https://gist.github.com/davidstanley01/6368e17482037222f46a

  • The source code is in the article.

  • angular.injector() creates an injector object. Many calls will create many injectors an each injector will load booksManager. There is no way to avoid it, just don’t call angular.injector().

  • You can put your delete method wherever you like, as long as you remember that only booksManager can create new instances of books. You can add removeBook() method to booksManager and call it right after the delete.

  • I preferred to use prototype because this let me tread a Book as a class where you can create many instances out of it. Using closures, for example, will not be intuitive.

  • booksManager.getBook(1) will give you a promise that will be resolved with the desired book:
    booksManager.getBook(1).then(function(book) { $scope.book = book; });

  • Your question is not silly. Services are singleton and I used them in order to create a Book class. The code that creates the Book class – runs once. Once I have Book class, I can use it to create many books instances (var book = new Book();).

    • Alex

      But you are using `factory`-s ? :confused:

  • Hi,
    1) I understand this might confuse others, but this is the terminology. Anyway, service() and factory() are both provider().
    2) There is no significance whether to use the Service method or the Factory method. They both can be shared by controllers. Use whatever easy for you.

    For both of your question, please read this: http://www.webdeveasy.com/service-providers-in-angularjs-and-logger-implementation/ . You may find it useful.

  • The following will return a book instance that was created using booksManager: booksManager.setBook({ // book data });

  • Restangular is a service that handles backend request. It has nothing with the method represented in this article. You can use Restangular and still manage your entities using entityManager.

  • Hi Siva,
    I don’t understand what is a view-model with required structure. Can you explain more?

    • Guest

      When we get the data from either from api or server whatever it is, we just bind the data directly to the $scope. For ex $scope.customer.name=name, $scope.customer.id = id etc. here name, id are from service as we know.
      What my query is i need to create the object customer with required properties and need to bind it directly from the server.
      like $scope.customer = custFromServer.
      Here the customer should be the object.
      Is this possible?

  • Ruwanka Madhushan

    You are using an object pool in client side. What if another user changed the back end object then you are having invalid object how do you handle such a situation? Also I am confused with how object properties validation is happening here. Can you explain further?

    • Nestor Magalhaes

      so you need to use socket.io to handler with it.

  • marcelkalveram

    Excellent article, exactly what I was looking for, as I had to deal with a similar situation where I was unsure about how to store model data the most elegant way.

  • Ivan Horn

    1 year into the future and this article is still one of the most helpful posts out there when it come to angularJS.
    I’ve used your guide and modified it to support localStorage (cordova) and an external database via SOAP.
    Works great, keep up the good work.

  • I must say after looking around for a long time this was one of the best examples i could find on this topic. Thanks for the post. A couple of comments and questions around some stuff i am trying to wrap my head around.
    Comment:
    1) there are a couple of cleanups in your code you should quickly spruce up like in this.setData(bookData): should be a ; not a : and a additional }, after your set book function.

    Questions:

    1) I think i am daft here but i would think that these singletons would be in memory of the app so that if were to go forward to another view that contained the edit of a book like books/book/{id} (book with {id} was loaded in books so it exists) that when I passed in the booksManager into the controller it should already be hydrated with in booksManager and should not calling the service again and my thinking here is that _pools[] remains in memory and would contain the book. When i run your code with a sample book service this is not the case the view calls the book service every time, am i missing something?
    2) I really like where you were going on the gist would it be possible to put up a end to end example somewhere with stubbed json data as the service return?
    thanks for the post again.
    G

  • Tally

    If you implment the create on the book manager, I assume it will do there the push into the books array as well as the post. Will this be reflected on the list or do you need to refresh $scope.books? ($scope.books = load all books)?

  • Oblio Leitch

    I’m uncomfortable putting so much data access logic into the object itself. I would rather put the crud actions into the bookManager service, and allow the Book to keep its property editing functions. How would you handle nested objects? Let’s say that Books had Pages. How would you handle dependency injection?

    • I think that there isn’t one way to manage your data and I only presented an idea (or better say a start of an idea). Do whatever feels you right and suits your project. Regarding model dependencies, one possible solution can be using an events that are thrown by models managers whenever data arrives, or a parent manager that responsible for injecting data to the managers (I messed up with the terminology 🙂 ). Anyway, I suggest you to look on Flux architecture by facebook, you might find something that will help you continue your research.

      Hope this helps,
      NaorYe

  • Thanks for writing this out, it was really helpful to think of models as objects and separating out the methods for objects and the methods for a collection of objects.

    One question, when I call delete on a book instance, how does the bookManager know to update the pool so that the removed instance isn’t in the _pool object anymore.

    Thanks
    Vanessa

    • Hi Venessa,
      There isn’t only one solution 🙂 One possible solution can be as following:
      The same as creating a book, where only manager can create, only manager will be able to delete a book. This way you have to call `bookManager.delete(bookId)` in order to delete the book. You can add a `delete` method on a book that calls `bookManager.delete(this.bookId)` and made some cleanup that related to that book instance (remove events, remove dependencies, inform other components about the removal and so on).

      Hope this helps,
      NaorYe

      • Matvey

        Without this this explanation your main post do not make scense. thanks

  • l0co

    Very nice article and solution for sharing the same object instances across multiple controllers, that I’ve been looking for some time ago. This is the real problem in applications designed in angular way, especially if they are asynchronous.

    However there’s still a problem with this solution I faced as well, when you have not flat objects tree coming from the server side, like for example books with collections of authors. This is the point where these “entity classes” are becoming more and more complex, especially when you need to start to consider that you may have the same books in different views on your UI (for example lists don’t use authors collection, but the book details view does).

    In current project I started with similar concept, but then we decided to migrate to more general solution that struggles with instances share problem in a single place (you can find it here https://github.com/l0co/angular-reftracker, small concept module written in one weekend from which we started to build newer architecture). Anyway it’s nice to see that someone is struggling with the same things, I consider this is a really common problem but there’s a little of good solutions on the net (at least I couldn’t find anything with really good idea).

  • G van Ginkel

    This is a really nice post! Helped me a lot to get my Angular Model layer right. Thanks!

  • Saeed Habibi

    When i’m using prototype method to retrieve data, http request are handled but data can not manipulate for using. Angullar error is: “TypeError: a is not a function”. I’m using angular v1.3.15 .

    Example:

    clientManager.getItem(id).then(
    function(res) {
    console.log(res.getUserData());
    }
    );

    Above error is for res.getUserData() . getUserData method is a method ike getImageUrl() on this Book factory.

    • Ishay Green

      +1

  • Sreekumar

    Great architecture!!.

    Need one help, what if I want to insert a new Book to database using a API call (a POST call). ?

  • João Martins

    I used this article so much on my daily work. Just had pass here again after all this time to thank you for this great article.

  • sumatheendra v r

    Good Article! and I tried implementing this. I’ve a model by name ‘Product’ instead of ‘Book’. I’m able to retrieve the json response from server and saving it as instance = new Product(productData), but when I’m retrieving it using getProduct(productid), returning me an Object instead of the Product model.

    Am I missing something here? Could you please guide me.

  • amit

    Exact what I required at this moment! Thanks Naorye 🙂

  • abc

    1. How can the model be cached in localStorage & retrieved when app is offline?
    2. Can you advice on a way to implement queue for unsynced models to be send to the server when get online again?

    **Pease advice..

  • Ashish Chopra

    Hi Naorye,

    First of all, thank you so much for explaining this concept so easily. I read your other articles too, they are as cool as this one. I was actually intuitively doing the similar thing in one of my project. I have a dashboardController controller and a WidgetManager service. THe dashboardController keeps the list of widgets returned by WidgetManager in a reference var widgetCollection bound to this. Also the WidgetManager maintains an object pool of the widgets. So when someone clicks the remove button on the screen, it triggers the WidgetManager.removeWidget(widgetId) method via controller. And i have found that the widget is removed from the pool inside the widgetManager but the widgetCollection model still holds a reference to the object. And i have to manually remove it from widgetCollection model holding the list which is displayed on the UI. Can you tell me the best way so that i do not need to repeat the deletion at multiple places, because i expected that if i delete the object from widgetmanager, it will delete from everywhere. Please correct my facts.

  • Khanh

    Please show the idea to insert new book into database by api. Should I have an insert function in Book service. How to call the function and handle the callback.

  • Excellent! Helped me a lot!

  • Joe

    I’m new to Angular.js but am trying to learn it. This is a great article, but for a novice like me is a bit hard to follow. Is there a good book or set of books that you recommend that teaches in detail the basics involved in what you have covered in this article? Clearly there are many books out there, so I’m interested in your expert opinion on a reference library.

    Thanks!