diff --git a/modules/@angular/router/src/create_router_state.ts b/modules/@angular/router/src/create_router_state.ts index eb39717ff1..c05b32d8bb 100644 --- a/modules/@angular/router/src/create_router_state.ts +++ b/modules/@angular/router/src/create_router_state.ts @@ -21,7 +21,6 @@ function createNode(curr: TreeNode, prevState?: TreeNode if (prevState && equalRouteSnapshots(prevState.value.snapshot, curr.value)) { const value = prevState.value; value._futureSnapshot = curr.value; - const children = createOrReuseChildren(curr, prevState); return new TreeNode(value, children); diff --git a/modules/@angular/router/src/index.ts b/modules/@angular/router/src/index.ts index 3e96abe201..d897c46424 100644 --- a/modules/@angular/router/src/index.ts +++ b/modules/@angular/router/src/index.ts @@ -18,6 +18,7 @@ export {RouterOutletMap} from './router_outlet_map'; export {NoPreloading, PreloadAllModules, PreloadingStrategy} from './router_preloader'; export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state'; export {PRIMARY_OUTLET, Params} from './shared'; -export {DefaultUrlSerializer, UrlSegment, UrlSerializer, UrlTree} from './url_tree'; +export {UrlHandlingStrategy} from './url_handling_strategy'; +export {DefaultUrlSerializer, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree'; export * from './private_export' diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index 38c6cd917e..31b11b02bc 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -30,6 +30,7 @@ import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader'; import {RouterOutletMap} from './router_outlet_map'; import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state'; import {NavigationCancelingError, PRIMARY_OUTLET, Params} from './shared'; +import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy'; import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree'; import {andObservables, forEach, merge, shallowEqual, waitForMap, wrapIntoObservable} from './utils/collection'; import {TreeNode} from './utils/tree'; @@ -285,6 +286,9 @@ function defaultErrorHandler(error: any): any { */ export class Router { private currentUrlTree: UrlTree; + private rawUrlTree: UrlTree; + private lastNavigation: UrlTree; + private currentRouterState: RouterState; private locationSubscription: Subscription; private routerEvents: Subject; @@ -303,6 +307,11 @@ export class Router { */ navigated: boolean = false; + /** + * Extracts and merges URLs. Used for Angular 1 to Angular 2 migrations. + */ + urlHandlingStrategy: UrlHandlingStrategy = new DefaultUrlHandlingStrategy(); + /** * Creates the router service. */ @@ -314,6 +323,7 @@ export class Router { this.resetConfig(config); this.routerEvents = new Subject(); this.currentUrlTree = createEmptyUrlTree(); + this.rawUrlTree = this.currentUrlTree; this.configLoader = new RouterConfigLoader(loader, compiler); this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType); } @@ -344,12 +354,20 @@ export class Router { // Zone.current.wrap is needed because of the issue with RxJS scheduler, // which does not work properly with zone.js in IE and Safari this.locationSubscription = this.location.subscribe(Zone.current.wrap((change: any) => { - const tree = this.urlSerializer.parse(change['url']); - // we fire multiple events for a single URL change - // we should navigate only once - return this.currentUrlTree.toString() !== tree.toString() ? - this.scheduleNavigation(tree, {skipLocationChange: change['pop'], replaceUrl: true}) : - null; + const rawUrlTree = this.urlSerializer.parse(change['url']); + const tree = this.urlHandlingStrategy.extract(rawUrlTree); + + + setTimeout(() => { + // we fire multiple events for a single URL change + // we should navigate only once + if (!this.lastNavigation || this.lastNavigation.toString() !== tree.toString()) { + this.scheduleNavigation( + rawUrlTree, tree, {skipLocationChange: change['pop'], replaceUrl: true}); + } else { + this.rawUrlTree = rawUrlTree; + } + }, 0); })); } @@ -470,10 +488,10 @@ export class Router { navigateByUrl(url: string|UrlTree, extras: NavigationExtras = {skipLocationChange: false}): Promise { if (url instanceof UrlTree) { - return this.scheduleNavigation(url, extras); + return this.scheduleNavigation(this.rawUrlTree, url, extras); } else { const urlTree = this.urlSerializer.parse(url); - return this.scheduleNavigation(urlTree, extras); + return this.scheduleNavigation(this.rawUrlTree, urlTree, extras); } } @@ -500,7 +518,7 @@ export class Router { */ navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}): Promise { - return this.scheduleNavigation(this.createUrlTree(commands, extras), extras); + return this.scheduleNavigation(this.rawUrlTree, this.createUrlTree(commands, extras), extras); } /** @@ -525,16 +543,34 @@ export class Router { } } - private scheduleNavigation(url: UrlTree, extras: NavigationExtras): Promise { - const id = ++this.navigationId; - this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url))); - return Promise.resolve().then( - (_) => this.runNavigate(url, extras.skipLocationChange, extras.replaceUrl, id)); + private scheduleNavigation(rawUrl: UrlTree, url: UrlTree, extras: NavigationExtras): + Promise { + if (this.urlHandlingStrategy.shouldProcessUrl(url)) { + const id = ++this.navigationId; + this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url))); + + return Promise.resolve().then( + (_) => this.runNavigate( + rawUrl, url, extras.skipLocationChange, extras.replaceUrl, id, null)); + + // we cannot process the current URL, but we could process the previous one => + // we need to do some cleanup + } else if (this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree)) { + const id = ++this.navigationId; + this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url))); + + return Promise.resolve().then( + (_) => this.runNavigate( + rawUrl, url, false, false, id, createEmptyState(url, this.rootComponentType))); + } else { + this.rawUrlTree = rawUrl; + return Promise.resolve(null); + } } private runNavigate( - url: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean, - id: number): Promise { + rawUrl: UrlTree, url: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean, + id: number, precreatedState: RouterState): Promise { if (id !== this.navigationId) { this.location.go(this.urlSerializer.serialize(this.currentUrlTree)); this.routerEvents.next(new NavigationCancel( @@ -553,23 +589,33 @@ export class Router { const storedState = this.currentRouterState; const storedUrl = this.currentUrlTree; - const redirectsApplied$ = applyRedirects(this.injector, this.configLoader, url, this.config); + let routerState$: any; - const snapshot$ = mergeMap.call(redirectsApplied$, (u: UrlTree) => { - appliedUrl = u; - return recognize( - this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl)); - }); + if (!precreatedState) { + const redirectsApplied$ = + applyRedirects(this.injector, this.configLoader, url, this.config); - const emitRecognzied$ = map.call(snapshot$, (newRouterStateSnapshot: RouterStateSnapshot) => { - this.routerEvents.next(new RoutesRecognized( - id, this.serializeUrl(url), this.serializeUrl(appliedUrl), newRouterStateSnapshot)); - return newRouterStateSnapshot; - }); + const snapshot$ = mergeMap.call(redirectsApplied$, (u: UrlTree) => { + appliedUrl = u; + return recognize( + this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl)); + }); - const routerState$ = map.call(emitRecognzied$, (routerStateSnapshot: RouterStateSnapshot) => { - return createRouterState(routerStateSnapshot, this.currentRouterState); - }); + const emitRecognzied$ = + map.call(snapshot$, (newRouterStateSnapshot: RouterStateSnapshot) => { + this.routerEvents.next(new RoutesRecognized( + id, this.serializeUrl(url), this.serializeUrl(appliedUrl), + newRouterStateSnapshot)); + return newRouterStateSnapshot; + }); + + routerState$ = map.call(emitRecognzied$, (routerStateSnapshot: RouterStateSnapshot) => { + return createRouterState(routerStateSnapshot, this.currentRouterState); + }); + } else { + appliedUrl = url; + routerState$ = of (precreatedState); + } const preactivation$ = map.call(routerState$, (newState: RouterState) => { state = newState; @@ -595,11 +641,14 @@ export class Router { return; } + this.lastNavigation = appliedUrl; this.currentUrlTree = appliedUrl; + this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, rawUrl); + this.currentRouterState = state; if (!shouldPreventPushState) { - let path = this.urlSerializer.serialize(appliedUrl); + let path = this.urlSerializer.serialize(this.rawUrlTree); if (this.location.isCurrentPathEqualTo(path) || shouldReplaceUrl) { this.location.replaceState(path); } else { @@ -641,7 +690,8 @@ export class Router { if (id === this.navigationId) { this.currentRouterState = storedState; this.currentUrlTree = storedUrl; - this.location.replaceState(this.serializeUrl(storedUrl)); + this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, rawUrl); + this.location.replaceState(this.serializeUrl(this.rawUrlTree)); } }); }); diff --git a/modules/@angular/router/src/router_module.ts b/modules/@angular/router/src/router_module.ts index 63381e34c1..e145aff3d8 100644 --- a/modules/@angular/router/src/router_module.ts +++ b/modules/@angular/router/src/router_module.ts @@ -18,6 +18,7 @@ import {ROUTES} from './router_config_loader'; import {RouterOutletMap} from './router_outlet_map'; import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader'; import {ActivatedRoute} from './router_state'; +import {UrlHandlingStrategy} from './url_handling_strategy'; import {DefaultUrlSerializer, UrlSerializer} from './url_tree'; import {flatten} from './utils/collection'; @@ -55,7 +56,7 @@ export const ROUTER_PROVIDERS: Provider[] = [ useFactory: setupRouter, deps: [ ApplicationRef, UrlSerializer, RouterOutletMap, Location, Injector, NgModuleFactoryLoader, - Compiler, ROUTES, ROUTER_CONFIGURATION + Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()] ] }, RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]}, @@ -236,16 +237,20 @@ export interface ExtraOptions { export function setupRouter( ref: ApplicationRef, urlSerializer: UrlSerializer, outletMap: RouterOutletMap, location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler, - config: Route[][], opts: ExtraOptions = {}) { - const r = new Router( + config: Route[][], opts: ExtraOptions = {}, urlHandlingStrategy?: UrlHandlingStrategy) { + const router = new Router( null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(config)); + if (urlHandlingStrategy) { + router.urlHandlingStrategy = urlHandlingStrategy; + } + if (opts.errorHandler) { - r.errorHandler = opts.errorHandler; + router.errorHandler = opts.errorHandler; } if (opts.enableTracing) { - r.events.subscribe(e => { + router.events.subscribe(e => { console.group(`Router Event: ${(e.constructor).name}`); console.log(e.toString()); console.log(e); @@ -253,7 +258,7 @@ export function setupRouter( }); } - return r; + return router; } export function rootRoute(router: Router): ActivatedRoute { diff --git a/modules/@angular/router/src/url_handling_strategy.ts b/modules/@angular/router/src/url_handling_strategy.ts new file mode 100644 index 0000000000..1728087e64 --- /dev/null +++ b/modules/@angular/router/src/url_handling_strategy.ts @@ -0,0 +1,46 @@ +/** + * @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 {UrlTree} from './url_tree'; + +/** + * @whatItDoes Provides a way to migrate Angular 1 applications to Angular 2. + * + * @experimental + */ +export abstract class UrlHandlingStrategy { + /** + * Tells the router if this URL should be processed. + * + * When it returns true, the router will execute the regular navigation. + * When it returns false, the router will set the router state to an empty state. + * As a result, all the active components will be destroyed. + * + */ + abstract shouldProcessUrl(url: UrlTree): boolean; + + /** + * Extracts the part of the URL that should be handled by the router. + * The rest of the URL will remain untouched. + */ + abstract extract(url: UrlTree): UrlTree; + + /** + * Merges the URL fragment with the rest of the URL. + */ + abstract merge(newUrlPart: UrlTree, rawUrl: UrlTree): UrlTree; +} + +/** + * @experimental + */ +export class DefaultUrlHandlingStrategy implements UrlHandlingStrategy { + shouldProcessUrl(url: UrlTree): boolean { return true; } + extract(url: UrlTree): UrlTree { return url; } + merge(newUrlPart: UrlTree, wholeUrl: UrlTree): UrlTree { return newUrlPart; } +} \ No newline at end of file diff --git a/modules/@angular/router/test/integration.spec.ts b/modules/@angular/router/test/integration.spec.ts index 5e4f08d6e8..f9af0d3c99 100644 --- a/modules/@angular/router/test/integration.spec.ts +++ b/modules/@angular/router/test/integration.spec.ts @@ -14,8 +14,9 @@ import {Observable} from 'rxjs/Observable'; import {of } from 'rxjs/observable/of'; import {map} from 'rxjs/operator/map'; -import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, PreloadAllModules, PreloadingStrategy, Resolve, Router, RouterModule, RouterStateSnapshot, RoutesRecognized} from '../index'; +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 {RouterPreloader} from '../src/router_preloader'; +import {forEach} from '../src/utils/collection'; import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing'; @@ -58,7 +59,6 @@ describe('Integration', () => { }))); it('should work when an outlet is in an ngIf (and is removed)', fakeAsync(() => { - @Component({ selector: 'someRoot', template: `
` @@ -87,6 +87,7 @@ describe('Integration', () => { let recordedError: any = null; router.navigateByUrl('/blank').catch(e => recordedError = e); advance(fixture); + expect(recordedError.message).toEqual('Cannot find primary outlet to load \'BlankCmp\''); })); @@ -1757,10 +1758,146 @@ describe('Integration', () => { }))); }); + + describe('custom url handling strategies', () => { + class CustomUrlHandlingStrategy implements UrlHandlingStrategy { + shouldProcessUrl(url: UrlTree): boolean { + return url.toString().startsWith('/include') || url.toString() === '/'; + } + + extract(url: UrlTree): UrlTree { + const oldRoot = url.root; + const root = new UrlSegmentGroup( + oldRoot.segments, {[PRIMARY_OUTLET]: oldRoot.children[PRIMARY_OUTLET]}); + return new UrlTree(root, url.queryParams, url.fragment); + } + + merge(newUrlPart: UrlTree, wholeUrl: UrlTree): UrlTree { + const oldRoot = newUrlPart.root; + + const children: any = {}; + if (oldRoot.children[PRIMARY_OUTLET]) { + children[PRIMARY_OUTLET] = oldRoot.children[PRIMARY_OUTLET]; + } + + forEach(wholeUrl.root.children, (v: any, k: any) => { + if (k !== PRIMARY_OUTLET) { + children[k] = v; + } + v.parent = this; + }); + const root = new UrlSegmentGroup(oldRoot.segments, children); + return new UrlTree(root, newUrlPart.queryParams, newUrlPart.fragment); + } + } + + beforeEach(() => { + TestBed.configureTestingModule( + {providers: [{provide: UrlHandlingStrategy, useClass: CustomUrlHandlingStrategy}]}); + }); + + it('should work', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([{ + path: 'include', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, {path: 'simple', component: SimpleCmp} + ] + }]); + + let events: any[] = []; + router.events.subscribe(e => events.push(e)); + + // supported URL + router.navigateByUrl('/include/user/kate'); + advance(fixture); + + expect(location.path()).toEqual('/include/user/kate'); + expectEvents(events, [ + [NavigationStart, '/include/user/kate'], [RoutesRecognized, '/include/user/kate'], + [NavigationEnd, '/include/user/kate'] + ]); + expect(fixture.nativeElement).toHaveText('team [ user kate, right: ]'); + events.splice(0); + + // unsupported URL + router.navigateByUrl('/exclude/one'); + advance(fixture); + + expect(location.path()).toEqual('/exclude/one'); + expect(Object.keys(router.routerState.root.children).length).toEqual(0); + expect(fixture.nativeElement).toHaveText(''); + expectEvents( + events, [[NavigationStart, '/exclude/one'], [NavigationEnd, '/exclude/one']]); + events.splice(0); + + // another unsupported URL + location.go('/exclude/two'); + advance(fixture); + + expect(location.path()).toEqual('/exclude/two'); + expectEvents(events, []); + + // back to a supported URL + location.go('/include/simple'); + advance(fixture); + + expect(location.path()).toEqual('/include/simple'); + expect(fixture.nativeElement).toHaveText('team [ simple, right: ]'); + + expectEvents(events, [ + [NavigationStart, '/include/simple'], [RoutesRecognized, '/include/simple'], + [NavigationEnd, '/include/simple'] + ]); + }))); + + it('should handle the case when the router takes only the primary url', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([{ + path: 'include', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, {path: 'simple', component: SimpleCmp} + ] + }]); + + let events: any[] = []; + router.events.subscribe(e => events.push(e)); + + location.go('/include/user/kate(aux:excluded)'); + advance(fixture); + + expect(location.path()).toEqual('/include/user/kate(aux:excluded)'); + expectEvents(events, [ + [NavigationStart, '/include/user/kate'], [RoutesRecognized, '/include/user/kate'], + [NavigationEnd, '/include/user/kate'] + ]); + events.splice(0); + + location.go('/include/user/kate(aux:excluded2)'); + advance(fixture); + expectEvents(events, []); + + router.navigateByUrl('/include/simple'); + advance(fixture); + + expect(location.path()).toEqual('/include/simple(aux:excluded2)'); + expectEvents(events, [ + [NavigationStart, '/include/simple'], [RoutesRecognized, '/include/simple'], + [NavigationEnd, '/include/simple'] + ]); + }))); + }); }); }); function expectEvents(events: Event[], pairs: any[]) { + expect(events.length).toEqual(pairs.length); for (let i = 0; i < events.length; ++i) { expect((events[i].constructor).name).toBe(pairs[i][0].name); expect((events[i]).url).toBe(pairs[i][1]); @@ -1771,11 +1908,7 @@ function expectEvents(events: Event[], pairs: any[]) { class StringLinkCmp { } -@Component({ - selector: 'link-cmp', - template: `` -}) +@Component({selector: 'link-cmp', template: ``}) class StringLinkButtonCmp { } diff --git a/modules/@angular/router/testing/router_testing_module.ts b/modules/@angular/router/testing/router_testing_module.ts index 5903b040d5..c71de9bffa 100644 --- a/modules/@angular/router/testing/router_testing_module.ts +++ b/modules/@angular/router/testing/router_testing_module.ts @@ -8,12 +8,13 @@ import {Location, LocationStrategy} from '@angular/common'; import {MockLocationStrategy, SpyLocation} from '@angular/common/testing'; -import {Compiler, Injectable, Injector, ModuleWithProviders, NgModule, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core'; -import {NoPreloading, PreloadingStrategy, Route, Router, RouterModule, RouterOutletMap, Routes, UrlSerializer, provideRoutes} from '@angular/router'; +import {Compiler, Injectable, Injector, ModuleWithProviders, NgModule, NgModuleFactory, NgModuleFactoryLoader, Optional} from '@angular/core'; +import {NoPreloading, PreloadingStrategy, Route, Router, RouterModule, RouterOutletMap, Routes, UrlHandlingStrategy, UrlSerializer, provideRoutes} from '@angular/router'; import {ROUTER_PROVIDERS, ROUTES, flatten} from './private_import_router'; + /** * @whatItDoes Allows to simulate the loading of ng modules in tests. * @@ -68,9 +69,14 @@ export class SpyNgModuleFactoryLoader implements NgModuleFactoryLoader { */ export function setupTestingRouter( urlSerializer: UrlSerializer, outletMap: RouterOutletMap, location: Location, - loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][]) { - return new Router( + loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][], + urlHandlingStrategy?: UrlHandlingStrategy) { + const router = new Router( null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(routes)); + if (urlHandlingStrategy) { + router.urlHandlingStrategy = urlHandlingStrategy; + } + return router; } /** @@ -107,7 +113,8 @@ export function setupTestingRouter( provide: Router, useFactory: setupTestingRouter, deps: [ - UrlSerializer, RouterOutletMap, Location, NgModuleFactoryLoader, Compiler, Injector, ROUTES + UrlSerializer, RouterOutletMap, Location, NgModuleFactoryLoader, Compiler, Injector, ROUTES, + [UrlHandlingStrategy, new Optional()] ] }, {provide: PreloadingStrategy, useExisting: NoPreloading}, provideRoutes([]) diff --git a/tools/public_api_guard/router/index.d.ts b/tools/public_api_guard/router/index.d.ts index a2fd10639d..109bc39f7a 100644 --- a/tools/public_api_guard/router/index.d.ts +++ b/tools/public_api_guard/router/index.d.ts @@ -202,6 +202,7 @@ export declare class Router { navigated: boolean; routerState: RouterState; url: string; + urlHandlingStrategy: UrlHandlingStrategy; constructor(rootComponentType: Type, urlSerializer: UrlSerializer, outletMap: RouterOutletMap, location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler, config: Routes); createUrlTree(commands: any[], {relativeTo, queryParams, fragment, preserveQueryParams, preserveFragment}?: NavigationExtras): UrlTree; dispose(): void; @@ -322,6 +323,13 @@ export declare class RoutesRecognized { toString(): string; } +/** @experimental */ +export declare abstract class UrlHandlingStrategy { + abstract extract(url: UrlTree): UrlTree; + abstract merge(newUrlPart: UrlTree, rawUrl: UrlTree): UrlTree; + abstract shouldProcessUrl(url: UrlTree): boolean; +} + /** @stable */ export declare class UrlSegment { parameters: { @@ -336,6 +344,23 @@ export declare class UrlSegment { toString(): string; } +/** @stable */ +export declare class UrlSegmentGroup { + children: { + [key: string]: UrlSegmentGroup; + }; + numberOfChildren: number; + parent: UrlSegmentGroup; + segments: UrlSegment[]; + constructor( + segments: UrlSegment[], + children: { + [key: string]: UrlSegmentGroup; + }); + hasChildren(): boolean; + toString(): string; +} + /** @stable */ export declare abstract class UrlSerializer { abstract parse(url: string): UrlTree;