Skip to content
This repository
Browse code

Implement infinite scroll for events

This introduces a new view LoadMoreView which triggers a call to `loadMore` on the controller when the view is visible in the current viewport. The callback then can trigger a loading of more items...
  • Loading branch information...
commit 68d1728ec26dae5062eae5be43d61083cfc34f14 1 parent b263a56
Clemens Müller authored August 12, 2012
38  app/lib/controller.js
... ...
@@ -1,17 +1,51 @@
1 1
 require('dashboard/core');
2 2
 
  3
+Dashboard.LoadMoreMixin = Ember.Mixin.create(Ember.Evented, {
  4
+  canLoadMore: true,
  5
+  autoFetch: true,
  6
+  currentPage: 1,
  7
+  resetLoadMore: function() {
  8
+    this.set('currentPage', 1);
  9
+  },
  10
+  loadMore: Ember.K
  11
+});
  12
+
3 13
 Dashboard.ApplicationController = Ember.Controller.extend();
4 14
 
5 15
 Dashboard.UserController = Ember.ObjectController.extend();
6 16
 
7 17
 Dashboard.RepositoryController = Ember.ObjectController.extend();
8 18
 
9  
-Dashboard.EventsController = Ember.ArrayController.extend();
  19
+Dashboard.EventsController = Ember.ArrayController.extend(Dashboard.LoadMoreMixin, {
  20
+  canLoadMore: function() {
  21
+    return this.get('currentPage') < 10;
  22
+  }.property('currentPage'),
  23
+
  24
+  loadMore: function() {
  25
+    if (this.get('canLoadMore')) {
  26
+      var page = this.incrementProperty('currentPage');
  27
+      this.get('target').send('loadMoreEvents', page);
  28
+    }
  29
+  }
  30
+});
10 31
 
11  
-Dashboard.RepositoriesController = Ember.ArrayController.extend({
  32
+Dashboard.RepositoriesController = Ember.ArrayController.extend(Dashboard.LoadMoreMixin, {
12 33
   sortProperties: 'updated_at'.w(),
13 34
   sortAscending: false,
14 35
   loadWatchedRepositories: function(username) {
15 36
     this.get('dataSource').watchedRepositories(username, this, 'addObjects');
  37
+  },
  38
+
  39
+  canLoadMore: function() {
  40
+    var reposCount = this.get('owner.publicRepos');
  41
+    var length = this.get('length');
  42
+    return true || (length < reposCount);
  43
+  }.property('owner.publicRepos', 'length'),
  44
+
  45
+  loadMore: function() {
  46
+    if (this.get('canLoadMore')) {
  47
+      var page = this.incrementProperty('currentPage');
  48
+      this.get('target').send('loadMoreRepos', page);
  49
+    }
16 50
   }
17 51
 });
32  app/lib/github_adapter.js
@@ -28,16 +28,17 @@ Dashboard.GitHubAdpater = DS.Adapter.extend({
28 28
     }
29 29
   },
30 30
 
31  
-  _invoke: function(target, callback) {
  31
+  _invoke: function(target, callback, query) {
32 32
     return function(data) {
33 33
       Ember.tryInvoke(target, callback, [data]);
34  
-    }
  34
+      Ember.tryInvoke(query, 'isLoadedCallback');
  35
+    };
35 36
   },
36 37
 
37 38
   _storeLoad: function(store, type, id) {
38 39
     return function(data) {
39 40
       store.load(type, id, data);
40  
-    }
  41
+    };
41 42
   },
42 43
 
43 44
   find: function(store, type, id) {
@@ -58,12 +59,12 @@ Dashboard.GitHubAdpater = DS.Adapter.extend({
58 59
       this.ajax('/users/%@/repos'.fmt(query.username), this._invoke(modelArray, 'load'));
59 60
 
60 61
     // events for a repository
61  
-    } else if (Dashboard.Event.detect(type) && query.username && query.repository) {
62  
-      this.ajax('/repos/%@/%@/events'.fmt(query.username, query.repository), this._invoke(modelArray, 'load'));
  62
+    } else if (Dashboard.Event.detect(type) && query.repoName) {
  63
+      this.ajax('/repos/%@/events?page=%@'.fmt(query.repoName, query.page || 1), this._invoke(modelArray, 'load', query));
63 64
 
64 65
     // events for a user
65  
-    } else if (Dashboard.Event.detect(type) && query.username && !query.repository) {
66  
-      this.ajax('/users/%@/events'.fmt(query.username), this._invoke(modelArray, 'load'));
  66
+    } else if (Dashboard.Event.detect(type) && query.username) {
  67
+      this.ajax('/users/%@/events?page=%@'.fmt(query.username, query.page || 1), this._invoke(modelArray, 'load', query));
67 68
     }
68 69
   }
69 70
 });
@@ -71,12 +72,15 @@ Dashboard.GitHubAdpater = DS.Adapter.extend({
71 72
 Dashboard.GitHubFixtureAdpater = Dashboard.GitHubAdpater.extend({
72 73
   PREFIX: 'http://localhost:9292/app/tests/mock_response_data',
73 74
   _ajax: function(url, callback) {
74  
-    Ember.$.ajax({
75  
-      url: this.PREFIX + url + '.json',
76  
-      context: this,
77  
-      success: function(data) {
78  
-       callback.call(this, {meta: {}, data: data});
79  
-      }
80  
-    });
  75
+    var encodedUrl = url.replace(/\?/, '%3F').replace(/\=/, '%3D');
  76
+    Ember.run.later(this, function() {
  77
+      Ember.$.ajax({
  78
+        url: this.PREFIX + encodedUrl + '.json',
  79
+        context: this,
  80
+        success: function(data) {
  81
+         callback.call(this, {meta: {}, data: data});
  82
+        }
  83
+      });
  84
+    }, 500);
81 85
   }
82 86
 });
62  app/lib/router.js
@@ -28,18 +28,30 @@ Dashboard.Router = Ember.Router.extend({
28 28
           var username = router.get('userController.id');
29 29
           var store = router.get('store');
30 30
 
  31
+          router.get('eventsController').resetLoadMore();
  32
+          router.get('repositoriesController').resetLoadMore();
  33
+
  34
+          // set current query
  35
+          var query = { username: username, isLoadedCallback: function() {
  36
+            router.set('eventsController.isLoading', false);
  37
+          }};
  38
+          router.set('eventsController.query', query);
  39
+          router.set('eventsController.isLoading', true);
  40
+
31 41
           // get repositories for user
32  
-          var repos = store.findQuery(Dashboard.Repository, { username: username });
33  
-          router.set('repositoriesController.content', repos);
  42
+          var userRepositories = store.findQuery(Dashboard.Repository, { username: username });
34 43
 
35 44
           // get events performed by user
36  
-          var userEvents = store.findQuery(Dashboard.Event, { username: username });
37  
-          router.set('eventsController.content', userEvents);
  45
+          var filter = function(data) {
  46
+            if (Ember.get(data, 'savedData.org.login') === username) { return true; }
  47
+            return Ember.get(data, 'savedData.actor.login') === username;
  48
+          };
  49
+          var userEvents = store.filter(Dashboard.Event, query, filter);
38 50
 
39 51
           // connect user with events and watched repositories
40 52
           router.get('applicationController').connectOutlet('user');
41  
-          router.get('userController').connectOutlet('repositories', 'repositories');
42  
-          router.get('userController').connectOutlet('events', 'events');
  53
+          router.get('userController').connectOutlet('repositories', 'repositories', userRepositories);
  54
+          router.get('userController').connectOutlet('events', 'events', userEvents);
43 55
         }
44 56
       }),
45 57
 
@@ -48,22 +60,48 @@ Dashboard.Router = Ember.Router.extend({
48 60
         connectOutlets: function(router, context) {
49 61
           var username = router.get('userController.id');
50 62
           var repoName = context.repository;
  63
+          var name = '%@/%@'.fmt(username, repoName);
  64
+
  65
+          router.get('eventsController').resetLoadMore();
  66
+
  67
+          // set current query
  68
+          var query = {
  69
+            repoName: name,
  70
+            isLoadedCallback: function() {
  71
+              router.set('eventsController.isLoading', false);
  72
+            }
  73
+          };
  74
+          router.set('eventsController.query', query);
  75
+          router.set('eventsController.isLoading', true);
51 76
 
52 77
           // fetch repo for current user
53  
-          var repo = router.get('store').find(Dashboard.Repository, '%@/%@'.fmt(username, repoName));
54  
-          router.set('repositoryController.content', repo);
  78
+          var repository = router.get('store').find(Dashboard.Repository, name);
55 79
 
56  
-          var events = router.get('store').findQuery(Dashboard.Event, {
57  
-            username: username,
58  
-            repository: repoName
59  
-          });
  80
+          // get all events for this repository
  81
+          var filter = function(data) {
  82
+            return Ember.get(data, 'savedData.repo.name') === name;
  83
+          };
  84
+          var events = router.get('store').filter(Dashboard.Event, query, filter);
60 85
 
61 86
           // connect repository and events
  87
+          router.set('repositoryController.content', repository);
62 88
           router.get('applicationController').connectOutlet('repository');
63 89
           router.get('repositoryController').connectOutlet('events', 'events', events);
64 90
         }
65 91
       }),
66 92
 
  93
+      loadMoreEvents: function(router, page) {
  94
+        var query = router.get('eventsController.query');
  95
+        query.page = page;
  96
+        router.get('store').findQuery(Dashboard.Event, query);
  97
+        router.set('eventsController.isLoading', true);
  98
+      },
  99
+      loadMoreRepos: function(router, page) {
  100
+        var username = router.get('userController.id');
  101
+        var query = { username: username };
  102
+        var store = router.get('store').findQuery(Dashboard.Repository, query);
  103
+      },
  104
+
67 105
       showUserOfEvent: function(router, evt) {
68 106
         var e = evt.context;
69 107
         var username = e.get('actor.login');
13  app/lib/view.js
... ...
@@ -1,10 +1,23 @@
1 1
 require('dashboard/core');
2 2
 require('dashboard/handlebars_helpers');
  3
+require('jquery.inview');
3 4
 
4 5
 Dashboard.ApplicationView = Ember.View.extend({
5 6
   templateName: 'application'
6 7
 });
7 8
 
  9
+Dashboard.LoadMoreView = Ember.View.extend({
  10
+  templateName: 'loadMore',
  11
+  didInsertElement: function() {
  12
+    if (this.get('controller.autoFetch')) {
  13
+      var view = this;
  14
+      this.$().bind('inview', function(event, isInView, visiblePartX, visiblePartY) {
  15
+        if (isInView) Ember.tryInvoke(view.get('controller'), 'loadMore');
  16
+      });
  17
+    }
  18
+  }
  19
+});
  20
+
8 21
 Dashboard.EventsView = Ember.View.extend({
9 22
   templateName: 'events',
10 23
   EventView: Ember.View.extend({
BIN  app/static/img/ajax-loader.gif
3  app/templates/events.handlebars
@@ -2,7 +2,6 @@
2 2
 <ul class="unstyled">
3 3
     {{#each event in controller}}
4 4
         {{view view.EventView eventBinding="event"}}
5  
-    {{else}}
6  
-        loading ...
7 5
     {{/each}}
  6
+    {{view Dashboard.LoadMoreView controllerBinding="view.controller" }}
8 7
 </ul>
9  app/templates/loadMore.handlebars
... ...
@@ -0,0 +1,9 @@
  1
+{{#if isLoading}}
  2
+    fetching some more stuff <img width="10" src="img/ajax-loader.gif" >
  3
+{{else}}
  4
+    {{#if canLoadMore}}
  5
+        <a {{action "loadMore" target="controller" }} >click to load more</a>
  6
+    {{else}}
  7
+        <strong><em>no more items</em></strong>
  8
+    {{/if}}
  9
+{{/if}}
2  app/templates/repositories.handlebars
@@ -8,7 +8,5 @@
8 8
             <div>{{repository.description}}</div>
9 9
             <dl class="dl-horizontal">
10 10
         </li>
11  
-    {{else}}
12  
-        loading ...
13 11
     {{/each}}
14 12
 </ul>
118  app/vendor/jquery.inview.js
... ...
@@ -0,0 +1,118 @@
  1
+/**
  2
+ * author Christopher Blum
  3
+ *    - based on the idea of Remy Sharp, http://remysharp.com/2009/01/26/element-in-view-event-plugin/
  4
+ *    - forked from http://github.com/zuk/jquery.inview/
  5
+ */
  6
+(function ($) {
  7
+  var inviewObjects = {}, viewportSize, viewportOffset,
  8
+      d = document, w = window, documentElement = d.documentElement, expando = $.expando;
  9
+
  10
+  $.event.special.inview = {
  11
+    add: function(data) {
  12
+      inviewObjects[data.guid + "-" + this[expando]] = { data: data, $element: $(this) };
  13
+    },
  14
+
  15
+    remove: function(data) {
  16
+      try { delete inviewObjects[data.guid + "-" + this[expando]]; } catch(e) {}
  17
+    }
  18
+  };
  19
+
  20
+  function getViewportSize() {
  21
+    var mode, domObject, size = { height: w.innerHeight, width: w.innerWidth };
  22
+
  23
+    // if this is correct then return it. iPad has compat Mode, so will
  24
+    // go into check clientHeight/clientWidth (which has the wrong value).
  25
+    if (!size.height) {
  26
+      mode = d.compatMode;
  27
+      if (mode || !$.support.boxModel) { // IE, Gecko
  28
+        domObject = mode === 'CSS1Compat' ?
  29
+          documentElement : // Standards
  30
+          d.body; // Quirks
  31
+        size = {
  32
+          height: domObject.clientHeight,
  33
+          width:  domObject.clientWidth
  34
+        };
  35
+      }
  36
+    }
  37
+
  38
+    return size;
  39
+  }
  40
+
  41
+  function getViewportOffset() {
  42
+    return {
  43
+      top:  w.pageYOffset || documentElement.scrollTop   || d.body.scrollTop,
  44
+      left: w.pageXOffset || documentElement.scrollLeft  || d.body.scrollLeft
  45
+    };
  46
+  }
  47
+
  48
+  function checkInView() {
  49
+    var $elements = $(), elementsLength, i = 0;
  50
+
  51
+    $.each(inviewObjects, function(i, inviewObject) {
  52
+      var selector  = inviewObject.data.selector,
  53
+          $element  = inviewObject.$element;
  54
+      $elements = $elements.add(selector ? $element.find(selector) : $element);
  55
+    });
  56
+
  57
+    elementsLength = $elements.length;
  58
+    if (elementsLength) {
  59
+      viewportSize   = viewportSize   || getViewportSize();
  60
+      viewportOffset = viewportOffset || getViewportOffset();
  61
+
  62
+      for (; i<elementsLength; i++) {
  63
+        // Ignore elements that are not in the DOM tree
  64
+        if (!$.contains(documentElement, $elements[i])) {
  65
+          continue;
  66
+        }
  67
+
  68
+        var $element      = $($elements[i]),
  69
+            elementSize   = { height: $element.height(), width: $element.width() },
  70
+            elementOffset = $element.offset(),
  71
+            inView        = $element.data('inview'),
  72
+            visiblePartX,
  73
+            visiblePartY,
  74
+            visiblePartsMerged;
  75
+        
  76
+        // Don't ask me why because I haven't figured out yet:
  77
+        // viewportOffset and viewportSize are sometimes suddenly null in Firefox 5.
  78
+        // Even though it sounds weird:
  79
+        // It seems that the execution of this function is interferred by the onresize/onscroll event
  80
+        // where viewportOffset and viewportSize are unset
  81
+        if (!viewportOffset || !viewportSize) {
  82
+          return;
  83
+        }
  84
+        
  85
+        if (elementOffset.top + elementSize.height > viewportOffset.top &&
  86
+            elementOffset.top < viewportOffset.top + viewportSize.height &&
  87
+            elementOffset.left + elementSize.width > viewportOffset.left &&
  88
+            elementOffset.left < viewportOffset.left + viewportSize.width) {
  89
+          visiblePartX = (viewportOffset.left > elementOffset.left ?
  90
+            'right' : (viewportOffset.left + viewportSize.width) < (elementOffset.left + elementSize.width) ?
  91
+            'left' : 'both');
  92
+          visiblePartY = (viewportOffset.top > elementOffset.top ?
  93
+            'bottom' : (viewportOffset.top + viewportSize.height) < (elementOffset.top + elementSize.height) ?
  94
+            'top' : 'both');
  95
+          visiblePartsMerged = visiblePartX + "-" + visiblePartY;
  96
+          if (!inView || inView !== visiblePartsMerged) {
  97
+            $element.data('inview', visiblePartsMerged).trigger('inview', [true, visiblePartX, visiblePartY]);
  98
+          }
  99
+        } else if (inView) {
  100
+          $element.data('inview', false).trigger('inview', [false]);
  101
+        }
  102
+      }
  103
+    }
  104
+  }
  105
+
  106
+  $(w).bind("scroll resize", function() {
  107
+    viewportSize = viewportOffset = null;
  108
+  });
  109
+
  110
+  // Use setInterval in order to also make sure this captures elements within
  111
+  // "overflow:scroll" elements or elements that appeared in the dom tree due to
  112
+  // dom manipulation and reflow
  113
+  // old: $(window).scroll(checkInView);
  114
+  //
  115
+  // By the way, iOS (iPad, iPhone, ...) seems to not execute, or at least delays
  116
+  // intervals while the user scrolls. Therefore the inview event might fire a bit late there
  117
+  setInterval(checkInView, 250);
  118
+})(jQuery);

0 notes on commit 68d1728

Please sign in to comment.
Something went wrong with that request. Please try again.