{"id":13426670,"url":"https://github.com/adopted-ember-addons/ember-infinity","last_synced_at":"2025-04-07T18:11:26.367Z","repository":{"id":29126703,"uuid":"32656538","full_name":"adopted-ember-addons/ember-infinity","owner":"adopted-ember-addons","description":":zap: Simple, flexible Infinite Scroll for Ember CLI Apps.","archived":false,"fork":false,"pushed_at":"2024-09-23T14:33:30.000Z","size":3865,"stargazers_count":377,"open_issues_count":24,"forks_count":131,"subscribers_count":13,"default_branch":"main","last_synced_at":"2024-10-29T17:34:34.707Z","etag":null,"topics":["ember","ember-cli","ember-cli-addon","ember-data","infinity","infinity-loader","javascript"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adopted-ember-addons.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-03-22T01:00:39.000Z","updated_at":"2024-09-23T14:32:00.000Z","dependencies_parsed_at":"2024-01-09T05:01:51.084Z","dependency_job_id":"53b539a0-11c3-4298-ba68-e74e3aafffd6","html_url":"https://github.com/adopted-ember-addons/ember-infinity","commit_stats":{"total_commits":367,"total_committers":63,"mean_commits":5.825396825396825,"dds":0.7247956403269755,"last_synced_commit":"1dce72b394df6dc9f8cdba3dff88922e3c820872"},"previous_names":["ember-infinity/ember-infinity"],"tags_count":98,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adopted-ember-addons%2Fember-infinity","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adopted-ember-addons%2Fember-infinity/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adopted-ember-addons%2Fember-infinity/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adopted-ember-addons%2Fember-infinity/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adopted-ember-addons","download_url":"https://codeload.github.com/adopted-ember-addons/ember-infinity/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247261273,"owners_count":20910085,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ember","ember-cli","ember-cli-addon","ember-data","infinity","infinity-loader","javascript"],"created_at":"2024-07-31T00:01:40.735Z","updated_at":"2025-04-07T18:11:26.320Z","avatar_url":"https://github.com/adopted-ember-addons.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# Ember Infinity\n\n![Download count all time](https://img.shields.io/npm/dt/ember-infinity.svg)\n[![npm version](https://badge.fury.io/js/ember-infinity.svg)](http://badge.fury.io/js/ember-infinity)\n[![Ember Observer Score](http://emberobserver.com/badges/ember-infinity.svg)](http://emberobserver.com/addons/ember-infinity)\n\nDemo: [adopted-ember-addons.github.io/ember-infinity/](https://adopted-ember-addons.github.io/ember-infinity/)\n\nSimple, flexible infinite scrolling for Ember CLI Apps. Works out of the box\nwith the [Kaminari Gem](https://github.com/amatsuda/kaminari.git).\n\nTable of Contents:\n\n- [Installation](#installation)\n- [Basic Usage](#basic-usage)\n- [Service Methods](#service-methods)\n- [Non Blocking Model Hook](#non-blocking-model-hook)\n- [Advanced Usage](#advanced-usage)\n- [Model Event Hooks](#model-event-hooks)\n- [Custom Store](#custom-store)\n- [Infinity Loader](#infinity-loader)\n- [Load Previous Pages](#load-previous-pages)\n- [Ember Concurrency Usage](#ember-concurrency-usage)\n- [Testing](#testing)\n\nAlso:\n\n![Fastbootable](https://s3.amazonaws.com/f.cl.ly/items/392o0m1N0R2515091z25/ember-infinity.gif?v=13181cd7)\n\n## Installation\n\n`ember install ember-infinity`\n\nWe test against `ember-source \u003e= 3.28`. 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.\n\n## Basic Usage\n\n`ember-infinity` exposes 3 consumable items for your application.\n\n1. **infinity service**\n\n2. **infinity-loader component**\n\n3. **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/adopted-ember-addons/ember-infinity/blob/2e0cb02e5845a97cad8783893cd7f4ddcf5dc5a7/README.md]\n\n### Service Component Approach\n\nEmber 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).\n\nAs 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.\n\nMoreover, you are not restricted to only fetching items in the route. Fetch away in any top-level component!\n\nLet'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!\n\n```js\nimport Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nexport default class InfinityRoute extends Route {\n  @service infinity;\n\n  model() {\n    return this.infinity.model('product');\n  }\n}\n```\n\n```hbs\n{{#each model as |product|}}\n  \u003ch1\u003e{{product.name}}\u003c/h1\u003e\n  \u003ch2\u003e{{product.description}}\u003c/h2\u003e\n{{/each}}\n\n\u003cInfinityLoader @infinityModel={{model}} /\u003e\n```\n\nWhenever the `infinity-loader` component is in view, we will fetch the next page for you.\n\n### Response Meta Expectations\n\nBy 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](#advanced-usage).\n\n### Multiple Infinity Models in one Route\n\nLet's look at a more complicated example using multiple infinity models in a route. Super easy!\n\n```js\nimport Route from '@ember/routing/route';\nimport RSVP from 'rsvp';\nimport { inject as service } from '@ember/service';\n\nexport default class InfinityRoute extends Route {\n  @service infinity;\n\n  model() {\n    return RSVP.hash({\n      products: this.infinity.model('product'),\n      users: this.infinity.model('user'),\n    });\n  }\n}\n```\n\n```hbs\n{{!-- templates/products.hbs --}}\n\n\u003caside\u003e\n  {{#each model.users as |user|}}\n    \u003ch1\u003e{{user.username}}\u003c/h1\u003e\n  {{/each}}\n\n  \u003cInfinityLoader @infinityModel={{model.users}} /\u003e\n\u003c/aside\u003e\n\n\u003csection\u003e\n  {{#each model.products as |product|}}\n    \u003ch1\u003e{{product.name}}\u003c/h1\u003e\n    \u003ch2\u003e{{product.description}}\u003c/h2\u003e\n  {{/each}}\n\n  \u003cInfinityLoader @infinityModel={{model.products}} /\u003e\n\u003csection\u003e\n```\n\n## Service Methods\n\nThe infinity service also exposes 5 methods to fetch \u0026 mutate your collection:\n\n1. model\n2. replace\n3. flush\n4. pushObjects\n5. unshiftObjects\n\nThe `model` hook will fetch the first page you request and pass the result to your template.\n\n```js\nimport Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nexport default class ProductsRoute extends Route {\n  @service infinity;\n\n  model() {\n    return this.infinity.model('product');\n  }\n}\n```\n\nMoreover, 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.\n\n```js\nimport Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nexport default class ProductsRoute extends Route {\n  @service infinity;\n\n  model() {\n    return this.infinity.model('product', { infinityCache: 36000 }); // timestamp expiry of 10 minutes (in ms)\n  }\n}\n```\n\nLet's see an example of using `replace`.\n\n```js\nimport Controller from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nexport default class Products extends Route {\n  @service infinity;\n\n  actions: {\n    /**\n      @method filterProducts\n      @param {String} query\n    */\n    async filterProducts(query) {\n      let products = await this.store.query('product', { query });\n      // model is the collection returned from the route model hook\n      this.infinity.replace(get(this, 'model'), products);\n    }\n  }\n}\n```\n\n```js\nimport Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nexport default class ProductsRoute extends Route {\n  @service infinity;\n\n  model() {\n    return this.infinity.model('product');\n  }\n}\n```\n\n```hbs\n\u003cinput\n  type='search'\n  placeholder='Search Products'\n  oninput={{action 'filterProducts'}}\n/\u003e\n\n{{#each model as |product|}}\n  \u003ch1\u003e{{product.name}}\u003c/h1\u003e\n  \u003ch2\u003e{{product.description}}\u003c/h2\u003e\n{{/each}}\n\n\u003cInfinityLoader @infinityModel={{model.products}} /\u003e\n```\n\n### Closure Actions\u003ca name=\"ClosureActions\"\u003e\u003c/a\u003e\n\nIf 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:\n\nSee the Ember docs on passing actions to components [here](https://guides.emberjs.com/v3.0.0/components/triggering-changes-with-actions/#toc_passing-the-action-to-the-component).\n\n```js\nimport Controller from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\nimport { action } from '@ember/object';\n\nexport default class ProductsController extends Controller {\n  @service infinity;\n\n  /**\n    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:\n    // closure action in infinity-loader component\n    get(this, 'infinityLoad')(infinityModelContent);\n\n    @method loadMoreProduct\n    @param {InfinityModel} products\n  */\n  @action\n  loadMoreProduct(products) {\n    // Perform other logic ....\n    this.infinity.infinityLoad(products);\n  }\n}\n```\n\n```js\nimport Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nexport default class ProductsRoute extends Route {\n  @service infinity;\n\n  model() {\n    return this.infinity.model('product');\n  }\n}\n```\n\n```hbs\n{{! some nested component in your template file where action bubbling does not reach your route }}\n{{#each model as |product|}}\n  \u003ch1\u003e{{product.name}}\u003c/h1\u003e\n  \u003ch2\u003e{{product.description}}\u003c/h2\u003e\n{{/each}}\n\n{{infinity-loader infinityModel=model infinityLoad=(action 'loadMoreProduct')}}\n```\n\n## Non-Blocking Model Hook\n\nIn the world of optimistic route transitions \u0026 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.\n\n```js\nmodel() {\n  return {\n    posts: this.infinity.model('post')\n  };\n}\n```\n\n## Advanced Usage\n\n### JSON Request/Response Customization\n\nBy default, `ember-infinity` will send pagination parameters as part of a GET request as follows\n\n```\n/items?per_page=5\u0026page=1\n```\n\nand will expect to receive metadata in the response payload via a `total_pages` param in a `meta` object\n\n```js\n{\n  items: [\n    {id: 1, name: 'Test'},\n    {id: 2, name: 'Test 2'}\n  ],\n  meta: {\n    total_pages: 3\n  }\n}\n```\n\nIf 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:\n\nDefault:\n\n- perPageParam: `per_page`,\n- pageParam: `page`,\n- totalPagesParam: `meta.total_pages`,\n- countParam: `meta.count`,\n\nExample Customization shown below:\n\n- perPageParam: `per`,\n- pageParam: `pg`,\n- totalPagesParam: `meta.total`,\n- countParam: `meta.records`,\n\n```js\nimport Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nexport default class ProductsRoute extends Route {\n  @service infinity;\n\n  model() {\n    /* Load pages of the Product Model, starting from page 1, in groups of 12. Also set query params by handing off to infinityModel */\n    return this.infinity.model('product', {\n      perPage: 12,\n      startingPage: 1,\n      perPageParam: 'per',\n      pageParam: 'pg',\n      totalPagesParam: 'meta.total',\n      countParam: 'meta.records',\n    });\n  }\n}\n```\n\nThis will result in request query params being sent out as follows\n\n```\n/items?per=5\u0026pg=1\n```\n\nand `ember-infinity` will be set up to parse the total number of pages from a JSON response like this:\n\n```js\n{\n  items: [\n    ...\n  ],\n  meta: {\n    total: 3\n  }\n}\n```\n\nYou can also prevent the `per_page` or `page` parameters from being sent by setting `perPageParam` or `pageParam` to `null`, respectively.\nMoreover, if your backend passes the total number of records instead of total pages, then as it's replacement, set the `countParam`.\n\nLastly, if you need some global configuration for these params, setup an extended infinity model to import in each of your routes.\n\n### Example JSON-API customization\n\n```js\nimport Route from '@ember/routing/route';\nimport { inject } from '@ember/service';\n\nexport default class ProductsRoute extends Route {\n  @service infinity\n\n  model() {\n    return this.infinity.model('product', {\n      perPage: 20,\n      startingPage: 1,\n      perPageParam: 'page[size]',\n      pageParam: 'page[number]'\n    });\n  },\n}\n```\n\n### Cursor-based pagination\n\nIf you are serving a continuously updating stream, it's helpful to keep track\nof your place in the list while paginating to avoid duplicates. This is known\nas **cursor-based pagination** and is common in popular APIs like Twitter,\nFacebook, and Instagram. Instead of relying on `page_number` to paginate,\nyou'll want to extract the `min_id` or `min_updated_at` from each page of\nresults, so that you can fetch the next page without risking duplicates if new\nitems are added to the top of the list by other users in between requests.\n\nTo do this, implement the `afterInfinityModel` hook as follows:\n\n```js\nimport Route from '@ember/routing/route';\nimport InfinityModel from 'ember-infinity/lib/infinity-model';\n\nconst ExtendedInfinityModel = InfinityModel.extend({\n  buildParams() {\n    let params = this._super(...arguments);\n    params['min_id']: get(this, '_minId'); // where `this` is the infinityModel instance\n    params['min_updated_at']: get(this, '_minUpdatedAt');\n    return params;\n  },\n  afterInfinityModel(posts) {\n    let loadedAny = posts.length \u003e 0;\n    this.set('canLoadMore', loadedAny);\n\n    this.set('_minId', posts.lastObject.id);\n    this.set('_minUpdatedAt', posts.lastObject.updated_at.toISOString());\n  }\n});\n\nexport default class PostsRoute extends Route {\n  @service infinity\n\n  model() {\n    return this.infinity.model('post', {}, ExtendedInfinityModel);\n  }\n}\n```\n\n### Static parameters\n\nYou can also provide additional static parameters to `infinityModel` that\nwill be passed to your backend server in addition to the\npagination params. For instance, in the following example a `category`\nparameter is added:\n\n```js\nreturn this.infinity.model('product', {\n  perPage: 12,\n  startingPage: 1,\n  category: 'furniture',\n});\n```\n\n### Extending InfinityModel\n\nAs 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.\n\n```js\nimport InfinityModel from 'ember-infinity/lib/infinity-model';\n\nconst ExtendedInfinityModel = InfinityModel.extend({\n  buildParams() {\n    let params = this._super(...arguments);\n    params['category_id'] = get(this, 'global.categoryId');\n    return params;\n  },\n});\n\nexport default class ProductsRoute extends Route {\n  @service infinity;\n  @service global;\n\n  @computed('global.categoryId')\n  get categoryId() {\n    return get(this, 'global.categoryId');\n  }\n\n  model() {\n    const { global } = this;\n    this.infinity.model(\n      'product',\n      {},\n      ExtendedInfinityModel.extend({ global })\n    );\n  }\n}\n```\n\nThere 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.\n\n```js\nimport InfinityModel from 'ember-infinity/lib/infinity-model';\n\nclass ExtendedInfinityModel extends InfinityModel {\n  canLoadMore = true;\n}\n\nexport default class ProductsRoute extends Route {\n  @service infinity;\n\n  model() {\n    this.infinity.model('product', {}, ExtendedInfinityModel.extend());\n  }\n}\n```\n\n## Model Public Properties\n\n- **isLoaded**\n\n`isLoaded` says if the model is loaded after fetching results\n\n- **loadingMore**\n\n`loadingMore` says if the model is currently loading more items\n\n- **isError**\n\n`isError` says if the fetch failed\n\n## Model Event Hooks\n\nThe infinity model also provides following hooks:\n\n**afterInfinityModel**\n\nIn some cases, a single call to your data store isn't enough. The `afterInfinityModel`\nmethod is available for those cases when you need to chain together functions or\npromises after fetching a model.\n\nAs a simple example, let's say you had a blog and just needed to set a property\non each Post model after fetching all of them:\n\n#### Using the `ember-infinity` Service approach\n\n```js\nimport Route from '@ember/routing/route';\nimport InfinityModel from 'ember-infinity/lib/infinity-model';\n\nconst ExtendedInfinityModel = InfinityModel.extend({\n  afterInfinityModel(posts) {\n    this.setEach('author', 'Jane Smith');\n  },\n});\n\nexport default class PostsRoute extends Route {\n  @service infinity;\n\n  model() {\n    return this.infinity.model('post', {}, ExtendedInfinityModel);\n  }\n}\n```\n\nAs a more complex example, let's say you had a blog with Posts and Authors as separate\nrelated models and you needed to extract an association from Posts. In that case,\nreturn the collection you want from afterInfinityModel:\n\n```js\nimport Route from '@ember/routing/route';\nimport InfinityModel from 'ember-infinity/lib/infinity-model';\n\nconst ExtendedInfinityModel = InfinityModel.extend({\n  afterInfinityModel(posts) {\n    return posts.mapBy('author').uniq();\n  },\n});\n\nexport default class PostsRoute extends Route {\n  @service infinity;\n\n  model() {\n    return this.infinity.model('post', {}, ExtendedInfinityModel);\n  }\n}\n```\n\n`afterInfinityModel` should return either a promise, ArrayProxy, or a\nfalsy value. The returned value, when not falsy, will take the place of the\nresolved promise object and, if it is a promise, will hold execution until resolved.\nIn the case of a falsy value, the original promise result is used.\n\nSo relating this to the examples above... In the first example, `afterInfinityModel`\ndoes not have an explicit return defined so the original posts promise result is used.\nIn the second example, the returned collection of authors is used.\n\n**infinityModelUpdated**\n\nTriggered on the route whenever new objects are pushed into the infinityModel.\n\n**Args:**\n\n- lastPageLoaded\n\n- totalPages\n\n- infinityModel\n\n**infinityModelLoaded**\n\nTriggered on InfinityModel when is fully loaded.\n\n**Args:**\n\n- totalPages\n\n```js\nimport Route from '@ember/routing/route';\nimport InfinityModel from 'ember-infinity/lib/infinity-model';\n\nconst ExtendedInfinityModel = InfinityModel.extend({\n  infinityModelUpdated({ lastPageLoaded, totalPages, newObjects }) {\n    Ember.Logger.debug('updated with more items');\n  },\n  infinityModelLoaded({ totalPages }) {\n    Ember.Logger.info('no more items to load');\n  },\n});\n\nexport default class ProductsRoute extends Route {\n  @service infinity;\n\n  model() {\n    return this.infinity.model(\n      'product',\n      { perPage: 12, startingPage: 1 },\n      ExtendedInfinityModel\n    );\n  }\n}\n```\n\n## Custom store\n\nChances 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:\n\n```js\nimport { inject as service } from '@ember/service';\n\nexport default class ProductsRoute extends Route {\n  @service infinity;\n  @service('my-custom-store') customStore;\n\n  model(params) {\n    return this.infinity.model('product', {\n      perPage: 12,\n      startingPage: 1,\n      store: this.customStore, // custom ember-data store or ember-redux / ember-cli-simple-store / your own hand rolled store (see test-app)\n      storeFindMethod: 'findAll', // should return a promise (optional if custom store method uses `query`)\n    });\n  }\n}\n```\n\n## Infinity Loader\n\nThe `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.\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API\n\n- **infinityLoad**\n\nClosure actions are enabled in the `1.0.0` series.\n\n```hbs\n\u003cInfinityLoader\n  @infinityModel={{model}}\n  @infinityLoad={{action 'loadMoreProducts'}}\n/\u003e\n```\n\n- **hideOnInfinity**\n\n```hbs\n\u003cInfinityLoader @infinityModel={{model}} @hideOnInfinity={{true}} /\u003e\n```\n\nNow, when the Infinity Model is fully loaded, the `infinity-loader` will hide itself and set `isDoneLoading` to `true`.\n\n**_Versions less than 1.0.0 called this property destroyOnInfinity_**\n\n- **developmentMode**\n\n```hbs\n\u003cInfinityLoader\n  @infinityModel={{model}}\n  @infinityLoad={{action 'loadMoreProducts'}}\n  @developmentMode={{true}}\n/\u003e\n```\n\nThis simply stops the `infinity-loader` from fetching triggering loads, so that\nyou can work on its appearance.\n\n- **loadingText \u0026 loadedText**\n\n```hbs\n\u003cInfinityLoader\n  @infinityModel={{model}}\n  @infinityLoad={{action 'loadMoreProducts'}}\n  loadingText='Loading...'\n  loadedText='Loaded!'\n/\u003e\n```\n\nBy default, the `infinity-loader` will just output a `span` showing its status.\n\n- **Providing a block**\n\n```hbs\n{{#infinity-loader infinityModel=model infinityLoad=(action 'infinityLoad')}}\n  \u003cimg src='loading-spinner.gif' /\u003e\n{{/infinity-loader}}\n```\n\nIf you provide a block to the component, it will render the block instead of\nrendering `loadingText` or `loadedText`. This will allow you to provide your\nown custom markup or styling for the loading state.\n\n- **reached-infinity Class Name**\n\n```scss\n.infinity-loader {\n  background-color: wheat;\n  \u0026.reached-infinity {\n    background-color: lavender;\n  }\n}\n```\n\nWhen the Infinity Model loads entirely, the `reached-infinity` class is added to the\ncomponent.\n\n- **infinity-template generator**\n\n`ember generate infinity-template`\n\nWill install the default `infinity-loader` template into your host app, at\n`app/templates/components/infinity-loader`.\n\n- **scrollable**\n\n```hbs\n\u003cInfinityLoader @scrollable='#content' /\u003e\n```\n\nYou 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.\n\n- **loadPrevious**\n\n```hbs\n\u003cInfinityLoader @loadPrevious={{true}} /\u003e\n\n\u003cul\u003e...\u003c/ul\u003e\n\n\u003cInfinityLoader /\u003e\n\nTo load elements above your list on load, place an infinity-loader component\nabove the list with `loadPrevious=true`.\n```\n\n- **triggerOffset**\n\n```hbs\n\u003cInfinityLoader @triggerOffset={{offset}} /\u003e\n```\n\nYou can optionally pass an offset value. This value will be used when calculating if the bottom of the scrollable has been reached.\n\n- **eventDebounce**\n\n```hbs\n\u003cInfinityLoader @eventDebounce={{50}} /\u003e\n```\n\nDefault is 50ms. You can optionally pass a debounce time to delay loading the list when reach bottom of list\n\n### Use `ember-infinity` with button\n\nYou can use the service loading magic of ember-infinity without using the InfinityLoader component.\n\nload-more-button.js:\n\n```js\nexport default class InfinityComponent extends Component {\n  @service infinity;\n\n  loadText = 'Load more';\n  loadedText = 'Loaded';\n\n  onClick() {\n    this.infinity.infinityLoad(this.infinityModel);\n  }\n}\n```\n\nload-more-button.hbs:\n\n```hbs\n{{#if @infinityModel.reachedInfinity}}\n  \u003cbutton\u003e{{loadedText}}\u003c/button\u003e\n{{else}}\n  \u003cbutton\u003e{{loadText}}\u003c/button\u003e\n{{/if}}\n```\n\ntemplate.hbs:\n\n```hbs\n\u003cul class='test-list'\u003e\n  {{#each @model as |item|}}\n    \u003cli\u003e{{item.name}}\u003c/li\u003e\n  {{/each}}\n\u003c/ul\u003e\n\n\u003cLoadMoreButton @infinityModel={{model}} /\u003e\n```\n\n### Delay start of infinite loading until user has indicated they would like to load more\n\ntemplate.hbs:\n\n```hbs\n{{#if hasClickedLoadMore}}\n  {{infinity-loader infinityModel=model triggerOffset=400}}\n{{else}}\n  \u003cbutton {{action (toggle 'hasClickedLoadMore' this)}}\u003eLoad more\u003c/button\u003e\n{{/if}}\n```\n\n## Load Previous Pages\n\nThe basic idea here is to:\n\n1. Place an infinity-loader component above and below your content.\n2. Ensure loadPrevious is set to true on the infinity-loader above the content.\n\nIf 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.\n\n```hbs\n\u003cul\u003e\n  \u003cInfinityLoader\n    @infinityModel={{model}}\n    @loadPrevious={{true}}\n    @loadedText={{null}}\n    @loadingText={{null}}\n  /\u003e\n\n  {{#each @model as |item|}}\n    \u003cli\u003e{{item.id}}. {{item.name}}\u003c/li\u003e\n  {{/each}}\n\n  \u003cInfinityLoader\n    @infinityModel={{model}}\n    @loadingText='Loading more awesome records...'\n    @loadedText='Loaded all the records!'\n    @triggerOffset={{500}}\n  /\u003e\n\u003c/ul\u003e\n```\n\n## Ember Concurrency Usage\n\n**Coming**\n\n## Testing\n\nTesting can be a breeze once you have an example. So here is an example! Note this is using Ember's new testing APIs.\n\n```hbs\nimport { find, findAll, visit, waitFor, waitUntil } from '@ember/test-helpers';\ntest('fetches more data when scrolled into viewport', async function(assert) {\nawait visit('/infinity-scrollable'); assert.equal(findAll('.t-items').length,\n10); assert.equal(findAll('.infinity-scrollable.inactive').length, 1, 'component\nis inactive before fetching more data');\ndocument.querySelector('.infinity-scrollable').scrollIntoView(); await\nwaitFor('.infinity-scrollable.inactive');\nassert.equal(findAll('.t-items').length, 20);\nassert.equal(findAll('.infinity-scrollable.inactive').length, 1, 'component is\ninactive after fetching more data'); }); test('fetch more data using waitUntil',\nasync function(assert) { await visit('/infinity-scrollable');\nassert.equal(findAll('.t-items').length, 10);\nassert.equal(findAll('.infinity-scrollable.inactive').length, 1, 'component is\ninactive before fetching more data');\ndocument.querySelector('.infinity-scrollable').scrollIntoView(); await\nwaitUntil(() =\u003e { return findAll('.t-items').length === 20; });\nassert.equal(findAll('.t-items').length, 20);\nassert.equal(findAll('.infinity-scrollable.inactive').length, 1, 'component is\ninactive after fetching more data'); });\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadopted-ember-addons%2Fember-infinity","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadopted-ember-addons%2Fember-infinity","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadopted-ember-addons%2Fember-infinity/lists"}