From c4528cd6f519f85bbd1f64a7c17b8e66c4acd1bb Mon Sep 17 00:00:00 2001 From: Bruce Williams Date: Mon, 6 Apr 2015 20:55:07 -0700 Subject: [PATCH] Support additional parameters for infinityModel() Additional options can be provided in the object passed to InfinityRoute's infinityModel() for use by DS.Store.find(). The perPage and startingPage properties will still be passed as per_page and page params, respectively. Tests are provided using a new /category/:category route in the dummy application. --- README.md | 14 ++ addon/mixins/route.js | 100 +++++++------ .../infinity-route-with-meta-test.js | 77 ++++++++++ ...js => infinity-route-without-meta-test.js} | 2 +- tests/dummy/app/models/post.js | 5 +- tests/dummy/app/router.js | 1 + tests/dummy/app/routes/category.js | 9 ++ tests/dummy/app/templates/category.hbs | 9 ++ tests/unit/mixins/route-test.js | 132 ++++++++++++++++++ 9 files changed, 305 insertions(+), 44 deletions(-) create mode 100644 tests/acceptance/infinity-route-with-meta-test.js rename tests/acceptance/{infinity-route-test.js => infinity-route-without-meta-test.js} (99%) create mode 100644 tests/dummy/app/routes/category.js create mode 100644 tests/dummy/app/templates/category.hbs diff --git a/README.md b/README.md index 0d1928e4..f3e8001a 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,20 @@ When the new records are loaded, they will automatically be pushed into the Mode ## Advanced Usage +### infinityModel + +You can also provide additional parameters to `infinityModel` that +will be passed to your backend server in addition to the +pagination params. For instance, in the following example a `category` +parameter is added: + +```js +return this.infinityModel("product", { perPage: 12, startingPage: 1, + category: "furniture" }); +``` + +### infinity-loader + The `infinity-loader` component as some extra options to make working with it easy! * **destroyOnInfinity** diff --git a/addon/mixins/route.js b/addon/mixins/route.js index e8445dbd..b7f604e2 100644 --- a/addon/mixins/route.js +++ b/addon/mixins/route.js @@ -28,6 +28,14 @@ export default Ember.Mixin.create({ */ _currentPage: 0, + /** + @private + @property _extraParams + @type Object + @default {} + */ + _extraParams: {}, + /** @private @property _loadingMore @@ -84,13 +92,18 @@ export default Ember.Mixin.create({ this.set('_infinityModelName', modelName); - options = options || {}; + options = options ? Ember.merge({}, options) : {}; var startingPage = options.startingPage || 1; var perPage = options.perPage || this.get('_perPage'); + delete options.startingPage; + delete options.perPage; + this.set('_perPage', perPage); + this.set('_extraParams', options); - var promise = this.store.find(modelName, { page: startingPage, per_page: perPage }); + var params = Ember.merge({ page: startingPage, per_page: perPage }, options); + var promise = this.store.find(modelName, params); promise.then( function(infinityModel) { @@ -108,48 +121,53 @@ export default Ember.Mixin.create({ return promise; }, - actions: { - /** - Trigger a load of the next page of results. - - @method infinityLoad - @return {Boolean} - */ - infinityLoad: function() { - var _this = this; - var nextPage = this.get('_currentPage') + 1; - var perPage = this.get('_perPage'); - var totalPages = this.get('_totalPages'); - var model = this.get('controller.model'); - var modelName = this.get('_infinityModelName'); - - if (!this.get('_loadingMore') && this.get('_canLoadMore')) { - this.set('_loadingMore', true); - - var promise = this.store.find(modelName, { page: nextPage, per_page: perPage }); - promise.then( - function(infinityModel) { - model.pushObjects(infinityModel.get('content')); - _this.set('_loadingMore', false); - _this.set('_currentPage', nextPage); - Ember.run.scheduleOnce('afterRender', _this, 'infinityModelUpdated', { lastPageLoaded: nextPage, totalPages: totalPages, newObjects: infinityModel }); - if (!_this.get('_canLoadMore')) { - _this.set('controller.model.reachedInfinity', true); - Ember.run.scheduleOnce('afterRender', _this, 'infinityModelLoaded', { totalPages: totalPages }); - } - }, - function() { - _this.set('_loadingMore', false); - throw new Ember.Error("You must pass a Model Name to infinityModel"); + /** + Trigger a load of the next page of results. + + @method infinityLoad + @return {Boolean} + */ + _infinityLoad: function() { + var _this = this; + var nextPage = this.get('_currentPage') + 1; + var perPage = this.get('_perPage'); + var totalPages = this.get('_totalPages'); + var model = this.get('controller.model'); + var modelName = this.get('_infinityModelName'); + + if (!this.get('_loadingMore') && this.get('_canLoadMore')) { + this.set('_loadingMore', true); + + var params = Ember.merge({ page: nextPage, per_page: perPage }, this.get('_extraParams')); + var promise = this.store.find(modelName, params); + promise.then( + function(infinityModel) { + model.pushObjects(infinityModel.get('content')); + _this.set('_loadingMore', false); + _this.set('_currentPage', nextPage); + Ember.run.scheduleOnce('afterRender', _this, 'infinityModelUpdated', { lastPageLoaded: nextPage, totalPages: totalPages, newObjects: infinityModel }); + if (!_this.get('_canLoadMore')) { + _this.set('controller.model.reachedInfinity', true); + Ember.run.scheduleOnce('afterRender', _this, 'infinityModelLoaded', { totalPages: totalPages }); } - ); - } else { - if (!this.get('_canLoadMore')) { - this.set('controller.model.reachedInfinity', true); - Ember.run.scheduleOnce('afterRender', _this, 'infinityModelLoaded', { totalPages: totalPages }); + }, + function() { + _this.set('_loadingMore', false); + throw new Ember.Error("You must pass a Model Name to infinityModel"); } + ); + } else { + if (!this.get('_canLoadMore')) { + this.set('controller.model.reachedInfinity', true); + Ember.run.scheduleOnce('afterRender', _this, 'infinityModelLoaded', { totalPages: totalPages }); } - return false; + } + return false; + }, + + actions: { + infinityLoad: function() { + this._infinityLoad(); } } }); diff --git a/tests/acceptance/infinity-route-with-meta-test.js b/tests/acceptance/infinity-route-with-meta-test.js new file mode 100644 index 00000000..b7c81672 --- /dev/null +++ b/tests/acceptance/infinity-route-with-meta-test.js @@ -0,0 +1,77 @@ +import Ember from 'ember'; +import { module, test } from 'qunit'; +import startApp from '../helpers/start-app'; +import Pretender from 'pretender'; + +var App, server; + +var posts = [ + { id: 1, name: "Squarepusher", category: "a" }, + { id: 2, name: "Aphex Twin", category: "b" }, + { id: 3, name: "Universal Indicator", category: "a" }, + { id: 4, name: "Mike & Rich", category: "b" }, + { id: 5, name: "Alroy Road Tracks", category: "a" }, + { id: 6, name: "AFX", category: "b" } +]; + +module('Acceptance: Infinity Route', { + setup: function() { + App = startApp(); + server = new Pretender(function() { + this.get('/posts', function(request) { + var body, subset, perPage, startPage, offset; + + if (request.queryParams.category) { + subset = posts.filter(function(post) { + return post.category === request.queryParams.category; + }); + } else { + subset = posts; + } + perPage = parseInt(request.queryParams.per_page); + startPage = parseInt(request.queryParams.page); + + var pageCount = Math.ceil(subset.length / perPage); + offset = perPage * (startPage - 1); + subset = subset.slice(offset, offset + perPage); + + body = { posts: subset, meta: { total_pages: pageCount } }; + + return [200, {"Content-Type": "application/json"}, JSON.stringify(body)]; + }); + }); + }, + teardown: function() { + Ember.run(App, 'destroy'); + server.shutdown(); + } +}); + +test('it works when meta is present in payload', function(assert) { + visit('/'); + + andThen(function() { + var postsTitle = find('#posts-title'); + var postList = find('ul'); + var infinityLoader = find('.infinity-loader'); + + assert.equal(postsTitle.text(), "Listing Posts"); + assert.equal(postList.find('li').length, 6); + assert.equal(infinityLoader.hasClass('reached-infinity'), true); + }); +}); + +test('it works with parameters', function(assert) { + visit('/category/a?per_page=2'); + + andThen(function() { + var postsTitle = find('#posts-title'); + var postList = find('ul'); + var infinityLoader = find('.infinity-loader'); + + assert.equal(postsTitle.text(), "Listing Posts using Parameters"); + assert.equal(postList.find('li').length, 2); + assert.equal(postList.find('li:first-child').text(), "Squarepusher"); + assert.equal(infinityLoader.hasClass('reached-infinity'), false); + }); +}); diff --git a/tests/acceptance/infinity-route-test.js b/tests/acceptance/infinity-route-without-meta-test.js similarity index 99% rename from tests/acceptance/infinity-route-test.js rename to tests/acceptance/infinity-route-without-meta-test.js index 1fa43f9a..2f7d2456 100644 --- a/tests/acceptance/infinity-route-test.js +++ b/tests/acceptance/infinity-route-without-meta-test.js @@ -36,4 +36,4 @@ test('it works when meta is not present in payload', function(assert) { assert.equal(postList.find('li').length, 2); assert.equal(infinityLoader.hasClass('reached-infinity'), true); }); -}); \ No newline at end of file +}); diff --git a/tests/dummy/app/models/post.js b/tests/dummy/app/models/post.js index 3fd06667..40807bc4 100644 --- a/tests/dummy/app/models/post.js +++ b/tests/dummy/app/models/post.js @@ -1,5 +1,6 @@ import DS from 'ember-data'; export default DS.Model.extend({ - name: DS.attr('string') -}); \ No newline at end of file + name: DS.attr('string'), + category: DS.attr('string') +}); diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 90619ae8..ce83027e 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -7,6 +7,7 @@ var Router = Ember.Router.extend({ Router.map(function() { this.route('home', { path: '/' }); + this.route('category', { path: '/category/:category' }); }); export default Router; diff --git a/tests/dummy/app/routes/category.js b/tests/dummy/app/routes/category.js new file mode 100644 index 00000000..8b98bca6 --- /dev/null +++ b/tests/dummy/app/routes/category.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; +import InfinityRoute from 'ember-infinity/mixins/route'; + +export default Ember.Route.extend(InfinityRoute, { + model: function(params) { + return this.infinityModel('post', { category: params.category, + perPage: 2 }); + } +}); diff --git a/tests/dummy/app/templates/category.hbs b/tests/dummy/app/templates/category.hbs new file mode 100644 index 00000000..8c9657aa --- /dev/null +++ b/tests/dummy/app/templates/category.hbs @@ -0,0 +1,9 @@ +

Listing Posts using Parameters

+ + + +{{infinity-loader infinityModel=model}} diff --git a/tests/unit/mixins/route-test.js b/tests/unit/mixins/route-test.js index 179396ee..9b9f8900 100644 --- a/tests/unit/mixins/route-test.js +++ b/tests/unit/mixins/route-test.js @@ -49,3 +49,135 @@ test('it can not use infinityModel without a Model Name', function(assert) { assert.equal(infinityError.message, "You must pass a Model Name to infinityModel"); }); +test('it sets state before it reaches the end', function(assert) { + + var RouteObject = Ember.Route.extend(RouteMixin, { + model: function() { + return this.infinityModel('item'); + } + }); + var route = RouteObject.create(); + + var dummyStore = { + find: function() { + return new Ember.RSVP.Promise(function(resolve) { + Ember.run(this, resolve, Ember.Object.create({ + items: [{id: 1, name: 'Test'}], + meta: { + total_pages: 31 + } + })); + }); + } + }; + + route.store = dummyStore; + + var model; + Ember.run(function() { + route.model().then(function(result) { + model = result; + }); + }); + + assert.equal(31, route.get('_totalPages')); + assert.equal(1, route.get('_currentPage')); + assert.equal(true, route.get('_canLoadMore')); + assert.ok(Ember.$.isEmptyObject(route.get('_extraParams'))); + assert.ok(!model.get('reachedInfinity'), 'Should not reach infinity'); +}); + +test('it sets state when it reaches the end', function(assert) { + + var RouteObject = Ember.Route.extend(RouteMixin, { + model: function() { + return this.infinityModel('item', {startingPage: 31}); + } + }); + var route = RouteObject.create(); + + var dummyStore = { + find: function() { + return new Ember.RSVP.Promise(function(resolve) { + Ember.run(this, resolve, Ember.Object.create({ + items: [{id: 1, name: 'Test'}], + meta: { + total_pages: 31 + } + })); + }); + } + }; + + route.store = dummyStore; + + var model; + Ember.run(function() { + route.model().then(function(result) { + model = result; + }); + }); + + assert.equal(31, route.get('_totalPages')); + assert.equal(31, route.get('_currentPage')); + assert.ok(Ember.$.isEmptyObject(route.get('_extraParams'))); + assert.equal(false, route.get('_canLoadMore')); + assert.ok(model.get('reachedInfinity'), 'Should reach infinity'); +}); + +test('it uses extra params when loading more data', function(assert) { + + assert.expect(8); + + var RouteObject = Ember.Route.extend(RouteMixin, { + model: function() { + return this.infinityModel('item', {extra: 'param'}); + } + }); + var route = RouteObject.create(); + + var dummyStore = { + find: function(name, params) { + assert.equal('param', params.extra); + return new Ember.RSVP.Promise(function(resolve) { + Ember.run(this, resolve, Ember.Object.create({ + items: [{id: 1, name: 'Test'}], + pushObjects: Ember.K, + meta: { + total_pages: 2 + } + })); + }); + } + }; + + route.store = dummyStore; + + var model; + Ember.run(function() { + route.model().then(function(result) { + model = result; + }); + }); + + // The controller needs to be set so _infinityLoad() can call + // pushObjects() + var dummyController = Ember.Object.create({ + model: model + }); + route.set('controller', dummyController); + + assert.equal('param', route.get('_extraParams.extra')); + assert.equal(true, route.get('_canLoadMore')); + + // Load more + Ember.run(function() { + route._infinityLoad(); + }); + + assert.equal('param', route.get('_extraParams.extra')); + assert.equal(false, route.get('_canLoadMore')); + assert.equal(2, route.get('_currentPage')); + assert.ok(model.get('reachedInfinity'), 'Should reach infinity'); + +});