8

I am using a factory in angular.js and $http.get method to grab and process JSON data. The JSON data seems successfully parsed into factory, but I have problem access property of this JSON data.

Here is my js code:

var app = angular.module("app", []);      

app.factory('mainInfo', function($http) { 

    var obj = {content:null};
    //the php will return json data below
    $http.get('http://localhost/test.php').success(function(response){ 
        obj.content = response.records;

    });    

    return obj;    
});


app.controller('Ctrl', function($scope, mainInfo){
    $scope.foo = mainInfo.content;
}) ;

Now if I try to access foo within Ctrl controller, the webpage will display no data:
<div ng-controller="Ctrl">Controller: {{foo}}</div>
However, if I change to $scope.foo = mainInfo in the Ctrl, then the webpage will display correctly the JSON data.

May I know what is the proper way to access mainInfo.content property in Ctrl controller?

The reason why I need to access JSON property is because I need to pre-process the data. I intend to use these data in a Chart, as in below controller. Currently this controller is not working either, because I have the same problem accessing JSON property as in the Ctrl controller.

app.controller("LineCtrl", function ($scope, mainInfo) {
    var timePoints = [];
    var percentagePoints = [];
    var i = 0;
    for( i = 0; i < mainInfo.content.length; i ++) {
        timePoints.push(mainInfo.content[i].Time);
        percentagePoints.push(mainInfo.content[i].Percentage);
    }

    $scope.labels = timePoints;

    $scope.data = percentagePoints;

    $scope.onClick = function (points, evt) {
        console.log(points, evt);
    };
});

The json data:

{
"records": [
    {
        "Id": "1",
        "Time": "2015-07-25 08:00:00",
        "Percentage": "60"
    },
    {
        "Id": "2",
        "Time": "2015-07-25 09:00:00",
        "Percentage": "70"
    },
    {
        "Id": "3",
        "Time": "2015-07-25 10:00:00",
        "Percentage": "80"
    }
    ]
}

With regards to the factory-controller communication, I am just referring the solution from another post: reference

Community
  • 1
  • 1
modeller
  • 3,770
  • 3
  • 25
  • 49

3 Answers3

7

$http.get returns a promise -- your problem is that you are returning "obj" immediately, and your controller tries to access the data before the $http.get promise is resolved.

use $http like so (no need to use $q.defer() as shown in the other comment):

var app = angular.module("app", []);      

app.factory('mainInfo', function($http) { 
    var getRecordsPromise = $http.get('http://localhost/test.php').then(function(response){ 
        //whatever is returned here will be accessible in the .then that is chained
        return response.data.records;
    });    

    return {
        getRecords: getRecordsPromise
    };    
});


app.controller('Ctrl', function($scope, mainInfo){
    mainInfo.getRecords.then(function (records) {
        $scope.foo = records;
    });
}) ;
William B
  • 1,411
  • 8
  • 10
  • That's actually not the problem at all. OP is trying to maintain data inside their factory. You'll notice they said it works fine if the use `$scope.foo = mainInfo`. The problem is due to JavaScript references – Phil Jul 30 '15 at 04:10
  • $scope.foo = mainInfo sets the scope object to the same object returned by the factory -- when you do this, changes to the object's properties will be reflected in the template once mainInfo.content (or $scope.foo.content, same reference) is set. However, this does not change that the controller is setting the value before the promise has resolved if you do not chain to the $http.get promise. If the Factory were to return a primitive value instead of an object, you will face the same problem if you do not use the promise – William B Jul 30 '15 at 04:19
  • If you check the post referenced by the OP, it offers the same two options -- returning an object whose data will be set later (as in the OP), or returning the $http promise itself. Also, the question was specifically "What is the proper way to access this data in the controller?" The only answer is to use a promise -- having the factory return an object whose data will be set later is fine for templating purposes but to execute code in the controller on the data you should chain to the $http.get promise – William B Jul 30 '15 at 04:25
  • Hmm, fair point but it's not the only answer. You can have the best of both worlds; see my update – Phil Jul 30 '15 at 04:36
  • Also, I don't think `success` is chainable, ie the return value from `success` isn't passed into chained `then` calls. Better to use `then` and just unwrap `response.data` – Phil Jul 30 '15 at 04:46
  • 1
    You are right about chaining to success not being proper here, though it is chainable. It returns the original promise and not a new promise so the ".then" chained will get the original response and not the value returned by .success -- I overlooked this, and have made the appropriate edit. – William B Jul 30 '15 at 04:54
  • 1
    Note that the data inside the factory will already persist and be accessible via mainInfo.getRecords, which will be a resolved promise when accessed by other modules in the future – William B Jul 30 '15 at 04:58
2

Try this instead

var obj = {content: []};
obj.$promise = $http.get('http://localhost/test.php').then(function(response) {
    angular.copy(response.data.records, obj.content);
    return response.data;
});
return obj;

The reason your former method didn't work was because you were re-assigning the content property of obj, thus destroying any previous references.

This is also why using $scope.foo = mainInfo worked because the reference to obj (via mainInfo) was maintained.

Using angular.copy maintains the previous reference whilst populating the data.


That should take care of references in the template that get updated when the $http promise resolves (due to $http triggering a digest cycle). For accessing the data in your controller, use the $promise property

$scope.foo = mainInfo.content;
mainInfo.$promise.then(function(data) {
    // access data.records here
});

Maintaining the data inside your factory is a good idea if you share your factory with multiple consumers (eg controllers). They may all reference mainInfo.content and mainInfo.$promise and all will be accessing the same data set.

Phil
  • 157,677
  • 23
  • 242
  • 245
0

Try this:

var app = angular.module("app", []);      

app.factory('mainInfo', function($http) { 
    return {
        getData: function() {
            var deferred = $q.defer();


            //the php will return json data below
            $http.get('http://localhost/test.php').success(function(response){ 
                deferred.resolve(response.records);});



            return deferred.promise;
        }
    }
});


app.controller('Ctrl', function($scope, mainInfo){
    mainInfo.getData().then(function(result) {
        $scope.foo = result.content;
    });
}) ;
GPicazo
  • 6,516
  • 3
  • 21
  • 24
  • 1
    Ah, the [explicit promise construction anti-pattern](http://stackoverflow.com/questions/23803743/what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it) rears its ugly head once again – Phil Jul 30 '15 at 04:09
  • I agree that in this case, I should just have chained the $http promise... My mistake. However, I tend to find this pattern useful in many cases where I prefer to do my error handling or processing of the data in the services instead of each individual controller that consumes the service. Doesn't seem to me like the reasons for it being an anti-pattern outweigh the DRY-ability it provides in those cases. – GPicazo Jul 30 '15 at 04:22
  • @GPicazo that's why promise chaining is so effective. You can handle errors, transformations, further requests, etc all in your services and just return a promise in the end. You don't have to use a deferred object when you already have a promise – Phil Jul 30 '15 at 04:51
  • @GPicazo you can return inside the chained .then to process data/handling error. For example in your post just replace `deferred.resolve(response.records);` with `return response.records;` – Icycool Jul 30 '15 at 04:52