Skip to content

Commit 332461b

Browse files
atscottdylhunn
authored andcommitted
feat(router): Add ability to override onSameUrlNavigation default per-navigation (#48050)
The router providers a configurable `onSameUrlNavigation` value that allows developers to configure whether navigations to the same URL as the current one should be processed or ignored. However, this only acts as a default value and there isn't an API for easily overriding this for a single navigation. Instead, developers are forced to update the value of the property on the router instance and remember to reset it. This feature fills a small gap in the Router APIs that enables developers to accomplish the task of force reloading a bit easier. Lengthy discussion about this here: #21115 PR Close #48050
1 parent 6a8ab30 commit 332461b

7 files changed

Lines changed: 103 additions & 30 deletions

File tree

goldens/public-api/router/index.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ export interface Navigation {
379379

380380
// @public
381381
export interface NavigationBehaviorOptions {
382+
onSameUrlNavigation?: Extract<OnSameUrlNavigation, 'reload'>;
382383
replaceUrl?: boolean;
383384
skipLocationChange?: boolean;
384385
state?: {
@@ -493,6 +494,9 @@ export class NoPreloading implements PreloadingStrategy {
493494
static ɵprov: i0.ɵɵInjectableDeclaration<NoPreloading>;
494495
}
495496

497+
// @public
498+
export type OnSameUrlNavigation = 'reload' | 'ignore';
499+
496500
// @public
497501
export class OutletContext {
498502
// (undocumented)
@@ -677,7 +681,7 @@ export class Router {
677681
// (undocumented)
678682
ngOnDestroy(): void;
679683
// @deprecated
680-
onSameUrlNavigation: 'reload' | 'ignore';
684+
onSameUrlNavigation: OnSameUrlNavigation;
681685
// @deprecated
682686
paramsInheritanceStrategy: 'emptyOnly' | 'always';
683687
parseUrl(url: string): UrlTree;
@@ -709,7 +713,7 @@ export const ROUTER_INITIALIZER: InjectionToken<(compRef: ComponentRef<any>) =>
709713
// @public
710714
export interface RouterConfigOptions {
711715
canceledNavigationResolution?: 'replace' | 'computed';
712-
onSameUrlNavigation?: 'reload' | 'ignore';
716+
onSameUrlNavigation?: OnSameUrlNavigation;
713717
paramsInheritanceStrategy?: 'emptyOnly' | 'always';
714718
urlUpdateStrategy?: 'deferred' | 'eager';
715719
}

packages/router/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export {RouterLink, RouterLinkWithHref} from './directives/router_link';
1212
export {RouterLinkActive} from './directives/router_link_active';
1313
export {RouterOutlet, RouterOutletContract} from './directives/router_outlet';
1414
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, EventType, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode as NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
15-
export {CanActivate, CanActivateChild, CanActivateChildFn, CanActivateFn, CanDeactivate, CanDeactivateFn, CanLoad, CanLoadFn, CanMatch, CanMatchFn, Data, DefaultExport, LoadChildren, LoadChildrenCallback, NavigationBehaviorOptions, QueryParamsHandling, Resolve, ResolveData, ResolveFn, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models';
15+
export {CanActivate, CanActivateChild, CanActivateChildFn, CanActivateFn, CanDeactivate, CanDeactivateFn, CanLoad, CanLoadFn, CanMatch, CanMatchFn, Data, DefaultExport, LoadChildren, LoadChildrenCallback, NavigationBehaviorOptions, OnSameUrlNavigation, QueryParamsHandling, Resolve, ResolveData, ResolveFn, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models';
1616
export {Navigation, NavigationExtras, UrlCreationOptions} from './navigation_transition';
1717
export {DefaultTitleStrategy, TitleStrategy} from './page_title_strategy';
1818
export {DebugTracingFeature, DisabledInitialNavigationFeature, EnabledBlockingInitialNavigationFeature, InitialNavigationFeature, InMemoryScrollingFeature, PreloadingFeature, provideRouter, provideRoutes, RouterConfigurationFeature, RouterFeature, RouterFeatures, withDebugTracing, withDisabledInitialNavigation, withEnabledBlockingInitialNavigation, withInMemoryScrolling, withPreloading, withRouterConfig} from './provide_router';

packages/router/src/models.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,31 @@ import {DeprecatedLoadChildren} from './deprecated_load_children';
1313
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
1414
import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
1515

16+
/**
17+
* How to handle a navigation request to the current URL. One of:
18+
*
19+
* - `'ignore'` : The router ignores the request it is the same as the current state.
20+
* - `'reload'` : The router processes the URL even if it is not different from the current state.
21+
* One example of when you might want this option is if a `canMatch` guard depends on
22+
* application state and initially rejects navigation to a route. After fixing the state, you want
23+
* to re-navigate to the same URL so the route with the `canMatch` guard can activate.
24+
*
25+
* Note that this only configures whether the Route reprocesses the URL and triggers related
26+
* action and events like redirects, guards, and resolvers. By default, the router re-uses a
27+
* component instance when it re-navigates to the same component type without visiting a different
28+
* component first. This behavior is configured by the `RouteReuseStrategy`. In order to reload
29+
* routed components on same url navigation, you need to set `onSameUrlNavigation` to `'reload'`
30+
* _and_ provide a `RouteReuseStrategy` which returns `false` for `shouldReuseRoute`. Additionally,
31+
* resolvers and most guards for routes do not run unless the path or path params changed
32+
* (configured by `runGuardsAndResolvers`).
33+
*
34+
* @publicApi
35+
* @see RouteReuseStrategy
36+
* @see RunGuardsAndResolvers
37+
* @see NavigationBehaviorOptions
38+
* @see RouterConfigOptions
39+
*/
40+
export type OnSameUrlNavigation = 'reload'|'ignore';
1641

1742
/**
1843
* Represents a route configuration for the Router service.
@@ -550,11 +575,19 @@ export interface Route {
550575
loadChildren?: LoadChildren;
551576

552577
/**
553-
* Defines when guards and resolvers will be run. One of
554-
* - `paramsOrQueryParamsChange` : Run when query parameters change.
578+
* A policy for when to run guards and resolvers on a route.
579+
*
580+
* Guards and/or resolvers will always run when a route is activated or deactivated. When a route
581+
* is unchanged, the default behavior is the same as `paramsChange`.
582+
*
583+
* `paramsChange` : Rerun the guards and resolvers when path or
584+
* path param changes. This does not include query parameters. This option is the default.
555585
* - `always` : Run on every execution.
556-
* By default, guards and resolvers run only when the matrix
557-
* parameters of the route change.
586+
* - `pathParamsChange` : Rerun guards and resolvers when the path params
587+
* change. This does not compare matrix or query parameters.
588+
* - `paramsOrQueryParamsChange` : Run when path, matrix, or query parameters change.
589+
* - `pathParamsOrQueryParamsChange` : Rerun guards and resolvers when the path params
590+
* change or query params have changed. This does not include matrix parameters.
558591
*
559592
* @see RunGuardsAndResolvers
560593
*/
@@ -1194,6 +1227,17 @@ export type CanLoadFn = (route: Route, segments: UrlSegment[]) =>
11941227
* @publicApi
11951228
*/
11961229
export interface NavigationBehaviorOptions {
1230+
/**
1231+
* How to handle a navigation request to the current URL.
1232+
*
1233+
* This value is a subset of the options available in `OnSameUrlNavigation` and
1234+
* will take precedence over the default value set for the `Router`.
1235+
*
1236+
* @see `OnSameUrlNavigation`
1237+
* @see `RouterConfigOptions`
1238+
*/
1239+
onSameUrlNavigation?: Extract<OnSameUrlNavigation, 'reload'>;
1240+
11971241
/**
11981242
* When true, navigates without pushing a new state into history.
11991243
*

packages/router/src/navigation_transition.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,10 @@ export class NavigationTransitions {
344344
// try processing the URL again.
345345
browserUrlTree !== this.router.currentUrlTree.toString();
346346

347-
if (!urlTransition && this.router.onSameUrlNavigation !== 'reload') {
347+
348+
const onSameUrlNavigation =
349+
t.extras.onSameUrlNavigation ?? this.router.onSameUrlNavigation;
350+
if (!urlTransition && onSameUrlNavigation !== 'reload') {
348351
const reason = NG_DEV_MODE ?
349352
`Navigation to ${
350353
t.rawUrl} was ignored because it is the same as the current Router URL.` :

packages/router/src/router.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {BehaviorSubject, Observable, of, Subject, SubscriptionLike} from 'rxjs';
1313
import {createUrlTree} from './create_url_tree';
1414
import {RuntimeErrorCode} from './errors';
1515
import {Event, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationTrigger, RouteConfigLoadEnd, RouteConfigLoadStart} from './events';
16-
import {NavigationBehaviorOptions, Route, Routes} from './models';
16+
import {NavigationBehaviorOptions, OnSameUrlNavigation, Route, Routes} from './models';
1717
import {Navigation, NavigationExtras, NavigationTransition, NavigationTransitions, RestoredState, UrlCreationOptions} from './navigation_transition';
1818
import {TitleStrategy} from './page_title_strategy';
1919
import {RouteReuseStrategy} from './route_reuse_strategy';
@@ -281,24 +281,15 @@ export class Router {
281281
titleStrategy?: TitleStrategy = inject(TitleStrategy);
282282

283283
/**
284-
* How to handle a navigation request to the current URL. One of:
284+
* How to handle a navigation request to the current URL.
285285
*
286-
* - `'ignore'` : The router ignores the request.
287-
* - `'reload'` : The router reloads the URL. Use to implement a "refresh" feature.
288-
*
289-
* Note that this only configures whether the Route reprocesses the URL and triggers related
290-
* action and events like redirects, guards, and resolvers. By default, the router re-uses a
291-
* component instance when it re-navigates to the same component type without visiting a different
292-
* component first. This behavior is configured by the `RouteReuseStrategy`. In order to reload
293-
* routed components on same url navigation, you need to set `onSameUrlNavigation` to `'reload'`
294-
* _and_ provide a `RouteReuseStrategy` which returns `false` for `shouldReuseRoute`.
295286
*
296287
* @deprecated Configure this through `provideRouter` or `RouterModule.forRoot` instead.
297288
* @see `withRouterConfig`
298289
* @see `provideRouter`
299290
* @see `RouterModule`
300291
*/
301-
onSameUrlNavigation: 'reload'|'ignore' = 'ignore';
292+
onSameUrlNavigation: OnSameUrlNavigation = 'ignore';
302293

303294
/**
304295
* How to merge parameters, data, resolved data, and title from parent to child

packages/router/src/router_config.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {InjectionToken} from '@angular/core';
1010

11+
import {OnSameUrlNavigation} from './models';
1112
import {UrlSerializer, UrlTree} from './url_tree';
1213

1314
const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode;
@@ -74,13 +75,13 @@ export interface RouterConfigOptions {
7475
canceledNavigationResolution?: 'replace'|'computed';
7576

7677
/**
77-
* Define what the router should do if it receives a navigation request to the current URL.
78-
* Default is `ignore`, which causes the router ignores the navigation.
79-
* This can disable features such as a "refresh" button.
80-
* Use this option to configure the behavior when navigating to the
81-
* current URL. Default is 'ignore'.
78+
* Configures the default for handling a navigation request to the current URL.
79+
*
80+
* If unset, the `Router` will use `'ignore'`.
81+
*
82+
* @see `OnSameUrlNavigation`
8283
*/
83-
onSameUrlNavigation?: 'reload'|'ignore';
84+
onSameUrlNavigation?: OnSameUrlNavigation;
8485

8586
/**
8687
* Defines how the router merges parameters, data, and resolved data from parent to child

packages/router/test/integration.spec.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,18 @@ describe('Integration', () => {
6060
})));
6161

6262
describe('navigation', function() {
63-
it('should navigate to the current URL', fakeAsync(inject([Router], (router: Router) => {
64-
router.onSameUrlNavigation = 'reload';
63+
it('should navigate to the current URL', fakeAsync(() => {
64+
TestBed.configureTestingModule({
65+
providers: [
66+
provideRouter([], withRouterConfig({onSameUrlNavigation: 'reload'})),
67+
]
68+
});
69+
const router = TestBed.inject(Router);
6570
router.resetConfig([
6671
{path: '', component: SimpleCmp},
6772
{path: 'simple', component: SimpleCmp},
6873
]);
6974

70-
const fixture = createRoot(router, RootCmp);
7175
const events: Event[] = [];
7276
router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e));
7377

@@ -81,8 +85,34 @@ describe('Integration', () => {
8185
[NavigationStart, '/simple'], [NavigationEnd, '/simple'], [NavigationStart, '/simple'],
8286
[NavigationEnd, '/simple']
8387
]);
84-
})));
88+
}));
8589

90+
it('should override default onSameUrlNavigation with extras', async () => {
91+
TestBed.configureTestingModule({
92+
providers: [
93+
provideRouter([], withRouterConfig({onSameUrlNavigation: 'ignore'})),
94+
]
95+
});
96+
const router = TestBed.inject(Router);
97+
router.resetConfig([
98+
{path: '', component: SimpleCmp},
99+
{path: 'simple', component: SimpleCmp},
100+
]);
101+
102+
const events: Event[] = [];
103+
router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e));
104+
105+
await router.navigateByUrl('/simple');
106+
await router.navigateByUrl('/simple');
107+
// By default, the second navigation is ignored
108+
expectEvents(events, [[NavigationStart, '/simple'], [NavigationEnd, '/simple']]);
109+
await router.navigateByUrl('/simple', {onSameUrlNavigation: 'reload'});
110+
// We overrode the `onSameUrlNavigation` value. This navigation should be processed.
111+
expectEvents(events, [
112+
[NavigationStart, '/simple'], [NavigationEnd, '/simple'], [NavigationStart, '/simple'],
113+
[NavigationEnd, '/simple']
114+
]);
115+
});
86116

87117
it('should ignore empty paths in relative links',
88118
fakeAsync(inject([Router], (router: Router) => {

0 commit comments

Comments
 (0)