Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat(router): Add ability to override onSameUrlNavigation default p…
…er-navigation

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
  • Loading branch information
atscott committed Nov 23, 2022
commit 74b6631306694d69a6c0a101d342cb2dca540e35
8 changes: 6 additions & 2 deletions goldens/public-api/router/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ export interface Navigation {

// @public
export interface NavigationBehaviorOptions {
onSameUrlNavigation?: Extract<OnSameUrlNavigation, 'reload'>;
replaceUrl?: boolean;
skipLocationChange?: boolean;
state?: {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -677,7 +681,7 @@ export class Router {
// (undocumented)
ngOnDestroy(): void;
// @deprecated
onSameUrlNavigation: 'reload' | 'ignore';
onSameUrlNavigation: OnSameUrlNavigation;
// @deprecated
paramsInheritanceStrategy: 'emptyOnly' | 'always';
parseUrl(url: string): UrlTree;
Expand Down Expand Up @@ -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';
}
Expand Down
2 changes: 1 addition & 1 deletion packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
52 changes: 48 additions & 4 deletions packages/router/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.
*
Expand Down
5 changes: 4 additions & 1 deletion packages/router/src/navigation_transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.` :
Expand Down
15 changes: 3 additions & 12 deletions packages/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions packages/router/src/router_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
38 changes: 34 additions & 4 deletions packages/router/test/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand All @@ -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) => {
Expand Down