Demo: ember-infinity.github.io/ember-infinity/
Simple, flexible infinite scrolling for Ember CLI Apps. Works out of the boxwith the Kaminari Gem.
Table of Contents:
Also:
ember install ember-infinity
As of v2.0.0
, we support Node 10 and above. We test against ember-source > 3.8
. Try out v2.0.0
. If it doesn't work or you don't have the right polyfills because you are on an older Ember version, then v1.4.9
will be your best bet.
ember-infinity
exposes 3 consumable items for your application.
infinity service
infinity-loader component
Route Mixin (deprecated and removed as of 1.1). If you still want to upgrade, but keep your Route mixins, install 1.0.2
. See old docs (here)[https://github.com/ember-infinity/ember-infinity/blob/2e0cb02e5845a97cad8783893cd7f4ddcf5dc5a7/README.md]
Ember Infinity is based on a component-service approach wherein your application is viewed as an interaction between your components (ephemeral state) and service (long term state).
As a result, we can intelligently store your model state to provide you the ability to cache and invalidate your cache when you need to. If you provide an optional infinityCache
timestamp (in ms), the infinity service model
hook will return the existing collection (and not make a network request) if the timestamp has not yet expired. Be careful as this will also circumvent your ability to receive fresh data on every route visit.
Moreover, you are not restricted to only fetching items in the route. Fetch away in any top-level component!
Let's see how simple it is to fetch a list of products. Instead of this.store.query('product')
or this.store.findAll('product')
, you simply invoke this.infinity.model('product')
and under the hood, ember-infinity
will query the store and manage fetching new records for you!
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class InfinityRoute extends Route {
@service infinity
model() {
return this.infinity.model('product');
}
}
Whenever the infinity-loader
component is in view, we will fetch the next page for you.
By default, ember-infinity
expects the server response to contain something about how many total pages it can expect to fetch. ember-infinity
defaults to looking for something like meta: { total_pages: 20 }
in your response. See Advanced Usage.
Let's look at a more complicated example using multiple infinity models in a route. Super easy!
import Route from '@ember/routing/route';
import RSVP from 'rsvp';
import { inject as service } from '@ember/service';
export default class InfinityRoute extends Route {
@service infinity;
model() {
return RSVP.hash({
products: this.infinity.model('product'),
users: this.infinity.model('user')
});
}
}
The infinity service also exposes 5 methods to fetch & mutate your collection:
The model
hook will fetch the first page you request and pass the result to your template.
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ProductsRoute extends Route {
@service infinity;
model() {
return this.infinity.model('product');
}
}
Moreover, if you want to intelligently cache your infinity model, pass { infinityCache: timestamp }
and we will return the cached collection if the future timestamp is less than the current time (in ms) if your users revisit the same route.
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ProductsRoute extends Route {
@service infinity;
model() {
return this.infinity.model('product', { infinityCache: 36000 }); // timestamp expiry of 10 minutes (in ms)
}
}
Let's see an example of using replace
.
import Controller from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class Products extends Route {
@service infinity;
actions: {
/**
@method filterProducts
@param {String} query
*/
async filterProducts(query) {
let products = await this.store.query('product', { query });
// model is the collection returned from the route model hook
this.infinity.replace(get(this, 'model'), products);
}
}
}
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ProductsRoute extends Route {
@service infinity
model() {
return this.infinity.model('product');
}
}
If you want to use closure actions with ember-infinity
and the infinity-loader
component, you need to be a little bit more explicit. Generally you should let the infinity service handle fetching records for you, but if you have a special case, this is how you would do it:
See the Ember docs on passing actions to components here.
import Controller from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class ProductsController extends Controller {
@service infinity
/**
Note this must be handled by you. An action will be called with the result of your Route model hook from the `infinity-loader` component, similar to this:
// closure action in infinity-loader component
get(this, 'infinityLoad')(infinityModelContent);
@method loadMoreProduct
@param {InfinityModel} products
*/
@action
loadMoreProduct(products) {
// Perform other logic ....
this.infinity.infinityLoad(products);
}
}
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ProductsRoute extends Route {
@service infinity
model() {
return this.infinity.model('product');
}
}
In the world of optimistic route transitions & skeleton UI, it's necessary to return a POJO or similar primitive to Ember's Route#model hook to ensure the transition is not blocked by promise.
model() {
return {
posts: this.infinity.model('post')
};
}
By default, ember-infinity
will send pagination parameters as part of a GET request as follows
/items?per_page=5&page=1
and will expect to receive metadata in the response payload via a total_pages
param in a meta
object
{
items: [
{id: 1, name: 'Test'},
{id: 2, name: 'Test 2'}
],
meta: {
total_pages: 3
}
}
If you wish to customize some aspects of the JSON contract for pagination, you may do so via your model hook. For example, you may want to customize the following:
Default:
per_page
,page
,meta.total_pages
,meta.count
,Example Customization shown below:
per
,pg
,meta.total
,meta.records
,import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ProductsRoute extends Route {
@service infinity
model() {
/* Load pages of the Product Model, starting from page 1, in groups of 12. Also set query params by handing off to infinityModel */
return this.infinity.model('product', { perPage: 12, startingPage: 1,
perPageParam: 'per', pageParam: 'pg', totalPagesParam: 'meta.total', countParam: 'meta.records' });
}
}
This will result in request query params being sent out as follows
/items?per=5&pg=1
and ember-infinity
will be set up to parse the total number of pages from a JSON response like this:
{
items: [
...
],
meta: {
total: 3
}
}
You can also prevent the per_page
or page
parameters from being sent by setting perPageParam
or pageParam
to null
, respectively.Moreover, if your backend passes the total number of records instead of total pages, then as it's replacement, set the countParam
.
Lastly, if you need some global configuration for these params, setup an extended infinity model to import in each of your routes.
import Route from '@ember/routing/route';
import { inject } from '@ember/service';
export default class ProductsRoute extends Route {
@service infinity
model() {
return this.infinity.model('product', {
perPage: 20,
startingPage: 1,
perPageParam: 'page[size]',
pageParam: 'page[number]'
});
},
}
If you are serving a continuously updating stream, it's helpful to keep trackof your place in the list while paginating to avoid duplicates. This is knownas cursor-based pagination and is common in popular APIs like Twitter,Facebook, and Instagram. Instead of relying on page_number
to paginate,you'll want to extract the min_id
or min_updated_at
from each page ofresults, so that you can fetch the next page without risking duplicates if newitems are added to the top of the list by other users in between requests.
To do this, implement the afterInfinityModel
hook as follows:
import Route from '@ember/routing/route';
import InfinityModel from 'ember-infinity/lib/infinity-model';
const ExtendedInfinityModel = InfinityModel.extend({
buildParams() {
let params = this._super(...arguments);
params['min_id']: get(this, '_minId'); // where `this` is the infinityModel instance
params['min_updated_at']: get(this, '_minUpdatedAt');
return params;
},
afterInfinityModel(posts) {
let loadedAny = posts.get('length') > 0;
this.set('canLoadMore', loadedAny);
this.set('_minId', posts.get('lastObject.id'));
this.set('_minUpdatedAt', posts.get('lastObject.updated_at').toISOString());
}
});
export default class PostsRoute extends Route {
@service infinity
model() {
return this.infinity.model('post', {}, ExtendedInfinityModel);
}
}
You can also provide additional static parameters to infinityModel
thatwill be passed to your backend server in addition to thepagination params. For instance, in the following example a category
parameter is added:
return this.infinity.model('product', { perPage: 12, startingPage: 1,
category: 'furniture' });
As of 1.0+, you can override or extend the behavior of Ember Infinity by providing a class that extends InfinityModel as a third argument to the Route#infinityModel hook.
import InfinityModel from 'ember-infinity/lib/infinity-model';
const ExtendedInfinityModel = InfinityModel.extend({
buildParams() {
let params = this._super(...arguments);
params['category_id'] = get(this, 'global.categoryId');
return params;
}
});
export default class ProductsRoute extends Route {
@service infinity
@service global
@computed('global.categoryId')
get categoryId() {
return get(this, 'global.categoryId');
}
model() {
const { global } = this;
this.infinity.model('product', {}, ExtendedInfinityModel.extend({ global }));
}
}
There is a lot you can do with this! Here is a simple use case where, say you have an API that does not return total_pages
or count
and you also don't need a loading spinner. Just set canLoadMore
to true and ember-infinity
will always try to fetch new records when the infinity-loader
comes into viewport.
import InfinityModel from 'ember-infinity/lib/infinity-model';
class ExtendedInfinityModel extends InfinityModel {
canLoadMore = true;
}
export default class ProductsRoute extends Route {
@service infinity
model() {
this.infinity.model('product', {}, ExtendedInfinityModel.extend());
}
}
isLoaded
says if the model is loaded after fetching results
loadingMore
says if the model is currently loading more items
isError
says if the fetch failed
The infinity model also provides following hooks:
afterInfinityModel
In some cases, a single call to your data store isn't enough. The afterInfinityModel
method is available for those cases when you need to chain together functions orpromises after fetching a model.
As a simple example, let's say you had a blog and just needed to set a propertyon each Post model after fetching all of them:
ember-infinity
Service approachimport Route from '@ember/routing/route';
import InfinityModel from 'ember-infinity/lib/infinity-model';
const ExtendedInfinityModel = InfinityModel.extend({
afterInfinityModel(posts) {
this.setEach('author', 'Jane Smith');
}
});
export default class PostsRoute extends Route {
@service infinity
model() {
return this.infinity.model('post', {}, ExtendedInfinityModel);
}
}
As a more complex example, let's say you had a blog with Posts and Authors as separaterelated models and you needed to extract an association from Posts. In that case,return the collection you want from afterInfinityModel:
import Route from '@ember/routing/route';
import InfinityModel from 'ember-infinity/lib/infinity-model';
const ExtendedInfinityModel = InfinityModel.extend({
afterInfinityModel(posts) {
return posts.mapBy('author').uniq();
}
});
export default class PostsRoute extends Route {
@service infinity
model() {
return this.infinity.model('post', {}, ExtendedInfinityModel);
}
}
afterInfinityModel
should return either a promise, ArrayProxy, or afalsy value. The returned value, when not falsy, will take the place of theresolved promise object and, if it is a promise, will hold execution until resolved.In the case of a falsy value, the original promise result is used.
So relating this to the examples above... In the first example, afterInfinityModel
does not have an explicit return defined so the original posts promise result is used.In the second example, the returned collection of authors is used.
infinityModelUpdated
Triggered on the route whenever new objects are pushed into the infinityModel.
Args:
lastPageLoaded
totalPages
infinityModel
infinityModelLoaded
Triggered on InfinityModel when is fully loaded.
Args:
import Route from '@ember/routing/route';
import InfinityModel from 'ember-infinity/lib/infinity-model';
const ExtendedInfinityModel = InfinityModel.extend({
infinityModelUpdated({ lastPageLoaded, totalPages, newObjects }) {
Ember.Logger.debug('updated with more items');
},
infinityModelLoaded({ totalPages }) {
Ember.Logger.info('no more items to load');
}
});
export default class ProductsRoute extends Route {
@service infinity
model() {
return this.infinity.model('product', { perPage: 12, startingPage: 1 }, ExtendedInfinityModel);
}
}
Chances are you'll want to scroll some source other than the default ember-data store to infinity. You can do that by injecting your store into the route and specifying the store to the infinityModel options:
import { inject as service } from '@ember/service';
export default class ProductsRoute extends Route {
@service infinity
@service('my-custom-store') customStore
model(params) {
return this.infinity.model('product', {
perPage: 12,
startingPage: 1,
store: this.customStore, // custom ember-data store or ember-redux / ember-cli-simple-store / your own hand rolled store (see dummy app)
storeFindMethod: 'findAll' // should return a promise (optional if custom store method uses `query`)
})
}
}
The infinity-loader
component as some extra options to make working with it easy! It is based on the IntersectionObserver API. In essence, instead of basing your scrolling on Events (synchronous), it instead behaves asynchronously, thus not blocking the main thread.
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
Closure actions are enabled in the 1.0.0
series.
Now, when the Infinity Model is fully loaded, the infinity-loader
will hide itself and set isDoneLoading
to true
.
Versions less than 1.0.0 called this property destroyOnInfinity
This simply stops the infinity-loader
from fetching triggering loads, so thatyou can work on its appearance.
By default, the infinity-loader
will just output a span
showing its status.
If you provide a block to the component, it will render the block instead ofrendering loadingText
or loadedText
. This will allow you to provide yourown custom markup or styling for the loading state.
.infinity-loader {
background-color: wheat;
&.reached-infinity {
background-color: lavender;
}
}
When the Infinity Model loads entirely, the reached-infinity
class is added to thecomponent.
ember generate infinity-template
Will install the default infinity-loader
template into your host app, atapp/templates/components/infinity-loader
.
You can optionally pass in a CSS style selector string. If not present, scrollable will default to using the window. This is useful for scrollable areas that are constrained in the window.
You can optionally pass an offset value. This value will be used when calculating if the bottom of the scrollable has been reached.
Default is 50ms. You can optionally pass a debounce time to delay loading the list when reach bottom of list
ember-infinity
with buttonYou can use the service loading magic of ember-infinity without using the InfinityLoader component.
load-more-button.js:
export default class InfinityComponent extends Component {
@service infinity
loadText = 'Load more';
loadedText = 'Loaded';
onClick() {
this.infinity.infinityLoad(this.infinityModel);
}
}
load-more-button.hbs:
template.hbs:
template.hbs:
The basic idea here is to:
If your route loads on page 3, it will fetch page 2 on load. As the user scrolls up, it will fetch page 1 and stop loading from there. If you are already on page 1, no actions will be fired to fetch the previous page.
Coming
Testing can be a breeze once you have an example. So here is an example! Note this is using Ember's new testing APIs.
Ember检查器是一个浏览器插件,用于调试Ember应用程序。 灰烬检查员包括以下主题 - S.No. 灰烬检查员方式和描述 1 安装Inspector 您可以安装Ember检查器来调试您的应用程序。 2 Object Inspector Ember检查器允许与Ember对象进行交互。 3 The View Tree 视图树提供应用程序的当前状态。 4 检查路由,数据选项卡和库信息 您可以看到检查
英文原文: http://emberjs.com/guides/getting-ember/index/ Ember构建 Ember的发布管理团队针对Ember和Ember Data维护了不同的发布方法。 频道 最新的Ember和Ember Data的 Release,Beta 和 Canary 构建可以在这里找到。每一个频道都提供了一个开发版、最小化版和生产版。更多关于不同频道的信息可以查看博客
ember-emojione ember-emojione is your emoji solution for Ember, based on the EmojiOne project. EmojiOne version 2 is used, which is free to use for everyone (CC BY-SA 4.0), you're only required to giv
Ember 3D Ember 3D is an Ember addon for using Three.js - an easy to use, lightweight, javascript 3D library. It is designed to: Prescribe a solid file structure to Three.js code using ES6 modules. Ena
Ember Table An addon to support large data set and a number of features around table. Ember Table canhandle over 100,000 rows without any rendering or performance issues. Ember Table 3.x supports: Emb
vscode-ember This is the VSCode extension to use the Ember Language Server. Features All features currently only work in Ember-CLI apps that use classic structure and are a rough first draft with a lo