feat(router): implement scrolling restoration service (#20030)
For documentation, see `RouterModule.scrollPositionRestoration` Fixes #13636 #10929 #7791 #6595 PR Close #20030
This commit is contained in:
parent
1b253e14ff
commit
49c5234c68
|
@ -25,3 +25,4 @@ export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCase
|
||||||
export {DeprecatedDatePipe, DeprecatedCurrencyPipe, DeprecatedDecimalPipe, DeprecatedPercentPipe} from './pipes/deprecated/index';
|
export {DeprecatedDatePipe, DeprecatedCurrencyPipe, DeprecatedDecimalPipe, DeprecatedPercentPipe} from './pipes/deprecated/index';
|
||||||
export {PLATFORM_BROWSER_ID as ɵPLATFORM_BROWSER_ID, PLATFORM_SERVER_ID as ɵPLATFORM_SERVER_ID, PLATFORM_WORKER_APP_ID as ɵPLATFORM_WORKER_APP_ID, PLATFORM_WORKER_UI_ID as ɵPLATFORM_WORKER_UI_ID, isPlatformBrowser, isPlatformServer, isPlatformWorkerApp, isPlatformWorkerUi} from './platform_id';
|
export {PLATFORM_BROWSER_ID as ɵPLATFORM_BROWSER_ID, PLATFORM_SERVER_ID as ɵPLATFORM_SERVER_ID, PLATFORM_WORKER_APP_ID as ɵPLATFORM_WORKER_APP_ID, PLATFORM_WORKER_UI_ID as ɵPLATFORM_WORKER_UI_ID, isPlatformBrowser, isPlatformServer, isPlatformWorkerApp, isPlatformWorkerUi} from './platform_id';
|
||||||
export {VERSION} from './version';
|
export {VERSION} from './version';
|
||||||
|
export {ViewportScroller, NullViewportScroller as ɵNullViewportScroller} from './viewport_scroller';
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {defineInjectable, inject} from '@angular/core';
|
||||||
|
|
||||||
|
import {DOCUMENT} from './dom_tokens';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Manages the scroll position.
|
||||||
|
*/
|
||||||
|
export abstract class ViewportScroller {
|
||||||
|
// De-sugared tree-shakable injection
|
||||||
|
// See #23917
|
||||||
|
/** @nocollapse */
|
||||||
|
static ngInjectableDef = defineInjectable(
|
||||||
|
{providedIn: 'root', factory: () => new BrowserViewportScroller(inject(DOCUMENT), window)});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Configures the top offset used when scrolling to an anchor.
|
||||||
|
*
|
||||||
|
* When given a tuple with two number, the service will always use the numbers.
|
||||||
|
* When given a function, the service will invoke the function every time it restores scroll
|
||||||
|
* position.
|
||||||
|
*/
|
||||||
|
abstract setOffset(offset: [number, number]|(() => [number, number])): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Returns the current scroll position.
|
||||||
|
*/
|
||||||
|
abstract getScrollPosition(): [number, number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Sets the scroll position.
|
||||||
|
*/
|
||||||
|
abstract scrollToPosition(position: [number, number]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Scrolls to the provided anchor.
|
||||||
|
*/
|
||||||
|
abstract scrollToAnchor(anchor: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Disables automatic scroll restoration provided by the browser.
|
||||||
|
* See also [window.history.scrollRestoration
|
||||||
|
* info](https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration)
|
||||||
|
*/
|
||||||
|
abstract setHistoryScrollRestoration(scrollRestoration: 'auto'|'manual'): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Manages the scroll position.
|
||||||
|
*/
|
||||||
|
export class BrowserViewportScroller implements ViewportScroller {
|
||||||
|
private offset: () => [number, number] = () => [0, 0];
|
||||||
|
|
||||||
|
constructor(private document: any, private window: any) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Configures the top offset used when scrolling to an anchor.
|
||||||
|
*
|
||||||
|
* * When given a number, the service will always use the number.
|
||||||
|
* * When given a function, the service will invoke the function every time it restores scroll
|
||||||
|
* position.
|
||||||
|
*/
|
||||||
|
setOffset(offset: [number, number]|(() => [number, number])): void {
|
||||||
|
if (Array.isArray(offset)) {
|
||||||
|
this.offset = () => offset;
|
||||||
|
} else {
|
||||||
|
this.offset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Returns the current scroll position.
|
||||||
|
*/
|
||||||
|
getScrollPosition(): [number, number] {
|
||||||
|
if (this.supportScrollRestoration()) {
|
||||||
|
return [this.window.scrollX, this.window.scrollY];
|
||||||
|
} else {
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Sets the scroll position.
|
||||||
|
*/
|
||||||
|
scrollToPosition(position: [number, number]): void {
|
||||||
|
if (this.supportScrollRestoration()) {
|
||||||
|
this.window.scrollTo(position[0], position[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Scrolls to the provided anchor.
|
||||||
|
*/
|
||||||
|
scrollToAnchor(anchor: string): void {
|
||||||
|
if (this.supportScrollRestoration()) {
|
||||||
|
const elSelectedById = this.document.querySelector(`#${anchor}`);
|
||||||
|
if (elSelectedById) {
|
||||||
|
this.scrollToElement(elSelectedById);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const elSelectedByName = this.document.querySelector(`[name='${anchor}']`);
|
||||||
|
if (elSelectedByName) {
|
||||||
|
this.scrollToElement(elSelectedByName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Disables automatic scroll restoration provided by the browser.
|
||||||
|
*/
|
||||||
|
setHistoryScrollRestoration(scrollRestoration: 'auto'|'manual'): void {
|
||||||
|
if (this.supportScrollRestoration()) {
|
||||||
|
const history = this.window.history;
|
||||||
|
if (history && history.scrollRestoration) {
|
||||||
|
history.scrollRestoration = scrollRestoration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollToElement(el: any): void {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const left = rect.left + this.window.pageXOffset;
|
||||||
|
const top = rect.top + this.window.pageYOffset;
|
||||||
|
const offset = this.offset();
|
||||||
|
this.window.scrollTo(left - offset[0], top - offset[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We only support scroll restoration when we can get a hold of window.
|
||||||
|
* This means that we do not support this behavior when running in a web worker.
|
||||||
|
*
|
||||||
|
* Lifting this restriction right now would require more changes in the dom adapter.
|
||||||
|
* Since webworkers aren't widely used, we will lift it once RouterScroller is
|
||||||
|
* battle-tested.
|
||||||
|
*/
|
||||||
|
private supportScrollRestoration(): boolean {
|
||||||
|
try {
|
||||||
|
return !!this.window && !!this.window.scrollTo;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Provides an empty implementation of the viewport scroller. This will
|
||||||
|
* live in @angular/common as it will be used by both platform-server and platform-webworker.
|
||||||
|
*/
|
||||||
|
export class NullViewportScroller implements ViewportScroller {
|
||||||
|
/**
|
||||||
|
* @whatItDoes empty implementation
|
||||||
|
*/
|
||||||
|
setOffset(offset: [number, number]|(() => [number, number])): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes empty implementation
|
||||||
|
*/
|
||||||
|
getScrollPosition(): [number, number] { return [0, 0]; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes empty implementation
|
||||||
|
*/
|
||||||
|
scrollToPosition(position: [number, number]): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes empty implementation
|
||||||
|
*/
|
||||||
|
scrollToAnchor(anchor: string): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes empty implementation
|
||||||
|
*/
|
||||||
|
setHistoryScrollRestoration(scrollRestoration: 'auto'|'manual'): void {}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ɵAnimationEngine} from '@angular/animations/browser';
|
import {ɵAnimationEngine} from '@angular/animations/browser';
|
||||||
import {PlatformLocation, ɵPLATFORM_SERVER_ID as PLATFORM_SERVER_ID} from '@angular/common';
|
import {PlatformLocation, ViewportScroller, ɵNullViewportScroller as NullViewportScroller, ɵPLATFORM_SERVER_ID as PLATFORM_SERVER_ID} from '@angular/common';
|
||||||
import {HttpClientModule} from '@angular/common/http';
|
import {HttpClientModule} from '@angular/common/http';
|
||||||
import {Injectable, InjectionToken, Injector, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, Provider, RendererFactory2, RootRenderer, StaticProvider, Testability, createPlatformFactory, isDevMode, platformCore, ɵALLOW_MULTIPLE_PLATFORMS as ALLOW_MULTIPLE_PLATFORMS} from '@angular/core';
|
import {Injectable, InjectionToken, Injector, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, Provider, RendererFactory2, RootRenderer, StaticProvider, Testability, createPlatformFactory, isDevMode, platformCore, ɵALLOW_MULTIPLE_PLATFORMS as ALLOW_MULTIPLE_PLATFORMS} from '@angular/core';
|
||||||
import {HttpModule} from '@angular/http';
|
import {HttpModule} from '@angular/http';
|
||||||
|
@ -74,6 +74,7 @@ export const SERVER_RENDER_PROVIDERS: Provider[] = [
|
||||||
SERVER_RENDER_PROVIDERS,
|
SERVER_RENDER_PROVIDERS,
|
||||||
SERVER_HTTP_PROVIDERS,
|
SERVER_HTTP_PROVIDERS,
|
||||||
{provide: Testability, useValue: null},
|
{provide: Testability, useValue: null},
|
||||||
|
{provide: ViewportScroller, useClass: NullViewportScroller},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ServerModule {
|
export class ServerModule {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {CommonModule, ɵPLATFORM_WORKER_APP_ID as PLATFORM_WORKER_APP_ID} from '@angular/common';
|
import {CommonModule, ViewportScroller, ɵNullViewportScroller as NullViewportScroller, ɵPLATFORM_WORKER_APP_ID as PLATFORM_WORKER_APP_ID} from '@angular/common';
|
||||||
import {APP_INITIALIZER, ApplicationModule, ErrorHandler, NgModule, NgZone, PLATFORM_ID, PlatformRef, RendererFactory2, RootRenderer, StaticProvider, createPlatformFactory, platformCore} from '@angular/core';
|
import {APP_INITIALIZER, ApplicationModule, ErrorHandler, NgModule, NgZone, PLATFORM_ID, PlatformRef, RendererFactory2, RootRenderer, StaticProvider, createPlatformFactory, platformCore} from '@angular/core';
|
||||||
import {DOCUMENT, ɵBROWSER_SANITIZATION_PROVIDERS as BROWSER_SANITIZATION_PROVIDERS} from '@angular/platform-browser';
|
import {DOCUMENT, ɵBROWSER_SANITIZATION_PROVIDERS as BROWSER_SANITIZATION_PROVIDERS} from '@angular/platform-browser';
|
||||||
|
|
||||||
|
@ -71,6 +71,7 @@ export function setupWebWorker(): void {
|
||||||
{provide: ErrorHandler, useFactory: errorHandler, deps: []},
|
{provide: ErrorHandler, useFactory: errorHandler, deps: []},
|
||||||
{provide: MessageBus, useFactory: createMessageBus, deps: [NgZone]},
|
{provide: MessageBus, useFactory: createMessageBus, deps: [NgZone]},
|
||||||
{provide: APP_INITIALIZER, useValue: setupWebWorker, multi: true},
|
{provide: APP_INITIALIZER, useValue: setupWebWorker, multi: true},
|
||||||
|
{provide: ViewportScroller, useClass: NullViewportScroller, deps: []},
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
|
|
@ -401,6 +401,28 @@ export class ActivationEnd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description
|
||||||
|
*
|
||||||
|
* Represents a scrolling event.
|
||||||
|
*/
|
||||||
|
export class Scroll {
|
||||||
|
constructor(
|
||||||
|
/** @docsNotRequired */
|
||||||
|
readonly routerEvent: NavigationEnd,
|
||||||
|
|
||||||
|
/** @docsNotRequired */
|
||||||
|
readonly position: [number, number]|null,
|
||||||
|
|
||||||
|
/** @docsNotRequired */
|
||||||
|
readonly anchor: string|null) {}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
const pos = this.position ? `${this.position[0]}, ${this.position[1]}` : null;
|
||||||
|
return `Scroll(anchor: '${this.anchor}', position: '${pos}')`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description
|
* @description
|
||||||
*
|
*
|
||||||
|
@ -423,8 +445,9 @@ export class ActivationEnd {
|
||||||
* - `NavigationEnd`,
|
* - `NavigationEnd`,
|
||||||
* - `NavigationCancel`,
|
* - `NavigationCancel`,
|
||||||
* - `NavigationError`
|
* - `NavigationError`
|
||||||
|
* - `Scroll`
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Event = RouterEvent | RouteConfigLoadStart | RouteConfigLoadEnd | ChildActivationStart |
|
export type Event = RouterEvent | RouteConfigLoadStart | RouteConfigLoadEnd | ChildActivationStart |
|
||||||
ChildActivationEnd | ActivationStart | ActivationEnd;
|
ChildActivationEnd | ActivationStart | ActivationEnd | Scroll;
|
||||||
|
|
|
@ -11,7 +11,7 @@ export {Data, LoadChildren, LoadChildrenCallback, ResolveData, Route, Routes, Ru
|
||||||
export {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
export {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
||||||
export {RouterLinkActive} from './directives/router_link_active';
|
export {RouterLinkActive} from './directives/router_link_active';
|
||||||
export {RouterOutlet} from './directives/router_outlet';
|
export {RouterOutlet} from './directives/router_outlet';
|
||||||
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized} from './events';
|
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
|
||||||
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
|
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
|
||||||
export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
|
export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
|
||||||
export {NavigationExtras, Router} from './router';
|
export {NavigationExtras, Router} from './router';
|
||||||
|
|
|
@ -543,7 +543,6 @@ export class Router {
|
||||||
rawUrl: UrlTree, source: NavigationTrigger, state: {navigationId: number}|null,
|
rawUrl: UrlTree, source: NavigationTrigger, state: {navigationId: number}|null,
|
||||||
extras: NavigationExtras): Promise<boolean> {
|
extras: NavigationExtras): Promise<boolean> {
|
||||||
const lastNavigation = this.navigations.value;
|
const lastNavigation = this.navigations.value;
|
||||||
|
|
||||||
// If the user triggers a navigation imperatively (e.g., by using navigateByUrl),
|
// If the user triggers a navigation imperatively (e.g., by using navigateByUrl),
|
||||||
// and that navigation results in 'replaceState' that leads to the same URL,
|
// and that navigation results in 'replaceState' that leads to the same URL,
|
||||||
// we should skip those.
|
// we should skip those.
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {APP_BASE_HREF, HashLocationStrategy, LOCATION_INITIALIZED, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
|
import {APP_BASE_HREF, HashLocationStrategy, LOCATION_INITIALIZED, Location, LocationStrategy, PathLocationStrategy, PlatformLocation, ViewportScroller} from '@angular/common';
|
||||||
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Compiler, ComponentRef, Inject, Injectable, InjectionToken, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
|
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Compiler, ComponentRef, Inject, Injectable, InjectionToken, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
|
||||||
import {ɵgetDOM as getDOM} from '@angular/platform-browser';
|
import {ɵgetDOM as getDOM} from '@angular/platform-browser';
|
||||||
import {Subject, of } from 'rxjs';
|
import {Subject, of } from 'rxjs';
|
||||||
|
@ -21,6 +21,7 @@ import {ErrorHandler, Router} from './router';
|
||||||
import {ROUTES} from './router_config_loader';
|
import {ROUTES} from './router_config_loader';
|
||||||
import {ChildrenOutletContexts} from './router_outlet_context';
|
import {ChildrenOutletContexts} from './router_outlet_context';
|
||||||
import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
|
import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
|
||||||
|
import {RouterScroller} from './router_scroller';
|
||||||
import {ActivatedRoute} from './router_state';
|
import {ActivatedRoute} from './router_state';
|
||||||
import {UrlHandlingStrategy} from './url_handling_strategy';
|
import {UrlHandlingStrategy} from './url_handling_strategy';
|
||||||
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
|
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
|
||||||
|
@ -165,6 +166,11 @@ export class RouterModule {
|
||||||
PlatformLocation, [new Inject(APP_BASE_HREF), new Optional()], ROUTER_CONFIGURATION
|
PlatformLocation, [new Inject(APP_BASE_HREF), new Optional()], ROUTER_CONFIGURATION
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: RouterScroller,
|
||||||
|
useFactory: createRouterScroller,
|
||||||
|
deps: [Router, ViewportScroller, ROUTER_CONFIGURATION]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: PreloadingStrategy,
|
provide: PreloadingStrategy,
|
||||||
useExisting: config && config.preloadingStrategy ? config.preloadingStrategy :
|
useExisting: config && config.preloadingStrategy ? config.preloadingStrategy :
|
||||||
|
@ -184,6 +190,14 @@ export class RouterModule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createRouterScroller(
|
||||||
|
router: Router, viewportScroller: ViewportScroller, config: ExtraOptions): RouterScroller {
|
||||||
|
if (config.scrollOffset) {
|
||||||
|
viewportScroller.setOffset(config.scrollOffset);
|
||||||
|
}
|
||||||
|
return new RouterScroller(router, viewportScroller, config);
|
||||||
|
}
|
||||||
|
|
||||||
export function provideLocationStrategy(
|
export function provideLocationStrategy(
|
||||||
platformLocationStrategy: PlatformLocation, baseHref: string, options: ExtraOptions = {}) {
|
platformLocationStrategy: PlatformLocation, baseHref: string, options: ExtraOptions = {}) {
|
||||||
return options.useHash ? new HashLocationStrategy(platformLocationStrategy, baseHref) :
|
return options.useHash ? new HashLocationStrategy(platformLocationStrategy, baseHref) :
|
||||||
|
@ -291,6 +305,77 @@ export interface ExtraOptions {
|
||||||
*/
|
*/
|
||||||
onSameUrlNavigation?: 'reload'|'ignore';
|
onSameUrlNavigation?: 'reload'|'ignore';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures if the scroll position needs to be restored when navigating back.
|
||||||
|
*
|
||||||
|
* * 'disabled'--does nothing (default).
|
||||||
|
* * 'top'--set the scroll position to 0,0..
|
||||||
|
* * 'enabled'--set the scroll position to the stored position. This option will be the default in
|
||||||
|
* the future.
|
||||||
|
*
|
||||||
|
* When enabled, the router store store scroll positions when navigating forward, and will
|
||||||
|
* restore the stored positions whe navigating back (popstate). When navigating forward,
|
||||||
|
* the scroll position will be set to [0, 0], or to the anchor if one is provided.
|
||||||
|
*
|
||||||
|
* You can implement custom scroll restoration behavior as follows.
|
||||||
|
* ```typescript
|
||||||
|
* 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]);
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* You can also implement component-specific scrolling like this:
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* 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]);
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
scrollPositionRestoration?: 'disabled'|'enabled'|'top';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures if the router should scroll to the element when the url has a fragment.
|
||||||
|
*
|
||||||
|
* * 'disabled'--does nothing (default).
|
||||||
|
* * 'enabled'--scrolls to the element. This option will be the default in the future.
|
||||||
|
*
|
||||||
|
* Anchor scrolling does not happen on 'popstate'. Instead, we restore the position
|
||||||
|
* that we stored or scroll to the top.
|
||||||
|
*/
|
||||||
|
anchorScrolling?: 'disabled'|'enabled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the scroll offset the router will use when scrolling to an element.
|
||||||
|
*
|
||||||
|
* When given a tuple with two numbers, the router will always use the numbers.
|
||||||
|
* When given a function, the router will invoke the function every time it restores scroll
|
||||||
|
* position.
|
||||||
|
*/
|
||||||
|
scrollOffset?: [number, number]|(() => [number, number]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines how the router merges params, data and resolved data from parent to child
|
* Defines how the router merges params, data and resolved data from parent to child
|
||||||
* routes. Available options are:
|
* routes. Available options are:
|
||||||
|
@ -406,6 +491,7 @@ export class RouterInitializer {
|
||||||
bootstrapListener(bootstrappedComponentRef: ComponentRef<any>): void {
|
bootstrapListener(bootstrappedComponentRef: ComponentRef<any>): void {
|
||||||
const opts = this.injector.get(ROUTER_CONFIGURATION);
|
const opts = this.injector.get(ROUTER_CONFIGURATION);
|
||||||
const preloader = this.injector.get(RouterPreloader);
|
const preloader = this.injector.get(RouterPreloader);
|
||||||
|
const routerScroller = this.injector.get(RouterScroller);
|
||||||
const router = this.injector.get(Router);
|
const router = this.injector.get(Router);
|
||||||
const ref = this.injector.get<ApplicationRef>(ApplicationRef);
|
const ref = this.injector.get<ApplicationRef>(ApplicationRef);
|
||||||
|
|
||||||
|
@ -420,6 +506,7 @@ export class RouterInitializer {
|
||||||
}
|
}
|
||||||
|
|
||||||
preloader.setUpPreloading();
|
preloader.setUpPreloading();
|
||||||
|
routerScroller.init();
|
||||||
router.resetRootComponentType(ref.componentTypes[0]);
|
router.resetRootComponentType(ref.componentTypes[0]);
|
||||||
this.resultOfPreactivationDone.next(null !);
|
this.resultOfPreactivationDone.next(null !);
|
||||||
this.resultOfPreactivationDone.complete();
|
this.resultOfPreactivationDone.complete();
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {ViewportScroller} from '@angular/common';
|
||||||
|
import {OnDestroy} from '@angular/core';
|
||||||
|
import {Unsubscribable} from 'rxjs';
|
||||||
|
|
||||||
|
import {NavigationEnd, NavigationStart, Scroll} from './events';
|
||||||
|
import {Router} from './router';
|
||||||
|
|
||||||
|
export class RouterScroller implements OnDestroy {
|
||||||
|
private routerEventsSubscription: Unsubscribable;
|
||||||
|
private scrollEventsSubscription: Unsubscribable;
|
||||||
|
|
||||||
|
private lastId = 0;
|
||||||
|
private lastSource: 'imperative'|'popstate'|'hashchange'|undefined = 'imperative';
|
||||||
|
private restoredId = 0;
|
||||||
|
private store: {[key: string]: [number, number]} = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
/** @docsNotRequired */ public readonly viewportScroller: ViewportScroller, private options: {
|
||||||
|
scrollPositionRestoration?: 'disabled' | 'enabled' | 'top',
|
||||||
|
anchorScrolling?: 'disabled'|'enabled'
|
||||||
|
} = {}) {}
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
// we want to disable the automatic scrolling because having two places
|
||||||
|
// responsible for scrolling results race conditions, especially given
|
||||||
|
// that browser don't implement this behavior consistently
|
||||||
|
if (this.options.scrollPositionRestoration !== 'disabled') {
|
||||||
|
this.viewportScroller.setHistoryScrollRestoration('manual');
|
||||||
|
}
|
||||||
|
this.routerEventsSubscription = this.createScrollEvents();
|
||||||
|
this.scrollEventsSubscription = this.consumeScrollEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createScrollEvents() {
|
||||||
|
return this.router.events.subscribe(e => {
|
||||||
|
if (e instanceof NavigationStart) {
|
||||||
|
// store the scroll position of the current stable navigations.
|
||||||
|
this.store[this.lastId] = this.viewportScroller.getScrollPosition();
|
||||||
|
this.lastSource = e.navigationTrigger;
|
||||||
|
this.restoredId = e.restoredState ? e.restoredState.navigationId : 0;
|
||||||
|
} else if (e instanceof NavigationEnd) {
|
||||||
|
this.lastId = e.id;
|
||||||
|
this.scheduleScrollEvent(e, this.router.parseUrl(e.urlAfterRedirects).fragment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private consumeScrollEvents() {
|
||||||
|
return this.router.events.subscribe(e => {
|
||||||
|
if (!(e instanceof Scroll)) return;
|
||||||
|
// a popstate event. The pop state event will always ignore anchor scrolling.
|
||||||
|
if (e.position) {
|
||||||
|
if (this.options.scrollPositionRestoration === 'top') {
|
||||||
|
this.viewportScroller.scrollToPosition([0, 0]);
|
||||||
|
} else if (this.options.scrollPositionRestoration === 'enabled') {
|
||||||
|
this.viewportScroller.scrollToPosition(e.position);
|
||||||
|
}
|
||||||
|
// imperative navigation "forward"
|
||||||
|
} else {
|
||||||
|
if (e.anchor && this.options.anchorScrolling === 'enabled') {
|
||||||
|
this.viewportScroller.scrollToAnchor(e.anchor);
|
||||||
|
} else if (this.options.scrollPositionRestoration !== 'disabled') {
|
||||||
|
this.viewportScroller.scrollToPosition([0, 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleScrollEvent(routerEvent: NavigationEnd, anchor: string|null): void {
|
||||||
|
this.router.triggerEvent(new Scroll(
|
||||||
|
routerEvent, this.lastSource === 'popstate' ? this.store[this.restoredId] : null, anchor));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.routerEventsSubscription) {
|
||||||
|
this.routerEventsSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
if (this.scrollEventsSubscription) {
|
||||||
|
this.scrollEventsSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,13 +6,13 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {APP_BASE_HREF} from '@angular/common';
|
import {APP_BASE_HREF, Location, ViewportScroller} from '@angular/common';
|
||||||
import {ApplicationRef, CUSTOM_ELEMENTS_SCHEMA, Component, NgModule, destroyPlatform} from '@angular/core';
|
import {ApplicationRef, CUSTOM_ELEMENTS_SCHEMA, Component, NgModule, destroyPlatform} from '@angular/core';
|
||||||
import {inject} from '@angular/core/testing';
|
import {inject} from '@angular/core/testing';
|
||||||
import {BrowserModule, DOCUMENT, ɵgetDOM as getDOM} from '@angular/platform-browser';
|
import {BrowserModule, DOCUMENT, ɵgetDOM as getDOM} from '@angular/platform-browser';
|
||||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
import {NavigationEnd, Resolve, Router, RouterModule} from '@angular/router';
|
import {NavigationEnd, NavigationStart, Resolve, Router, RouterModule} from '@angular/router';
|
||||||
|
import {filter, first} from 'rxjs/operators';
|
||||||
|
|
||||||
describe('bootstrap', () => {
|
describe('bootstrap', () => {
|
||||||
if (isNode) return;
|
if (isNode) return;
|
||||||
|
@ -86,7 +86,7 @@ describe('bootstrap', () => {
|
||||||
expect(log).toEqual([
|
expect(log).toEqual([
|
||||||
'TestModule', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart',
|
'TestModule', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart',
|
||||||
'ChildActivationStart', 'ActivationStart', 'GuardsCheckEnd', 'ResolveStart', 'ResolveEnd',
|
'ChildActivationStart', 'ActivationStart', 'GuardsCheckEnd', 'ResolveStart', 'ResolveEnd',
|
||||||
'RootCmp', 'ActivationEnd', 'ChildActivationEnd', 'NavigationEnd'
|
'RootCmp', 'ActivationEnd', 'ChildActivationEnd', 'NavigationEnd', 'Scroll'
|
||||||
]);
|
]);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@ -248,4 +248,78 @@ describe('bootstrap', () => {
|
||||||
appRef.components[0].destroy();
|
appRef.components[0].destroy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should restore the scrolling position', async(done) => {
|
||||||
|
@Component({
|
||||||
|
selector: 'component-a',
|
||||||
|
template: `
|
||||||
|
<div style="height: 3000px;"></div>
|
||||||
|
<div id="marker1"></div>
|
||||||
|
<div style="height: 3000px;"></div>
|
||||||
|
<div id="marker2"></div>
|
||||||
|
<div style="height: 3000px;"></div>
|
||||||
|
<a name="marker3"></a>
|
||||||
|
<div style="height: 3000px;"></div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class TallComponent {
|
||||||
|
}
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
RouterModule.forRoot(
|
||||||
|
[
|
||||||
|
{path: '', pathMatch: 'full', redirectTo: '/aa'},
|
||||||
|
{path: 'aa', component: TallComponent}, {path: 'bb', component: TallComponent},
|
||||||
|
{path: 'cc', component: TallComponent},
|
||||||
|
{path: 'fail', component: TallComponent, canActivate: ['returnFalse']}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
useHash: true,
|
||||||
|
scrollPositionRestoration: 'enabled',
|
||||||
|
anchorScrolling: 'enabled',
|
||||||
|
scrollOffset: [0, 100],
|
||||||
|
onSameUrlNavigation: 'reload'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
declarations: [TallComponent, RootCmp],
|
||||||
|
bootstrap: [RootCmp],
|
||||||
|
providers: [...testProviders, {provide: 'returnFalse', useValue: () => false}],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
class TestModule {
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await platformBrowserDynamic([]).bootstrapModule(TestModule);
|
||||||
|
const router = res.injector.get(Router);
|
||||||
|
const location: Location = res.injector.get(Location);
|
||||||
|
|
||||||
|
await router.navigateByUrl('/aa');
|
||||||
|
window.scrollTo(0, 5000);
|
||||||
|
|
||||||
|
await router.navigateByUrl('/fail');
|
||||||
|
expect(window.scrollY).toEqual(5000);
|
||||||
|
|
||||||
|
await router.navigateByUrl('/bb');
|
||||||
|
window.scrollTo(0, 3000);
|
||||||
|
|
||||||
|
expect(window.scrollY).toEqual(3000);
|
||||||
|
|
||||||
|
await router.navigateByUrl('/cc');
|
||||||
|
expect(window.scrollY).toEqual(0);
|
||||||
|
|
||||||
|
await router.navigateByUrl('/aa#marker2');
|
||||||
|
expect(window.scrollY >= 5900).toBe(true);
|
||||||
|
expect(window.scrollY < 6000).toBe(true); // offset
|
||||||
|
|
||||||
|
await router.navigateByUrl('/aa#marker3');
|
||||||
|
expect(window.scrollY >= 8900).toBe(true);
|
||||||
|
expect(window.scrollY < 9000).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
function waitForNavigationToComplete(router: Router): Promise<any> {
|
||||||
|
return router.events.pipe(filter((e: any) => e instanceof NavigationEnd), first()).toPromise();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {fakeAsync, tick} from '@angular/core/testing';
|
||||||
|
import {DefaultUrlSerializer, NavigationEnd, NavigationStart, RouterEvent} from '@angular/router';
|
||||||
|
import {Subject} from 'rxjs';
|
||||||
|
import {filter, switchMap} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import {Scroll} from '../src/events';
|
||||||
|
import {RouterScroller} from '../src/router_scroller';
|
||||||
|
|
||||||
|
describe('RouterScroller', () => {
|
||||||
|
describe('scroll to top', () => {
|
||||||
|
it('should scroll to the top', () => {
|
||||||
|
const {events, viewportScroller} =
|
||||||
|
createRouterScroller({scrollPositionRestoration: 'top', anchorScrolling: 'disabled'});
|
||||||
|
|
||||||
|
events.next(new NavigationStart(1, '/a'));
|
||||||
|
events.next(new NavigationEnd(1, '/a', '/a'));
|
||||||
|
expect(viewportScroller.scrollToPosition).toHaveBeenCalledWith([0, 0]);
|
||||||
|
|
||||||
|
events.next(new NavigationStart(2, '/a'));
|
||||||
|
events.next(new NavigationEnd(2, '/b', '/b'));
|
||||||
|
expect(viewportScroller.scrollToPosition).toHaveBeenCalledWith([0, 0]);
|
||||||
|
|
||||||
|
events.next(new NavigationStart(3, '/a', 'popstate'));
|
||||||
|
events.next(new NavigationEnd(3, '/a', '/a'));
|
||||||
|
expect(viewportScroller.scrollToPosition).toHaveBeenCalledWith([0, 0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scroll to the stored position', () => {
|
||||||
|
it('should scroll to the stored position on popstate', () => {
|
||||||
|
const {events, viewportScroller} =
|
||||||
|
createRouterScroller({scrollPositionRestoration: 'enabled', anchorScrolling: 'disabled'});
|
||||||
|
|
||||||
|
events.next(new NavigationStart(1, '/a'));
|
||||||
|
events.next(new NavigationEnd(1, '/a', '/a'));
|
||||||
|
setScroll(viewportScroller, 10, 100);
|
||||||
|
expect(viewportScroller.scrollToPosition).toHaveBeenCalledWith([0, 0]);
|
||||||
|
|
||||||
|
events.next(new NavigationStart(2, '/b'));
|
||||||
|
events.next(new NavigationEnd(2, '/b', '/b'));
|
||||||
|
setScroll(viewportScroller, 20, 200);
|
||||||
|
expect(viewportScroller.scrollToPosition).toHaveBeenCalledWith([0, 0]);
|
||||||
|
|
||||||
|
events.next(new NavigationStart(3, '/a', 'popstate', {navigationId: 1}));
|
||||||
|
events.next(new NavigationEnd(3, '/a', '/a'));
|
||||||
|
expect(viewportScroller.scrollToPosition).toHaveBeenCalledWith([10, 100]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('anchor scrolling', () => {
|
||||||
|
it('should work (scrollPositionRestoration is disabled)', () => {
|
||||||
|
const {events, viewportScroller} =
|
||||||
|
createRouterScroller({scrollPositionRestoration: 'disabled', anchorScrolling: 'enabled'});
|
||||||
|
events.next(new NavigationStart(1, '/a#anchor'));
|
||||||
|
events.next(new NavigationEnd(1, '/a#anchor', '/a#anchor'));
|
||||||
|
expect(viewportScroller.scrollToAnchor).toHaveBeenCalledWith('anchor');
|
||||||
|
|
||||||
|
events.next(new NavigationStart(2, '/a#anchor2'));
|
||||||
|
events.next(new NavigationEnd(2, '/a#anchor2', '/a#anchor2'));
|
||||||
|
expect(viewportScroller.scrollToAnchor).toHaveBeenCalledWith('anchor2');
|
||||||
|
viewportScroller.scrollToAnchor.calls.reset();
|
||||||
|
|
||||||
|
// we never scroll to anchor when navigating back.
|
||||||
|
events.next(new NavigationStart(3, '/a#anchor', 'popstate'));
|
||||||
|
events.next(new NavigationEnd(3, '/a#anchor', '/a#anchor'));
|
||||||
|
expect(viewportScroller.scrollToAnchor).not.toHaveBeenCalled();
|
||||||
|
expect(viewportScroller.scrollToPosition).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work (scrollPositionRestoration is enabled)', () => {
|
||||||
|
const {events, viewportScroller} =
|
||||||
|
createRouterScroller({scrollPositionRestoration: 'enabled', anchorScrolling: 'enabled'});
|
||||||
|
events.next(new NavigationStart(1, '/a#anchor'));
|
||||||
|
events.next(new NavigationEnd(1, '/a#anchor', '/a#anchor'));
|
||||||
|
expect(viewportScroller.scrollToAnchor).toHaveBeenCalledWith('anchor');
|
||||||
|
|
||||||
|
events.next(new NavigationStart(2, '/a#anchor2'));
|
||||||
|
events.next(new NavigationEnd(2, '/a#anchor2', '/a#anchor2'));
|
||||||
|
expect(viewportScroller.scrollToAnchor).toHaveBeenCalledWith('anchor2');
|
||||||
|
viewportScroller.scrollToAnchor.calls.reset();
|
||||||
|
|
||||||
|
// we never scroll to anchor when navigating back
|
||||||
|
events.next(new NavigationStart(3, '/a#anchor', 'popstate', {navigationId: 1}));
|
||||||
|
events.next(new NavigationEnd(3, '/a#anchor', '/a#anchor'));
|
||||||
|
expect(viewportScroller.scrollToAnchor).not.toHaveBeenCalled();
|
||||||
|
expect(viewportScroller.scrollToPosition).toHaveBeenCalledWith([0, 0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extending a scroll service', () => {
|
||||||
|
it('work', fakeAsync(() => {
|
||||||
|
const {events, viewportScroller, router} = createRouterScroller(
|
||||||
|
{scrollPositionRestoration: 'disabled', anchorScrolling: 'disabled'});
|
||||||
|
|
||||||
|
router.events
|
||||||
|
.pipe(filter(e => e instanceof Scroll && !!e.position), switchMap(p => {
|
||||||
|
// can be any delay (e.g., we can wait for NgRx store to emit an event)
|
||||||
|
const r = new Subject<any>();
|
||||||
|
setTimeout(() => {
|
||||||
|
r.next(p);
|
||||||
|
r.complete();
|
||||||
|
}, 1000);
|
||||||
|
return r;
|
||||||
|
}))
|
||||||
|
.subscribe((e: Scroll) => { viewportScroller.scrollToPosition(e.position); });
|
||||||
|
|
||||||
|
events.next(new NavigationStart(1, '/a'));
|
||||||
|
events.next(new NavigationEnd(1, '/a', '/a'));
|
||||||
|
setScroll(viewportScroller, 10, 100);
|
||||||
|
|
||||||
|
events.next(new NavigationStart(2, '/b'));
|
||||||
|
events.next(new NavigationEnd(2, '/b', '/b'));
|
||||||
|
setScroll(viewportScroller, 20, 200);
|
||||||
|
|
||||||
|
events.next(new NavigationStart(3, '/c'));
|
||||||
|
events.next(new NavigationEnd(3, '/c', '/c'));
|
||||||
|
setScroll(viewportScroller, 30, 300);
|
||||||
|
|
||||||
|
events.next(new NavigationStart(4, '/a', 'popstate', {navigationId: 1}));
|
||||||
|
events.next(new NavigationEnd(4, '/a', '/a'));
|
||||||
|
|
||||||
|
tick(500);
|
||||||
|
expect(viewportScroller.scrollToPosition).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
events.next(new NavigationStart(5, '/a', 'popstate', {navigationId: 1}));
|
||||||
|
events.next(new NavigationEnd(5, '/a', '/a'));
|
||||||
|
|
||||||
|
tick(5000);
|
||||||
|
expect(viewportScroller.scrollToPosition).toHaveBeenCalledWith([10, 100]);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function createRouterScroller({scrollPositionRestoration, anchorScrolling}: {
|
||||||
|
scrollPositionRestoration: 'disabled' | 'enabled' | 'top',
|
||||||
|
anchorScrolling: 'disabled' | 'enabled'
|
||||||
|
}) {
|
||||||
|
const events = new Subject<RouterEvent>();
|
||||||
|
const router = <any>{
|
||||||
|
events,
|
||||||
|
parseUrl: (url: any) => new DefaultUrlSerializer().parse(url),
|
||||||
|
triggerEvent: (e: any) => events.next(e)
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewportScroller = jasmine.createSpyObj(
|
||||||
|
'viewportScroller',
|
||||||
|
['getScrollPosition', 'scrollToPosition', 'scrollToAnchor', 'setHistoryScrollRestoration']);
|
||||||
|
setScroll(viewportScroller, 0, 0);
|
||||||
|
|
||||||
|
const scroller =
|
||||||
|
new RouterScroller(router, viewportScroller, {scrollPositionRestoration, anchorScrolling});
|
||||||
|
scroller.init();
|
||||||
|
|
||||||
|
return {events, viewportScroller, router};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setScroll(viewportScroller: any, x: number, y: number) {
|
||||||
|
viewportScroller.getScrollPosition.and.returnValue([x, y]);
|
||||||
|
}
|
||||||
|
});
|
|
@ -433,6 +433,15 @@ export declare class UpperCasePipe implements PipeTransform {
|
||||||
|
|
||||||
export declare const VERSION: Version;
|
export declare const VERSION: Version;
|
||||||
|
|
||||||
|
export declare abstract class ViewportScroller {
|
||||||
|
abstract getScrollPosition(): [number, number];
|
||||||
|
abstract scrollToAnchor(anchor: string): void;
|
||||||
|
abstract scrollToPosition(position: [number, number]): void;
|
||||||
|
abstract setHistoryScrollRestoration(scrollRestoration: 'auto' | 'manual'): void;
|
||||||
|
abstract setOffset(offset: [number, number] | (() => [number, number])): void;
|
||||||
|
static ngInjectableDef: never;
|
||||||
|
}
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare enum WeekDay {
|
export declare enum WeekDay {
|
||||||
Sunday = 0,
|
Sunday = 0,
|
||||||
|
|
|
@ -108,15 +108,18 @@ export declare class DefaultUrlSerializer implements UrlSerializer {
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare type DetachedRouteHandle = {};
|
export declare type DetachedRouteHandle = {};
|
||||||
|
|
||||||
export declare type Event = RouterEvent | RouteConfigLoadStart | RouteConfigLoadEnd | ChildActivationStart | ChildActivationEnd | ActivationStart | ActivationEnd;
|
export declare type Event = RouterEvent | RouteConfigLoadStart | RouteConfigLoadEnd | ChildActivationStart | ChildActivationEnd | ActivationStart | ActivationEnd | Scroll;
|
||||||
|
|
||||||
export interface ExtraOptions {
|
export interface ExtraOptions {
|
||||||
|
anchorScrolling?: 'disabled' | 'enabled';
|
||||||
enableTracing?: boolean;
|
enableTracing?: boolean;
|
||||||
errorHandler?: ErrorHandler;
|
errorHandler?: ErrorHandler;
|
||||||
initialNavigation?: InitialNavigation;
|
initialNavigation?: InitialNavigation;
|
||||||
onSameUrlNavigation?: 'reload' | 'ignore';
|
onSameUrlNavigation?: 'reload' | 'ignore';
|
||||||
paramsInheritanceStrategy?: 'emptyOnly' | 'always';
|
paramsInheritanceStrategy?: 'emptyOnly' | 'always';
|
||||||
preloadingStrategy?: any;
|
preloadingStrategy?: any;
|
||||||
|
scrollOffset?: [number, number] | (() => [number, number]);
|
||||||
|
scrollPositionRestoration?: 'disabled' | 'enabled' | 'top';
|
||||||
useHash?: boolean;
|
useHash?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -465,6 +468,17 @@ export declare class RoutesRecognized extends RouterEvent {
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare type RunGuardsAndResolvers = 'paramsChange' | 'paramsOrQueryParamsChange' | 'always';
|
export declare type RunGuardsAndResolvers = 'paramsChange' | 'paramsOrQueryParamsChange' | 'always';
|
||||||
|
|
||||||
|
export declare class Scroll {
|
||||||
|
readonly anchor: string | null;
|
||||||
|
readonly position: [number, number] | null;
|
||||||
|
readonly routerEvent: NavigationEnd;
|
||||||
|
constructor(
|
||||||
|
routerEvent: NavigationEnd,
|
||||||
|
position: [number, number] | null,
|
||||||
|
anchor: string | null);
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare abstract class UrlHandlingStrategy {
|
export declare abstract class UrlHandlingStrategy {
|
||||||
abstract extract(url: UrlTree): UrlTree;
|
abstract extract(url: UrlTree): UrlTree;
|
||||||
|
|
Loading…
Reference in New Issue