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
This commit is contained in:
Andrew Scott
2022-11-10 12:40:12 -08:00
committed by Dylan Hunn
parent 6a8ab3014c
commit 332461bd0c
7 changed files with 103 additions and 30 deletions
+6 -2
View File
@@ -379,6 +379,7 @@ export interface Navigation {
// @public
export interface NavigationBehaviorOptions {
onSameUrlNavigation?: Extract<OnSameUrlNavigation, 'reload'>;
replaceUrl?: boolean;
skipLocationChange?: boolean;
state?: {
@@ -493,6 +494,9 @@ export class NoPreloading implements PreloadingStrategy {
static ɵprov: i0.ɵɵInjectableDeclaration<NoPreloading>;
}
// @public
export type OnSameUrlNavigation = 'reload' | 'ignore';
// @public
export class OutletContext {
// (undocumented)
@@ -677,7 +681,7 @@ export class Router {
// (undocumented)
ngOnDestroy(): void;
// @deprecated
onSameUrlNavigation: 'reload' | 'ignore';
onSameUrlNavigation: OnSameUrlNavigation;
// @deprecated
paramsInheritanceStrategy: 'emptyOnly' | 'always';
parseUrl(url: string): UrlTree;
@@ -709,7 +713,7 @@ export const ROUTER_INITIALIZER: InjectionToken<(compRef: ComponentRef<any>) =>
// @public
export interface RouterConfigOptions {
canceledNavigationResolution?: 'replace' | 'computed';
onSameUrlNavigation?: 'reload' | 'ignore';
onSameUrlNavigation?: OnSameUrlNavigation;
paramsInheritanceStrategy?: 'emptyOnly' | 'always';
urlUpdateStrategy?: 'deferred' | 'eager';
}
+1 -1
View File
@@ -12,7 +12,7 @@ export {RouterLink, RouterLinkWithHref} from './directives/router_link';
export {RouterLinkActive} from './directives/router_link_active';
export {RouterOutlet, RouterOutletContract} from './directives/router_outlet';
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';
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';
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';
export {Navigation, NavigationExtras, UrlCreationOptions} from './navigation_transition';
export {DefaultTitleStrategy, TitleStrategy} from './page_title_strategy';
export {DebugTracingFeature, DisabledInitialNavigationFeature, EnabledBlockingInitialNavigationFeature, InitialNavigationFeature, InMemoryScrollingFeature, PreloadingFeature, provideRouter, provideRoutes, RouterConfigurationFeature, RouterFeature, RouterFeatures, withDebugTracing, withDisabledInitialNavigation, withEnabledBlockingInitialNavigation, withInMemoryScrolling, withPreloading, withRouterConfig} from './provide_router';
+48 -4
View File
@@ -13,6 +13,31 @@ import {DeprecatedLoadChildren} from './deprecated_load_children';
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
/**
* How to handle a navigation request to the current URL. One of:
*
* - `'ignore'` : The router ignores the request it is the same as the current state.
* - `'reload'` : The router processes the URL even if it is not different from the current state.
* One example of when you might want this option is if a `canMatch` guard depends on
* application state and initially rejects navigation to a route. After fixing the state, you want
* to re-navigate to the same URL so the route with the `canMatch` guard can activate.
*
* Note that this only configures whether the Route reprocesses the URL and triggers related
* action and events like redirects, guards, and resolvers. By default, the router re-uses a
* component instance when it re-navigates to the same component type without visiting a different
* component first. This behavior is configured by the `RouteReuseStrategy`. In order to reload
* routed components on same url navigation, you need to set `onSameUrlNavigation` to `'reload'`
* _and_ provide a `RouteReuseStrategy` which returns `false` for `shouldReuseRoute`. Additionally,
* resolvers and most guards for routes do not run unless the path or path params changed
* (configured by `runGuardsAndResolvers`).
*
* @publicApi
* @see RouteReuseStrategy
* @see RunGuardsAndResolvers
* @see NavigationBehaviorOptions
* @see RouterConfigOptions
*/
export type OnSameUrlNavigation = 'reload'|'ignore';
/**
* Represents a route configuration for the Router service.
@@ -550,11 +575,19 @@ export interface Route {
loadChildren?: LoadChildren;
/**
* Defines when guards and resolvers will be run. One of
* - `paramsOrQueryParamsChange` : Run when query parameters change.
* A policy for when to run guards and resolvers on a route.
*
* Guards and/or resolvers will always run when a route is activated or deactivated. When a route
* is unchanged, the default behavior is the same as `paramsChange`.
*
* `paramsChange` : Rerun the guards and resolvers when path or
* path param changes. This does not include query parameters. This option is the default.
* - `always` : Run on every execution.
* By default, guards and resolvers run only when the matrix
* parameters of the route change.
* - `pathParamsChange` : Rerun guards and resolvers when the path params
* change. This does not compare matrix or query parameters.
* - `paramsOrQueryParamsChange` : Run when path, matrix, or query parameters change.
* - `pathParamsOrQueryParamsChange` : Rerun guards and resolvers when the path params
* change or query params have changed. This does not include matrix parameters.
*
* @see RunGuardsAndResolvers
*/
@@ -1194,6 +1227,17 @@ export type CanLoadFn = (route: Route, segments: UrlSegment[]) =>
* @publicApi
*/
export interface NavigationBehaviorOptions {
/**
* How to handle a navigation request to the current URL.
*
* This value is a subset of the options available in `OnSameUrlNavigation` and
* will take precedence over the default value set for the `Router`.
*
* @see `OnSameUrlNavigation`
* @see `RouterConfigOptions`
*/
onSameUrlNavigation?: Extract<OnSameUrlNavigation, 'reload'>;
/**
* When true, navigates without pushing a new state into history.
*
+4 -1
View File
@@ -344,7 +344,10 @@ export class NavigationTransitions {
// try processing the URL again.
browserUrlTree !== this.router.currentUrlTree.toString();
if (!urlTransition && this.router.onSameUrlNavigation !== 'reload') {
const onSameUrlNavigation =
t.extras.onSameUrlNavigation ?? this.router.onSameUrlNavigation;
if (!urlTransition && onSameUrlNavigation !== 'reload') {
const reason = NG_DEV_MODE ?
`Navigation to ${
t.rawUrl} was ignored because it is the same as the current Router URL.` :
+3 -12
View File
@@ -13,7 +13,7 @@ import {BehaviorSubject, Observable, of, Subject, SubscriptionLike} from 'rxjs';
import {createUrlTree} from './create_url_tree';
import {RuntimeErrorCode} from './errors';
import {Event, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationTrigger, RouteConfigLoadEnd, RouteConfigLoadStart} from './events';
import {NavigationBehaviorOptions, Route, Routes} from './models';
import {NavigationBehaviorOptions, OnSameUrlNavigation, Route, Routes} from './models';
import {Navigation, NavigationExtras, NavigationTransition, NavigationTransitions, RestoredState, UrlCreationOptions} from './navigation_transition';
import {TitleStrategy} from './page_title_strategy';
import {RouteReuseStrategy} from './route_reuse_strategy';
@@ -281,24 +281,15 @@ export class Router {
titleStrategy?: TitleStrategy = inject(TitleStrategy);
/**
* How to handle a navigation request to the current URL. One of:
* How to handle a navigation request to the current URL.
*
* - `'ignore'` : The router ignores the request.
* - `'reload'` : The router reloads the URL. Use to implement a "refresh" feature.
*
* Note that this only configures whether the Route reprocesses the URL and triggers related
* action and events like redirects, guards, and resolvers. By default, the router re-uses a
* component instance when it re-navigates to the same component type without visiting a different
* component first. This behavior is configured by the `RouteReuseStrategy`. In order to reload
* routed components on same url navigation, you need to set `onSameUrlNavigation` to `'reload'`
* _and_ provide a `RouteReuseStrategy` which returns `false` for `shouldReuseRoute`.
*
* @deprecated Configure this through `provideRouter` or `RouterModule.forRoot` instead.
* @see `withRouterConfig`
* @see `provideRouter`
* @see `RouterModule`
*/
onSameUrlNavigation: 'reload'|'ignore' = 'ignore';
onSameUrlNavigation: OnSameUrlNavigation = 'ignore';
/**
* How to merge parameters, data, resolved data, and title from parent to child
+7 -6
View File
@@ -8,6 +8,7 @@
import {InjectionToken} from '@angular/core';
import {OnSameUrlNavigation} from './models';
import {UrlSerializer, UrlTree} from './url_tree';
const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode;
@@ -74,13 +75,13 @@ export interface RouterConfigOptions {
canceledNavigationResolution?: 'replace'|'computed';
/**
* Define what the router should do if it receives a navigation request to the current URL.
* Default is `ignore`, which causes the router ignores the navigation.
* This can disable features such as a "refresh" button.
* Use this option to configure the behavior when navigating to the
* current URL. Default is 'ignore'.
* Configures the default for handling a navigation request to the current URL.
*
* If unset, the `Router` will use `'ignore'`.
*
* @see `OnSameUrlNavigation`
*/
onSameUrlNavigation?: 'reload'|'ignore';
onSameUrlNavigation?: OnSameUrlNavigation;
/**
* Defines how the router merges parameters, data, and resolved data from parent to child
+34 -4
View File
@@ -60,14 +60,18 @@ describe('Integration', () => {
})));
describe('navigation', function() {
it('should navigate to the current URL', fakeAsync(inject([Router], (router: Router) => {
router.onSameUrlNavigation = 'reload';
it('should navigate to the current URL', fakeAsync(() => {
TestBed.configureTestingModule({
providers: [
provideRouter([], withRouterConfig({onSameUrlNavigation: 'reload'})),
]
});
const router = TestBed.inject(Router);
router.resetConfig([
{path: '', component: SimpleCmp},
{path: 'simple', component: SimpleCmp},
]);
const fixture = createRoot(router, RootCmp);
const events: Event[] = [];
router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e));
@@ -81,8 +85,34 @@ describe('Integration', () => {
[NavigationStart, '/simple'], [NavigationEnd, '/simple'], [NavigationStart, '/simple'],
[NavigationEnd, '/simple']
]);
})));
}));
it('should override default onSameUrlNavigation with extras', async () => {
TestBed.configureTestingModule({
providers: [
provideRouter([], withRouterConfig({onSameUrlNavigation: 'ignore'})),
]
});
const router = TestBed.inject(Router);
router.resetConfig([
{path: '', component: SimpleCmp},
{path: 'simple', component: SimpleCmp},
]);
const events: Event[] = [];
router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e));
await router.navigateByUrl('/simple');
await router.navigateByUrl('/simple');
// By default, the second navigation is ignored
expectEvents(events, [[NavigationStart, '/simple'], [NavigationEnd, '/simple']]);
await router.navigateByUrl('/simple', {onSameUrlNavigation: 'reload'});
// We overrode the `onSameUrlNavigation` value. This navigation should be processed.
expectEvents(events, [
[NavigationStart, '/simple'], [NavigationEnd, '/simple'], [NavigationStart, '/simple'],
[NavigationEnd, '/simple']
]);
});
it('should ignore empty paths in relative links',
fakeAsync(inject([Router], (router: Router) => {