Skip to content

Commit

Permalink
Add setup and reset lifecycle hooks + replace support (#30)
Browse files Browse the repository at this point in the history
* Add setup and reset hooks for a more complete life cycle

* Rename event name to ParachuteEvent and add some overrides

* Add support for `replace`, cleanup, fix tests, and update readme

* Update readme

* Add route based tests and inline docs

* Cleanup route test

* Dont overwrite the qpMapForRoute

* Use initializer instead of an instance-initializer, make route test a unit test
  • Loading branch information
offirgolan authored Jul 25, 2017
1 parent 9a79c14 commit 063d958
Show file tree
Hide file tree
Showing 13 changed files with 407 additions and 54 deletions.
85 changes: 71 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export const myQueryParams = new QueryParams({
},
page: {
defaultValue: 1,
refresh: true
refresh: true,
replace: true
},
search: {
defaultValue: '',
Expand All @@ -66,13 +67,23 @@ export const myQueryParams = new QueryParams({
export default Ember.Controller.extend(myQueryParams.Mixin, {
queryParamsChanged: Ember.computed.or('queryParamsState.{page,search,tags}.changed'),

setup({ queryParams }) {
this.fetchData(queryParams);
},

queryParamsDidChange({ shouldRefresh, queryParams }) {
// if any query param with `refresh: true` is changed, `shouldRefresh` is `true`
if (shouldRefresh) {
this.fetchData(queryParams);
}
},

reset({ queryParams }, isExiting) {
if (isExiting) {
this.resetQueryParams();
}
},

fetchData(queryParams) {
// fetch data
},
Expand All @@ -86,7 +97,7 @@ export default Ember.Controller.extend(myQueryParams.Mixin, {
});
```

In the above example, the mixin adds a `queryParamsDidChange` hook. You can use this hook to perform tasks like fetching data based on the query params. Additionally, you can create a computed property observing `queryParamsState` that will allow you to display a button in your UI that can clear all query params via `resetQueryParams`.
In the above example, the mixin adds the `setup`, `reset`, and `queryParamsDidChange` hooks. You can use these hooks to perform tasks like fetching data based on the query params or resetting them when leaving the route. Additionally, you can create a computed property observing `queryParamsState` that will allow you to display a button in your UI that can clear all query params via `resetQueryParams`.

Please continue reading for more advanced usage.

Expand All @@ -105,7 +116,8 @@ const myQueryParams = new QueryParams({
},
page: {
defaultValue: 1,
refresh: true
refresh: true,
replace: true
},
search: {
defaultValue: '',
Expand All @@ -121,6 +133,7 @@ interface QueryParamOption {
as?: string;
defaultValue: any; // required
refresh?: boolean;
replace?: boolean;
scope?: 'controller';
serialize?(value: any): any;
deserialize?(value: any): any;
Expand Down Expand Up @@ -156,6 +169,10 @@ The `as` option lets you optionally override the query param URL key for a query

When `refresh` is `true`, the `queryParamsDidChange` hook provided by the mixin will notify you when a refreshable query param has changed. You can use that value to determine whether or not you need to refetch data.

### `replace`

By default, Ember will use **pushState** to update the URL in the address bar in response to a controller query param property change, but when `replace` is `true` it will use **replaceState** instead (which prevents an additional item from being added to your browser's history).

### `scope`

`scope` can only be one value if specified: `controller`. This is equivalent to the `scope` option in regular Ember query params. You can read more about it in the bottom paragraph [here][ember-qp-docs].
Expand Down Expand Up @@ -279,9 +296,9 @@ queryParamsChanged: Ember.computed.or('queryParamsState.{page,search,tags}.chang

You can then use this CP to conditionally display a button that can clear all query params to their default values.

### Function - `queryParamsDidChange`
### Hooks

The mixin also adds a hook that you can use to update your controller when any query params change. The hook receives a single argument:
All hooks will receives a `ParachuteEvent` as an argument which can be defined as:

```ts
// what changed
Expand All @@ -299,18 +316,20 @@ interface QueryParams {
[queryParamKey: string]: any;
}

interface QueryParamsChangedEvent {
interface ParachuteEvent {
changes: QueryParamsChanges;
changed: QueryParamsChanged;
queryParams: QueryParams;
routeName: string;
shouldRefresh: boolean;
}

function queryParamsDidChange(queryParamsChangedEvent: QueryParamsChangedEvent): void;
```

You can destructure and use only what you need:
### Hook - `queryParamsDidChange`

```ts
function queryParamsDidChange(queryParamsChangedEvent: ParachuteEvent): void;
```

```js
export default Controller.extend(myQueryParams.Mixin, {
Expand All @@ -326,13 +345,51 @@ export default Controller.extend(myQueryParams.Mixin, {
});
```

### Event - `queryParamsDidChange`
### Hook - `setup`

```ts
function setup(queryParamsChangedEvent: ParachuteEvent): void;
```

```js
export default Controller.extend(myQueryParams.Mixin, {
setup({ routeName, shouldRefresh, queryParams, changed, changes }) {
// Fetch some initial data & setup the controller
}
});
```

### Hook - `reset`

```ts
function reset(queryParamsChangedEvent: ParachuteEvent, isExiting: boolean): void;
```

```js
export default Controller.extend(myQueryParams.Mixin, {
reset({ routeName, shouldRefresh, queryParams, changed, changes }, isExiting) {
if (isExiting) {
this.resetQueryParams();
}
}
});
```

### Events

The controller also emits an event when query params change. This receives the same `QueryParamsChangedEvent` object as the `queryParamsDidChange` hook:
The controller also emits an event for each hook which receives the same arguments:

```ts
export default Ember.Controller.extend({
onQueryParamsChanged: Ember.on('queryParamsDidChange', function(queryParamsChangedEvent: QueryParamsChangedEvent) {
onChange: Ember.on('queryParamsDidChange', function(queryParamsChangedEvent: ParachuteEvent) {
// ...
}),

onSetup: Ember.on('setup', function(queryParamsChangedEvent: ParachuteEvent) {
// ...
}),

onReset: Ember.on('reset', function(queryParamsChangedEvent: ParachuteEvent, isExiting: boolean) {
// ...
})
});
Expand Down Expand Up @@ -386,10 +443,10 @@ controller.setDefaultQueryParamValue('search', 'foo');
controller.setDefaultQueryParamValue('direction', 'asc');
```

__NOTE__: Changing the defaultValue at any point will not clear the query paramater from being shown in the URI. We do not have control over that as it is private API.
__NOTE__: Changing the defaultValue at any point will not clear the query parameter from being shown in the URI. We do not have control over that as it is private API.

[changelog]: CHANGELOG.md
[demo]: https://offirgolan.github.io/ember-parachute
[ember-metrics]: https://github.com/poteto/ember-metrics
[ember-qp-docs]: https://guides.emberjs.com/v2.5.0/routing/query-params/
[ember-qp-docs]: https://guides.emberjs.com/v2.14.0/routing/query-params/
[skeleton-ui]: https://medium.com/ux-for-india/facilitating-better-interactions-using-skeleton-screens-a034a51120a5
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,26 @@ const {
* Change event generated by query params changing.
*
* @export
* @class QueryParamsChangeEvent
* @class ParachuteEvent
*/
export default class QueryParamsChangeEvent {
export default class ParachuteEvent {
/**
* Creates an instance of QueryParamsChangeEvent.
* Creates an instance of ParachuteEvent.
*
* @param {string} routeName
* @param {Ember.Controller} controller
* @param {object} [changed={}]
*
* @memberof QueryParamsChangeEvent
* @memberof ParachuteEvent
*/
constructor(routeName, controller, changed = {}) {
let { queryParams, queryParamsArray } = QueryParams.metaFor(controller);
let state = QueryParams.stateFor(controller);

/**
* The route the event was fired from
* @type {string}
*/
this.routeName = routeName;

/**
Expand Down
19 changes: 19 additions & 0 deletions addon/-private/parachute-meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,28 @@ export default class ParachuteMeta {
qps[key] = new QueryParam(key, queryParams[key]);
return qps;
}, {});

/** @type {Ember.NativeArray} */
this.queryParamsArray = emberArray(keys(this.queryParams).map((key) => {
return this.queryParams[key];
}));

/** @type {object} */
this.qpMapForController = this.queryParamsArray.reduce((qps, { key, as, scope }) => {
qps[key] = { as, scope };
return qps;
}, {});

/** @type {object} */
this.qpMapForRoute = this.queryParamsArray.reduce((qps, { key, replace }) => {
qps[key] = { replace };
return qps;
}, {});

/** @type {object} */
this.defaultValues = this.queryParamsArray.reduce((defaults, { key, defaultValue }) => {
defaults[key] = defaultValue;
return defaults;
}, {});
}
}
9 changes: 9 additions & 0 deletions addon/-private/query-param.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,25 @@ export default class QueryParam {

/** @type {string} */
this.key = key;

/** @type {string} */
this.as = options.as || key;

/** @type {"controller" | undefined} */
this.scope = options.scope;

/** @type {any} */
this.defaultValue = options.defaultValue;

/** @type {boolean} */
this.refresh = Boolean(options.refresh);

/** @type {boolean} */
this.replace = Boolean(options.replace);

/** @type {function(any): any} */
this.serialize = options.serialize;

/** @type {function(any): any} */
this.deserialize = options.deserialize;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Ember from 'ember';
import QueryParams from '../query-params';
import QueryParamsChangeEvent from '../-private/query-param-change-event';
import ParachuteEvent from '../-private/parachute-event';
import lookupController from '../utils/lookup-controller';

const {
Expand All @@ -11,8 +11,63 @@ const {
sendEvent
} = Ember;

const {
keys
} = Object;

export function initialize(/* application */) {
Ember.Route.reopen({
/**
* Setup the route's `queryParams` map and call the `setup` hook
* on the controller.
*
* @method setupController
* @public
* @param {Ember.Controller} controller
* @returns {void}
*/
setupController(controller) {
this._super(...arguments);

if (QueryParams.hasParachute(controller)) {
this._setupParachuteQueryParamsMap(controller);

let { routeName } = this;
let event = new ParachuteEvent(routeName, controller, {});

// Overrides
event.changed = event.changes;
event.shouldRefresh = true;

tryInvoke(controller, 'setup', [event]);
sendEvent(controller, 'setup', [event]);
}
},

/**
* Call the `reset` hook on the controller.
*
* @method resetController
* @public
* @param {Ember.Controller} controller
* @param {Boolean} isExiting
* @returns {void}
*/
resetController(controller, isExiting) {
this._super(...arguments);

if (QueryParams.hasParachute(controller)) {
let { routeName } = this;
let event = new ParachuteEvent(routeName, controller, {});

// Overrides
event.shouldRefresh = false;

tryInvoke(controller, 'reset', [event, isExiting]);
sendEvent(controller, 'reset', [event, isExiting]);
}
},

/**
* Serialize query param value if a given query param has a `serialize`
* method.
Expand Down Expand Up @@ -70,13 +125,36 @@ export function initialize(/* application */) {
*/
_scheduleParachuteChangeEvent(routeName, controller, changed = {}) {
run.schedule('afterRender', this, () => {
let changeEvent = new QueryParamsChangeEvent(routeName, controller, changed);
let event = new ParachuteEvent(routeName, controller, changed);

tryInvoke(controller, 'queryParamsDidChange', [changeEvent]);
sendEvent(controller, 'queryParamsDidChange', [changeEvent]);
tryInvoke(controller, 'queryParamsDidChange', [event]);
sendEvent(controller, 'queryParamsDidChange', [event]);
});
},

/**
* Setup the route's `queryParams` map if it doesnt already exist from
* the controller's Parachute meta.
*
* @method _setupParachuteQueryParamsMap
* @private
* @param {Ember.Controller} controller
* @returns {void}
*/
_setupParachuteQueryParamsMap(controller) {
if (!this.__hasSetupParachuteQPs) {
let qpMap = this.get('queryParams');
let { qpMapForRoute } = QueryParams.metaFor(controller);

keys(qpMapForRoute).forEach(key => {
qpMapForRoute[key] = assign({}, qpMapForRoute[key], qpMap[key]);
});

this.set('queryParams', qpMapForRoute);
this.__hasSetupParachuteQPs = true;
}
},

actions: {
/**
* Route hook that fires when query params are changed.
Expand Down
Loading

0 comments on commit 063d958

Please sign in to comment.