feat(router): add support for custom route reuse strategies

This commit is contained in:
vsavkin 2016-11-29 23:21:41 -08:00
parent c4bbafc291
commit 42cf06fa12
9 changed files with 319 additions and 35 deletions

View File

@ -8,38 +8,65 @@
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
import {TreeNode} from './utils/tree';
export function createRouterState(curr: RouterStateSnapshot, prevState: RouterState): RouterState {
const root = createNode(curr._root, prevState ? prevState._root : undefined);
export function createRouterState(
routeReuseStrategy: RouteReuseStrategy, curr: RouterStateSnapshot,
prevState: RouterState): RouterState {
const root = createNode(routeReuseStrategy, curr._root, prevState ? prevState._root : undefined);
return new RouterState(root, curr);
}
function createNode(curr: TreeNode<ActivatedRouteSnapshot>, prevState?: TreeNode<ActivatedRoute>):
TreeNode<ActivatedRoute> {
if (prevState && equalRouteSnapshots(prevState.value.snapshot, curr.value)) {
function createNode(
routeReuseStrategy: RouteReuseStrategy, curr: TreeNode<ActivatedRouteSnapshot>,
prevState?: TreeNode<ActivatedRoute>): TreeNode<ActivatedRoute> {
// reuse an activated route that is currently displayed on the screen
if (prevState && routeReuseStrategy.shouldReuseRoute(curr.value, prevState.value.snapshot)) {
const value = prevState.value;
value._futureSnapshot = curr.value;
const children = createOrReuseChildren(curr, prevState);
const children = createOrReuseChildren(routeReuseStrategy, curr, prevState);
return new TreeNode<ActivatedRoute>(value, children);
// retrieve an activated route that is used to be displayed, but is not currently displayed
} else if (routeReuseStrategy.retrieve(curr.value)) {
const tree: TreeNode<ActivatedRoute> =
(<DetachedRouteHandleInternal>routeReuseStrategy.retrieve(curr.value)).route;
setFutureSnapshotsOfActivatedRoutes(curr, tree);
return tree;
} else {
const value = createActivatedRoute(curr.value);
const children = curr.children.map(c => createNode(c));
const children = curr.children.map(c => createNode(routeReuseStrategy, c));
return new TreeNode<ActivatedRoute>(value, children);
}
}
function setFutureSnapshotsOfActivatedRoutes(
curr: TreeNode<ActivatedRouteSnapshot>, result: TreeNode<ActivatedRoute>): void {
if (curr.value.routeConfig !== result.value.routeConfig) {
throw new Error('Cannot reattach ActivatedRouteSnapshot created from a different route');
}
if (curr.children.length !== result.children.length) {
throw new Error('Cannot reattach ActivatedRouteSnapshot with a different number of children');
}
result.value._futureSnapshot = curr.value;
for (let i = 0; i < curr.children.length; ++i) {
setFutureSnapshotsOfActivatedRoutes(curr.children[i], result.children[i]);
}
}
function createOrReuseChildren(
curr: TreeNode<ActivatedRouteSnapshot>, prevState: TreeNode<ActivatedRoute>) {
routeReuseStrategy: RouteReuseStrategy, curr: TreeNode<ActivatedRouteSnapshot>,
prevState: TreeNode<ActivatedRoute>) {
return curr.children.map(child => {
for (const p of prevState.children) {
if (equalRouteSnapshots(p.value.snapshot, child.value)) {
return createNode(child, p);
if (routeReuseStrategy.shouldReuseRoute(p.value.snapshot, child.value)) {
return createNode(routeReuseStrategy, child, p);
}
}
return createNode(child);
return createNode(routeReuseStrategy, child);
});
}
@ -48,7 +75,3 @@ function createActivatedRoute(c: ActivatedRouteSnapshot) {
new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams),
new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c);
}
function equalRouteSnapshots(a: ActivatedRouteSnapshot, b: ActivatedRouteSnapshot): boolean {
return a._routeConfig === b._routeConfig;
}

View File

@ -67,11 +67,27 @@ export class RouterOutlet implements OnDestroy {
return this._activatedRoute;
}
detach(): ComponentRef<any> {
if (!this.activated) throw new Error('Outlet is not activated');
this.location.detach();
const r = this.activated;
this.activated = null;
this._activatedRoute = null;
return r;
}
attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute) {
this.activated = ref;
this._activatedRoute = activatedRoute;
this.location.insert(ref.hostView);
}
deactivate(): void {
if (this.activated) {
const c = this.component;
this.activated.destroy();
this.activated = null;
this._activatedRoute = null;
this.deactivateEvents.emit(c);
}
}

View File

@ -12,6 +12,7 @@ export {RouterLink, RouterLinkWithHref} from './directives/router_link';
export {RouterLinkActive} from './directives/router_link_active';
export {RouterOutlet} from './directives/router_outlet';
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
export {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationExtras, NavigationStart, Router, RoutesRecognized} from './router';
export {ExtraOptions, ROUTER_CONFIGURATION, ROUTER_INITIALIZER, RouterModule, provideRoutes} from './router_module';
export {RouterOutletMap} from './router_outlet_map';

View File

@ -0,0 +1,64 @@
/**
* @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 {ComponentRef} from '@angular/core';
import {ActivatedRoute, ActivatedRouteSnapshot} from './router_state';
import {TreeNode} from './utils/tree';
/**
* @whatItDoes Represents the detached route tree.
*
* This is an opaque value the router will give to a custom route reuse strategy
* to store and retrieve later on.
*
* @experimental
*/
export type DetachedRouteHandle = {};
/**
* @internal
*/
export type DetachedRouteHandleInternal = {
componentRef: ComponentRef<any>,
route: TreeNode<ActivatedRoute>
};
/**
* @whatItDoes Provides a way to customize when activated routes get reused.
*
* @experimental
*/
export abstract class RouteReuseStrategy {
/**
* Determines if this route (and its subtree) should be detached to be reused later.
*/
abstract shouldDetach(route: ActivatedRouteSnapshot): boolean;
/**
* Stores the detached route.
*/
abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void;
/**
* Determines if this route (and its subtree) should be reattached.
*/
abstract shouldAttach(route: ActivatedRouteSnapshot): boolean;
/**
* Retrieves the previously stored route.
*/
abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle;
/**
* Determines if a route should be reused.
*/
abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
}

View File

@ -28,6 +28,7 @@ import {createRouterState} from './create_router_state';
import {createUrlTree} from './create_url_tree';
import {RouterOutlet} from './directives/router_outlet';
import {recognize} from './recognize';
import {DetachedRouteHandle, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader';
import {RouterOutletMap} from './router_outlet_map';
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState, equalParamsAndUrlSegments, inheritedParamsDataResolve} from './router_state';
@ -287,6 +288,20 @@ type NavigationParams = {
promise: Promise<boolean>
};
/**
* Does not detach any subtrees. Reuses routes as long as their route config is the same.
*/
export class DefaultRouteReuseStrategy implements RouteReuseStrategy {
shouldDetach(route: ActivatedRouteSnapshot): boolean { return false; }
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {}
shouldAttach(route: ActivatedRouteSnapshot): boolean { return false; }
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { return null; }
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
}
/**
* @whatItDoes Provides the navigation and url manipulation capabilities.
*
@ -326,6 +341,8 @@ export class Router {
*/
urlHandlingStrategy: UrlHandlingStrategy = new DefaultUrlHandlingStrategy();
routeReuseStrategy: RouteReuseStrategy = new DefaultRouteReuseStrategy();
/**
* Creates the router service.
*/
@ -703,7 +720,8 @@ export class Router {
const routerState$ =
map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => {
if (shouldActivate) {
const state = createRouterState(snapshot, this.currentRouterState);
const state =
createRouterState(this.routeReuseStrategy, snapshot, this.currentRouterState);
return {appliedUrl, state, shouldActivate};
} else {
return {appliedUrl, state: null, shouldActivate};
@ -738,7 +756,8 @@ export class Router {
}
}
new ActivateRoutes(state, storedState).activate(this.outletMap);
new ActivateRoutes(this.routeReuseStrategy, state, storedState)
.activate(this.outletMap);
navigationIsSuccessful = true;
})
@ -1007,7 +1026,9 @@ export class PreActivation {
}
class ActivateRoutes {
constructor(private futureState: RouterState, private currState: RouterState) {}
constructor(
private routeReuseStrategy: RouteReuseStrategy, private futureState: RouterState,
private currState: RouterState) {}
activate(parentOutletMap: RouterOutletMap): void {
const futureRoot = this.futureState._root;
@ -1087,9 +1108,18 @@ class ActivateRoutes {
if (future.component) {
advanceActivatedRoute(future);
const outlet = getOutlet(parentOutletMap, futureNode.value);
if (this.routeReuseStrategy.shouldAttach(future.snapshot)) {
const stored =
(<DetachedRouteHandleInternal>this.routeReuseStrategy.retrieve(future.snapshot));
this.routeReuseStrategy.store(future.snapshot, null);
outlet.attach(stored.componentRef, stored.route.value);
advanceActivatedRouteNodeAndItsChildren(stored.route);
} else {
const outletMap = new RouterOutletMap();
this.placeComponentIntoOutlet(outletMap, future, outlet);
this.activateChildRoutes(futureNode, null, outletMap);
}
// if we have a componentless route, we recurse but keep the same outlet map.
} else {
@ -1125,6 +1155,22 @@ class ActivateRoutes {
private deactiveRouteAndItsChildren(
route: TreeNode<ActivatedRoute>, parentOutletMap: RouterOutletMap): void {
if (this.routeReuseStrategy.shouldDetach(route.value.snapshot)) {
this.detachAndStoreRouteSubtree(route, parentOutletMap);
} else {
this.deactiveRouteAndOutlet(route, parentOutletMap);
}
}
private detachAndStoreRouteSubtree(
route: TreeNode<ActivatedRoute>, parentOutletMap: RouterOutletMap): void {
const outlet = getOutlet(parentOutletMap, route.value);
const componentRef = outlet.detach();
this.routeReuseStrategy.store(route.value.snapshot, {componentRef, route});
}
private deactiveRouteAndOutlet(route: TreeNode<ActivatedRoute>, parentOutletMap: RouterOutletMap):
void {
const prevChildren: {[key: string]: any} = nodeChildrenAsMap(route);
let outlet: RouterOutlet = null;
@ -1151,6 +1197,11 @@ class ActivateRoutes {
}
}
function advanceActivatedRouteNodeAndItsChildren(node: TreeNode<ActivatedRoute>): void {
advanceActivatedRoute(node.value);
node.children.forEach(advanceActivatedRouteNodeAndItsChildren);
}
function parentLoadedConfig(snapshot: ActivatedRouteSnapshot): LoadedRouterConfig {
let s = snapshot.parent;
while (s) {

View File

@ -8,11 +8,13 @@
import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, ApplicationRef, Compiler, ComponentRef, Inject, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, OpaqueToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
import {Route, Routes} from './config';
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
import {RouterLinkActive} from './directives/router_link_active';
import {RouterOutlet} from './directives/router_outlet';
import {getDOM} from './private_import_platform-browser';
import {RouteReuseStrategy} from './route_reuse_strategy';
import {ErrorHandler, Router} from './router';
import {ROUTES} from './router_config_loader';
import {RouterOutletMap} from './router_outlet_map';
@ -23,6 +25,7 @@ import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
import {flatten} from './utils/collection';
/**
* @whatItDoes Contains a list of directives
* @stable
@ -48,7 +51,8 @@ export const ROUTER_PROVIDERS: Provider[] = [
useFactory: setupRouter,
deps: [
ApplicationRef, UrlSerializer, RouterOutletMap, Location, Injector, NgModuleFactoryLoader,
Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()]
Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()],
[RouteReuseStrategy, new Optional()]
]
},
RouterOutletMap,
@ -240,7 +244,8 @@ export interface ExtraOptions {
export function setupRouter(
ref: ApplicationRef, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler,
config: Route[][], opts: ExtraOptions = {}, urlHandlingStrategy?: UrlHandlingStrategy) {
config: Route[][], opts: ExtraOptions = {}, urlHandlingStrategy?: UrlHandlingStrategy,
routeReuseStrategy?: RouteReuseStrategy) {
const router = new Router(
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(config));
@ -248,6 +253,10 @@ export function setupRouter(
router.urlHandlingStrategy = urlHandlingStrategy;
}
if (routeReuseStrategy) {
router.routeReuseStrategy = routeReuseStrategy;
}
if (opts.errorHandler) {
router.errorHandler = opts.errorHandler;
}

View File

@ -9,18 +9,21 @@
import {Routes} from '../src/config';
import {createRouterState} from '../src/create_router_state';
import {recognize} from '../src/recognize';
import {DefaultRouteReuseStrategy} from '../src/router';
import {ActivatedRoute, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from '../src/router_state';
import {PRIMARY_OUTLET} from '../src/shared';
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree} from '../src/url_tree';
import {TreeNode} from '../src/utils/tree';
describe('create router state', () => {
const reuseStrategy = new DefaultRouteReuseStrategy();
const emptyState = () =>
createEmptyState(new UrlTree(new UrlSegmentGroup([], {}), {}, null), RootComponent);
it('should work create new state', () => {
const state = createRouterState(
createState(
reuseStrategy, createState(
[
{path: 'a', component: ComponentA},
{path: 'b', component: ComponentB, outlet: 'left'},
@ -43,9 +46,10 @@ describe('create router state', () => {
{path: 'c', component: ComponentC, outlet: 'left'}
];
const prevState = createRouterState(createState(config, 'a(left:b)'), emptyState());
const prevState =
createRouterState(reuseStrategy, createState(config, 'a(left:b)'), emptyState());
advanceState(prevState);
const state = createRouterState(createState(config, 'a(left:c)'), prevState);
const state = createRouterState(reuseStrategy, createState(config, 'a(left:c)'), prevState);
expect(prevState.root).toBe(state.root);
const prevC = prevState.children(prevState.root);
@ -65,9 +69,11 @@ describe('create router state', () => {
}];
const prevState = createRouterState(createState(config, 'a/1;p=11/(b//right:c)'), emptyState());
const prevState = createRouterState(
reuseStrategy, createState(config, 'a/1;p=11/(b//right:c)'), emptyState());
advanceState(prevState);
const state = createRouterState(createState(config, 'a/2;p=22/(b//right:c)'), prevState);
const state =
createRouterState(reuseStrategy, createState(config, 'a/2;p=22/(b//right:c)'), prevState);
expect(prevState.root).toBe(state.root);
const prevP = prevState.firstChild(prevState.root);

View File

@ -13,7 +13,7 @@ import {expect} from '@angular/platform-browser/testing/matchers';
import {Observable} from 'rxjs/Observable';
import {map} from 'rxjs/operator/map';
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, Params, PreloadAllModules, PreloadingStrategy, Resolve, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '../index';
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DetachedRouteHandle, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, Params, PreloadAllModules, PreloadingStrategy, Resolve, RouteReuseStrategy, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '../index';
import {RouterPreloader} from '../src/router_preloader';
import {forEach} from '../src/utils/collection';
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
@ -2392,6 +2392,105 @@ describe('Integration', () => {
})));
});
});
describe('Custom Route Reuse Strategy', () => {
class AttachDetachReuseStrategy implements RouteReuseStrategy {
stored: {[k: string]: DetachedRouteHandle} = {};
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return route.routeConfig.path === 'a';
}
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {
this.stored[route.routeConfig.path] = detachedTree;
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!this.stored[route.routeConfig.path];
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
return this.stored[route.routeConfig.path];
}
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
}
class ShortLifecycle implements RouteReuseStrategy {
shouldDetach(route: ActivatedRouteSnapshot): boolean { return false; }
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {}
shouldAttach(route: ActivatedRouteSnapshot): boolean { return false; }
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { return null; }
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
if (future.routeConfig !== curr.routeConfig) {
return false;
} else if (Object.keys(future.params).length !== Object.keys(curr.params).length) {
return false;
} else {
return Object.keys(future.params).every(k => future.params[k] === curr.params[k]);
}
}
}
it('should support attaching & detaching fragments',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
router.routeReuseStrategy = new AttachDetachReuseStrategy();
router.resetConfig([
{path: 'a', component: TeamCmp, children: [{path: 'b', component: SimpleCmp}]},
{path: 'c', component: UserCmp}
]);
router.navigateByUrl('/a/b');
advance(fixture);
const teamCmp = fixture.debugElement.children[1].componentInstance;
const simpleCmp = fixture.debugElement.children[1].children[1].componentInstance;
expect(location.path()).toEqual('/a/b');
expect(teamCmp).toBeDefined();
expect(simpleCmp).toBeDefined();
router.navigateByUrl('/c');
advance(fixture);
expect(location.path()).toEqual('/c');
expect(fixture.debugElement.children[1].componentInstance).toBeAnInstanceOf(UserCmp);
router.navigateByUrl('/a;p=1/b;p=2');
advance(fixture);
const teamCmp2 = fixture.debugElement.children[1].componentInstance;
const simpleCmp2 = fixture.debugElement.children[1].children[1].componentInstance;
expect(location.path()).toEqual('/a;p=1/b;p=2');
expect(teamCmp2).toBe(teamCmp);
expect(simpleCmp2).toBe(simpleCmp);
expect(teamCmp.route).toBe(router.routerState.root.firstChild);
expect(teamCmp.route.snapshot).toBe(router.routerState.snapshot.root.firstChild);
expect(teamCmp.route.snapshot.params).toEqual({p: '1'});
expect(teamCmp.route.firstChild.snapshot.params).toEqual({p: '2'});
})));
it('should support shorter lifecycles',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
router.routeReuseStrategy = new ShortLifecycle();
router.resetConfig([{path: 'a', component: SimpleCmp}]);
router.navigateByUrl('/a');
advance(fixture);
const simpleCmp1 = fixture.debugElement.children[1].componentInstance;
expect(location.path()).toEqual('/a');
router.navigateByUrl('/a;p=1');
advance(fixture);
expect(location.path()).toEqual('/a;p=1');
const simpleCmp2 = fixture.debugElement.children[1].componentInstance;
expect(simpleCmp1).not.toBe(simpleCmp2);
})));
});
});
function expectEvents(events: Event[], pairs: any[]) {

View File

@ -66,6 +66,9 @@ export declare class DefaultUrlSerializer implements UrlSerializer {
serialize(tree: UrlTree): string;
}
/** @experimental */
export declare type DetachedRouteHandle = {};
/** @stable */
export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized;
@ -201,6 +204,7 @@ export declare class Router {
errorHandler: ErrorHandler;
events: Observable<Event>;
navigated: boolean;
routeReuseStrategy: RouteReuseStrategy;
routerState: RouterState;
url: string;
urlHandlingStrategy: UrlHandlingStrategy;
@ -224,6 +228,15 @@ export declare const ROUTER_CONFIGURATION: OpaqueToken;
/** @experimental */
export declare const ROUTER_INITIALIZER: OpaqueToken;
/** @experimental */
export declare abstract class RouteReuseStrategy {
abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle;
abstract shouldAttach(route: ActivatedRouteSnapshot): boolean;
abstract shouldDetach(route: ActivatedRouteSnapshot): boolean;
abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void;
}
/** @stable */
export declare class RouterLink {
fragment: string;
@ -298,7 +311,9 @@ export declare class RouterOutlet implements OnDestroy {
outletMap: RouterOutletMap;
constructor(parentOutletMap: RouterOutletMap, location: ViewContainerRef, resolver: ComponentFactoryResolver, name: string);
activate(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void;
attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute): void;
deactivate(): void;
detach(): ComponentRef<any>;
ngOnDestroy(): void;
}