Creating a Feed in Ionic

posted in: Uncategorized | 20

In my previous posts Understanding Ionic’s Infinite Scroll and Understanding Pull to Refresh I covered two of the basic building blocks of building the popular feed pattern often seen in apps, especially social media apps.

In this post we’ll break down putting the two techniques from the articles above to create the full feed pattern with a pull to refresh and infinite scroll. We’ll also look at some bonus functionality to add in. You can follow along on CodePen.

View

As with our previous articles, we’ll be working with the following list markup to start:

<ion-list>
	<ion-item class="item-avatar" ng-repeat="item in items">
		<img src="{{item.user.picture.thumbnail}} " />
		<h2>{{item.user.name.first}} {{item.user.name.last}}</h2>
		<p>{{item.user.location.city}} {{item.user.password}}</p>
	</ion-item>
</ion-list>

We’ll add in the ion-refresher above the list and the ion-infinite-scroll below the list.

<ion-refresher on-refresh="doRefresh()"></ion-refresher>
<ion-list>
	<ion-item class="item-avatar" ng-repeat="item in items">
		<img src="{{item.user.picture.thumbnail}} " />
		<h2>{{item.user.name.first}} {{item.user.name.last}}</h2>
		<p>{{item.user.location.city}} {{item.user.password}}</p>
	</ion-item>
</ion-list>
<ion-infinite-scroll on-infinite="loadMore()" distance="5%"></ion-infinite-scroll>

One extra element we are going to add in is a list item that notifies the user if there are new items to be loaded in at the top of the list. It will only display if there are new items. This pattern can be seen on the Twitter web app. When clicked, it will call the same doRefresh() function that the ion-refresher calls.

<ion-refresher on-refresh="doRefresh()"></ion-refresher>
<ion-list>
	<ion-item ng-if="newItems.length > 0" ng-click="doRefresh()">
		{{newItems.length}} new items
	</ion-item>
	<ion-item class="item-avatar" ng-repeat="item in items">
		<img src="{{item.user.picture.thumbnail}} " />
		<h2>{{item.user.name.first}} {{item.user.name.last}}</h2>
		<p>{{item.user.location.city}} {{item.user.password}}</p>
	</ion-item>
</ion-list>
<ion-infinite-scroll on-infinite="loadMore()" distance="5%"></ion-infinite-scroll>

Factory

By taking the factories from the previous articles and mixing them together (with some slight renaming), we’ll end up with a factory with three methods: GetFeed, GetNewUsers, and GetOldUsers.

.factory('PersonService', function($http){
	var BASE_URL = "http://api.randomuser.me/";
	var items = [];
	
	return {
		GetFeed: function(){
			return $http.get(BASE_URL+'?results=10').then(function(response){
				items = response.data.results;
				return items;
			});
		},
		GetNewUsers: function(){
			return $http.get(BASE_URL+'?results=2').then(function(response){
				items = response.data.results;
				return items;
			});
		},
		GetOldUsers: function(){
			return $http.get(BASE_URL+'?results=10').then(function(response){
				items = response.data.results;
				return items;
			});
		}
	}
})

Controller

The controller will be very similar to the previous two articles with the exception of some modifications made to support the new items indicator at the top of the list.

For this, we’ll need two arrays. One will hold the currently loaded items and the other will hold new items that are ready to be loaded to the top of the list.

$scope.items = [];
$scope.newItems = [];

As with our previous articles, we will start by filling the feed with an initial 10 items.

PersonService.GetFeed().then(function(items){
	$scope.items = items;
});

Our loadMore function for our infinite scrolling is more or less the same, calling the GetOldUsers method of our PersonService.

$scope.loadMore = function(){
	PersonService.GetOldUsers().then(function(items) {
		$scope.items = $scope.items.concat(items);

		$scope.$broadcast('scroll.infiniteScrollComplete');
	});
};

A new chunk of code we’re going to add in is to support the “X new items” indicator. The idea is that every X number of seconds (We’ll use 10 seconds for our example) we call our factory to see if there are any new items. If there are, we’ll add those items to the newItems array.

var CheckNewItems = function(){
	$timeout(function(){
		PersonService.GetNewUsers().then(function(items){
			$scope.newItems = items.concat($scope.newItems);

			CheckNewItems();
		});
	},10000);
}

CheckNewItems();

For this code we’re using Angular’s $timeout (Official Documentation) so we’ll need to make sure to reference this in our controller.

.controller('MyCtrl', function($scope, $timeout, PersonService) {

});

Feed with New Items Indicator

Finally, let’s take a look at the doRefresh function. There is a bit of logic change here from the previous article. The first thing we’ll do in this function is check if there are new items waiting to be loaded in from the newItems array. If there are, we’ll add these new items to the top of the items array, broadcast that the refresh is complete, and reset the newItems array.

If there are NOT any new items already waiting, we’ll do the normal path from the previous article where we call the PersonService to get new items.

Remember that this method is called both on a pull to refresh and when the “X new items” list item is clicked. Here’s the code for that function.

$scope.doRefresh = function() {
	if($scope.newItems.length > 0){
		$scope.items = $scope.newItems.concat($scope.items);

		//Stop the ion-refresher from spinning
		$scope.$broadcast('scroll.refreshComplete');

		$scope.newItems = [];
	} else {
		PersonService.GetNewUsers().then(function(items){
			$scope.items = items.concat($scope.items);

			//Stop the ion-refresher from spinning
			$scope.$broadcast('scroll.refreshComplete');
		});
	}
};

In its entirety, our controller looks like this:

.controller('MyCtrl', function($scope, $timeout, PersonService) {
  $scope.items = [];
  $scope.newItems = [];
  
  PersonService.GetFeed().then(function(items){
	$scope.items = items;
  });
  
  $scope.doRefresh = function() {
		if($scope.newItems.length > 0){
			$scope.items = $scope.newItems.concat($scope.items);
				
			//Stop the ion-refresher from spinning
			$scope.$broadcast('scroll.refreshComplete');
			
			$scope.newItems = [];
		} else {
			PersonService.GetNewUsers().then(function(items){
				$scope.items = items.concat($scope.items);
				
				//Stop the ion-refresher from spinning
				$scope.$broadcast('scroll.refreshComplete');
			});
		}
  };
  
  $scope.loadMore = function(){
    PersonService.GetOldUsers().then(function(items) {
      $scope.items = $scope.items.concat(items);
	  
      $scope.$broadcast('scroll.infiniteScrollComplete');
    });
  };
  
   var CheckNewItems = function(){
		$timeout(function(){
			PersonService.GetNewUsers().then(function(items){
				$scope.newItems = items.concat($scope.newItems);
			
				CheckNewItems();
			});
		},10000);
   }
  
  CheckNewItems();
});

Bonus: Collection Repeat

One awesome directive in Ionic is the collection-repeat (Official Documentation). This directive improves the performance of a list of items by only rendering the items that are currently on the screen. This allows for you to have blazing fast lists of thousands of items. (Follow Along: Bonus CodePen)

The biggest gotcha with the directive, however, is that you have to know the height of the items ahead of time. This can be a challenge and a road block. In the case of our example, we know how tall items are.

Let’s convert our view to use collection-repeat. To do this, we’ll replace ng-repeat with collection-repeat and add in the collection-item-width and collection-item-height attributes.

<ion-item class="item-avatar" collection-repeat="item in items" collection-item-width="'100%'" collection-item-height="'70px'">

We’ll also need to (as stated in the docs) make each item position properly to both edges of the screen.

.item {
  left: 0;
  right: 0;
}

.. and that’s it! You’re now ready for thousands of items to be loaded into your list and still have great scrolling performance.

Conclusion: You have a Feed!

Using the code and techniques above, you can accomplish this very popular feed pattern that is widely used in many mobile apps for real-time communication. Questions? Feel free to comment below!

My name is Andrew McGivery. I currently work full time as an application developer at Manulife Financial in Canada. My current passion is building and leading highly engaged teams where employee happiness, learning, and growth is a priority.

20 Responses

  1. Cool! I did notice that the infinite scroll loading icon is only displayed the first time though. On the second (etc) loads it doesn’t show the icon. Bug?

    Awesome stuff though.

  2. melovett2

    Andrew, It’s so nice to have found this awesome resource! Thanks much. Im just getting started in Ionic and Angular and I am hoping you might answer a question.

    I have a small ionic angular hack app that returns a public wordpress feed successfully, however, when I try and retrieve my own wordpress site feed, it wont display. Installed the WP API REST plugin. I was trying your http get request code above. Been trying to get it working for quite a while. If you get a chance I sure would appreciate it if you take a peek at my code on github here:
    https://github.com/marklovett/ionic-blogApp/blob/master/www/js/app.js

    In the service, the commented out http request to the public feed works: // $http.jsonp(‘https://public-api.wordpress.com/rest/v1/freshly-pressed?callback=JSON_CALLBACK’)

    But, the other request to my site feed doesnt work:
    $http.get(‘http://marklovettphotography.com/wp-json/posts?results=10’)

    In chrome/network tab, I get status OK, and I can see 10 posts under Preview, but something is preventing them from displaying. Help! Thanks so much. I look forward to studying your articles!

    • Hey Mark. Thanks for your comment. Glad my stuff could be of help.

      So, there’s a couple things going on here. First, I did some reorganizing of your code in an Ionic Playground to move $scope to the controller. (You shouldn’t be doing ANYTHING with $scope in your services!)

      http://play.ionic.io/app/16a2e2304cfe

      If you check the console and do the pull to refresh, what I’m getting when it tries to send the request is that you are having a CORS problem.

      XMLHttpRequest cannot load http://marklovettphotography.com/wp-json/posts?results=10. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://play.ionic.io' is therefore not allowed access.
      

      If you check the network request it says 200 all good, but looking over the headers, there is indeed no ‘Access-Control-Allow-Origin’ header. What’s going on here is the browser is saying that the server hasn’t indicated that it allows cross domain requests.

      The reason why the jsonp is working is because jsonp works a bit differently. While $http.get sends an XHR GET request, jsonp inserts a script tag into the head of the page, waits for it to load, and then gets the response. While a normal XHR returns the data, jsonp returns the data wrapped in a javascript function call. If you go to each of the URLS you specified above (the jsonp one and the GET one), you’ll see what I mean.

      Hope this helps.

      • melovett2

        Yes. Absolutely this helps! Thank you for your quick insightful response. Also thank you for adding it the ionic playground. Didnt even know that existed!

        I thought this was the case, but not quite sure how to solve it. I tried adding the same query string to the end of my request but that didnt work. Might you care to hint at a solution?

        • So, what you need is for your server to send the header:

          Access-Control-Allow-Origin: *
          

          Depending on how you are hosting your blog will change how you accomplish this.

          For example, on Apache (from http://enable-cors.org/server_apache.html which is a useful site on the topic), you need to modify your .htaccess file.

          Header set Access-Control-Allow-Origin "*"
          
          • melovett2

            Hmm… Im using Bluehost. I thought the WP REST API plugin was supposed to enable it. Maybe I’ll call bluehost. I tried your refactored code on my local and couldnt get it to work with the public feed or my feed. Any particular reason for that? Did you try your code on the public feed and was it working?

          • melovett2

            I’m also wondering how safe it is to enable CORS on my site. Not sure if it would cause security problems.

      • melovett2

        Let me see if I understand you correctly. I checked out ionic playground and see your code changes…..moving the refresh code line as well as adding return in the service. The playground doesnt work for either request, whereas on my local machine the public feed works. Not sure why the public feed works on my local but not in the playground. Really not sure where to go from here to get it working?

  3. João Ferreira

    Hi there Andrew, thank you so much for explaining an elegant way to create a feed in ionic.
    The reason im posting this reply is that im trying to use you’re code to get my instagram feed working, i was going in a different direction with a more raw way of doing the feed and it was working fine apart from the refresher, and then i stumbled upon your solution and it looks realy nice, however im having some troubles getting it to work on my app, im pretty new to angular.js and ionic and im trying to build an app for my hometown racetrack.

    The problem: I used ur code as it is and made some alterations to fit my needs like the base_url and the way of putting the results into the item array, long story short i get an error TypeError: $scope.items.concat is not a function and it happens everytime loadMore() and doRefresh() gets called, i dont realy know whats wrong and i’ve been trying to figure it out for hours with no result, im realy sorry if im wasting your time with a newbie question, so i would like to thank you again in advance if you could take a look into it 🙂
    here is the GitHub link for app.js in my project: https://github.com/eLevarJoao/civrInstagram/blob/master/www/js/app.js
    There are 2 more controllers there but you could ignore since if i can make this instagram one works ill do the same for them 😀

    • Could you possibly make a http://play.ionic.io/ of this demonstrating the problem?

      • João Ferreira

        Hi andrew i solved the issue described above, but i ended up with another problem that i think its related to instagram api that has “min_tag_id” and a “next_max_tag_id” for pagination purposes, so getFeed() needs to get the data from the photos and the data from “min_tag_id” and “next_max_tag_id” so i can use them to the getNewPhotos() to add the new results to the items array & getOldPhotos() to load the older posts but i seem to be stuck, must be a problem with javascript and i have a plnkr project i can link, you might need to activate CORS on ur browser to be able to see it “kinda” working :p

        here is the link: http://plnkr.co/edit/GrUUEt2QVmX6WyoxHAv6

        Thank you alot Andrew for your time and for all the knowledge you publish, if you can help me ill gladly refund you for your time!

        • Some silly mistakes. 🙂

          Around line 25-26: You forgot =’s in your URL

          return $http.get(BASE_URL+'&min_tag_id'+NewInsta).then(function(response) {
          

          to

          return $http.get(BASE_URL+'&min_tag_id='+NewInsta).then(function(response) {
          

          Same with line 37-39:

          return $http.get(BASE_URL+'&min_tag_id'+NewInsta+'&max_tag_id'+nextUrl).then(function(response) {
          

          to

          return $http.get(BASE_URL+'&min_tag_id='+NewInsta+'&max_tag_id='+nextUrl).then(function(response) {
          

          Around line 29, I also added an if statement because it was causing an error. If there are no new posts, the pagination object is empty, so we should keep the old values.

          if(response.data.data.length > 0){
                      NewInsta = response.data.pagination.min_tag_id;
                      nextUrl = response.data.pagination.next_max_tag_id;
                    }
          

          Finally, I added var NewInsta = 0; to line 9 so that the variable was where it should be.. in your factory. 🙂

          The console is your friend. 🙂

          • João Ferreira

            Hi Andrew, i actually didint realise you were changing the code on that plnkr and i was changing it too so i missed your changes, can you link me your changes? i’ve been through this for hours and i found ihe url errors too! however i’ve made some changes and forked a new project that u can check here: http://plnkr.co/edit/VBmkbMALpDehhXhjf9he and the only problem is the last results, somehow i got duplicates and loadMore wont stop loading!

            Thank you alot Andrew, do you have an email that is linked to a paypal account?

          • You didn’t follow my instructions in my last reply. For both methods of getting newer or older posts, you need to check if any results were returned. If not, you shouldn’t be overwriting nextUrl or NewInsta.

            GetNewPhotos: function() {
                    return $http.get(BASE_URL + '&min_tag_id=' + NewInsta).then(function(response) {
            
            
                      items = response.data.data;
                      if(response.data.data.length > 0){
                        NewInsta = response.data.pagination.min_tag_id;
                      }
            
                      return items;
                    });
                  },
                  GetOldPhotos: function() {
                    return $http.get(BASE_URL + '&max_tag_id=' + nextUrl).then(function(response) {
            
                      if(response.data.data.length > 0){
                        nextUrl = response.data.pagination.next_max_tag_id;
                      }
                      
                      items = response.data.data;
            
            
                      return items;
            
                    });
                  }
            

            As for PayPal, andrew [at] this domain. Thanks. 🙂

  4. Hey Andrew, nice post!
    Do you have any example or tips about using collection-repeat with variable sizes?
    That would be a great help, since ng-repeat has a little buggy feeling in android, and it gets worst when using pull to refresh (any sugestion about this would be great, also).

    Thank you very much.

    • Don’t really have any specific advice off hand, but if you have a specific problem you may want to consider posting it to the Ionic Forum.

  5. Great work Andrew…could this be modified to do the following?

    1) login management (also allow users to post anonymously)
    2) create a new post from within the app (that’s allows image upload)
    3) allow users to Like and/or comment on a post (allow emoticons within a post)

Leave a Reply