Support for in/inter page linking / scrolling in EmberJS
Tags: ember, javascript
Published: 2024-01-31 09:05:48

## The problem

Navigating to URLs with `#hash-targets` in them is not supported by most single-page-app frameworks due to the async rendering nature of modern web apps -- the browser can't scroll to a `#hash-target` on page load / transition because the element hasn't rendered yet. There is an issue about this for Ember [here on the RFCs repo](https://github.com/emberjs/rfcs/issues/709).

## Solution

### ember-url-hash-polyfill

The addon [ember-url-hash-polyfill](https://github.com/CrowdStrike/ember-url-hash-polyfill?tab=readme-ov-file) provides a way to support the behaviour that is in normally native to browsers where an anchor tag with `href="#some-id-or-name"` would scroll down the page when clicked.

The problem with `ember-url-hash-polyfill` solution is that it  skips the use of [`<LinkTo>`](https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo) in favour of native [HTML `<a>` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a). If you can't/don't want do that, the next section is for you.

### ember-scroll-modifiers

The [ember-scroll-modifiers addon](https://github.com/elwayman02/ember-scroll-modifiers) offers `scroll-into-view` modifier which can be used here. Drawback being that we won't be using [hash property of the URL](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash), but rather [query-param](https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams).

First we need to define our query param which will be named `scrollTo` and will live on `application` controller, so that we have access to it from every route:

```ts
// controllers/application.ts
import { service } from '@ember/service';
import type ScrollService from 'my-app/services/scroll';

export default class ApplicationController extends Controller {
  @service declare scroll: ScrollService;

  queryParams = ['scrollTo'];
}
```

Then we will create `scroll` service which will be able to tell us which `scrollTo` was activated:

```ts
// services/scroll.ts

export default class ScrollService extends Service {
  @service declare router: RouterService;

  get inView() {
    return this.router.currentRoute.queryParams['scrollTo'];
  }

  get heroInView() { // cheap example, add yours to expand
    return this.inView === 'hero';
  }
}
```

And finally and example of template that defines a link with `scrollTo` query parameter. And an element that we wish to scroll to marked with `scroll-int-view` modifier.

```hbs
// templates/application.hbs

<LinkTo
  @route="index"
  @query={{hash scrollTo="hero"}}
>
  Link to Hero
</LinkTo>

...

<div
  {{scroll-into-view
    shouldScroll=this.scroll.heroInView
    options=(hash behavior="smooth")
  }}
>
  I am hero!
</div>
```

Nice thing is that we can also invoke route transition & scrollTo programatically via [router service transitionTo](https://api.emberjs.com/ember/5.5/classes/RouterService/methods/transitionTo?anchor=transitionTo):

```ts
this.router.transitionTo('index', {
  queryParams: { scrollTo: 'hero' },
});
```

---

Cover image generated via Midjourney prompt: `scrolling in web browser --ar 16:9`