diff --git a/goldens/public-api/router/index.md b/goldens/public-api/router/index.md index 8cf2e442cf4..c540fef35ea 100644 --- a/goldens/public-api/router/index.md +++ b/goldens/public-api/router/index.md @@ -379,6 +379,7 @@ export interface Navigation { // @public export interface NavigationBehaviorOptions { + onSameUrlNavigation?: Extract; replaceUrl?: boolean; skipLocationChange?: boolean; state?: { @@ -493,6 +494,9 @@ export class NoPreloading implements PreloadingStrategy { static ɵprov: i0.ɵɵInjectableDeclaration; } +// @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) => // @public export interface RouterConfigOptions { canceledNavigationResolution?: 'replace' | 'computed'; - onSameUrlNavigation?: 'reload' | 'ignore'; + onSameUrlNavigation?: OnSameUrlNavigation; paramsInheritanceStrategy?: 'emptyOnly' | 'always'; urlUpdateStrategy?: 'deferred' | 'eager'; } diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 4366793065c..086da37a869 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -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'; diff --git a/packages/router/src/models.ts b/packages/router/src/models.ts index 8ffce766e30..e28df79f32f 100644 --- a/packages/router/src/models.ts +++ b/packages/router/src/models.ts @@ -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; + /** * When true, navigates without pushing a new state into history. * diff --git a/packages/router/src/navigation_transition.ts b/packages/router/src/navigation_transition.ts index 189de1f3cb8..b1fa80ab7cc 100644 --- a/packages/router/src/navigation_transition.ts +++ b/packages/router/src/navigation_transition.ts @@ -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.` : diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 8574edeec41..14e01747b69 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -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 diff --git a/packages/router/src/router_config.ts b/packages/router/src/router_config.ts index 4bd09646d77..88fea43b1cb 100644 --- a/packages/router/src/router_config.ts +++ b/packages/router/src/router_config.ts @@ -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 diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index d97fb050e8e..61dfe658d1c 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -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) => {