1

WARNING: fairly long question

I am new to angular and I've gone through a few tutorials and examples such as the official tutorial on the angularjs website. The recommended way to get data into your app and share that data between controllers seems very clear. You create a service that is shared by all controllers which makes an asynchronous request to the server for the data in JSON format.

This is great in theory, and seems fine in the extremely simple examples that only show one controller, or when controllers don't share logic that depends on the shared data. Take the following example of a simple budget application based on yearly income and taxes:

Create the app with a dependency on ngResource:

var app = angular.module('budgetApp', ['ngResource']);

NetIncomeCtrl controller that handles income and tax items, and calculates net income:

app.controller('NetIncomeCtrl', function ($scope, BudgetData) {
    var categoryTotal = function (category) {
        var total = 0;
        angular.forEach(category.transactions, function (transaction) {
            total += transaction.amount;
        });
        return total;
    };

    $scope.model = BudgetData.get(function (model) {
        $scope.totalIncome = categoryTotal(model.income);
        $scope.totalTaxes = categoryTotal(model.taxes);
    });

    $scope.netIncome = function () {
        return $scope.totalIncome - $scope.totalTaxes;
    };
});

BudgetData service that uses $resource to retrieve the JSON data from the server:

app.factory('BudgetData', function ($resource) {
    return $resource('data/budget.json');
});

budget.json contains the JSON data returned from the server:

{
    "income": {
        "transactions": [
            {
                "description": "Employment",
                "amount": 45000
            },
            {
                "description": "Investments",
                "amount": 5000
            }
        ]
    }, 
    "taxes": {
        "transactions": [
            {
                "description": "State",
                "amount": 5000
            },
            {
                "description": "Federal",
                "amount": 10000
            }
        ]
    },
}

Then on my screen I have two repeaters that show the income and tax items (which you can edit), and then the net income is calculated and displayed.

This works great, and is the standard approach I've seen used in tutorials. However, if I just add one more controller that depends on some of the same data and logic, it begins to unravel:

ExpensesCtrl controller for expenses which will in the end calculate the surplus (net income - expenses):

app.controller('ExpensesCtrl', function ($scope, BudgetData) {
    var categoryTotal = function (category) {
        var total = 0;
        angular.forEach(category.transactions, function (transaction) {
            total += transaction.amount;
        });
        return total;
    };

    $scope.model = BudgetData.get(function (model) {
        $scope.totalIncome = categoryTotal(model.income);
        $scope.totalTaxes = categoryTotal(model.taxes);
        $scope.totalExpenses = categoryTotal(model.expenses);
    });

    $scope.netIncome = function () {
        return $scope.totalIncome - $scope.totalTaxes;
    };

    $scope.surplus = function () {
        return $scope.netIncome() - $scope.totalExpenses;
    };
});

budget.json adds the expenses data:

"expenses": {
    "transactions": [
        {
            "description": "Mortgage",
            "amount": 12000
        },
        {
            "description": "Car Payments",
            "amount": 3600
        }
    ]
}

Then on a separate part of the screen I have a section that uses this controller and uses a repeater to show the expense items, and then re-displays the net income, and finally shows the resulting surplus.

This example works, but there are several problems (questions) with it:

1) In this example I've managed to keep most of my controller's logic out of the callback function, but all controllers start out in a callback because everything depends on the model being loaded. I understand this is the nature of javascript, but angular is supposed to reduce the need for all of these callbacks, and this just doesn't seem clean. The only reason I was able to take some of the logic out of the callbacks here is because of the magic that angular does under the hood to substitute a fake object for the model until the model gets loaded. Since this 'magic' is not intuitive, it's hard to tell if your code will work as expected.

Is there a consistent way people deal with this? I really don't want some elaborate solution that makes this 101 intro app into something really complicated. Is there a simple and standard approach to restructuring this code somehow to avoid so many callbacks and make the code more intuitive?

2) I have a bunch of repeated logic. The categoryTotal and netIncome logic should only be in one place, and shared between controllers. These controllers are used in completely separate parts of the screen, so they can't use scope inheritance. Even when using scope inheritance there could be problems, because the child controller's scope can't depend on the parent scope's model being loaded even when its own model is loaded.

The categoryTotal logic is just a helper function that isn't tied directly to the model, so I don't know where you put generic helper functions in angular. As for the netIncome which does directly depend on the model and needs to be in the scope, it would be possible to add it to the service or the model. However, the service should only be concerned with retrieving the model from the server, and the model should just contain data, and be as anemic as possible. Also, it seems like this kind of logic belongs in a controller. Is there a standard way to deal with this?

3) Every time the service is called to fetch the data, it does an http request to the server, even though the data is the same every time. The model should be cached after the first request, so there is only one request made.

I realize I could handle this in the service. I could just store the model as a local variable and then check that variable to see if the data already exists before making the request. It just seems odd that none of the tutorials I read even mentioned this. Also, I'm looking for a standard 'angular' way of dealing with it.

Sorry for the incredibly long post. I would really like to keep this app at the 101 intro level, and not get into really complicated areas if possible. I'm also hoping there is a standard 'angular' way to deal with these problems that I have just not come across yet.

Thanks in advance!

1 Answer 1

3

This is how I do it. You create a service that handles data, and if it is changed it broadcasts messages to your controllers. It will get the initial data that can be gotten with BudgetData.data. If anyone changes the data

.service("BudgetData", function($http, $rootScope) {
  var this_ = this, data;

  $http.get('wikiArticles/categories', function(response) {
    this_.set(response.data);
  }

  this.get = function() {
    return data;
  }

  this.set = function(data_) {
    data = data_;
    $rootScope.$broadcast('event:data-change');
  }

});

In your controller you just need to listen for the events, and it will update your scope variables accordingly. You can use this in as many controllers as you want.

$rootScope.$on('event:data-change', function() {
  $scope.data = BudgetData.get();
}

$scope.update = function(d) {
  BudgetData.set(d);
}
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for the quick reply! This deals with my third problem, but not the first two. Is this a standard way of dealing with it, or something you came up with? I'm just new to angular, so I don't know if there are best practices that everyone uses for this scenario.
As far as communicating between controllers, services and $broadcast are the standard way.
So you have no advice on the other two problems of having to use callbacks in every controller and sharing logic between controllers, like netIncome and categoryTotal in my example?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.