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
This commit is contained in:
David Arvelo 2014-05-23 23:25:20 -04:00
parent a8164c7d3f
commit 12e6b09943
6 changed files with 160 additions and 5 deletions

View File

@ -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;

View File

@ -1,11 +1,27 @@
import styleBody from 'ghost/mixins/style-body'; import styleBody from 'ghost/mixins/style-body';
import AuthenticatedRoute from 'ghost/routes/authenticated'; import AuthenticatedRoute from 'ghost/routes/authenticated';
var paginationSettings = {
status: 'all',
staticPages: 'all',
page: 1,
limit: 15
};
var PostsRoute = AuthenticatedRoute.extend(styleBody, { var PostsRoute = AuthenticatedRoute.extend(styleBody, {
classNames: ['manage'], classNames: ['manage'],
model: function () { 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: { actions: {

View File

@ -1,5 +1,11 @@
export default Ember.Route.extend({ export default Ember.Route.extend({
model: function (params) { 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;
} }
}); });

View File

@ -6,7 +6,7 @@
</section> </section>
{{#link-to "new" class="button button-add" title="New Post"}}<span class="hidden">New Post</span>{{/link-to}} {{#link-to "new" class="button button-add" title="New Post"}}<span class="hidden">New Post</span>{{/link-to}}
</header> </header>
<section class="content-list-content"> {{#view 'content-list-content-view' tagName="section"}}
<ol class="posts-list"> <ol class="posts-list">
{{#each itemController="posts/post" itemView="post-item-view" itemTagName="li"}} {{#each itemController="posts/post" itemView="post-item-view" itemTagName="li"}}
{{!-- @TODO: Restore functionality where 'featured' and 'page' classes are added for proper posts --}} {{!-- @TODO: Restore functionality where 'featured' and 'page' classes are added for proper posts --}}
@ -30,7 +30,7 @@
{{/link-to}} {{/link-to}}
{{/each}} {{/each}}
</ol> </ol>
</section> {{/view}}
</section> </section>
<section class="content-preview js-content-preview"> <section class="content-preview js-content-preview">
{{outlet}} {{outlet}}

View File

@ -1,4 +1,43 @@
/* global ic */ /* global ic */
export default window.ajax = function () {
var ajax = window.ajax = function () {
return ic.ajax.request.apply(null, arguments); 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;

View File

@ -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;