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
9 changes: 9 additions & 0 deletions packages/core/test/bundling/router/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -1475,6 +1475,9 @@
{
"name": "isNameOnlyAttributeMarker"
},
{
"name": "isNavigationCancelingError"
},
{
"name": "isNodeMatchingSelector"
},
Expand All @@ -1496,6 +1499,9 @@
{
"name": "isPromise2"
},
{
"name": "isRedirectingNavigationCancelingError"
},
{
"name": "isScheduler"
},
Expand Down Expand Up @@ -1700,6 +1706,9 @@
{
"name": "redirectIfUrlTree"
},
{
"name": "redirectingNavigationError"
},
{
"name": "refCount"
},
Expand Down
9 changes: 6 additions & 3 deletions packages/router/src/apply_redirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import {EmptyError, from, Observable, of, throwError} from 'rxjs';
import {catchError, concatMap, first, last, map, mergeMap, scan, switchMap, tap} from 'rxjs/operators';

import {RuntimeErrorCode} from './errors';
import {NavigationCancellationCode} from './events';
import {LoadedRouterConfig, Route, Routes} from './models';
import {navigationCancelingError} from './navigation_canceling_error';
import {runCanLoadGuards} from './operators/check_guards';
import {RouterConfigLoader} from './router_config_loader';
import {navigationCancelingError, Params, PRIMARY_OUTLET} from './shared';
import {Params, PRIMARY_OUTLET} from './shared';
import {createRoot, squashSegmentGroup, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
import {forEach} from './utils/collection';
import {getOrCreateRouteInjectorIfNeeded, getOutlet, sortByMatchingOutlets} from './utils/config';
Expand Down Expand Up @@ -52,8 +54,9 @@ function namedOutletsRedirect(redirectTo: string): Observable<any> {
function canLoadFails(route: Route): Observable<LoadedRouterConfig> {
return throwError(navigationCancelingError(
NG_DEV_MODE &&
`Cannot load children because the guard of the route "path: '${
route.path}'" returned false`));
`Cannot load children because the guard of the route "path: '${
route.path}'" returned false`,
NavigationCancellationCode.GuardRejected));
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ 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 {CanActivate, CanActivateChild, CanDeactivate, CanLoad, CanMatch, CanMatchFn, Data, LoadChildren, LoadChildrenCallback, QueryParamsHandling, Resolve, ResolveData, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models';
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, CanMatch, CanMatchFn, Data, LoadChildren, LoadChildrenCallback, NavigationBehaviorOptions, QueryParamsHandling, Resolve, ResolveData, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models';
export {DefaultTitleStrategy, TitleStrategy} from './page_title_strategy';
export {BaseRouteReuseStrategy, DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
export {Navigation, NavigationBehaviorOptions, NavigationExtras, Router, UrlCreationOptions} from './router';
export {Navigation, NavigationExtras, Router, UrlCreationOptions} from './router';
export {ROUTES} from './router_config_loader';
export {ExtraOptions, InitialNavigation, provideRoutes, ROUTER_CONFIGURATION, ROUTER_INITIALIZER, RouterModule} from './router_module';
export {ChildrenOutletContexts, OutletContext} from './router_outlet_context';
Expand Down
53 changes: 53 additions & 0 deletions packages/router/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1139,3 +1139,56 @@ export interface CanLoad {

export type CanLoadFn = (route: Route, segments: UrlSegment[]) =>
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;


/**
* @description
*
* Options that modify the `Router` navigation strategy.
* Supply an object containing any of these properties to a `Router` navigation function to
* control how the navigation should be handled.
*
* @see [Router.navigate() method](api/router/Router#navigate)
* @see [Router.navigateByUrl() method](api/router/Router#navigatebyurl)
* @see [Routing and Navigation guide](guide/router)
*
* @publicApi
*/
export interface NavigationBehaviorOptions {
/**
* When true, navigates without pushing a new state into history.
*
* ```
* // Navigate silently to /view
* this.router.navigate(['/view'], { skipLocationChange: true });
* ```
*/
skipLocationChange?: boolean;

/**
* When true, navigates while replacing the current state in history.
*
* ```
* // Navigate to /view
* this.router.navigate(['/view'], { replaceUrl: true });
* ```
*/
replaceUrl?: boolean;

/**
* Developer-defined state that can be passed to any navigation.
* Access this value through the `Navigation.extras` object
* returned from the [Router.getCurrentNavigation()
* method](api/router/Router#getcurrentnavigation) while a navigation is executing.
*
* After a navigation completes, the router writes an object containing this
* value together with a `navigationId` to `history.state`.
* The value is written when `location.go()` or `location.replaceState()`
* is called before activating this route.
*
* Note that `history.state` does not pass an object equality test because
* the router adds the `navigationId` on each navigation.
*
*/
state?: {[k: string]: any};
Comment thread
atscott marked this conversation as resolved.
Outdated
}
55 changes: 55 additions & 0 deletions packages/router/src/navigation_canceling_error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {NavigationCancellationCode} from './events';
import {NavigationBehaviorOptions} from './models';
import {isUrlTree, UrlSerializer, UrlTree} from './url_tree';

export const NAVIGATION_CANCELING_ERROR = 'ngNavigationCancelingError';

export type NavigationCancelingError =
Error&{[NAVIGATION_CANCELING_ERROR]: true, cancellationCode: NavigationCancellationCode};
export type RedirectingNavigationCancelingError = NavigationCancelingError&{
url: UrlTree;
navigationBehaviorOptions?: NavigationBehaviorOptions;
cancellationCode: NavigationCancellationCode.Redirect;
};

export function redirectingNavigationError(
urlSerializer: UrlSerializer, redirect: UrlTree): RedirectingNavigationCancelingError {
const {redirectTo, navigationBehaviorOptions} =
isUrlTree(redirect) ? {redirectTo: redirect, navigationBehaviorOptions: undefined} : redirect;
const error =
navigationCancelingError(
ngDevMode && `Redirecting to "${urlSerializer.serialize(redirectTo)}"`,
NavigationCancellationCode.Redirect, redirect) as RedirectingNavigationCancelingError;
error.url = redirectTo;
error.navigationBehaviorOptions = navigationBehaviorOptions;
return error;
}

export function navigationCancelingError(
message: string|null|false, code: NavigationCancellationCode, redirectUrl?: UrlTree) {
const error =
new Error('NavigationCancelingError: ' + (message || '')) as NavigationCancelingError;
error[NAVIGATION_CANCELING_ERROR] = true;
error.cancellationCode = code;
if (redirectUrl) {
(error as RedirectingNavigationCancelingError).url = redirectUrl;
}
return error;
}

export function isRedirectingNavigationCancelingError(
error: unknown|
RedirectingNavigationCancelingError): error is RedirectingNavigationCancelingError {
return isNavigationCancelingError(error) && isUrlTree((error as any).url);
}
export function isNavigationCancelingError(error: unknown): error is NavigationCancelingError {
return error && (error as any)[NAVIGATION_CANCELING_ERROR];
}
11 changes: 4 additions & 7 deletions packages/router/src/operators/check_guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import {concatMap, first, map, mergeMap, tap} from 'rxjs/operators';

import {ActivationStart, ChildActivationStart, Event} from '../events';
import {CanLoad, CanLoadFn, CanMatch, CanMatchFn, Route} from '../models';
import {redirectingNavigationError} from '../navigation_canceling_error';
import {NavigationTransition} from '../router';
import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../router_state';
import {navigationCancelingError, REDIRECTING_CANCELLATION_REASON} from '../shared';
import {UrlSegment, UrlSerializer, UrlTree} from '../url_tree';
import {isUrlTree, UrlSegment, UrlSerializer, UrlTree} from '../url_tree';
import {wrapIntoObservable} from '../utils/collection';
import {CanActivate, CanDeactivate, getCanActivateChild, getToken} from '../utils/preactivation';
import {isBoolean, isCanActivate, isCanActivateChild, isCanDeactivate, isCanLoad, isCanMatch, isFunction, isUrlTree} from '../utils/type_guards';
import {isBoolean, isCanActivate, isCanActivateChild, isCanDeactivate, isCanLoad, isCanMatch} from '../utils/type_guards';

import {prioritizedGuardValue} from './prioritized_guard_value';

Expand Down Expand Up @@ -188,10 +188,7 @@ function redirectIfUrlTree(urlSerializer: UrlSerializer):
tap((result: UrlTree|boolean) => {
if (!isUrlTree(result)) return;

const error: Error&{url?: UrlTree} = navigationCancelingError(
REDIRECTING_CANCELLATION_REASON + urlSerializer.serialize(result));
error.url = result;
throw error;
throw redirectingNavigationError(urlSerializer, result);
}),
map(result => result === true),
);
Expand Down
3 changes: 1 addition & 2 deletions packages/router/src/operators/prioritized_guard_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
import {combineLatest, Observable, OperatorFunction} from 'rxjs';
import {filter, map, scan, startWith, switchMap, take} from 'rxjs/operators';

import {UrlTree} from '../url_tree';
import {isUrlTree} from '../utils/type_guards';
import {isUrlTree, UrlTree} from '../url_tree';

const INITIAL_VALUE = Symbol('INITIAL_VALUE');
declare type INTERIM_VALUES = typeof INITIAL_VALUE | boolean | UrlTree;
Expand Down
73 changes: 8 additions & 65 deletions packages/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ 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 {QueryParamsHandling, Route, Routes} from './models';
import {NavigationBehaviorOptions, QueryParamsHandling, Route, Routes} from './models';
import {isNavigationCancelingError, isRedirectingNavigationCancelingError, redirectingNavigationError} from './navigation_canceling_error';
import {activateRoutes} from './operators/activate_routes';
import {applyRedirects} from './operators/apply_redirects';
import {checkGuards} from './operators/check_guards';
Expand All @@ -27,12 +28,11 @@ import {DefaultRouteReuseStrategy, RouteReuseStrategy} from './route_reuse_strat
import {RouterConfigLoader} from './router_config_loader';
import {ChildrenOutletContexts} from './router_outlet_context';
import {ActivatedRoute, ActivatedRouteSnapshot, createEmptyState, RouterState, RouterStateSnapshot} from './router_state';
import {isNavigationCancelingError, navigationCancelingError, Params, REDIRECTING_CANCELLATION_REASON} from './shared';
import {Params} from './shared';
import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy';
import {containsTree, createEmptyUrlTree, IsActiveMatchOptions, UrlSerializer, UrlTree} from './url_tree';
import {containsTree, createEmptyUrlTree, IsActiveMatchOptions, isUrlTree, UrlSerializer, UrlTree} from './url_tree';
import {standardizeConfig, validateConfig} from './utils/config';
import {Checks, getAllRouteGuards} from './utils/preactivation';
import {isUrlTree} from './utils/type_guards';


const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode;
Expand Down Expand Up @@ -145,58 +145,6 @@ export interface UrlCreationOptions {
preserveFragment?: boolean;
}

/**
* @description
*
* Options that modify the `Router` navigation strategy.
* Supply an object containing any of these properties to a `Router` navigation function to
* control how the navigation should be handled.
*
* @see [Router.navigate() method](api/router/Router#navigate)
* @see [Router.navigateByUrl() method](api/router/Router#navigatebyurl)
* @see [Routing and Navigation guide](guide/router)
*
* @publicApi
*/
export interface NavigationBehaviorOptions {
/**
* When true, navigates without pushing a new state into history.
*
* ```
* // Navigate silently to /view
* this.router.navigate(['/view'], { skipLocationChange: true });
* ```
*/
skipLocationChange?: boolean;

/**
* When true, navigates while replacing the current state in history.
*
* ```
* // Navigate to /view
* this.router.navigate(['/view'], { replaceUrl: true });
* ```
*/
replaceUrl?: boolean;

/**
* Developer-defined state that can be passed to any navigation.
* Access this value through the `Navigation.extras` object
* returned from the [Router.getCurrentNavigation()
* method](api/router/Router#getcurrentnavigation) while a navigation is executing.
*
* After a navigation completes, the router writes an object containing this
* value together with a `navigationId` to `history.state`.
* The value is written when `location.go()` or `location.replaceState()`
* is called before activating this route.
*
* Note that `history.state` does not pass an object equality test because
* the router adds the `navigationId` on each navigation.
*
*/
state?: {[k: string]: any};
}

/**
* @description
*
Expand Down Expand Up @@ -761,11 +709,7 @@ export class Router {
checkGuards(this.ngModule.injector, (evt: Event) => this.triggerEvent(evt)),
tap(t => {
if (isUrlTree(t.guardsResult)) {
const error: Error&{url?: UrlTree} = navigationCancelingError(
REDIRECTING_CANCELLATION_REASON +
`"${this.serializeUrl(t.guardsResult)}"`);
error.url = t.guardsResult;
throw error;
throw redirectingNavigationError(this.urlSerializer, t.guardsResult);
}

const guardsEnd = new GuardsCheckEnd(
Expand Down Expand Up @@ -929,8 +873,7 @@ export class Router {
/* This error type is issued during Redirect, and is handled as a
* cancellation rather than an error. */
if (isNavigationCancelingError(e)) {
const redirecting = isUrlTree(e.url);
if (!redirecting) {
if (!isRedirectingNavigationCancelingError(e)) {
// Set property only if we're not redirecting. If we landed on a page and
// redirect to `/` route, the new navigation is going to see the `/`
// isn't a change from the default currentUrlTree and won't navigate.
Expand All @@ -941,12 +884,12 @@ export class Router {
}
const navCancel = new NavigationCancel(
t.id, this.serializeUrl(t.extractedUrl), e.message,
NavigationCancellationCode.Redirect);
e.cancellationCode);
eventsSubject.next(navCancel);

// When redirecting, we need to delay resolving the navigation
// promise and push it to the redirect navigation
if (!redirecting) {
if (!isRedirectingNavigationCancelingError(e)) {
t.resolve(false);
} else {
const mergedTree =
Expand Down
13 changes: 0 additions & 13 deletions packages/router/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,19 +111,6 @@ export function convertToParamMap(params: Params): ParamMap {
return new ParamsAsMap(params);
}

export const REDIRECTING_CANCELLATION_REASON = 'Redirecting to ';
const NAVIGATION_CANCELING_ERROR = 'ngNavigationCancelingError';

export function navigationCancelingError(message: string|null|false) {
const error = Error('NavigationCancelingError: ' + (message || ''));
(error as any)[NAVIGATION_CANCELING_ERROR] = true;
return error;
}

export function isNavigationCancelingError(error: Error) {
return error && (error as any)[NAVIGATION_CANCELING_ERROR];
}

// Matches the route configuration (`route`) against the actual URL (`segments`).
export function defaultUrlMatcher(
segments: UrlSegment[], segmentGroup: UrlSegmentGroup, route: Route): UrlMatchResult|null {
Expand Down
4 changes: 4 additions & 0 deletions packages/router/src/url_tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -779,3 +779,7 @@ function mergeTrivialChildren(s: UrlSegmentGroup): UrlSegmentGroup {

return s;
}

export function isUrlTree(v: any): v is UrlTree {
return v instanceof UrlTree;
}
Loading