Parse the Data Modeling Method in AngularJS framework, angularjs framework
As we know, AngularJS does not come with other available data modeling solutions. In a rather abstract way, we can use JSON data as the model in the controller. However, as time passes and projects grow, I realize that this modeling method no longer meets the needs of our projects. In this article, I will introduce how to process data modeling in my AngularJS application.
Define model for Controller
Let's start with a simple example. I want to display a book page. The following 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 book model, which can be used in the template below (templage.
Template for displaying a book
<div ng-controller="BookController"> Id: <span ng-bind="book.id"></span> Name:<input type="text" ng-model="book.name" /> Author: <input type="text" ng-model="book.author" /></div>
If we need to obtain the book data from the background api, we need to 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; });}]);
Note that bookData is still a JSON object. Next we want to use the data to do something. For example, update the book information, delete the book, or even some other operations that do not involve background operations, such as generating a url of the book Image Based on the requested image size, or determining whether the book is valid. These methods can be defined in the 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; }); };}]);
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="book.id"></span> Name:<input type="text" ng-model="book.name" /> Author: <input type="text" ng-model="book.author" /> Is Available: <span ng-bind="isAvailable() ? 'Yes' : 'No' "></span> <button ng-click="deleteBook()">Delete</button> <button ng-click="updateBook()">Update</button></div>
Share Model between controllers
If the structure and method of a book are only related to a controller, our current work can be handled. However, as applications grow, other controllers also need to deal with books. Many controllers also need to obtain a book, update it, delete it, or obtain its image url and check whether it is valid. Therefore, we need to share the book behavior between controllers. We need to use a factory that returns the book behavior for this purpose. Before writing a factory, I would like to mention here that we will create a factory to return objects with these book helper methods, but I prefer to use prototype to construct a Book class, I think this is a more correct 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;}]);
In this way, all Book-related behaviors are encapsulated in the Book service. Now, we use this eye-catching Book Service in 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 becomes very simple. It creates a Book instance, assigns it to the scope, and loads it from the background. When a book is loaded successfully, its attributes are changed and the template is updated as well. Remember that if other controllers want to use the Book function, simply inject the Book Service. In addition, we need to change the template's method of using book.
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> Name:<input type="text" ng-model="book.name" /> Author: <input type="text" ng-model="book.author" /> Is Available: <span ng-bind="book.isAvailable() ? 'Yes' : 'No' "></span> <button ng-click="book.delete()">Delete</button> <button ng-click="book.update()">Update</button></div>
Here, we know how to model a data, encapsulate its method into a class, and share it among multiple controllers without writing repeated code.
Use 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 assume that multiple controllers operate on books, but what would happen if two controllers simultaneously process the same book?
Assume that the name of all books is in one area of our page, and the name of a book can be updated in another area. For the two regions, we have two different controllers. The first is to load the book list, and the second is to load a specific book. Our users modified the book name in the second area and click "Update. After the update operation is successful, the book name will be changed. However, in the book list, this user will always see the modified name! The real situation is that we have created two different book instances for the same book-one for use in the book list, and the other for use when modifying the book. When you modify the book name, it only modifies the attributes of the last instance. However, the book instances in the book list have not changed.
The solution to this problem is to use the same book instance in all controllers. In this way, the page and Controller of the book list and book modification both hold the same book instance. Once this instance changes, it will be immediately reflected in all views. In this way, we need to create a booksManager Service (we do not have a letter B starting with uppercase because it is an object rather than a class) to manage all the book instance pools, and rich people return to 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, return it directly. Remember that all the book loading methods will eventually be defined in the booksManager service, because it is the only component that provides book 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;}]);
The following is the code of our EditableBookController and BooksListController controllers:
EditableBookController and BooksListController that uses booksManager
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;}]);
It should be noted that the original usage of the book instance is still maintained in the module (template. Now, the application only holds one book instance with id 1, and all changes made to it are reflected on the pages on which it is used.
Some pitfalls in AngularJS
UI flash
Angular's automatic data binding function is a bright spot. However, the other side is that before Angular initialization, there may be unresolved expressions displayed on the page. When DOM is ready, Angular calculates and replaces the corresponding value. This will lead to an ugly flickering effect.
The above is how the sample code is rendered in the Angular Tutorial:
<body ng-controller="PhoneListCtrl"> <ul> <li ng-repeat="phone in phones"> {{ phone.name }} <p>{{ phone.snippet }}</p> </li> </ul></body>
If you are using SPA (Single Page Application), this problem only occurs when the Page is loaded for the first time. Fortunately, this situation can be easily prevented: discard the {} expression and use the ng-bind command instead.
<body ng-controller="PhoneListCtrl"> <ul> <li ng-repeat="phone in phones"> <span ng-bind="phone.name"></span> <p ng-bind="phone.snippet">Optional: visually pleasing placeholder</p> </li> </ul></body>
You need a tag to include this command, so I added a <span> to the phone name.
So what will happen during initialization? The value in this tag will be displayed (but you can choose to set a null value ). then, when Angular is initialized and the internal value of the tag is replaced with the expression result, note that you do not need to add braces in ng-bind. More concise! If you need to conform to the expression, use ng-bind-template,
If you use this command to distinguish between string literal and expression, you need to use braces
Another method is to completely hide the element, or even hide the entire application until Angular is ready.
Angular also provides the ng-cloak command. The working principle is that the inject css rule is implemented during the initialization phase, or you can include this css hiding rule to your own stylesheet. After Angular is ready, the cloak style will be removed and our application (or element) will be rendered immediately.
Angular does not depend on jQuery. In fact, Angular Source Code contains an embedded lightweight jquery: jqLite. when Angular detects jQuery on your page, it will use jQuery instead of jqLite. The direct evidence is the element abstraction layer in Angular. For example, access the elements you want to apply in directive.
angular.module('jqdependency', []) .directive('failswithoutjquery', function() { return { restrict : 'A', link : function(scope, element, attrs) { element.hide(4000) } }});
But is this jqLite or jQuery element? Depending on what is written in the manual:
All element references in Angular are encapsulated by jQuery or jqLite. They are never referenced by pure DOM.
Therefore, if Angular does not detect jQuery, The jqLite element will be used. The hide () method value can be used for jQuery elements. Therefore, this sample code can only be used when jQuery is detected. If you (accidentally) modify the order in which AngularJS and jQuery appear, this code will become invalid! Although the sequence of scripts does not happen frequently, I am troubled by the modular code. Especially when you start to use the module loader (such as RequireJS), my solution is to declare that Angular does depend on jQuery in the configuration.
Another method is to use $ (element). hide (4000) instead of calling jQuery-specific methods through Angular element packaging. This dependency makes it okay even if the script Loading Sequence is modified.
Compression
Note the Angular application compression problem. Otherwise, the error message, such as 'unknown provider: aProvider <-A', cannot be reached. Like many other things, this error cannot be found in official documents. In short, Angular relies on parameter names for dependency injection. The compressors do not realize that this is different from the common parameter names in Angular. It is their responsibility to shorten the script as much as possible. What should I do? Use the "friendly compression method" for method injection. Here:
module.service('myservice', function($http, $q) {// This breaks when minified});to this:module.service('myservice', [ '$http', '$q', function($http, $q) {// Using the array syntax to declare dependencies works with minification<b>!</b>}]);
This array syntax solves this problem. My suggestion is to write this method from now on. If you decide to compress JavaScript, this method will save you a lot of detours. It seems to be an automatic rewriter mechanism, and I don't know how it works.
One final suggestion: If you want to rewrite your functions using array syntax, apply them to all Angular dependency injection points. Including directives and controllers in direve ve. Don't forget the comma (experience)
// the directive itself needs array injection syntax:module.directive('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: The link function does not need array syntax because it does not actually inject data. This is a function directly called by Angular. Directive-level dependency injection is also used in link functions.
Directive will never 'complete'
In directive, one thing that gets rid of hair is that directive has been 'finished ', but you will never know. This notification is especially important when jQuery plug-ins are integrated into directive. Suppose you want to use ng-repeat to display dynamic data in the form of jQuery datatable. After loading all the data on the page, you only need to call $ ('. mytable). able. However, Chen cannot do it!
Why? Angular Data Binding is implemented through a continuous digest loop. Based on this, there is no time to rest in the Angular framework. One solution is to put the jQuery dataTable call out of the current digest loop and use the timeout method.
angular.module('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) { scope.datas = ["one", "two", "three"] // Doesn't work, shows an empty table: // $('.mytable', element).dataTable() // But this does: $timeout(function() { $('.mytable', element).dataTable(); }, 0) } }}]);
Articles you may be interested in:
- Instance analysis: bidirectional data binding in AngularJS framework
- Details about the scope and data binding in the JavaScript AngularJS framework
- In-depth study of the two-way Data Binding Mechanism in AngularJS
- Multiple methods for obtaining AngularJS data sources
- Three ways to obtain data sources in AngularJS
- In AngularJS, how does one use $ http to add, delete, modify, and query the data table of the Alibaba lab?
- Automatically load data by Page scrolling Based on AngularJS
- Angularjs learning notes-two-way Data Binding