ElRudi
ElRudi

Reputation: 2324

$scope selectively updating on service resolving

Issue

I have a service that asynchronously fetches data and provides this to a controller. There, it is attached to the $scope and used in the view.
What I do not understand is the differences in updating, depending on how this service is used in the controller.

Example

The service is not providing promises to data, but rather exposing the data itself. I've simulated it below with timeouts:

(edit: I've added initialisation to the Opt variable to show that that does not solve the issue.)

app.service("Options", ["$q", "$timeout", function ($q, $timeout) {
    var Opt = {
        people:[],
        symbols:[],
        countries:[]
    };

    $timeout(function () {
        Opt.people = [ {frst: "Johnny", last: "Walker"},
                       {frst: "Jack", last: "Daniels"},
                       {frst: "Jim", last: "Bean"} ];
    }, 1000);

    $timeout(function () {
        Opt.symbols = [ {name: "Pi", descr: "circles"},
                        {name: "Phi", descr: "ratios"},
                        {name: "Psi", descr: "waves"},
                        {name: "Chi", descr: "distributions"} ];
    }, 2000);

    $timeout(function () {
        Opt.cities = [ {name: "Amsterdam", country: "Netherlands"},
                       {name: "Cairo", country: "Egypt"},
                       {name: "Santiago", country: "Chile"} ];
    }, 3000);

    /* etc */

    return Opt;
}]);

This is then used in the controller like so:

app.controller("Ctrller", ["$scope", "Options", "$timeout", function ($scope, Options, $timeout) {
    $scope.options = Options; //updating
    $scope.symbols = $scope.options.symbols; //not updating
    $scope.symbols = Options.symbols; //not updating either

    $timeout(function(){
        $scope.symbols = $scope.options.symbols; //updating
    }, 4000);

}]);

I've created a live demo in Plnkr here.

As you can see, the second select element in the view (and the 'symbol' property of $scope) is updated after 4 instead of 2 seconds. If there were no $timeout in the controller, this would not happen at all of course.

Why is this so? Why are the changes not cascaded through?

Thanks!

Upvotes: 3

Views: 59

Answers (2)

lostriebo
lostriebo

Reputation: 1503

You are trying to assign $scope.symbols to $scope.options.symbols or Options.symbols before the symbols array exists in the object returned by the Options service, so it ($scope.symbols) is assigned undefined rather than a reference to the symbols object. You can see this by adding a couple console statements before you attempt the assignments. Since the symbols object doesn't yet exist, your $scope.symbols variable can't reference it.

This isn't a problem with people or cities because you're accessing them through the $scope.options object which references the Opt object returned by the Options service. Since the base reference remains constant, your ng-repeats see changes to the people and cities children objects.

If you really want to assign Options.symbols to $scope.symbols, you could accomplish it with a watch on $scope.options.symbols:

$scope.$watch('options.symbols', function(value) {
    $scope.symbols = value;
});

Edit after the example plunker was updated

The revised plunker still exhibits the same behavior because the initialized people, cities, and symbols are being overwritten in the $timeout. If you want to keep the reference the same you must not use the = operator in the $timeout. One option would be to iterate through the returned list and add each item with a push:

$timeout(function () {
    results = [ {name: "Pi", descr: "circles"},
                {name: "Phi", descr: "ratios"},
                {name: "Psi", descr: "waves"},
                {name: "Chi", descr: "distributions"} ];
    angular.forEach(results, function(result) {
        Opt.symbols.push(result);
    });
}, 2000);

Here is a revised plunker with my suggested changes.

There may be better ways to accomplish this, but the key point is you can not reassign Opt.symbols to some other object (even if that other object is also an array).

Another approach would be to change Opt.symbols to be an object with an array inside it, your reference $scope.symbols points to the object (Opt.symbols) and can look at the internal values.

Opt = {
    people: [],
    symbols: {},
    cities: []
}

$timeout(function() {
    Opt.symbols.values = [ {name: "Pi", descr: "circles"},
                           {name: "Phi", descr: "ratios"},
                           {name: "Psi", descr: "waves"},
                           {name: "Chi", descr: "distributions"} ]; 
}, 2000);

If you went that route, you would need to adjust your ng-options to look like,

ng-options="s as s.name for s in symbols.values"

Here is a working plunker of this approach.

There are other approaches you could take beyond the two I've given examples for, which makes sense for your project will have to be a decision for you to make. The key point is that in the $timeout you cannot reassign the value of Opt.symbols and expect $scope.symbols to reference the new value.

Upvotes: 1

Rohan
Rohan

Reputation: 7976

In index.html you correctly specified the ng-options for 'options.people' and 'options.options' but forgot to do so for 'symbols'

Before:

ng-options="s as s.name for s in symbols"

After:

ng-options="s as s.name for s in options.symbols"

Here's the new plunker:

http://plnkr.co/edit/HqPQM5Qqfq7gi9KWQLKJ?p=preview

Upvotes: 0

Related Questions