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?
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>
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?
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</span>></span>{{article.state | limitToTransition:"SAVING":"SAVED" | translate}}<span><span></p</span>></span></span>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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!