Use service to share data between list with $http JSON request


#1

Hello,

I am using $http.get to load a list on the main view of my application. Now that I want to display a view with item details (when you tap on one element of the list), I see that I should implement a service to share data between views.

I have looked at: http://learn.ionicframework.com/formulas/data-the-right-way/#

However it is using a static list of elements. I can implement this, but I dont see how to use this in combination with a http request to fetch JSON data.

In other words, Can you help me combine these 2 pieces of code?

.controller('AdsCtrl', function($scope, $http, ads) {
  $scope.name = 'World';
  $scope.allads = [];
  $scope.ads = [];
  
  $http.get('http://www.mysite.com/list.json').success(function(data) {
    $scope.allads = data;
    $scope.loadMore();
  });
  
  var counter = 0;
  
  $scope.loadMore = function() {
    var data = [];
    var l = $scope.ads.length;
    var m = Math.min(l+10, $scope.allads.length);
    for ( var i = l; i < m; i++) {
      data.push($scope.allads[i]);
    }
    
    console.log('Loading more!', m);
    $scope.ads = $scope.ads.concat(data);
    $scope.$broadcast('scroll.infiniteScrollComplete');
  };

  $scope.$on('stateChangeSuccess', function() {
    $scope.loadMore();
  });
  
  // THIS METHOD LOADS 4 LOCAL TEST ITEMS WITH THE ADSSERVICE
  //$scope.ads = ads;
})

and

.service('AdsService', function($q) {

  return {
    ads : [
        { shortrelativetime:'30 min ago', title: 'TST อาหารเสริมเพี่มความสูง', group:'Vehicles', category:'Cars', province:'Vientiane Capital', district:'Xaythany', price:'24000', currency:'USD', photo:'20140202-040753-c1395444fd.jpg', id: 1 },
        { shortrelativetime:'1 day ago', title: 'TST Samsung Galaxy Grand', group:'High Tech', category:'Phones', province:'Vientiane Capital', district:'Xaythany', price:'6000', currency:'THB', photo:'20140218-101800-1602504d17.jpg', id: 5 },
        { shortrelativetime:'1 day ago', title: 'TST ຂາຍດິນປຸກສ້າງ', group:'Real Estate', category:'Land or House', province:'Vientiane Capital', district:'Xaythany', price:'850000', currency:'THB', photo:'20140715-080420-4a0ba40b89.jpg', id: 6 },
        { shortrelativetime:'08 Jun 2014', title: 'TST HTC One', group:'High Tech', category:'Phones', province:'Vientiane Capital', district:'Xaythany', price:'12000', currency:'THB', photo:'20140604-083644-046276fe30.jpg', id: 9 }
    ],
    getAds: function() {
        return this.ads
    },
    getAd: function(adId) {
        var dfd = $q.defer()
        this.ads.forEach(function(ad) {
            if (ad.id === adId) dfd.resolve(ad)
        })
        
        return dfd.promise
    }

  }
})

Thanks a lot for the tips!


#2

Based on the code you gave, your service would be something like this… (I used a factory instead)

.service('AdsService', function($q) {
  return {
	ads: [],
    getAds: function() {
        return  $http.get('http://www.mysite.com/list.json').success(function(data) {
			this.ads = data;
			return this.ads;
		});
    },
    getAd: function(adId) {
        var dfd = $q.defer()
        this.ads.forEach(function(ad) {
            if (ad.id === adId) dfd.resolve(ad)
        })

        return dfd.promise
    }
  }
})

And your controller something like this…

.controller('AdsCtrl', function($scope, $http, ads, AdsService) {
  $scope.name = 'World';
  $scope.allads = [];
  $scope.ads = [];
  
  AdsService.getAds().then(function(){
	$scope.allads = data;
    $scope.loadMore();
  });

  var counter = 0;

  $scope.loadMore = function() {
    var data = [];
    var l = $scope.ads.length;
    var m = Math.min(l+10, $scope.allads.length);
    for ( var i = l; i < m; i++) {
      data.push($scope.allads[i]);
    }

    console.log('Loading more!', m);
    $scope.ads = $scope.ads.concat(data);
    $scope.$broadcast('scroll.infiniteScrollComplete');
  };

  $scope.$on('stateChangeSuccess', function() {
    $scope.loadMore();
  });

  // THIS METHOD LOADS 4 LOCAL TEST ITEMS WITH THE ADSSERVICE
  //$scope.ads = ads;
})

#3

I would also use promises to handle requests you do not need .success .error only “then” and two callback function for success and error case.


#4

Hello Ionic enthusiasts!

I am a newbie to this framework and also AngularJS like many people here, I think…so I have a question for anybody that is more expert than me.

So I have a very similar case as @ceyquem, but without using the loadMore method.
I have a plugin installed on a wordpress which is creating JSON links with my posts and custom posts. I use that links in a controller with $http.get to get all the items and display them in a ng-repeat list.

What I also need is to have that single view of each item when I tap on any of them. Below you can find my code and if you’re kind, please explain to me how to approach this better as there are 3 ways of routing trough pages.

angular.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider
    .state('app', {
      url: "/app",
      abstract: true,
      templateUrl: "templates/menu.html",
      controller: 'AppCtrl'
    })
    .state('app.evenimente', {
      url: "/evenimente",
      views: {
        'menuContent' :{
          templateUrl: "templates/evenimente.html",
          controller: 'EventsCtrl'
        }
      }
    })
    .state('app.home', {
      url: "/home",
      views: {
        'menuContent' :{
          templateUrl: "templates/home.html"
        }
      }
    })
    .state('app.event', {
          url: "/event/:eventId",
          views: {
            'menuContent' :{
              templateUrl: "templates/event.html",
              controller: 'SingleEventCtrl'
            }
           }
    });
 $urlRouterProvider.otherwise('/app/home');

And my controllers.js file has this:

.controller('EventsCtrl', function($scope, $http){
$http.get('http://www.domain.com/api/get_posts/?post_type=event&custom_fields').then(function(response){
$scope.events = response.data.posts});
})
.controller('SingleEventCtrl',function($scope, $http) {
$http.get('http://www.domain.com/api/get_post/?post_type=event&post_id={{id}}').then(function(response){
$scope.titlu = response.data.post.title_plain
$scope.id = response.data.post.id
});
}) 

And this is my list view from .state app.evenimente:

<div class="list">
<a ng-repeat="event in events | filter:find | limitTo:15" ui-sref="app.event({id: eventId})" class="item item-thumbnail-left">
  <img ng-src="{{event.attachments[0].images.thumbnail.url}}" width="150">

  <h2 class="assertive">{{event.title_plain}}</h2>
  <h3 class="positive">Data eveniment:{{event.custom_fields.gp_event_date[0]}}</h3>
  <p>{{event.excerpt | htmlToPlaintext}}</p> 
</a>

What I want is: clicking on an item should go to #/app/event/14423 <- item id
And based on that unique ID I want to get the JSON data of that item. Because if I navigate to http://www.domain.com/api/get_post/?post_type=event&post_id=14423
I will get all the details of that particular post(event).

Can you help me with this? I couldn’t solve this on my own using documentation from this link:
http://learn.ionicframework.com/formulas/sharing-data-between-views/


#5

Please read something about factories and services of angularjs.
Controllers should only contain things about data-binding and data-models.

So you could write a service to send http-requests.
The service could have two function:

  • for the listView
  • for DetailView with a parameter e.g. the post id

Controllers are executed everytime you get to the associated states.
Services not -> so if you store data there -> you can access this data from different controllers at several times ;).


#6

Thank you for your reply.
I still don’t know how to update my code using factory and services but I hope I will learn this one day.

Have a great day!


#7

services are written in an very similar way like controllers.

They can be only collections of functions and object and so on.
In your controller you can load them like other angularjs services like
app.controller('myController', ['myService', function (myService) { .... }]);

You can call a service function via myService.functionName().

The service can hold simple variables and object you could acces them in the same way:
myService.data

If you have a function, that sends a request -> you can set a service variable with the response.
You controller can check the variable and get the values of it. Example service content:

this.posts = [];
this.getAll = function () {
  // create promise to handle your requests in controller like myService.getAll().then(success, error);
  var q = $q.defer(),
        self = this; // to store current scope

  $http.get('url').then(function(response) {
    self.posts = response.data.posts; // store data on our posts array
    q.resolve(self.posts); // resolve the promise
  }, q.reject);
};

Now in another controller / state / view you can access the service and all data… like on myService.posts :wink:


#8

So basically I have to delete my current controllers that I mentioned in my previous post

and prepare the factory with my service which seems easy to understand after I saw this video: http://youtu.be/QETUuZ27N0w

all this is very new to me… I don’t understand the meaning of the $q for example that you mentioned in your service exemple even reading https://docs.angularjs.org/api/ng/service/$q


#9

So my app is called “starter” then I have this controller:

angular.module('starter.controllers', [])
  .controller('EventsCtrl', ['$scope', 'Evenimente', function($scope, Evenimente){
    $scope.events = Evenimente.getAll();
  }])

and this service:

angular.module('starter.services', []).factory('Evenimente', ['$http', function($http){
  return {
    getAll: function(){
      return $http.get('http://www.mydomain.com/api/get_posts/?post_type=event&custom_fields').then(function(response){
        return response.data.posts;
      });
    }
  }
}]);

and my template:

<a ng-repeat="event in events | filter:find | limitTo:15" href="#" class="item item-thumbnail-left">
  <h2 class="assertive">{{event.title_plain}}</h2>

My $http.get should get a json something like this:

{
posts: [
{
id: 14448,
type: "event",
title_plain: "FREENETIK Warm Up Party"
}
]}

Although a “undefined” value is displayed at the data binding using: {{event.title_plain}} in Chrome when I try to view that template:

   angular.module('starter', ['ionic', 'starter.controllers', 'starter.services'])
   .config(function($stateProvider, $urlRouterProvider) {
  $stateProvider

    .state('app', {
      url: "/app",
      abstract: true,
      templateUrl: "templates/menu.html",
    })
    .state('app.evenimente', {
      url: "/evenimente",
      views: {
        'menuContent' :{
          templateUrl: "templates/evenimente.html",
          controller: 'EventsCtrl'
        }
      }
    });

Can you tell me what I am doing wrong, or what I am missing?


#10

The problem is that http.requests are async. You can not easy assign the return value of your factory to the $scope object.
There i used the $q (promises).

You are using it already in your factory because every $http call returns a promise. Thatswyh you can write something like this $http.get().then(function (response) { ... });

Now you can follow my example some posts above where i use $q.
With $q you can create your own promise var myPromise = $q.defer();

Service:
angular.module('starter.services', []).factory('Evenimente', ['$http', '$q', function ($http, $q) {
 return {
    getAll: function() {
      var myPromise = $q.defer();
      $http.get('http://www.mydomain.com/api/get_posts/?post_type=event&custom_fields').then(function(response){
        q.resolve(response.data.posts); // resolve our promise -> success
      }, q.reject); // reject our promise -> fail / error case
      return myPromise.promise; // the function returns now a promise so you can handle it in your controller like getAll().then(successCallback, errorCallback)
    }
  }
}]);

Controller:
angular.module('starter.controllers', [])
  .controller('EventsCtrl', ['$scope', 'Evenimente', function($scope, Evenimente){
    $scope.events = []; // init events as empty array
    Evenimente.getAll().then(function (events) {
      $scope.events = events;
    }, function (response) {
        // our q.reject gets the reponse and you can handle an error
    });
  }])

If you understand that you will quickly understand the advantages of promises.


#11

Thank you for your reply!

I hope I will understand how this works and try to update this code in order to get data for a specific event id to the single event detail page.


#12

The difficult part of that all is to understand what promises are and how they work.

You can imagine it something like that:
You have a strong but ordinary laserpointer (laser-light has simplified the speed of light^^).
And there are some reflecting things on the moon.

Now you are aiming exactly one of that reflecting things and turning your laserpointer on (starting a clock) for exactly 1 second.
The light takes 1.3 seconds from the earth to the moon.

Now you have a little bit (be fast!) time to do other stuff before your beam comes back^^.
You are expecting - 1.3 seconds * 2 (for both ways) - in 2.6 seconds your beam is back.

And after that you can be sure that everything works.

So in programming we need something to not break our code but can handle such async things and something that tells us “okay your laser beam is back”.

And that are promises.
You have the promise for a function. (thatswhy you need to return the promise itself ;))
And you need to resolve your promise or sadly reject your the promise.

In our little experiment:
You say -> if the light is back after 3 seconds -> the promise is resolved.
If it takes longer you are assuming that the reflection did not worked correctly (maybe your aiming is worser than expected^^) and you have to reject your promise.

After your experiment you need both possibilities to write a correct experiment report.

And now back to angular:

  • $q = promise service
  • $q.defer -> create a new promise for your purpose
  • myPromise.promise -> returns the promise object to handle success or error case
  • $q.resolve(data) -> says that you can hold your promise and it was a success
  • $q.reject(data) -> sadly but true… your promise failed :frowning:

Back to the experiment.
Now you want to send 5 beams at the same time… hm how to handle that promises and what if you want to send 23463466345635 billion beams at once?

No problem, you can build a list of promises and get informed if every promise has finished.

var tasks = [];
tasks.push(sendBeam(5, 10));
tasks.push(sendBeam(4, 13));
...
$q.all(tasks).then(function (results) {
     // all beams are back -> results is an array of the return values of each call of sendBeam
}, function () {
     // not all are back :(
});

#13

Thank you again for you explanation. I think I understand more now.

I updated my code with your suggestion but I still don’t see anything in the browser using Chrome with web security disabled trough ionic serve
Also in the console everything is blank, no thrown errors, warnings nothing.

Here is my updated code[edited after]:

angular.module('starter.controllers', [])
	.controller('EventsCtrl', ['$scope', 'Evenimente', function($scope, Evenimente){
	    $scope.events = [];
	    Evenimente.getAll().then(function (events) {
	      $scope.events = events;
	    }, function (response) {
	        // our q.reject gets the reponse and you can handle an error
	        console.log("Rejected connection for Evenimente");
	    });
	}]);

  angular.module('starter.services', [])
.factory('Evenimente', ['$http', '$q', function($http, $q){
  return {
    getAll: function(){
      var myPromise = $q.defer();
      $http.get('http://www.domain.com/api/get_posts/?post_type=event&custom_fields').then(function(response){
                  myPromise.resolve(response.data.posts); // resolve our promise -> success
               }, myPromise.reject); // reject our promise -> fail / error case
      return myPromise.promise;
    }
  };
}]);

I don’t know why I had two returns on the .services . Corrected the code and now it works.
Thank you for your advices and helpful explanations!


#14

Hello again!

So i’m back after a while… My problem now is that after I build the app-debug.apk file and install it on my device,
which is HTC One Mini (running on Android 4.4.2) the data is there… but when the JSON file is updated on the server, my list isn’t refreshed automatically.
But if I enter phone settings -> apps -> search for my app and tap on Delete data that the app created then when I restart the app… the new JSON data is retreived.

How can I make so that everytime I start the app on android to have the refreshed JSON data?

I also tried using a function in my controller to call it using ion-refresher but it isn’t refreshing the data. How can I control this localcache that the phone creates after the first run?

PS: I still have ionic 1.0.0-beta.11, I will try to update it to the latest stable version.


#15

I think if you restart your app all controllers and services are instantiate again, so you should send the “getAll” request and get the new data.

Otherwise you store the data somewhere in the localstorage and does not update them.


#16

Maybe the request is cached. I had a similar problem yesterday. I changed the JSON file but didn’t get updated data. I debugged it with the chrome dev tools and in network panel I could see “Status 200 cache” in the header of this request. You can check a checkbox there that disables the cache. Maybe that works.


#17

where should I send the getAll request to get the new data? because this is happening only if I delete the app data from my mobile device.

Why this caching is happening on the mobile device by default and how can I control it? How do I clear it ?


#18

Do you request really only a static file?

the solution is like you would prevent caching an image:
your url, yet: http://…/data.json
put a timestamp at the end:
new url: http://…/data.json?[TIMESTAMP_HERE]

in javascript you can simply call Date.now() and add the resulting timestamp at the end of your request url.
And do not forget the “?” between real url and timestamp


#19

Thank you for your reply. I updated the code with a timestamp created like you said. On the Chrome Dev Tools Mobile simulator, still gets the list and also is adding to the request the Date.now() result… I will test it out on my android to see if it refreshes the list and will come back with a reply.

EDIT: Yes it works updating the list now. Thank you for your instructions sir!


#20

Best explanation of promises I’ve ever seen! Kudos!