Skip to content

Instantly share code, notes, and snippets.

@jasonaden
Created August 27, 2018 23:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jasonaden/4779ce69101375a1635a87de700e3932 to your computer and use it in GitHub Desktop.
Save jasonaden/4779ce69101375a1635a87de700e3932 to your computer and use it in GitHub Desktop.
I looked at the issue on the weekend, but wanted to think more about it to see how it should be done and to put together real examples.
These are my thoughts.
## Observation 1: We need tot know when app is atable
To be able to restore scrolling position, we need to know when the application is stable. This cannot be known in a general fashion, it is only known in the context of a particular application.
To illustrate why this is the case, let's try to define stability.
### Attempt 1: Change detection runs the first time after NavigationEnd is emitted.
The following example won't work.
```
class MyComponent {
data: any[];
ngOnInit() {
fetch("/somedata").then((r) => {
this.data = r;
});
}
}
```
### Attempt 2: Change detection runs after appRef.stable returns true (no timers, no tasks are scheduled).
The example above will work, but the following example won't (the app will never get stable).
```
class MyComponent {
data: any[];
ngOnInit() {
setInterval(() => {
fetch("/somedata").then((r) => {
this.data = r;
});
}, 10000);
}
}
```
Long story short: it is not possible to define stability in a truly generic way.
## What is Possible
I can be done for a particular application. For instance, if your application uses NgRx, then all non-local state management will be done in the store, so having something like this will represent stability (it will work for all the whole application, i.e., it will work for all the components).
```
// this is a generic NgRx-based implementation
class AppModule {
constructor(router: Router, viewportScroller: ViewportScroller, store: Store<any>) {
router.events.pipe(
filter(e => e instanceof Scroll),
switchMap((e: Scroll) => {
if (!e.anchor && !e.position) { return of(e); }
return store.pipe(
startWith(null),
debounceTime(200),
first(),
map(() => e)
);
})
).subscribe(e => {
console.log('restoring');
if (e.position) {
viewportScroller.scrollToPosition(e.position);
} else if (e.anchor) {
viewportScroller.scrollToAnchor(e.anchor);
} else {
viewportScroller.scrollToPosition([0, 0]);
}
});
}
}
```
This is the case with pretty much any centralized state management systems (e.g., a similar helper can be written for Apollo).
Similarly, if you use HttpClientModule for all your data fetching, you can write an interceptor that you can use to define when your application is "stable", similar to the NgRx code above.
```
// this implementation is generic.
export class HttpActive {
active = new BehaviorSubject(false);
}
@Injectable()
export class HttpActiveInterceptor implements HttpInterceptor {
private requests: HttpRequest<any>[] = [];
constructor(private httpActive: HttpActive) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.reset([...this.requests, req]);
return next.handle(req).pipe(finalize(() => this.reset(this.requests.filter(r => r !== req))));
}
reset(reqs: HttpRequest<any>[]) {
this.requests = reqs;
this.httpActive.active.next(this.requests.length > 0);
}
}
@NgModule({
declarations: [
AppComponent,
OneComponent,
TwoComponent
],
imports: [
BrowserModule,
HttpClientModule,
RouterModule.forRoot([
{path: 'one', component: OneComponent},
{path: 'two', component: TwoComponent},
{path: '', pathMatch: 'full', redirectTo: '/one'}
], { scrollPositionRestoration: 'disabled' })
],
providers: [
HttpActive,
{provide: HTTP_INTERCEPTORS, multi: true, useClass: HttpActiveInterceptor}
],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(router: Router, viewportScroller: ViewportScroller, httpActive: HttpActive) {
router.events.pipe(
filter(e => e instanceof Scroll),
switchMap((e: Scroll) => {
if (!e.anchor && !e.position) { return of(e); }
return httpActive.active.pipe(
debounceTime(100), // to give component a chance to trigger a request
filter(v => !v),
first(),
map(() => e)
);
})
).subscribe(e => {
if (e.position) {
viewportScroller.scrollToPosition(e.position);
} else if (e.anchor) {
viewportScroller.scrollToAnchor(e.anchor);
} else {
viewportScroller.scrollToPosition([0, 0]);
}
});
}
}
```
None of the cases above are generic, and won't handle the following component.
```
class MyComponent {
data: any[];
ngOnInit() {
setTimeout(() => {
this.data = ['one', 'two'];
}, 2000);
}
}
```
## I know when my component is stable
It is also possible to reliably define "stability" for an individual component to trigger scroll position restoration. And you can do it with the current API. But I would it discourage doing it. Doing it in every single component is error prone and verbose.
## Next Action
1. I can send a PR updating the docs (fixing examples) and explaining what I covered here, but more briefly. So I think in practice, everyone will have to do something like this based on the state management approach folks choose.
2. The other issue he mentioned is that when we select "enabled", we restore scrolling right after NavigationEnd is fired. NavigationEnd used to be fire after a change detection run happened, but it was changed (some time ago), and now it happens before. I can change it to do it in a microtask, so a change detection run happens. What do you think?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment