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
23 changes: 22 additions & 1 deletion goldens/public-api/router/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export type DisabledInitialNavigationFeature = RouterFeature<RouterFeatureKind.D
export type EnabledBlockingInitialNavigationFeature = RouterFeature<RouterFeatureKind.EnabledBlockingInitialNavigationFeature>;

// @public
type Event_2 = RouterEvent | NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized | GuardsCheckStart | GuardsCheckEnd | RouteConfigLoadStart | RouteConfigLoadEnd | ChildActivationStart | ChildActivationEnd | ActivationStart | ActivationEnd | Scroll | ResolveStart | ResolveEnd;
type Event_2 = RouterEvent | NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized | GuardsCheckStart | GuardsCheckEnd | RouteConfigLoadStart | RouteConfigLoadEnd | ChildActivationStart | ChildActivationEnd | ActivationStart | ActivationEnd | Scroll | ResolveStart | ResolveEnd | NavigationSkipped;
export { Event_2 as Event }

// @public
Expand All @@ -272,6 +272,8 @@ export const enum EventType {
// (undocumented)
NavigationError = 3,
// (undocumented)
NavigationSkipped = 16,
// (undocumented)
NavigationStart = 0,
// (undocumented)
ResolveEnd = 6,
Expand Down Expand Up @@ -441,6 +443,25 @@ export class NavigationError extends RouterEvent {
export interface NavigationExtras extends UrlCreationOptions, NavigationBehaviorOptions {
}

// @public
export class NavigationSkipped extends RouterEvent {
constructor(
id: number,
url: string,
reason: string,
code?: NavigationSkippedCode | undefined);
readonly code?: NavigationSkippedCode | undefined;
reason: string;
// (undocumented)
readonly type = EventType.NavigationSkipped;
}

// @public
export const enum NavigationSkippedCode {
IgnoredByUrlHandlingStrategy = 1,
IgnoredSameUrlNavigation = 0
}

// @public
export class NavigationStart extends RouterEvent {
constructor(
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/router/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,9 @@
{
"name": "NavigationError"
},
{
"name": "NavigationSkipped"
},
{
"name": "NavigationStart"
},
Expand Down
62 changes: 58 additions & 4 deletions packages/router/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const enum EventType {
ActivationStart,
ActivationEnd,
Scroll,
NavigationSkipped,
}

/**
Expand Down Expand Up @@ -187,6 +188,26 @@ export const enum NavigationCancellationCode {
GuardRejected,
}

/**
* A code for the `NavigationSkipped` event of the `Router` to indicate the
* reason a navigation was skipped.
*
* @publicApi
*/
export const enum NavigationSkippedCode {
/**
* A navigation was skipped because the navigation URL was the same as the current Router URL.
*/
IgnoredSameUrlNavigation,
/**
* A navigation was skipped because the configured `UrlHandlingStrategy` return `false` for both
* the current Router URL and the target of the navigation.
*
* @see UrlHandlingStrategy
*/
IgnoredByUrlHandlingStrategy,
}

/**
* An event triggered when a navigation is canceled, directly or indirectly.
* This can happen for several reasons including when a route guard
Expand Down Expand Up @@ -226,6 +247,37 @@ export class NavigationCancel extends RouterEvent {
}
}

/**
* An event triggered when a navigation is skipped.
* This can happen for a couple reasons including onSameUrlHandling
* is set to `ignore` and the navigation URL is not different than the
* current state.
*
* @publicApi
*/
export class NavigationSkipped extends RouterEvent {
readonly type = EventType.NavigationSkipped;

constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/**
* A description of why the navigation was skipped. For debug purposes only. Use `code`
* instead for a stable skipped reason that can be used in production.
*/
public reason: string,
/**
* A code to indicate why the navigation was skipped. This code is stable for
* the reason and can be relied on whereas the `reason` string could change and should not be
* used in production.
*/
readonly code?: NavigationSkippedCode) {
super(id, url);
}
}

/**
* An event triggered when a navigation fails due to an unexpected error.
*
Expand Down Expand Up @@ -576,10 +628,10 @@ export class Scroll {
*
* @publicApi
*/
export type Event =
RouterEvent|NavigationStart|NavigationEnd|NavigationCancel|NavigationError|RoutesRecognized|
GuardsCheckStart|GuardsCheckEnd|RouteConfigLoadStart|RouteConfigLoadEnd|ChildActivationStart|
ChildActivationEnd|ActivationStart|ActivationEnd|Scroll|ResolveStart|ResolveEnd;
export type Event = RouterEvent|NavigationStart|NavigationEnd|NavigationCancel|NavigationError|
RoutesRecognized|GuardsCheckStart|GuardsCheckEnd|RouteConfigLoadStart|RouteConfigLoadEnd|
ChildActivationStart|ChildActivationEnd|ActivationStart|ActivationEnd|Scroll|ResolveStart|
ResolveEnd|NavigationSkipped;


export function stringifyEvent(routerEvent: Event): string {
Expand All @@ -605,6 +657,8 @@ export function stringifyEvent(routerEvent: Event): string {
routerEvent.state})`;
case EventType.NavigationCancel:
return `NavigationCancel(id: ${routerEvent.id}, url: '${routerEvent.url}')`;
case EventType.NavigationSkipped:
return `NavigationSkipped(id: ${routerEvent.id}, url: '${routerEvent.url}')`;
case EventType.NavigationEnd:
return `NavigationEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${
routerEvent.urlAfterRedirects}')`;
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 @@ -11,7 +11,7 @@ export {createUrlTreeFromSnapshot} from './create_url_tree';
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, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
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 {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
86 changes: 51 additions & 35 deletions packages/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {catchError, defaultIfEmpty, filter, finalize, map, switchMap, take, tap}
import {createRouterState} from './create_router_state';
import {createUrlTree} from './create_url_tree';
import {RuntimeErrorCode} from './errors';
import {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {NavigationBehaviorOptions, QueryParamsHandling, Route, Routes} from './models';
import {isNavigationCancelingError, isRedirectingNavigationCancelingError, redirectingNavigationError} from './navigation_canceling_error';
import {activateRoutes} from './operators/activate_routes';
Expand Down Expand Up @@ -638,12 +638,21 @@ export class Router {
// matching. If this is not the case, assume something went wrong and
// try processing the URL again.
browserUrlTree !== this.currentUrlTree.toString();
const processCurrentUrl =
(this.onSameUrlNavigation === 'reload' ? true : urlTransition) &&
this.urlHandlingStrategy.shouldProcessUrl(t.rawUrl);

if (!urlTransition && this.onSameUrlNavigation !== 'reload') {
const reason = NG_DEV_MODE ?
`Navigation to ${
t.rawUrl} was ignored because it is the same as the current Router URL.` :
'';
this.triggerEvent(new NavigationSkipped(
t.id, this.serializeUrl(overallTransitionState.rawUrl), reason,
NavigationSkippedCode.IgnoredSameUrlNavigation));
this.rawUrlTree = t.rawUrl;
t.resolve(null);
return EMPTY;
}

if (processCurrentUrl) {
if (this.urlHandlingStrategy.shouldProcessUrl(t.rawUrl)) {
// If the source of the navigation is from a browser event, the URL is
// already updated. We already need to sync the internal state.
if (isBrowserTriggeredNavigation(t.source)) {
Expand Down Expand Up @@ -703,37 +712,44 @@ export class Router {
this.serializeUrl(t.urlAfterRedirects!), t.targetSnapshot!);
eventsSubject.next(routesRecognized);
}));
} else if (
urlTransition &&
this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree)) {
// When the current URL shouldn't be processed, but the previous one
// was, we handle this by navigating from the current URL to an empty
// state so deactivate guards can run.
const {id, extractedUrl, source, restoredState, extras} = t;
const navStart = new NavigationStart(
id, this.serializeUrl(extractedUrl), source, restoredState);
eventsSubject.next(navStart);
const targetSnapshot =
createEmptyState(extractedUrl, this.rootComponentType).snapshot;

overallTransitionState = {
...t,
targetSnapshot,
urlAfterRedirects: extractedUrl,
extras: {...extras, skipLocationChange: false, replaceUrl: false},
};
return of(overallTransitionState);
} else {
const processPreviousUrl = urlTransition && this.rawUrlTree &&
this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree);
/* When the current URL shouldn't be processed, but the previous one
* was, we handle this "error condition" by navigating to the
* previously successful URL, but leaving the URL intact.*/
if (processPreviousUrl) {
const {id, extractedUrl, source, restoredState, extras} = t;
const navStart = new NavigationStart(
id, this.serializeUrl(extractedUrl), source, restoredState);
eventsSubject.next(navStart);
const targetSnapshot =
createEmptyState(extractedUrl, this.rootComponentType).snapshot;

overallTransitionState = {
...t,
targetSnapshot,
urlAfterRedirects: extractedUrl,
extras: {...extras, skipLocationChange: false, replaceUrl: false},
};
return of(overallTransitionState);
} else {
/* When neither the current or previous URL can be processed, do
* nothing other than update router's internal reference to the
* current "settled" URL. This way the next navigation will be coming
* from the current URL in the browser.
*/
this.rawUrlTree = t.rawUrl;
t.resolve(null);
return EMPTY;
}
/* When neither the current or previous URL can be processed, do
* nothing other than update router's internal reference to the
* current "settled" URL. This way the next navigation will be coming
* from the current URL in the browser.
*/
const reason = NG_DEV_MODE ?
`Navigation was ignored because the UrlHandlingStrategy` +
` indicated neither the current URL ${
this.rawUrlTree} nor target URL ${
t.rawUrl} should be processed.` :
'';
this.triggerEvent(new NavigationSkipped(
t.id, this.serializeUrl(overallTransitionState.extractedUrl),
reason, NavigationSkippedCode.IgnoredByUrlHandlingStrategy));
this.rawUrlTree = t.rawUrl;
t.resolve(null);
return EMPTY;
}
}),

Expand Down
8 changes: 5 additions & 3 deletions packages/router/test/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {ChangeDetectionStrategy, Component, EnvironmentInjector, inject as coreI
import {ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DefaultUrlSerializer, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, Navigation, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationStart, ParamMap, Params, PreloadAllModules, PreloadingStrategy, PRIMARY_OUTLET, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouteReuseStrategy, RouterEvent, RouterLink, RouterLinkActive, RouterModule, RouterOutlet, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlSerializer, UrlTree} from '@angular/router';
import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DefaultUrlSerializer, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, Navigation, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationStart, ParamMap, Params, PreloadAllModules, PreloadingStrategy, PRIMARY_OUTLET, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouteReuseStrategy, RouterEvent, RouterLink, RouterLinkActive, RouterModule, RouterOutlet, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlSerializer, UrlTree} from '@angular/router';
import {concat, EMPTY, Observable, Observer, of, Subscription} from 'rxjs';
import {delay, filter, first, last, map, mapTo, takeWhile, tap} from 'rxjs/operators';

Expand Down Expand Up @@ -6341,7 +6341,8 @@ describe('Integration', () => {
advance(fixture);

expect(location.path()).toEqual('/exclude/two');
expectEvents(events, []);
expectEvents(events, [[NavigationSkipped, '/exclude/two']]);
events.splice(0);

// back to a supported URL
location.simulateHashChange('/include/simple');
Expand Down Expand Up @@ -6386,7 +6387,8 @@ describe('Integration', () => {

location.simulateHashChange('/include/user/kate(aux:excluded2)');
advance(fixture);
expectEvents(events, []);
expectEvents(events, [[NavigationSkipped, '/include/user/kate(aux:excluded2)']]);
events.splice(0);

router.navigateByUrl('/include/simple');
advance(fixture);
Expand Down