Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ember.js restore scroll on history, reset scroll on link

Daniel F Pupius described the problem as:

Lots of sites get this wrong and it’s really annoying. When the user navigates using the browser’s forward or back button the scroll position should be the same as it was last time they were on the page. This sometimes works correctly on Facebook but sometimes doesn’t. Google+ always seems to lose your scroll position.

So there are already numerous questions about resetting the scroll to the top of the page when browsing to a new page. Ember.js's Cookbook also shows how to hook into Route.activate:

export default Ember.Mixin.create({
  activate: function() {
    this._super();
    window.scrollTo(0,0);
  }
});

However this only solves halve the problem. When the user uses the back/forward buttons, the scroll position will not be restored, but simply reset.

There are quite a few attempts into solving this, many by just storing the scroll position inside the Ember.Controller instance, for example in this article. However this is only a partial solution. If the controller is used multiple times, only a single scroll state will be preserved.

How can the default browser implementation of restoring the old scroll state be preserved? Thus, do nothing if Route.activate was triggered from a html5 history state change?

like image 783
Bouke Avatar asked May 06 '15 15:05

Bouke


3 Answers

I've just solved this problem in my own app.

If you only want to use it in places that need it, just use a mixin:

ember g mixin remember-scroll

Then in mixins/remember-scroll.js, replace with:

import Ember from 'ember';

export default Ember.Mixin.create({

  scrollSelector: window,

  activate: function() {
    this._super.apply(this, arguments);
    var self = this;
    if( this.get('lastScroll') ){

      Ember.run.next(function(){
        Ember.$(self.scrollSelector).scrollTop(self.get('lastScroll'));
      });

    } else {
      Ember.$(this.scrollSelector).scrollTop(0);
    }
  },

  deactivate: function() {
    this._super.apply(this, arguments);
    this.set('lastScroll',Ember.$(this.scrollSelector).scrollTop());
  },

});

If you want it automatically mixed in to all routes, then do the following.

In Ember CLI, create a new initialier.

ember g initializer remember-scroll

Then in initializers/remember-scroll.js, replace the code with this:

import Ember from "ember";

var rememberScroll = Ember.Mixin.create({

  scrollSelector: window,

  activate: function() {
    this._super.apply(this, arguments);
    var self = this;
    if( this.get('lastScroll') ){

      Ember.run.next(function(){
          Ember.$(self.scrollSelector).scrollTop(self.get('lastScroll'));
      });

    } else {
      Ember.$(this.scrollSelector).scrollTop(0);
    }
  },

  deactivate: function() {
    this._super.apply(this, arguments);
    this.set('lastScroll',Ember.$(this.scrollSelector).scrollTop());
  },

});

export function initialize(/* container, application */) {
  Ember.Route.reopen(rememberScroll);
}

export default {
  name: 'remember-scroll',
  initialize: initialize
};

You can remove the first mixin above if you're using the initializer version.

This should cause all of your routes to remember the last scroll position (if you've been there before), and apply it when you return there.

Though I would recommend against putting it in all routes, since users may not expect to be returned to a previous scroll position if a significant amount of time has passed since they were last there.

like image 60
JeremyTM Avatar answered Nov 17 '22 20:11

JeremyTM


Install this ember addon ember-router-scroll to reset scroll with scroll history restore capabilities. It makes use of the HistoryLocation API of the browsers so it works just out of the box.

like image 23
Musaffa Avatar answered Nov 17 '22 21:11

Musaffa


I've found the solution by JeremyTM here to be generally effective. However, using the deactivate method in my experience leads inconsistencies in the value set to lastScroll on transition whereas using the route's willTransition action method leads to consistent set behavior.

Here is the modification to his mixin that works better for me:

import Ember from 'ember';

export default Ember.Mixin.create({
  activate: function() {
    this._super.apply(this, arguments);
    var self = this;

    if(this.get('lastScroll')){
      Ember.run.next(function(){
        Ember.$(window).scrollTop(self.get('lastScroll'));
      });
    } else {
      Ember.$(window).scrollTop(0);
    }
  },

  actions: {
    willTransition: function() {
      this._super.apply(this, arguments);
      this.set('lastScroll', Ember.$(window).scrollTop());
    }
  }
});

Note as well that this still strikes me as a partial solution to the problem in that it doesn't reset scroll on all link clicks (in contrast to browser navigations / history changes). It does so on the first link click that loads a route, but all subsequent link clicks to that route will load the previous scroll position as well, thereby failing to recognize the user's intended action to load the route without reference to its previous view behavior (aka with a reset scroll position).

like image 36
Mark Hendrickson Avatar answered Nov 17 '22 21:11

Mark Hendrickson