Managing Client-Only State in AngularJS

Share this article

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.module('clientOnlyState.controllers')
    .controller('ArticleCtrl', function($scope, $resource, ArticleStates /* simple lookup */) {
        var Article = $resource('/article/:articleId', { articleId: '@id' });

        var article = new Article({ id: 1, title: 'A title', author: 'M Godfrey' });
        article.state = ArticleStates.NONE; // "NONE"

        $scope.article = article;

        $scope.save = function() {
            article.state = ArticleStates.SAVING; // "SAVING"

            article.$save(function success() {
                article.state = ArticleStates.SAVED; // "SAVED"
            });
        };
    });

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.module('clientOnlyState.services')
    .factory('Article', function($resource, ArticleStates) {

        var Article = $resource('/article/:articleId', { articleId: '@id' });

        // Consumers will think they're getting an Article instance, and eventually they are...
        return function(data) {
            var article = new Article(data);
            article.state = ArticleStates.NONE;
            return article;
        }
    });

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.module('clientOnlyState.services')
    .factory('wrapMethod', function() {
        return function(object, method, wrapper) {
            var fn = object[method];

            return object[method] = function() {
                return wrapper.apply(this, [fn.bind(this)].concat(
                    Array.prototype.slice.call(arguments))
                );
            };
        }
    });

This allows us to wrap the save and get methods of Article and do something different/additional before and after:

angular.module('clientOnlyState.services')
    .factory('Article', function($resource, ArticleStates, wrapMethod) {
        var Article = $resource('/article/:articleId', { articleId: '@id' });

        wrapMethod(Article, 'get', function(original, params) {
            var article = original(params);

            article.$promise.then(function(article) {
                article.state = ArticleStates.NONE;
            });

            return article;
        });

        // Consumers will actually call $save with optional params, success and error arguments
        // $save consolidates arguments and then calls our wrapper, additionally passing the Resource instance
        wrapMethod(Article, 'save', function(original, params, article, success, error) {
            article.state = ArticleStates.SAVING;

            return original.call(this, params, article, function (article) {
                article.state = ArticleStates.SAVED;
                success && success(article);
            }, function(article) {
                article.state = ArticleStates.ERROR;
                error && error(article);
            });
        });

        // $resource(...) returns a function that also has methods
        // As such we reference Article's own properties via extend
        // Which in the case of get and save are already wrapped functions
        return angular.extend(function(data) {
            var article = new Article(data);
            article.state = ArticleStates.NONE;
            return article;
        }, Article);
    });

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.module('clientOnlyState.controllers')
    .controller('ArticleCtrl', function($scope, Article) {
        var article = new Article({ id: 1, title: 'A title', author: 'M Godfrey' });
        
        console.log(article.state); // "NONE"

        $scope.article = article;

        $scope.save = function() {
            article.$save({}, function success() {
                console.log(article.state); // "SAVED"
            }, function error() {
                console.log(article.state); // "ERROR"
            });
        };
    });

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.module('clientOnlyState.controllers')
    .controller('ArticleCtrl', function($scope, Article, ArticleStates) {
        var article = new Article({ id: 1, title: 'A title', author: 'M Godfrey' });

        var translations = {};
        translations[ArticleStates.SAVED] = 'Saved, oh yeah!';
        translations['default'] = '';

        $scope.article = article;

        $scope.save = function() {
            article.$save({});
        };

        $scope.$watch('article.state', function(newState, oldState) {
            if (newState == ArticleStates.SAVED && oldState == ArticleStates.SAVING) {
                $scope.message = translations[newState];
            } else {
                $scope.message = translations['default'];
            }
        });
    });

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.

<p>{{article.state | limitToTransition:"SAVING":"SAVED" | translate}}</p>

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.module('clientOnlyState.filters')
    
    .filter('limitToTransition', function() {
        return function(state, prevState, from, to) {
            if(prevState == from && state == to)
                return to;

            return '';
        };
    })

    .filter('translate', function() {
        return function(text, translations) {
            return translations[text] || translations['default'] || '';
        };
    });

Because of this we need a slight amendment to Article:

function updateState(article, newState) {
    article.prevState = article.state;
    article.state = newState;
};

wrapMethod(Article, 'get', function(original, params) {
    var article = original(params);
    article.$promise.then(function(article) {
        updateState(article, ArticleStates.NONE);
    });
    return article;
});

The end result is not quite as pretty but is still very powerful:

<p>{{article.state | limitToTransition : article.prevState : states.SAVING : states.SAVED | translate : translations}}</p>

Our controller gets leaner again, especially if you consider the translations could be pulled out into an injectable service:

angular.module('clientOnlyState.controllers')
    .controller('ArticleCtrl', function($scope, Article, ArticleStates) {
        var article = new Article({ id: 1, title: 'A title', author: 'M Godfrey' });

        // Could be injected in...
        var translations = {};
        translations[ArticleStates.SAVED] = 'Saved, oh yeah!';
        translations['default'] = '';

        $scope.article = article;
        $scope.states = ArticleStates;
        $scope.translations = translations;

        $scope.save = function() {
            article.$save({});
        };
    });

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.

Michael GodfreyMichael Godfrey
View Author

Mike is a JavaScript engineer struggling to keep up with the rate of innovation in modern web development. He suspects he isn’t alone! He’s currently chosen AngularJS as his framework of choice, but occasionally samples others to make sure that remains a good decision. He blogs at http://crudbetter.com/.

angularjsColinILearn Angular
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week