Key Takeaways
- View models in AngularJS can have client-only state, such as ‘animation-started’ and ‘animation-ended’ or ‘dragged’ and ‘dropped’. This state can be managed when creating and saving view models using Angular’s $resource service.
- Encapsulating state change logic in one place, such as an injectable service, can simplify code and reduce errors, particularly for applications with multiple consumers.
- Techniques such as function wrapping can be used to do something different or additional before and after saving and retrieving data. This can enhance the functionality of the $resource service.
- Extracting view models into injectable services can help scale applications, particularly those with complex, real-time updates. Techniques such as watching and filtering can be used to manage state changes and improve the composability of an application’s API.
View models in JavaScript frameworks such as AngularJS can be different from domain models on the server – a view model doesn’t even have to exist on the server. It follows then that view models can have client only state, e.g. ‘animation-started’ and ‘animation-ended’ or ‘dragged’ and ‘dropped’. This post is going to concentrate on state changes when creating and saving view models using Angular’s $resource service.
It’s actually very easy for a $resource consumer, e.g. a controller, to set state, as shown below.
angular<span>.module('clientOnlyState.controllers') </span> <span>.controller('ArticleCtrl', function($scope, $resource, ArticleStates /* simple lookup */) { </span> <span>var Article = $resource('/article/:articleId', { articleId: '@id' }); </span> <span>var article = new Article({ id: 1, title: 'A title', author: 'M Godfrey' }); </span> article<span>.state = ArticleStates.NONE; // "NONE" </span> $scope<span>.article = article; </span> $scope<span>.save = function() { </span> article<span>.state = ArticleStates.SAVING; // "SAVING" </span> article<span>.$save(function success() { </span> article<span>.state = ArticleStates.SAVED; // "SAVED" </span> <span>}); </span> <span>}; </span> <span>});</span>
This approach is fine for applications containing single consumers. Imagine how boring and error prone replicating this code would be for multiple consumers! But, what if we could encapsulate the state change logic in one place?
$resource Services
Let’s start by pulling out our Article resource into an injectable service. Let’s also add the most trivial setting of state to NONE when an Article is first created.
angular<span>.module('clientOnlyState.services') </span> <span>.factory('Article', function($resource<span>, ArticleStates</span>) { </span> <span>var Article = $resource('/article/:articleId', { articleId: '@id' }); </span> <span>// Consumers will think they're getting an Article instance, and eventually they are... </span> <span>return function(data) { </span> <span>var article = new Article(data); </span> article<span>.state = ArticleStates.NONE; </span> <span>return article; </span> <span>} </span> <span>});</span>
What about retrieving and saving? We want Article to appear to consumers as a $resource service, so it must consistently work like one. A technique I learned in John Resig’s excellent book “Secrets of the JavaScript Ninja” is very useful here – function wrapping. Here is his implementation directly lifted into an injectable Angular service.
angular<span>.module('clientOnlyState.services') </span> <span>.factory('wrapMethod', function() { </span> <span>return function(object<span>, method, wrapper</span>) { </span> <span>var fn = object[method]; </span> <span>return object[method] = function() { </span> <span>return wrapper.apply(this, [fn.bind(this)].concat( </span> <span>Array.prototype.slice.call(arguments)) </span> <span>); </span> <span>}; </span> <span>} </span> <span>});</span>
This allows us to wrap the save and get methods of Article and do something different/additional before and after:
angular<span>.module('clientOnlyState.services') </span> <span>.factory('Article', function($resource<span>, ArticleStates, wrapMethod</span>) { </span> <span>var Article = $resource('/article/:articleId', { articleId: '@id' }); </span> <span>wrapMethod(Article, 'get', function(original<span>, params</span>) { </span> <span>var article = original(params); </span> article<span>.$promise.then(function(article) { </span> article<span>.state = ArticleStates.NONE; </span> <span>}); </span> <span>return article; </span> <span>}); </span> <span>// Consumers will actually call $save with optional params, success and error arguments </span> <span>// $save consolidates arguments and then calls our wrapper, additionally passing the Resource instance </span> <span>wrapMethod(Article, 'save', function(original<span>, params, article, success, error</span>) { </span> article<span>.state = ArticleStates.SAVING; </span> <span>return original.call(this, params, article, function (article) { </span> article<span>.state = ArticleStates.SAVED; </span> success <span>&& success(article); </span> <span>}, function(article) { </span> article<span>.state = ArticleStates.ERROR; </span> error <span>&& error(article); </span> <span>}); </span> <span>}); </span> <span>// $resource(...) returns a function that also has methods </span> <span>// As such we reference Article's own properties via extend </span> <span>// Which in the case of get and save are already wrapped functions </span> <span>return angular.extend(function(data) { </span> <span>var article = new Article(data); </span> article<span>.state = ArticleStates.NONE; </span> <span>return article; </span> <span>}, Article); </span> <span>});</span>
Our controller starts to get leaner because of this and is completely unaware of how the state is being set. This is good, because the controller shouldn’t care either.
angular<span>.module('clientOnlyState.controllers') </span> <span>.controller('ArticleCtrl', function($scope, $resource, ArticleStates /* simple lookup */) { </span> <span>var Article = $resource('/article/:articleId', { articleId: '@id' }); </span> <span>var article = new Article({ id: 1, title: 'A title', author: 'M Godfrey' }); </span> article<span>.state = ArticleStates.NONE; // "NONE" </span> $scope<span>.article = article; </span> $scope<span>.save = function() { </span> article<span>.state = ArticleStates.SAVING; // "SAVING" </span> article<span>.$save(function success() { </span> article<span>.state = ArticleStates.SAVED; // "SAVED" </span> <span>}); </span> <span>}; </span> <span>});</span>
Encapsulation Benefits
We’ve gone to reasonable lengths to encapsulate state changes outside our controllers, but what benefits have we gained?
Our controller can now make use of watch listeners being passed the old and new state to set a message. It could also perform a local translation, as shown below.
angular<span>.module('clientOnlyState.services') </span> <span>.factory('Article', function($resource<span>, ArticleStates</span>) { </span> <span>var Article = $resource('/article/:articleId', { articleId: '@id' }); </span> <span>// Consumers will think they're getting an Article instance, and eventually they are... </span> <span>return function(data) { </span> <span>var article = new Article(data); </span> article<span>.state = ArticleStates.NONE; </span> <span>return article; </span> <span>} </span> <span>});</span>
Consider for a moment that $scopes, directives and filters form the API of an application. HTML views consume this API. The greater the composability of an API the greater it’s potential for reuse. Can filters improve composability over new versus old watching?
Composing via Filters, a Panacea?
Something like the following is what I have in mind. Each part of the expression becomes reusable.
angular<span>.module('clientOnlyState.services') </span> <span>.factory('wrapMethod', function() { </span> <span>return function(object<span>, method, wrapper</span>) { </span> <span>var fn = object[method]; </span> <span>return object[method] = function() { </span> <span>return wrapper.apply(this, [fn.bind(this)].concat( </span> <span>Array.prototype.slice.call(arguments)) </span> <span>); </span> <span>}; </span> <span>} </span> <span>});</span>
As of Angular 1.3, filters can make use of the $stateful property, but its use is strongly discouraged as Angular cannot cache the result of calling the filter based on the value of the input parameters. As such we shall pass in stateful parameters to limitToTransition (previous state) and translate (available translations).
angular<span>.module('clientOnlyState.services') </span> <span>.factory('Article', function($resource<span>, ArticleStates, wrapMethod</span>) { </span> <span>var Article = $resource('/article/:articleId', { articleId: '@id' }); </span> <span>wrapMethod(Article, 'get', function(original<span>, params</span>) { </span> <span>var article = original(params); </span> article<span>.$promise.then(function(article) { </span> article<span>.state = ArticleStates.NONE; </span> <span>}); </span> <span>return article; </span> <span>}); </span> <span>// Consumers will actually call $save with optional params, success and error arguments </span> <span>// $save consolidates arguments and then calls our wrapper, additionally passing the Resource instance </span> <span>wrapMethod(Article, 'save', function(original<span>, params, article, success, error</span>) { </span> article<span>.state = ArticleStates.SAVING; </span> <span>return original.call(this, params, article, function (article) { </span> article<span>.state = ArticleStates.SAVED; </span> success <span>&& success(article); </span> <span>}, function(article) { </span> article<span>.state = ArticleStates.ERROR; </span> error <span>&& error(article); </span> <span>}); </span> <span>}); </span> <span>// $resource(...) returns a function that also has methods </span> <span>// As such we reference Article's own properties via extend </span> <span>// Which in the case of get and save are already wrapped functions </span> <span>return angular.extend(function(data) { </span> <span>var article = new Article(data); </span> article<span>.state = ArticleStates.NONE; </span> <span>return article; </span> <span>}, Article); </span> <span>});</span>
Because of this we need a slight amendment to Article:
angular<span>.module('clientOnlyState.controllers') </span> <span>.controller('ArticleCtrl', function($scope<span>, Article</span>) { </span> <span>var article = new Article({ id: 1, title: 'A title', author: 'M Godfrey' }); </span> <span>console.log(article.state); // "NONE" </span> $scope<span>.article = article; </span> $scope<span>.save = function() { </span> article<span>.$save({}, function success() { </span> <span>console.log(article.state); // "SAVED" </span> <span>}, function error() { </span> <span>console.log(article.state); // "ERROR" </span> <span>}); </span> <span>}; </span> <span>});</span>
The end result is not quite as pretty but is still very powerful:
angular<span>.module('clientOnlyState.controllers') </span> <span>.controller('ArticleCtrl', function($scope<span>, Article, ArticleStates</span>) { </span> <span>var article = new Article({ id: 1, title: 'A title', author: 'M Godfrey' }); </span> <span>var translations = {}; </span> translations<span>[ArticleStates.SAVED] = 'Saved, oh yeah!'; </span> translations<span>['default'] = ''; </span> $scope<span>.article = article; </span> $scope<span>.save = function() { </span> article<span>.$save({}); </span> <span>}; </span> $scope<span>.$watch('article.state', function(newState<span>, oldState</span>) { </span> <span>if (newState == ArticleStates.SAVED && oldState == ArticleStates.SAVING) { </span> $scope<span>.message = translations[newState]; </span> <span>} else { </span> $scope<span>.message = translations['default']; </span> <span>} </span> <span>}); </span> <span>});</span>
Our controller gets leaner again, especially if you consider the translations could be pulled out into an injectable service:
<span><span><span><p>></p></span>{{article.state | limitToTransition:"SAVING":"SAVED" | translate}}<span><span></span>></span></span></span>
Conclusion
Extracting view models into injectable services helps us scale applications. The example given in this post is intentionally simple. Consider an application that allows the trading of currency pairs (e.g. GBP to USD, EUR to GBP etc.). Each currency pair represents a product. In such an application there could be hundreds of products, with each receiving real-time price updates. A price update could be higher or lower than the current price. One part of the application may care about prices that have gone higher twice in a row, whilst another part may care about prices that have just gone lower. Being able to watch for these price change states greatly simplifies various consuming parts of the application.
I presented an alternative method to watching based on old and new values, filtering. Both are entirely acceptable techniques – in fact watching is what I had in mind when I began researching this post. Filtering was a potential improvement identified near post completion.
I would love to see if the techniques I’ve presented help you scale Angular apps. Any and all feedback will be greatly recieved in the comments!
The code samples created while researching this post are also available on GitHub.
Frequently Asked Questions on Managing Client State in AngularJS
What is the role of $stateProvider in managing client state in AngularJS?
The $stateProvider plays a crucial role in managing client state in AngularJS. It is a service that allows you to define states for your application. Each state corresponds to a “place” in the application in terms of the overall UI and navigation. $stateProvider provides APIs to route different views. When a state is activated, it can resolve a set of data via the resolve property. This data is then injected into the controller.
What is the best way to manage state in AngularJS?
The best way to manage state in AngularJS depends on the specific needs of your application. However, using UI-Router is a popular choice among developers. UI-Router is a third-party module that provides a flexible and robust solution for managing state. It allows for nested views and multiple named views, which can be very useful in larger applications.
How does UI-Router differ from the default routing system in AngularJS?
UI-Router is a more powerful and flexible alternative to the default routing system in AngularJS. While the default router uses routes to manage state, UI-Router uses states, which can be nested and organized in a hierarchical manner. This allows for more complex applications with multiple views and nested states.
Why is state management important in large-scale AngularJS applications?
State management is crucial in large-scale AngularJS applications because it helps maintain the user interface’s consistency and predictability. Without proper state management, it can become increasingly difficult to track changes and manage the application’s behavior, leading to bugs and a poor user experience.
Can you explain the concept of state in AngularJS?
In AngularJS, a state refers to the status of a system or an application at a specific point in time. It can include various things like user interface status, data model values, etc. States are used to define UI views, and they can be nested and organized hierarchically. Each state corresponds to a “place” in the application in terms of the overall UI and navigation.
How can I use the resolve property in $stateProvider?
The resolve property in $stateProvider is used to resolve a set of data before a state is activated. This data is then injected into the controller. The resolve property is an object that contains key-value pairs. The key is the name of the dependency to be injected into the controller, and the value is a function that returns the value of the dependency.
What are the benefits of using UI-Router for state management in AngularJS?
UI-Router provides several benefits for state management in AngularJS. It allows for nested views and multiple named views, which can be very useful in larger applications. It also provides state-based routing, which is more flexible and powerful than the default route-based routing in AngularJS.
How can I transition between states in AngularJS?
You can transition between states in AngularJS using the $state.go() method. This method takes the name of the state as its first argument, and an optional object of parameters as its second argument. The parameters object can be used to pass data to the state being transitioned to.
Can I use AngularJS without a state management tool?
Yes, you can use AngularJS without a state management tool. However, as your application grows in complexity, managing state can become increasingly difficult without the use of a tool like UI-Router. Using a state management tool can help maintain the consistency and predictability of your application’s user interface.
What are some common challenges in managing state in AngularJS?
Some common challenges in managing state in AngularJS include maintaining the consistency of the user interface, tracking changes in the application’s state, and managing the behavior of the application. These challenges can be mitigated by using a state management tool like UI-Router.
The above is the detailed content of Managing Client-Only State in AngularJS. For more information, please follow other related articles on the PHP Chinese website!

Detailed explanation of JavaScript string replacement method and FAQ This article will explore two ways to replace string characters in JavaScript: internal JavaScript code and internal HTML for web pages. Replace string inside JavaScript code The most direct way is to use the replace() method: str = str.replace("find","replace"); This method replaces only the first match. To replace all matches, use a regular expression and add the global flag g: str = str.replace(/fi

So here you are, ready to learn all about this thing called AJAX. But, what exactly is it? The term AJAX refers to a loose grouping of technologies that are used to create dynamic, interactive web content. The term AJAX, originally coined by Jesse J

10 fun jQuery game plugins to make your website more attractive and enhance user stickiness! While Flash is still the best software for developing casual web games, jQuery can also create surprising effects, and while not comparable to pure action Flash games, in some cases you can also have unexpected fun in your browser. jQuery tic toe game The "Hello world" of game programming now has a jQuery version. Source code jQuery Crazy Word Composition Game This is a fill-in-the-blank game, and it can produce some weird results due to not knowing the context of the word. Source code jQuery mine sweeping game

Article discusses creating, publishing, and maintaining JavaScript libraries, focusing on planning, development, testing, documentation, and promotion strategies.

This tutorial demonstrates how to create a captivating parallax background effect using jQuery. We'll build a header banner with layered images that create a stunning visual depth. The updated plugin works with jQuery 1.6.4 and later. Download the

The article discusses strategies for optimizing JavaScript performance in browsers, focusing on reducing execution time and minimizing impact on page load speed.

Matter.js is a 2D rigid body physics engine written in JavaScript. This library can help you easily simulate 2D physics in your browser. It provides many features, such as the ability to create rigid bodies and assign physical properties such as mass, area, or density. You can also simulate different types of collisions and forces, such as gravity friction. Matter.js supports all mainstream browsers. Additionally, it is suitable for mobile devices as it detects touches and is responsive. All of these features make it worth your time to learn how to use the engine, as this makes it easy to create a physics-based 2D game or simulation. In this tutorial, I will cover the basics of this library, including its installation and usage, and provide a

This article demonstrates how to automatically refresh a div's content every 5 seconds using jQuery and AJAX. The example fetches and displays the latest blog posts from an RSS feed, along with the last refresh timestamp. A loading image is optiona


Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

AI Hentai Generator
Generate AI Hentai for free.

Hot Article

Hot Tools

EditPlus Chinese cracked version
Small size, syntax highlighting, does not support code prompt function

ZendStudio 13.5.1 Mac
Powerful PHP integrated development environment

VSCode Windows 64-bit Download
A free and powerful IDE editor launched by Microsoft

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Dreamweaver Mac version
Visual web development tools