From 81203c6ec1269b41c9bebf07d7a685bbae1425a4 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Mon, 7 Nov 2022 10:05:02 -0800 Subject: [PATCH] refactor(router): Add opt-in provider for upcoming router change (#47988) This commit adds a swappable provider for an upcoming change to the implementation of `Router#createUrlTree`. This will be a breaking change and is planned to be included in V16. PR Close #47988 --- .../router/bundle.golden_symbols.json | 6 ++ packages/router/src/create_url_tree.ts | 5 +- .../router/src/create_url_tree_strategy.ts | 69 +++++++++++++++++++ packages/router/src/router.ts | 9 ++- 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 packages/router/src/create_url_tree_strategy.ts diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index 29eae1c0449..1c73b6aed2e 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -110,6 +110,9 @@ { "name": "Console" }, + { + "name": "CreateUrlTreeStrategy" + }, { "name": "DEFAULT_SERIALIZER" }, @@ -251,6 +254,9 @@ { "name": "LQuery_" }, + { + "name": "LegacyCreateUrlTree" + }, { "name": "LifecycleHooksFeature" }, diff --git a/packages/router/src/create_url_tree.ts b/packages/router/src/create_url_tree.ts index 4fc56408dad..8a67e9349b7 100644 --- a/packages/router/src/create_url_tree.ts +++ b/packages/router/src/create_url_tree.ts @@ -74,10 +74,11 @@ export function createUrlTreeFromSnapshot( return createUrlTreeFromSegmentGroup(relativeToUrlSegmentGroup, commands, queryParams, fragment); } -function createSegmentGroupFromRoute(route: ActivatedRouteSnapshot): UrlSegmentGroup { +export function createSegmentGroupFromRoute(route: ActivatedRouteSnapshot): UrlSegmentGroup { let targetGroup: UrlSegmentGroup|undefined; - function createSegmentGroupFromRouteRecursive(currentRoute: ActivatedRouteSnapshot) { + function createSegmentGroupFromRouteRecursive(currentRoute: ActivatedRouteSnapshot): + UrlSegmentGroup { const childOutlets: {[outlet: string]: UrlSegmentGroup} = {}; for (const childSnapshot of currentRoute.children) { const root = createSegmentGroupFromRouteRecursive(childSnapshot); diff --git a/packages/router/src/create_url_tree_strategy.ts b/packages/router/src/create_url_tree_strategy.ts new file mode 100644 index 00000000000..acebc6a40ce --- /dev/null +++ b/packages/router/src/create_url_tree_strategy.ts @@ -0,0 +1,69 @@ +/** + * @license + * Copyright Google LLC 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 {Injectable} from '@angular/core'; + +import {createSegmentGroupFromRoute, createUrlTree, createUrlTreeFromSegmentGroup} from './create_url_tree'; +import {ActivatedRoute, RouterState} from './router_state'; +import {Params} from './shared'; +import {UrlSegmentGroup, UrlTree} from './url_tree'; + +const NG_DEV_MODE = typeof ngDevMode === 'undefined' || ngDevMode; + +@Injectable() +export class LegacyCreateUrlTree implements CreateUrlTreeStrategy { + createUrlTree( + relativeTo: ActivatedRoute|null|undefined, currentState: RouterState, currentUrlTree: UrlTree, + commands: any[], queryParams: Params|null, fragment: string|null): UrlTree { + const a = relativeTo || currentState.root; + return createUrlTree(a, currentUrlTree, commands, queryParams, fragment); + } +} + +@Injectable() +export class CreateUrlTreeUsingSnapshot implements CreateUrlTreeStrategy { + createUrlTree( + relativeTo: ActivatedRoute|null|undefined, currentState: RouterState, currentUrlTree: UrlTree, + commands: any[], queryParams: Params|null, fragment: string|null): UrlTree { + let relativeToUrlSegmentGroup: UrlSegmentGroup|undefined; + try { + const relativeToSnapshot = relativeTo ? relativeTo.snapshot : currentState.snapshot.root; + relativeToUrlSegmentGroup = createSegmentGroupFromRoute(relativeToSnapshot); + } catch (e: unknown) { + // This is strictly for backwards compatibility with tests that create + // invalid `ActivatedRoute` mocks. + // Note: the difference between having this fallback for invalid `ActivatedRoute` setups and + // just throwing is ~500 test failures. Fixing all of those tests by hand is not feasible at + // the moment. + if (NG_DEV_MODE) { + console.warn( + `The ActivatedRoute has an invalid structure. This is likely due to an incomplete mock in tests.`); + } + if (typeof commands[0] !== 'string' || !commands[0].startsWith('/')) { + // Navigations that were absolute in the old way of creating UrlTrees + // would still work because they wouldn't attempt to match the + // segments in the `ActivatedRoute` to the `currentUrlTree` but + // instead just replace the root segment with the navigation result. + // Non-absolute navigations would fail to apply the commands because + // the logic could not find the segment to replace (so they'd act like there were no + // commands). + commands = []; + } + relativeToUrlSegmentGroup = currentUrlTree.root; + } + return createUrlTreeFromSegmentGroup( + relativeToUrlSegmentGroup, commands, queryParams, fragment); + } +} + +@Injectable({providedIn: 'root', useClass: LegacyCreateUrlTree}) +export abstract class CreateUrlTreeStrategy { + abstract createUrlTree( + relativeTo: ActivatedRoute|null|undefined, currentState: RouterState, currentUrlTree: UrlTree, + commands: any[], queryParams: Params|null, fragment: string|null): UrlTree; +} diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 6490031525c..a43eacb4b7d 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -10,7 +10,7 @@ import {Location} from '@angular/common'; import {Compiler, inject, Injectable, Injector, NgZone, Type, ɵConsole as Console, ɵRuntimeError as RuntimeError} from '@angular/core'; import {BehaviorSubject, Observable, of, SubscriptionLike} from 'rxjs'; -import {createUrlTree} from './create_url_tree'; +import {CreateUrlTreeStrategy} from './create_url_tree_strategy'; import {RuntimeErrorCode} from './errors'; import {Event, NavigationTrigger} from './events'; import {NavigationBehaviorOptions, OnSameUrlNavigation, Routes} from './models'; @@ -228,6 +228,9 @@ export class Router { */ routeReuseStrategy = inject(RouteReuseStrategy); + /** Strategy used to create a UrlTree. */ + private readonly urlCreationStrategy = inject(CreateUrlTreeStrategy); + /** * A strategy for setting the title based on the `routerState`. * @@ -503,7 +506,6 @@ export class Router { createUrlTree(commands: any[], navigationExtras: UrlCreationOptions = {}): UrlTree { const {relativeTo, queryParams, fragment, queryParamsHandling, preserveFragment} = navigationExtras; - const a = relativeTo || this.routerState.root; const f = preserveFragment ? this.currentUrlTree.fragment : fragment; let q: Params|null = null; switch (queryParamsHandling) { @@ -519,7 +521,8 @@ export class Router { if (q !== null) { q = this.removeEmptyProps(q); } - return createUrlTree(a, this.currentUrlTree, commands, q, f ?? null); + return this.urlCreationStrategy.createUrlTree( + relativeTo, this.routerState, this.currentUrlTree, commands, q, f ?? null); } /**