From 31ae7fc4ac176383fc174dc0b61c1886334f5025 Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Wed, 3 Aug 2016 07:21:38 -0400 Subject: [PATCH] Enable backbutton for same routes Closes #12 Closes #11 * Moved mixin to root of project so the README is accurate (#11) * Added new Location object that will add unique IDs to the History API's current `state`. This allows for the scroll position to be preserved across multiple instances of the same route in the history. (#12) * Added a service to store the scroll information * Doesn't rely on jQuery This change is breaking the previous API as it now requires that the Location object be added to `config/environment.js`. To enable this: ```js // config/environment.js locationType: 'router-scroll' ``` --- README.md | 30 +++++++---- addon/locations/router-scroll.js | 25 +++++++++ addon/services/router-scroll.js | 34 ++++++++++++ app/index.js | 2 - app/locations/router-scroll.js | 1 + app/services/router-scroll.js | 1 + tests/unit/mixins/router-scroll-test.js | 4 +- tests/unit/services/router-scroll-test.js | 63 +++++++++++++++++++++++ 8 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 addon/locations/router-scroll.js create mode 100644 addon/services/router-scroll.js delete mode 100644 app/index.js create mode 100644 app/locations/router-scroll.js create mode 100644 app/services/router-scroll.js create mode 100644 tests/unit/services/router-scroll-test.js diff --git a/README.md b/README.md index 2af18212..946ba570 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ Scroll to page top on transition, like a non-SPA website. An alternative scroll behavior for Ember applications. ### Before -![before-scroll](https://cloud.githubusercontent.com/assets/4430436/17122972/0a1fe454-5295-11e6-937f-f1f5beab9d6b.gif) +![before-scroll](https://cloud.githubusercontent.com/assets/4430436/17122972/0a1fe454-5295-11e6-937f-f1f5beab9d6b.gif) Notice that the in the full purple page, the user is sent to the **middle** of the page ### After -![after-scroll](https://cloud.githubusercontent.com/assets/4430436/17122970/07c1a3a0-5295-11e6-977f-37eb955d95b1.gif) +![after-scroll](https://cloud.githubusercontent.com/assets/4430436/17122970/07c1a3a0-5295-11e6-977f-37eb955d95b1.gif) Notice that the in the full purple page, the user is sent to the **top** of the page @@ -45,7 +45,7 @@ When `willTransition` is triggered, the scroll position is stored in a map with On `didTransition`, it first checks to see if the route transition was triggered by a `popStateEvent`. If so, go to the scroll position defined by the `scrollMap`. Otherwise, scroll to the top of the page. - *With one exception: if the queryParam `preserveScrollPosition` is set to `true`, it maintains the scroll position of the **previous route. See below for further information on this queryParam.** + **With one exception: if the queryParam `preserveScrollPosition` is set to `true`, it maintains the scroll position of the previous route. See below for further information on this queryParam.** ## Usage @@ -68,30 +68,40 @@ Add this in the dependencies block of your `package.json` file: In your app/router.js file, import the mixin: ```javascript -import RouterScrollMixin from 'ember-router-scroll'; +import RouterScroll from 'ember-router-scroll'; ``` -And add RouterScrollMixin as an extension to your Router object: +And add RouterScroll as an extension to your Router object: ```javascript -const Router = Ember.Router.extend(RouterScrollMixin,{} +const Router = Ember.Router.extend(RouterScroll, {} ``` -### Step 3: Profit +### Step 3: Update your app's locationType + +Edit `config/environment.js` and change `locationType` + +```js +locationType: 'router-scroll' +``` + +This location type inherits from Ember's `HistoryLocation`. + +### Step 4: Profit ## Preserve Scroll Position ### Before: -![before-preserve](https://cloud.githubusercontent.com/assets/4430436/17122971/0a1e34ce-5295-11e6-8d30-9f687dd69dbb.gif) +![before-preserve](https://cloud.githubusercontent.com/assets/4430436/17122971/0a1e34ce-5295-11e6-8d30-9f687dd69dbb.gif) Notice the unwanted scroll to top in this case. ### After: -![after-preserve](https://cloud.githubusercontent.com/assets/4430436/17122969/07acbb48-5295-11e6-9900-f9ba519affa4.gif) +![after-preserve](https://cloud.githubusercontent.com/assets/4430436/17122969/07acbb48-5295-11e6-9900-f9ba519affa4.gif) Adding a query parameter fixes this issue. In certain cases, you might want to have certain routes preserve scroll position when coming from a specific location. For example, inside your application, there is a way to get to a route where the user expects scroll position to be preserved (such as a tab section). -To use this feature: +To use this feature: #####Step 1. diff --git a/addon/locations/router-scroll.js b/addon/locations/router-scroll.js new file mode 100644 index 00000000..c713b6cc --- /dev/null +++ b/addon/locations/router-scroll.js @@ -0,0 +1,25 @@ +import Ember from 'ember'; + +const { + get, + HistoryLocation +} = Ember; + +export default HistoryLocation.extend({ + init(...args) { + this._super(...args); + this.stateCounter = 0; + }, + pushState(path) { + let id = `${this.stateCounter++}`; + let state = { path, id }; + get(this, 'history').pushState(state, null, path); + this._previousURL = this.getURL(); + }, + replaceState(path) { + let id = `${this.stateCounter++}`; + let state = { path, id }; + get(this, 'history').replaceState(state, null, path); + this._previousURL = this.getURL(); + } +}); diff --git a/addon/services/router-scroll.js b/addon/services/router-scroll.js new file mode 100644 index 00000000..48557bbb --- /dev/null +++ b/addon/services/router-scroll.js @@ -0,0 +1,34 @@ +import Ember from 'ember'; + +const { + get, + set, + computed, + getWithDefault, + Service +} = Ember; + +export default Service.extend({ + init(...args) { + this._super(...args); + set(this, 'scrollMap', {}); + set(this, 'key', null); + }, + update() { + let scrollMap = get(this, 'scrollMap'); + let key = get(this, 'key'); + + if (key) { + set(scrollMap, key, { x: window.scrollX, y: window.scrollY }); + } + }, + position: computed(function() { + let scrollMap = get(this, 'scrollMap'); + let stateId = get(window, 'history.state.id'); + + set(this, 'key', stateId); + let key = getWithDefault(this, 'key', '-1'); + + return getWithDefault(scrollMap, key, { x: 0, y: 0 }); + }).volatile() +}); diff --git a/app/index.js b/app/index.js deleted file mode 100644 index 1f6f761d..00000000 --- a/app/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import RouterScrollMixin from 'ember-router-scroll/mixins/router-scroll'; -export default RouterScrollMixin; diff --git a/app/locations/router-scroll.js b/app/locations/router-scroll.js new file mode 100644 index 00000000..d91754ef --- /dev/null +++ b/app/locations/router-scroll.js @@ -0,0 +1 @@ +export { default } from 'ember-router-scroll/locations/router-scroll'; diff --git a/app/services/router-scroll.js b/app/services/router-scroll.js new file mode 100644 index 00000000..e9b41794 --- /dev/null +++ b/app/services/router-scroll.js @@ -0,0 +1 @@ +export { default } from 'ember-router-scroll/services/router-scroll'; diff --git a/tests/unit/mixins/router-scroll-test.js b/tests/unit/mixins/router-scroll-test.js index 06d0477f..2327e625 100644 --- a/tests/unit/mixins/router-scroll-test.js +++ b/tests/unit/mixins/router-scroll-test.js @@ -1,12 +1,12 @@ import Ember from 'ember'; -import RouterScrollMixin from 'ember-router-scroll/mixins/router-scroll'; +import RouterScroll from 'ember-router-scroll'; import { module, test } from 'qunit'; module('Unit | Mixin | router scroll'); // Replace this with your real tests. test('it works', (assert) => { - const RouterScrollObject = Ember.Object.extend(RouterScrollMixin); + const RouterScrollObject = Ember.Object.extend(RouterScroll); const subject = RouterScrollObject.create(); assert.ok(subject); }); diff --git a/tests/unit/services/router-scroll-test.js b/tests/unit/services/router-scroll-test.js new file mode 100644 index 00000000..dba33361 --- /dev/null +++ b/tests/unit/services/router-scroll-test.js @@ -0,0 +1,63 @@ +import Ember from 'ember'; +import { moduleFor, test } from 'ember-qunit'; + +const { + get, + set +} = Ember; + +moduleFor('service:router-scroll', 'Unit | Service | router scroll', { + // Specify the other units that are required for this test. + // needs: ['service:foo'] +}); + +// Replace this with your real tests. +test('it inits `scrollMap` and `key`', function(assert) { + let service = this.subject(); + assert.deepEqual(get(service, 'scrollMap'), {}); + assert.deepEqual(get(service, 'key'), null); +}); + +test('updating will set `scrollMap` to the current scroll position', function(assert) { + let service = this.subject(); + + let expected = { x: window.scrollX, y: window.scrollY }; + set(service, 'key', '123'); + service.update(); + assert.deepEqual(get(service, 'scrollMap'), { '123': expected }); +}); + +test('updating will not set `scrollMap` to the current scroll position if `key` is not yet set', function(assert) { + let service = this.subject(); + + service.update(); + assert.deepEqual(get(service, 'scrollMap'), { }); +}); + +test('computing the position for an existing state id return the coords', function(assert) { + let service = this.subject(); + let state = window.history.state; + window.history.replaceState({id: '123'}, null); + + let expected = { x: 1, y: 1 }; + set(service, 'scrollMap.123', expected); + assert.deepEqual(get(service, 'position'), expected); + window.history.replaceState(state, null); +}); + +test('computing the position for a state without a cached scroll position returns default', function(assert) { + let service = this.subject(); + let state = window.history.state; + window.history.replaceState({id: '123'}, null); + + let expected = { x: 0, y: 0 }; + assert.deepEqual(get(service, 'position'), expected); + window.history.replaceState(state, null); +}); + +test('computing the position for a non-existing state returns default', function(assert) { + let service = this.subject(); + + let expected = { x: 0, y: 0 }; + assert.deepEqual(get(service, 'position'), expected); +});