scrollPositionRestoration has several problems
See original GitHub issueI’m submitting a…
[ ] Regression (a behavior that used to work and stopped working in a new release)
[x] Bug report
[ ] Performance issue
[x] Feature request
[x] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
[ ] Other... Please describe:
Current behavior
I started experimenting with the new scrollPositionRestoration
feature in the RouterModule extra options. I expected scroll restoration to work by default when setting the property to ‘enabled’, but it doesn’t. And the documentation has issues, too.
Expected behavior
The documentation says:
‘enabled’–set the scroll position to the stored position. This option will be the default in the future.
So I naïvely thought that setting the flag to ‘enabled’ would be sufficient to restore the scroll position. But it isn’t.
Indeed, the scroll event is fired, and the scroll position is restored, before the ngAfterViewInit
hook of the activated component has been called. So the view of the component is not ready yet when the router tries to restore the scroll position (i.e. there is no way to scroll to the end of a long list, because the list isn’t there yet).
And even if it was restored after the view is ready, that would only work if the activated component used a resolved guard to load the data.
So, the documentation should, IMHO, at least indicate that restoring the scroll position always requires to
- explicitly intercept the Scroll event, and scroll imperatively after a delay. This can be done in a single place, but I don’t see how to do that in a reliable way, since there is no way to know if the delay is sufficient for the data to have been loaded (but it would at least work if resolve guards are used consistently), or
- explicitly intercept the Scroll event in each routed component, and imperatively scroll when the data has been loaded and the view has been rendered. This is not a trivial task.
I read the remaining of the documentation, which has examples about doing this kind of stuff (although it doesn’t really say that they’re required). But those examples are all incorrect.
Here’s the first example:
class AppModule {
constructor(router: Router, viewportScroller: ViewportScroller, store: Store<AppState>) {
router.events.pipe(filter(e => e instanceof Scroll), switchMap(e => {
return store.pipe(first(), timeout(200), map(() => e));
}).subscribe(e => {
if (e.position) {
viewportScroller.scrollToPosition(e.position);
} else if (e.anchor) {
viewportScroller.scrollToAnchor(e.anchor);
} else {
viewportScroller.scrollToPosition([0, 0]);
}
});
}
}
This example uses a Store service, which is not part of Angular (I guess it’s part of ngrx). So that makes it hard to understand and adapt for those who don’t use ngrx.
Besides, it doesn’t compile, because a closing parenthesis is missing, and because e
is of type Event, and not of type Scroll, and thus has no position
property.
The second example is the following:
class ListComponent {
list: any[];
constructor(router: Router, viewportScroller: ViewportScroller, fetcher: ListFetcher) {
const scrollEvents = router.events.filter(e => e instanceof Scroll);
listFetcher.fetch().pipe(withLatestFrom(scrollEvents)).subscribe(([list, e]) => {
this.list = list;
if (e.position) {
viewportScroller.scrollToPosition(e.position);
} else {
viewportScroller.scrollToPosition([0, 0]);
}
});
}
}
It doesn’t compile because it still uses an old, non-pipeable operator, and because, once again, e
is of type Event, not Scroll.
But even after fixing the compilation errors, it doesn’t work because the view hasn’t been updated with the new list yet when viewportScroller.scrollToPosition(e.position);
is called.
So the code would have to be changed to the following in order to compile and work as expected
class ListComponent {
list: any[];
constructor(router: Router, viewportScroller: ViewportScroller, fetcher: ListFetcher) {
const scrollEvents = router.events.filter(e => e instanceof Scroll);
listFetcher.fetch().pipe(withLatestFrom(scrollEvents)).subscribe(([list, e]) => {
this.races = list;
const scrollEvent = e as Scroll;
of(scrollEvent).pipe(delay(1)).subscribe(s => {
if (s.position) {
viewportScroller.scrollToPosition(s.position);
} else {
viewportScroller.scrollToPosition([0, 0]);
}
});
});
}
}
I think that none of these solutions is really simple enough, though. Here are two ideas that could maybe make things easier:
- only fire the Scroll event and try to restore the position after the ngAfterViewInit hook has been called. This should at least make things work when a resolve guard is used to load the list. Or when the list is available immediately.
- for the other cases, allow to inject a service that the component could call when the list has been loaded. It would be up to this service to get the last scroll position or anchor, to wait until the view has been rendered, and then to restore the scroll position. It would ignore all but the first call after the component has been activated.
Minimal reproduction of the problem with instructions
Here’s a repo illustrating the various issues and solutions presented above: https://github.com/jnizet/scrollbug. It’s a standard angular-cli project. I can’t run it in stackblitz unfortunately (probably because Stackblitz doesn’t support the beta release of Angular).
What is the motivation / use case for changing the behavior?
First, the documentation should be fixed and made clearer
- it should not use ngrx
- it should contain examples that compile, and run as expected
- it should make it clear than simply setting the flag to ‘enabled’ is not sufficient to enable scroll restoration
Second, it should be way easier to make that feature work. See ideas above.
Environment
Angular version: 6.1.0-beta.1
Browser:
- [x] Chrome (desktop) version 67.0.3396.87
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [x] Firefox version 60.0.2
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
Issue Analytics
- State:
- Created 5 years ago
- Reactions:289
- Comments:114 (22 by maintainers)
I have resolved this issue by implementing custom scroll restoration behavior.
The reason of this behavior is that page didn’t already render and scrollToPosition no have effect.
There are not a good hack with timeout but it works.
Just leaving this here for all future visitors, as I did some debugging to determine why this wasn’t working. For anyone who sees this in the future. If you have the following code in your stylesheet…
This expected functionality will appear to be a bug for you.
Removing that style has a decent potential to fix this issue and make this PR do what was intended. Granted, removing that style may make a bunch of other things break, but that’s a different problem.
Ninja edit: It also appears that this seems to intermittently work with https://github.com/angular/material2
Working Version (My own personal anecdote): I have a working app with material (which uses
mat-sidenav
) and allwindow.scroll
variants work as expected.Non-working version (as reported by @crisbeto ): https://github.com/angular/material2/issues/11552