• Stars
    star
    204
  • Rank 192,063 (Top 4 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 8 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

πŸ—” Scroll to top with preserved browser history scroll position.

ember-router-scroll

GitHub Actions Build Status

Scroll to page top on transition, like a non-SPA website. An alternative scroll behavior for Ember applications.

Why Use it?

Ember expects an application to be rendered with nested views. The default behavior is for the scroll position to be preserved on every transition. However, not all Ember applications use nested views. For these applications, a user would expect to see the top of the page on most transitions.

In addition to scrolling to the top of the page on most transitions, a user would expect the scroll position to be preserved when using the back or forward browser buttons.

ember-router-scroll makes your single page application feel more like a regular website.

Compatibility

  • Ember.js v3.12 or above
  • Ember CLI v3.12 or above
  • Node.js v12 or above

Installation

ember install ember-router-scroll

Usage > 4.x

Users do not need to import and extend from ember-router-scroll anymore. In order to upgrade, you should remove this import.

This is what your router.js should look like.

import EmberRouter from '@ember/routing/router';

export default class Router extends EmberRouter {
  ...
}

Usage < 4.x

1. Import ember-router-scroll

Add RouterScroll as an extension to your Router object. This class extends EmberRouter.

// app/router.js

import EmberRouterScroll from 'ember-router-scroll';

class Router extends EmberRouterScroll {
  ...
}

In version prior to v2.0, you can import the mixin and use it like so. This is necessary if your application does not support JavaScript classes yet.

// app/router.js

import RouterScroll from 'ember-router-scroll';

const Router = EmberRouter.extend(RouterScroll, {
  ...
});

Remaining optional steps for all versions 2.x - 4.x

2. Enable historySupportMiddleware in your app

Edit config/environment.js and add historySupportMiddleware: true, to get live-reload working in nested routes. (See Issue #21)

historySupportMiddleware: true,

This location type inherits from Ember's HistoryLocation.



### Options

#### Target Elements

If you need to scroll to the top of an area that generates a vertical scroll bar, you can specify the id of an element
of the scrollable area. Default is `window` for using the scroll position of the whole viewport. You can pass an options
object in your application's `config/environment.js` file.

```javascript
ENV['routerScroll'] = {
  scrollElement: '#mainScrollElement'
};

If you want to scroll to a target element on the page, you can specify the id or class of the element on the page. This is particularly useful if instead of scrolling to the top of the window, you want to scroll to the top of the main content area (that does not generate a vertical scrollbar).

ENV['routerScroll'] = {
  targetElement: '#main-target-element' // or .main-target-element
};

Scroll Timing

You may want the default "out of the box" behaviour. We schedule scroll immediately after Ember's render. This occurs on the tightest schedule between route transition start and end.

However, you have other options. If you need an extra tick after render, set scrollWhenAfterRender: true. You also may need to delay scroll functionality until the route is idle (approximately after the first paint completes) using scrollWhenIdle: true in your config. scrollWhenIdle && scrollWhenAfterRender defaults to false.

This config property uses ember-app-scheduler, so be sure to follow the instructions in the README. We include the setupRouter and reset. This all happens after routeDidChange.

ENV['routerScroll'] = {
  scrollWhenIdle: true // ember-app-scheduler
};

Or

ENV['routerScroll'] = {
  scrollWhenAfterRender: true // scheduleOnce('afterRender', ...)
};

I would suggest trying all of them out and seeing which works best for your app!

A working example

See demo made by Jon Chua.

A visual demo

Before

before-scroll

Notice that the in the full purple page, the user is sent to the middle of the page.

After

after-scroll

Notice that the in the full purple page, the user is sent to the top of the page.

Issues with nested routes

Before:

before-preserve

Notice the unwanted scroll to top in this case.

After:

after-preserve

Adding a query parameter or controller property fixes this issue.

preserveScrollPosition with queryParams

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

1. Add query param in controller

Add preserveScrollPosition as a queryParam in the controller for the route that needs to preserve the scroll position.

Example:

import Controller from '@ember/controller';

export default class MyController extends Controller {
  queryParams = [
    'preserveScrollPosition',
  ];
}

2. Pass in query param

Next, in the place where a transition is triggered, pass in preserveScrollPosition=true. For example

<LinkTo "About Tab" "tab.about" {{query-params preserveScrollPosition=true}} />

preserveScrollPosition with a controller property

In other cases, you may have certain routes that always preserve scroll position, or routes where the controller can decide when to preserve scroll position. For instance, you may have some nested routes that have true nested UI where preserving scroll position is expected. Or you want a particular route to start off with the default scroll-to-top behavior but then preserve scroll position when query params change in response to user interaction. Using a controller property also allows the use of preserveScrollPosition without adding this to the query params.

1. Add query param to controller

Add preserveScrollPosition as a controller property for the route that needs to preserve the scroll position. In this example we have preserveScrollPosition initially set to false so that we get our normal scroll-to-top behavior when the route loads. Later on, when an action triggers a change to the filter query param, we also set preserveScrollPosition to true so that this user interaction does not trigger the scroll-to-top behavior.

Example:

import Controller from '@ember/controller';
import { action } from '@ember/object';

export default class MyController extends Controller {
  queryParams = ['filter'];

  preserveScrollPosition = false;

  @action
  changeFilter(filter) {
    this.set('preserveScrollPosition', true);
    this.set('filter', filter);
  }
}

2. Reset preserveScrollPosition if necessary

If your controller is changing the preserveScrollPosition property, you'll probably need to reset preserveScrollPosition back to the default behavior whenever the controller is reset. This is not necessary on routes where preserveScrollPosition is always set to true.

import Router from '@ember/routing/route';

export default class MyRoute extends Route {
  resetController(controller) {
    controller.set('preserveScrollPosition', false);
  }
}

preserveScrollPosition via service

You may need to programatically control preserveScrollPosition directly from a component. This can be achieved by toggling the preserveScrollPosition property on the routerScroll service.

One common use case for this is when using query-param-based pagination on a page where preserveScrollPosition is expected to be false.

For example, if a route should always scroll to top when loaded, preserveScrollPosition would be false. However, a user may then scroll down the page and paginate through some results (where each page is a query param). But because preserveScrollPosition is false, the page will scroll back to top on each of these paginations.

This can be fixed by temporarily setting preserveScrollPosition to true on the service in the pagination transition action and then disabling preserveScrollPosition after the transition occurs.

Note: if preserveScrollPosition is set to true on the service, it will override any values set on the current route's controller - whether query param or controller property.

1. Manage preserveScrollPosition via service

When you need to modify preserveScrollPosition on the service for a specific transition, you should always reset the value after the transition occurs, otherwise all future transitions will use the same preserveScrollPosition value.

Example:

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';

export default class MyComponent extends Component {
  @service routerScroll;
  @service router;

  @action
  async goToPaginationPage(pageNumber) {
    this.set('routerScroll.preserveScrollPosition', true);
    await this.router.transitionTo(
      this.router.currentRouteName,
      {
        queryParams: { page: pageNumber }
      }
    );

    // Reset `preserveScrollPosition` after transition so future transitions behave as expected
    this.set('routerScroll.preserveScrollPosition', false);
  }
}

Running Tests

  • npm test (Runs ember try:testall to test your addon against multiple Ember versions)
  • ember test
  • `ember test --serve

License

This project is licensed under the MIT License.

More Repositories

1

ember-composable-helpers

Composable helpers for declarative templating in Ember
JavaScript
636
star
2

elixir-mail

Build composable mail messages
Elixir
386
star
3

ember-route-action-helper

Bubble closure actions in routes
JavaScript
330
star
4

ember-in-viewport

Detect if an Ember View or Component is in the viewport @ 60FPS
JavaScript
246
star
5

ember-admin

Admin backend for ember-cli projects
JavaScript
241
star
6

ember-service-worker

A pluggable approach to Service Workers for Ember.js
JavaScript
238
star
7

flame_on

Flame Graph LiveView Component and LiveDashboard plugin
Elixir
228
star
8

ember-async-button

Async Button Component for Ember CLI apps
JavaScript
173
star
9

inquisitor

Composable query builder for Ecto
Elixir
170
star
10

ecto_fixtures

Fixtures for Elixir apps
Elixir
168
star
11

openid_connect

Elixir
66
star
12

eslint-plugin-ember-suave

DockYard's ESLint plugin for Ember apps
JavaScript
53
star
13

courier

Elixir
53
star
14

ember-cart

Shopping cart primitives for Ember
JavaScript
53
star
15

valid_field

Elixir
48
star
16

rein

Reinforcement Learning tooling built with Nx
Elixir
42
star
17

json_api_assert

Composable assertions for JSON API payload
Elixir
38
star
18

live_view_demo

Forkable repo for entries in Phoenix Phrenzy (https://phoenixphrenzy.com/)
Elixir
35
star
19

ember-service-worker-asset-cache

JavaScript
28
star
20

svelte-inline-compile

JavaScript
27
star
21

ember-cli-custom-assertions

Add custom QUnit assertions to your ember-cli test suite
JavaScript
26
star
22

ember-app-shell

JavaScript
24
star
23

easing

Elixir
22
star
24

design-sprints

HTML
22
star
25

ember-i18n-to-intl-migrator

Migrate ember-i18n to ember-intl
JavaScript
21
star
26

ember-service-worker-index

An Ember Service Worker plugin that caches the index.html file
JavaScript
20
star
27

ember-cli-deploy-compress

Compress your assets automatically choosing the best compression available for your browser targets
JavaScript
18
star
28

laptop-install

Shell
17
star
29

narwin-pack

Package of PostCSS plugins DockYard utilizes for PostCSS based projects!
JavaScript
16
star
30

ember-maybe-in-element

Conditionally render content elsewhere using #-in-element on ember apps
JavaScript
15
star
31

ember-service-worker-cache-fallback

JavaScript
15
star
32

inquisitor_jsonapi

JSON API Matchers for Inquisitor
Elixir
14
star
33

ember-one-way-select

JavaScript
10
star
34

svelte-inline-component

Utility and vite plugin to allow to create your own inline svelte components in tests
JavaScript
9
star
35

live_view_events

A simple library to unify sending and receiving messages in LiveView components
JavaScript
8
star
36

disklavier

Elixir
8
star
37

canon

All the must-read articles and must-watch videos for the DockYard engineering team.
8
star
38

plausible_proxy

An Elixir Plug to proxy calls to Plausible through your server
Elixir
7
star
39

netcdf

Elixir NetCDF Bindings
Rust
7
star
40

ember-service-worker-cache-first

JavaScript
7
star
41

qunit-notifications

Web Notifications support for QUnit in-browser test suites
JavaScript
6
star
42

auth_test_support

Authentication and authorization test functions
Elixir
4
star
43

broccoli-json-concat

JavaScript
3
star
44

boat-tracker

Elixir
3
star
45

ember-load-css

Ember CLI wrapper for loadCSS
JavaScript
3
star
46

drive-in-privacy-policy

2
star
47

boston_elixir

LiveView Native Workshop for Boston Elixir
Elixir
2
star
48

ketch

Simple proof-of-concept web application built with Next.js, Storybook, and Firebase.
JavaScript
1
star
49

courier_web

JavaScript
1
star
50

stylelint-config-narwin

DockYard stylelint configuration
JavaScript
1
star
51

liveview_tailwind_demo

Demo showing TailWind 3 integration in a Phoenix LiveView project
Elixir
1
star
52

ember-qunit-notifications

tomster-ified qunit-notifications
1
star
53

formation

Example of Phoenix LiveView Form logic
Elixir
1
star