From 12e6b09943a01c29f00d73b17f1a62279ef8b40d Mon Sep 17 00:00:00 2001 From: David Arvelo Date: Fri, 23 May 2014 23:25:20 -0400 Subject: [PATCH] Infinite Scroll on Posts List closes #2414 - Add PostsController with loadNextPage action - Collect and store pagination info from PostsRoute into PostsController - Use `store.filter` on PostsRoute model hook to auto-update posts list template as post models are added to the store - Add content list view and use it to check for scroll event - Make PostRoute only load a post that's already in the store, until we figure how to load multiple pages on refresh - Add util function from clientold that concats error messages from server response --- core/client/controllers/posts.js | 65 +++++++++++++++++++ core/client/routes/posts.js | 18 ++++- core/client/routes/posts/post.js | 8 ++- core/client/templates/posts.hbs | 4 +- core/client/utils/ajax.js | 41 +++++++++++- .../client/views/content-list-content-view.js | 29 +++++++++ 6 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 core/client/controllers/posts.js create mode 100644 core/client/views/content-list-content-view.js diff --git a/core/client/controllers/posts.js b/core/client/controllers/posts.js new file mode 100644 index 0000000000..a24e98712c --- /dev/null +++ b/core/client/controllers/posts.js @@ -0,0 +1,65 @@ +import { getRequestErrorMessage } from 'ghost/utils/ajax'; + +var PostsController = Ember.ArrayController.extend({ + // set from PostsRoute + paginationSettings: null, + + // holds the next page to load during infinite scroll + nextPage: null, + + // indicates whether we're currently loading the next page + isLoading: null, + + init: function () { + this._super(); + + var metadata = this.store.metadataFor('post'); + this.set('nextPage', metadata.pagination.next); + }, + + /** + * Takes an ajax response, concatenates any error messages, then generates an error notification. + * @param {jqXHR} response The jQuery ajax reponse object. + * @return + */ + reportLoadError: function (response) { + var message = 'A problem was encountered while loading more posts'; + + if (response) { + // Get message from response + message += ': ' + getRequestErrorMessage(response); + } else { + message += '.'; + } + + this.notifications.showError(message); + }, + + actions: { + /** + * Loads the next paginated page of posts into the ember-data store. Will cause the posts list UI to update. + * @return + */ + loadNextPage: function () { + var self = this, + store = this.get('store'), + nextPage = this.get('nextPage'), + paginationSettings = this.get('paginationSettings'); + + if (nextPage) { + this.set('isLoading', true); + this.set('paginationSettings.page', nextPage); + store.find('post', paginationSettings).then(function () { + var metadata = store.metadataFor('post'); + + self.set('nextPage', metadata.pagination.next); + self.set('isLoading', false); + }, function (response) { + self.reportLoadError(response); + }); + } + } + } +}); + +export default PostsController; diff --git a/core/client/routes/posts.js b/core/client/routes/posts.js index 5c3af9741a..5f9153ccac 100644 --- a/core/client/routes/posts.js +++ b/core/client/routes/posts.js @@ -1,11 +1,27 @@ import styleBody from 'ghost/mixins/style-body'; import AuthenticatedRoute from 'ghost/routes/authenticated'; +var paginationSettings = { + status: 'all', + staticPages: 'all', + page: 1, + limit: 15 +}; + var PostsRoute = AuthenticatedRoute.extend(styleBody, { classNames: ['manage'], model: function () { - return this.store.find('post', { status: 'all', staticPages: 'all' }); + // using `.filter` allows the template to auto-update when new models are pulled in from the server. + // we just need to 'return true' to allow all models by default. + return this.store.filter('post', paginationSettings, function () { + return true; + }); + }, + + setupController: function (controller, model) { + this._super(controller, model); + controller.set('paginationSettings', paginationSettings); }, actions: { diff --git a/core/client/routes/posts/post.js b/core/client/routes/posts/post.js index f5e0f1d13d..920b5b2129 100644 --- a/core/client/routes/posts/post.js +++ b/core/client/routes/posts/post.js @@ -1,5 +1,11 @@ export default Ember.Route.extend({ model: function (params) { - return this.store.find('post', params.post_id); + var post = this.modelFor('posts').findBy('id', params.post_id); + + if (!post) { + this.transitionTo('posts.index'); + } + + return post; } }); diff --git a/core/client/templates/posts.hbs b/core/client/templates/posts.hbs index 7d4a488271..d38136de66 100644 --- a/core/client/templates/posts.hbs +++ b/core/client/templates/posts.hbs @@ -6,7 +6,7 @@ {{#link-to "new" class="button button-add" title="New Post"}}{{/link-to}} -
+ {{#view 'content-list-content-view' tagName="section"}}
    {{#each itemController="posts/post" itemView="post-item-view" itemTagName="li"}} {{!-- @TODO: Restore functionality where 'featured' and 'page' classes are added for proper posts --}} @@ -30,7 +30,7 @@ {{/link-to}} {{/each}}
-
+ {{/view}}
{{outlet}} diff --git a/core/client/utils/ajax.js b/core/client/utils/ajax.js index 5b05dc365c..faac205034 100644 --- a/core/client/utils/ajax.js +++ b/core/client/utils/ajax.js @@ -1,4 +1,43 @@ /* global ic */ -export default window.ajax = function () { + +var ajax = window.ajax = function () { return ic.ajax.request.apply(null, arguments); }; + +// Used in API request fail handlers to parse a standard api error +// response json for the message to display +var getRequestErrorMessage = function (request) { + var message, + msgDetail; + + // Can't really continue without a request + if (!request) { + return null; + } + + // Seems like a sensible default + message = request.statusText; + + // If a non 200 response + if (request.status !== 200) { + try { + // Try to parse out the error, or default to "Unknown" + if (request.responseJSON.errors && Ember.isArray(request.responseJSON.errors)) { + + message = request.responseJSON.errors.map(function (errorItem) { + return errorItem.message; + }).join('; '); + } else { + message = request.responseJSON.error || "Unknown Error"; + } + } catch (e) { + msgDetail = request.status ? request.status + " - " + request.statusText : "Server was not available"; + message = "The server returned an error (" + msgDetail + ")."; + } + } + + return message; +}; + +export { getRequestErrorMessage, ajax }; +export default ajax; diff --git a/core/client/views/content-list-content-view.js b/core/client/views/content-list-content-view.js new file mode 100644 index 0000000000..2cd97c4ef3 --- /dev/null +++ b/core/client/views/content-list-content-view.js @@ -0,0 +1,29 @@ +var PostsListView = Ember.View.extend({ + classNames: ['content-list-content'], + + checkScroll: function (event) { + var element = event.target, + triggerPoint = 100, + controller = this.get('controller'), + isLoading = controller.get('isLoading'); + + // If we haven't passed our threshold, exit + if (isLoading || (element.scrollTop + element.clientHeight + triggerPoint <= element.scrollHeight)) { + return; + } + + controller.send('loadNextPage'); + }, + + didInsertElement: function () { + var el = this.$(); + el.bind('scroll', Ember.run.bind(this, this.checkScroll)); + }, + + willDestroyElement: function () { + var el = this.$(); + el.unbind('scroll'); + } +}); + +export default PostsListView;