Skip to content

Commit

Permalink
InfinityModels managed by service + closure actions (#271)
Browse files Browse the repository at this point in the history
  • Loading branch information
snewcomer committed Apr 13, 2018
1 parent 7530726 commit 90b905f
Show file tree
Hide file tree
Showing 32 changed files with 1,473 additions and 668 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.js
@@ -1,4 +1,7 @@
module.exports = {
globals: {
server: true,
},
root: true,
parserOptions: {
ecmaVersion: 2017,
Expand Down
1 change: 1 addition & 0 deletions .jshintrc
@@ -1,5 +1,6 @@
{
"predef": [
"server",
"document",
"window",
"-Promise"
Expand Down
2 changes: 0 additions & 2 deletions .travis.yml
Expand Up @@ -41,8 +41,6 @@ before_install:
- export PATH=$HOME/.yarn/bin:$PATH

install:
- npm install -g bower
- bower install
- yarn install --no-lockfile --non-interactive

script:
Expand Down
237 changes: 223 additions & 14 deletions README.md
Expand Up @@ -24,7 +24,16 @@ Also:

## Basic Usage

Importing the ember-infinity Route Mixin and extend your route will give you access to this.infinifyModel in your model hook.
`ember-infinity` exposes 3 consumable items for your application.

· **Route Mixin**

· **infinity-loader component**

· **infinity-loader service**


Importing the `ember-infinity` Route Mixin and extending your route will give you access to `this.infinifyModel` in your model hook.

```js
import Route from '@ember/routing/route';
Expand All @@ -38,7 +47,7 @@ export default Route.extend(InfinityRoute, {
});
```

Then, you'll need to add the Infinity Loader component to your template, like so, in which `model` is an instance of InfinityModel returned from your model hook.
Then, you'll need to add the `infinity-loader` component to your template, like so, in which `model` is an instance of InfinityModel returned from your model hook.

```hbs
{{#each model as |product|}}
Expand All @@ -49,12 +58,191 @@ Then, you'll need to add the Infinity Loader component to your template, like so
{{infinity-loader infinityModel=model}}
```

Now, whenever the `infinity-loader` is in view, it will send an action to the route
(the one where you initialized the infinityModel) to start loading the next page.
Now, whenever the `infinity-loader` component is in view, it will send an action to the route or to your specific `loadMoreProduct` action
(the one where you initialized the infinityModel) to start loading the next page. This method uses action bubbling, which may not be the preferred way of passing data around your application. See [Closure Actions](#ClosureActions).


When the new records are loaded, they will automatically be pushed into the Model array.

Lastly, 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](#AdvancedUsage).
Lastly, 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](#AdvancedUsage).


### Closure Actions<a name="ClosureActions"></a>

If you want to use closure actions with `ember-infinity` and the `infinity-loader` component, you need to be a little bit more explicit. No more secret bubbling of an `infinityLoad` action up to your route. This is how your code will look like with controller actions.

See 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).

```js
import Controller from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default Controller.extend({
infinityLoader: service(),

actions: {
/**
Note this must be handled by you. An action will be called with the result of your Route model hook from the infinityLoader component, similar to this:
// closure action in infinity-loader component
get(this, 'infinityLoad')(infinityModelContent);
@method loadMoreProduct
@param {InfinityModel} products
*/
loadMoreProduct(products) {
get(this, 'infinityLoader').infinityLoad(products);
}
}
});
```

```js
import Route from '@ember/routing/route';
import InfinityRoute from "ember-infinity/mixins/route";

export default Route.extend(InfinityRoute, {
model() {
return this.infinityModel("product");
}
});
```

```hbs
{{!-- some nested component in your template file where action bubbling does not reach your route --}}
{{#each model as |product|}}
<h1>{{product.name}}</h1>
<h2>{{product.description}}</h2>
{{/each}}
{{infinity-loader infinityModel=model infinityLoad=(action "loadMoreProduct")}}
```

### Multiple Infinity Models in one Route

Let's look at a more complicated example using multiple infinity models in a route.

```js
import Controller from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default Controller.extend({
infinityLoader: service(),

actions: {
/**
Note this must be handled by you. An action will be called with the result of your Route model hook from the infinityLoader component, similar to this:
// closure action in infinity-loader component
get(this, 'infinityLoad')(infinityModelContent);
@method loadMoreProduct
@param {InfinityModel} products
*/
loadMoreProduct(products) {
get(this, 'infinityLoader').infinityLoad(products);
}
/**
@method loadMoreUsers
@param {InfinityModel} users
*/
loadMoreUsers(users) {
get(this, 'infinityLoader').infinityLoad(users);
}
}
});
```

```js
import Route from '@ember/routing/route';
import RSVP from 'rsvp';
import InfinityRoute from "ember-infinity/mixins/route";

export default Route.extend(InfinityRoute, {
model() {
return RSVP.hash({
products: this.infinityModel("product"),
users: this.infinityModel("user")
});
}
});
```

```hbs
{{!-- templates/products.hbs --}}
<aside>
{{#each model.users as |user|}}
<h1>{{user.username}}</h1>
{{/each}}
{{infinity-loader infinityModel=model.users infinityLoad=(action "loadMoreUsers")}}
</aside>
<section>
{{#each model.products as |product|}}
<h1>{{product.name}}</h1>
<h2>{{product.description}}</h2>
{{/each}}
{{infinity-loader infinityModel=model.products infinityLoad=(action "loadMoreProduct")}}
<section>
```

The ability to use closure actions will be available in the `1.0.0-beta` series. Also, this method uses Controllers. Despite what you may have heard, Controllers are a great primitive in Ember's ecosystem. Their singleton nature is great for handling queryParams and actions propagated from somewhere in your component tree.


### Service Methods

The infinity-loader service exposes 4 methods:

1. replace
2. flush
3. pushObjects
3. unshiftObjects

Let's see an example of using `replace`.

```js
import Controller from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default Controller.extend({
infinityLoader: service(),

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
get(this, 'infinityLoader').replace(get(this, 'model'), products);
}
}
});
```

```js
import Route from '@ember/routing/route';
import InfinityRoute from "ember-infinity/mixins/route";

export default Route.extend(InfinityRoute, {
model() {
return this.infinityModel("product");
}
});
```

```hbs
<input type="search" placeholder="Search Products" oninput={{action "filterProducts"}} />
{{#each model as |product|}}
<h1>{{product.name}}</h1>
<h2>{{product.description}}</h2>
{{/each}}
{{infinity-loader infinityModel=model infinityLoad=(action "loadMoreProduct")}}
```

### Non-Blocking Model Hooks

Expand Down Expand Up @@ -296,20 +484,21 @@ Triggered on the route whenever new objects are pushed into the infinityModel.

**Args:**

* totalPages

**infinityModelLoaded**

Triggered on the route when the infinityModel is fully loaded.

**Args:**

* lastPageLoaded

* totalPages

* infinityModel

Triggered on the route when the infinityModel is fully loaded.

**Args:**

* totalPages


```js
import Ember from 'ember';
Expand Down Expand Up @@ -357,10 +546,23 @@ The `infinity-loader` component as some extra options to make working with it ea
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
* **infinityLoad**
Closure actions are enabled in the `1.0.0-beta` series.
```hbs
{{infinity-loader
infinityModel=model
infinityLoad=(action "loadMoreProducts")}}
```
* **hideOnInfinity**
```hbs
{{infinity-loader infinityModel=model hideOnInfinity=true}}
{{infinity-loader
infinityModel=model
infinityLoad=(action "loadMoreProducts")
hideOnInfinity=true}}
```
Now, when the Infinity Model is fully loaded, the `infinity-loader` will hide itself.
Expand All @@ -370,7 +572,10 @@ Now, when the Infinity Model is fully loaded, the `infinity-loader` will hide it
* **developmentMode**
```hbs
{{infinity-loader infinityModel=model developmentMode=true}}
{{infinity-loader
infinityModel=model
infinityLoad=(action "loadMoreProducts")
developmentMode=true}}
```
This simply stops the `infinity-loader` from fetching triggering loads, so that
Expand All @@ -379,15 +584,19 @@ you can work on its appearance.
* **loadingText & loadedText**
```hbs
{{infinity-loader infinityModel=model loadingText="Loading..." loadedText="Loaded!"}}
{{infinity-loader
infinityModel=model
infinityLoad=(action "loadMoreProducts")
loadingText="Loading..."
loadedText="Loaded!"}}
```
By default, the `infinity-loader` will just output a `span` showing its status.
* **Providing a block**
```hbs
{{#infinity-loader infinityModel=model}}
{{#infinity-loader infinityModel=model infinityLoad=(action "infinityLoad")}}
<img src="loading-spinner.gif" />
{{/infinity-loader}}
```
Expand Down
23 changes: 19 additions & 4 deletions addon/components/infinity-loader.js
Expand Up @@ -6,8 +6,8 @@ import { get, set, computed, observer, defineProperty } from '@ember/object';
import Component from '@ember/component';

const InfinityLoaderComponent = Component.extend(InViewportMixin, {
classNames: ["infinity-loader"],
classNameBindings: ["infinityModelContent.reachedInfinity", "viewportEntered:in-viewport"],
classNames: ['infinity-loader'],
classNameBindings: ['infinityModelContent.reachedInfinity', 'viewportEntered:in-viewport'],
/**
* @public
* @property eventDebounce
Expand Down Expand Up @@ -179,7 +179,13 @@ const InfinityLoaderComponent = Component.extend(InViewportMixin, {
const infinityModelContent = get(this, 'infinityModelContent');

function loadPreviousPage() {
this.sendAction('loadPreviousAction', infinityModelContent, -1);
if (typeof(get(this, 'infinityLoad')) === 'function') {
// closure action
return get(this, 'infinityLoad')(infinityModelContent, -1);
} else {
// old action
this.sendAction('loadPreviousAction', infinityModelContent, -1);
}
}

if (get(infinityModelContent, 'firstPage') > 1 && get(infinityModelContent, 'currentPage') > 0) {
Expand All @@ -196,7 +202,16 @@ const InfinityLoaderComponent = Component.extend(InViewportMixin, {
Without this debounce, all rows will be rendered causing immense performance problems
*/
function loadMore() {
this.sendAction('loadMoreAction', get(this, 'infinityModelContent'));
let infinityModelContent = get(this, 'infinityModelContent');

if (typeof(get(this, 'infinityLoad')) === 'function') {
// closure action
return get(this, 'infinityLoad')(infinityModelContent);
} else {
// old action
this.sendAction('loadMoreAction', infinityModelContent);
}

}
this._debounceTimer = run.debounce(this, loadMore, get(this, 'eventDebounce'));
},
Expand Down

0 comments on commit 90b905f

Please sign in to comment.