SoFunction
Updated on 2025-04-04

Analysis of how to process data modeling in AngularJS framework

We know that AngularJS does not have its own data modeling solutions that can be used. Instead, in a rather abstract way, let's use JSON data as a model in the controller. But over time and the project grew, I realized that this modeling method could no longer meet the needs of our project. In this article I will explain how data modeling is handled in my AngularJS application.

Define the model for the Controller

Let's start with a simple example. I want to display a page of a book. Here is the controller:

BookController

('BookController', ['$scope', function($scope) {
  $ = {
    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 book model that we can use in the templates behind.

template for displaying a book

<div ng-controller="BookController">
  Id: <span ng-bind=""></span>
   
  Name:<input type="text" ng-model="" />
   
  Author: <input type="text" ng-model="" />
</div>

If we need to get the data of the book from the background API, we need to use $http:
BookController with $http

('BookController', ['$scope', '$http', function($scope, $http) {
  var bookId = 1;
 
  $('ourserver/books/' + bookId).success(function(bookData) {
    $ = bookData;
  });
}]);

Note that bookData here is still a JSON object. Next we want to do something with this data. For example, update book information, delete books, and even some other operations that do not involve background operations, such as generating a url of a book picture based on the requested image size, or determining whether the book is valid. These methods can all be defined in the controller.

BookController with several book actions

('BookController', ['$scope', '$http', function($scope, $http) {
  var bookId = 1;
 
  $('ourserver/books/' + bookId).success(function(bookData) {
    $ = bookData;
  });
 
  $ = function() {
    $('ourserver/books/' + bookId);
  };
 
  $ = function() {
    $('ourserver/books/' + bookId, $);
  };
 
  $ = function(width, height) {
    return 'our/image/service/' + bookId + '/width/height';
  };
 
  $ = function() {
    if (!$ || $ === 0) {
      return false;
    }
    return $(function(store) {
      return  > 0;
    });
  };
}]);

Then in our template:

template for displaying a complete book

<div ng-controller="BookController">
  <div ng-style="{ backgroundImage: 'url(' + getBookImageUrl(100, 100) + ')' }"></div>
  Id: <span ng-bind=""></span>
   
  Name:<input type="text" ng-model="" />
   
  Author: <input type="text" ng-model="" />
   
  Is Available: <span ng-bind="isAvailable() ? 'Yes' : 'No' "></span>
   
  <button ng-click="deleteBook()">Delete</button>
   
  <button ng-click="updateBook()">Update</button>
</div>

Sharing Models between controllers
If the structure and method of the book are only related to one controller, then our current work can be handled. But as the application grows, other controllers will also need to deal with books. Those controllers often need to get the book, update it, delete it, or get its picture url and see if it works. Therefore, we need to share the behavior of these books between controllers. We need to use a factory that returns the book behavior to achieve this. Before writing a factory, I would like to mention here first, we create a factory to return objects with these book helper methods, but I prefer to use prototype to construct a Book class, which I think is a more correct choice:

Book model service

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

In this way, all behaviors related to books are encapsulated within Book services. Now, we are using this eye-catching Book service in BookController.

BookController that uses Book model

('BookController', ['$scope', 'Book', function($scope, Book) {
  $ = new Book();
  $(1);
}]);

As you can see, the controller becomes very simple. It creates a Book instance, assigns it to scope, and loads it from the background. When the book is loaded successfully, its properties will be changed and the template will be updated as well. Remember that other controllers want to use the book function, just simply inject the Book service. In addition, we need to change the method of using book in template.

template that uses book instance

<div ng-controller="BookController">
  <div ng-style="{ backgroundImage: 'url(' + (100, 100) + ')' }"></div>
  Id: <span ng-bind=""></span>
   
  Name:<input type="text" ng-model="" />
   
  Author: <input type="text" ng-model="" />
   
  Is Available: <span ng-bind="() ? 'Yes' : 'No' "></span>
   
  <button ng-click="()">Delete</button>
   
  <button ng-click="()">Update</button>
</div>

At this point, we know how to model a data, encapsulate its method into a class, and share it in multiple controllers without writing duplicate code.
Using the same book model in multiple controllers

We defined a book model and used it in multiple controllers. After using this modeling architecture, you will notice a serious problem. So far, we've assumed multiple controllers operate on books, but what would happen if two controllers were working on the same book at the same time?

Suppose one area of ​​our page has the names of all our books, and another area can update a certain book. Corresponding to these two areas, we have two different controllers. The first one loads the book list and the second one loads the specific book. Our users have modified the book's name in the second area and clicked the "Update" button. After the update operation is successful, the name of the book will be changed. But in the book list, what this user always sees is modifying the previous name! The real situation is that we create two different examples of the same book – one used in the book list and the other used when modifying the book. When the user changes the book name, it actually only modifies the properties in the latter instance. However, the book examples in the book list have not been changed.

The solution to this problem is to use the same book instance in all controllers. In this way, the book list and the page and controller of the book modification hold the same book instance, and once this instance changes, it will be immediately reflected in all views. Then, in this way, we need to create a booksManager service (we don't have the b letter starting with capital because it's an object instead of a class) to manage all the book instance pools and return these book instances. If the requested book instance is not in the instance pool, this service creates it. If it is already in the pool, then return it directly. Keep in mind that all methods of loading books will eventually be defined in the booksManager service because it is the only component that provides book instances.

booksManager service

('booksManager', ['$http', '$q', 'Book', function($http, $q, Book) {
  var booksManager = {
    _pool: {},
    _retrieveInstance: function(bookId, bookData) {
      var instance = this._pool[bookId];
 
      if (instance) {
        (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;
 
      $('ourserver/books/' + bookId)
        .success(function(bookData) {
          var book = scope._retrieveInstance(, bookData);
          (book);
        })
        .error(function() {
          ();
        });
    },
    
/* Public Methods */
    
/* Use this function in order to get a book instance by it's id */
    getBook: function(bookId) {
      var deferred = $();
      var book = this._search(bookId);
      if (book) {
        (book);
      } else {
        this._load(bookId, deferred);
      }
      return ;
    },
    
/* Use this function in order to get instances of all the books */
    loadAllBooks: function() {
      var deferred = $();
      var scope = this;
      $('ourserver/books)
        .success(function(booksArray) {
          var books = [];
          (function(bookData) {
            var book = scope._retrieveInstance(, bookData);
            (book);
          });
 
          (books);
        })
        .error(function() {
          ();
        });
      return ;
    },
    
/* 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();
      if (book) {
        (bookData);
      } else {
        book = scope._retrieveInstance(bookData);
      }
      return book;
    },
 
  };
  return booksManager;
}]);

Here is the code for our EditableBookController and BooksListController:

EditableBookController and BooksListController that uses booksManager

('Book', ['$http', function($http) {
  function Book(bookData) {
    if (bookData) {
      (bookData):
    }
    
// Some other initializations related to book
  };
   = {
    setData: function(bookData) {
      (this, bookData);
    },
    delete: function() {
      $('ourserver/books/' + bookId);
    },
    update: function() {
      $('ourserver/books/' + bookId, this);
    },
    getImageUrl: function(width, height) {
      return 'our/image/service/' +  + '/width/height';
    },
    isAvailable: function() {
      if (! ||  === 0) {
        return false;
      }
      return (function(store) {
        return  > 0;
      });
    }
  };
  return Book;
}]);

It should be noted that the original way of using book instances is still maintained in the module (template). Now only one book instance with id 1 is held in the application, and all changes that occur will be reflected on the various pages that use it.

Some pitfalls in AngularJS
UI flashing

Angular's automatic data binding feature is the highlight, however, its other side is that before Angular is initialized, the page may present an expression without parsing to the user. When the DOM is ready, Angular calculates and replaces the corresponding value. This will lead to an ugly flickering effect.
The above situation is what renders the sample code in the Angular tutorial:

<body ng-controller="PhoneListCtrl">
 <ul>
  <li ng-repeat="phone in phones">
   {{  }}
   <p>{{  }}</p>
  </li>
 </ul>
</body>

If you are doing SPA (Single Page Application), this problem will only occur when the page is loaded for the first time. Fortunately, this can be easily prevented: Abandon the {{ }} expression and use the ng-bind directive instead

<body ng-controller="PhoneListCtrl">
 <ul>
  <li ng-repeat="phone in phones">
   <span ng-bind=""></span>
   <p ng-bind="">Optional: visually pleasing placeholder</p>
  </li>
 </ul>
</body>
 

You need a tag to include this directive, so I added a <span> to the phone name.

So what happens when initializing? The value in this tag will be displayed (but you can choose to set a null value). Then, when Angular initializes and replaces the internal value of the tag with the expression result, note that you do not need to add braces inside ng-bind. More concise! If you need to match expressions, use ng-bind-template.

If you use this directive, in order to distinguish string literals and expressions, you need to use braces

Another way is to hide the elements completely, and even hide the entire application until Angular is ready.

Angular also provides an ng-cloak directive for this, which works by injecting the css rule during the initialization stage, or you can include this css hidden rule to your own stylesheet. When Angular is ready, this cloak style will be removed and our application (or element) will be rendered immediately.

Angular does not rely on jQuery. In fact, the Angular source code contains an embedded lightweight jquery:jqLite. When Angular detects that jQuery appears on your page, it will use this jQuery instead of jqLite. The direct evidence is the element abstraction layer in Angular. For example, access the elements you want to apply to in directive.

('jqdependency', [])
 .directive('failswithoutjquery', function() {
  return {
   restrict : 'A',
   link : function(scope, element, attrs) {
        (4000)
       }
  }
});

But is this element jqLite or jQuery element? Depends on the manual:

All element references in Angular will be wrapped by jQuery or jqLite; they are never pure DOM references

Therefore, if Angular does not detect jQuery, it will use the jqLite element. The hide() method value can be used for the jQuery element, so this example code can only be used when jQuery is detected. If you (accidentally) modify the occurrence order of AngularJS and jQuery, this code will be invalid! Although it doesn't happen often when I move the script order, it does cause trouble for me when I start modular code. Especially when you start using module loaders (such as RequireJS), my solution is that the statement shown in the configuration Angular does depend on jQuery

Another way is that you don't call jQuery specific methods through the Angular element wrapper, but use $(element).hide(4000) to express your intention. This dependency will be fine even if the script loading order is modified.

compression

What needs to be paid special attention to is the compression problem of Angular application. Otherwise, error messages such as ‘Unknown provider:aProvider  <- a’ will make you confused. Like many other things, this error is not available in the official documentation. In short, Angular relies on parameter names for dependency injection. The compressor doesn't realize how different this is from the ordinary parameter names in Angular. It is their responsibility to shorten the script as much as possible. What to do? Use the "friendly compression method" to inject the method. Look here:

('myservice', function($http, $q) {
// This breaks when minified
});
to this:

('myservice', [ '$http', '$q', function($http, $q) {
// Using the array syntax to declare dependencies works with minification<b>!</b>
}]);

 

This array syntax solves this problem very well. My suggestion is to start writing this method from now on. If you decide to compress JavaScript, this method can help you avoid many detours. It seems to be an automatic rewriter mechanism, and I don't know much about how it works.

Final advice: If you want to rewrite your functions in array syntax, apply it in all Angular dependency injections. Including directives, and controllers in directives. Don't forget the comma (experience)

// the directive itself needs array injection syntax:
('directive-with-controller', ['myservice', function(myservice) {
  return {
   controller: ['$timeout', function($timeout) {
    
// but this controller needs array injection syntax, too! 
   }],
   link : function(scope, element, attrs, ctrl) {
 
   }
  }
}]);

Note: link function does not require array syntax because it does not have real injection. This is a function called directly by Angular. Directive-level dependency injection is also used in link functions.

 

Directive will never be ‘completed’

One thing that makes hair fall out in directive is that directive is ‘complete’ but you will never know. This notification is particularly important when integrating jQuery plug-in into directive. Suppose you want to use ng-repeat to display dynamic data in the form of jQuery datatable. When all the data is loaded in the page, you only need to call $('.mytable).dataTable(). But I can't do it!

Why? Angular's data binding is achieved through a continuous digest loop. Based on this, there is no time in the Angular framework that is ‘rest’ at all. A solution is to place the call of jQuery dataTable outside the current digest loop and use the timeout method to do it.

('table',[]).directive('mytable', ['$timeout', function($timeout) {
  return {
   restrict : 'E',
   template: '<table class="mytable">' +
          '<thead><tr><th>counting</th></tr></thead>' +
          '<tr ng-repeat="data in datas"><td></td></tr>' +
        '</table>',
   link : function(scope, element, attrs, ctrl) {
      = ["one", "two", "three"]
     
// Doesn't work, shows an empty table:
     
// $('.mytable', element).dataTable() 
     
// But this does:
     $timeout(function() {
      $('.mytable', element).dataTable();
     }, 0)
   }
  }
}]);