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
4 changes: 2 additions & 2 deletions packages/router/src/apply_redirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {catchError, concatMap, first, last, map, mergeMap, scan, tap} from 'rxjs
import {CanLoadFn, LoadedRouterConfig, Route, Routes} from './models';
import {prioritizedGuardValue} from './operators/prioritized_guard_value';
import {RouterConfigLoader} from './router_config_loader';
import {navigationCancelingError, Params, PRIMARY_OUTLET} from './shared';
import {navigationCancelingError, Params, PRIMARY_OUTLET, REDIRECTING_CANCELLATION_REASON} from './shared';
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
import {forEach, wrapIntoObservable} from './utils/collection';
import {getOrCreateRouteInjectorIfNeeded, getOutlet, sortByMatchingOutlets} from './utils/config';
Expand Down Expand Up @@ -378,7 +378,7 @@ class ApplyRedirects {
if (!isUrlTree(result)) return;

const error: Error&{url?: UrlTree} = navigationCancelingError(
`Redirecting to "${this.urlSerializer.serialize(result)}"`);
REDIRECTING_CANCELLATION_REASON + this.urlSerializer.serialize(result));
error.url = result;
throw error;
}),
Expand Down
12 changes: 7 additions & 5 deletions packages/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ 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} from './shared';
import {isNavigationCancelingError, navigationCancelingError, Params, REDIRECTING_CANCELLATION_REASON} from './shared';
import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy';
import {containsTree, createEmptyUrlTree, IsActiveMatchOptions, UrlSerializer, UrlTree} from './url_tree';
import {standardizeConfig, validateConfig} from './utils/config';
Expand Down Expand Up @@ -410,7 +410,8 @@ export class Router {
private disposed = false;

private locationSubscription?: SubscriptionLike;
private navigationId: number = 0;
/** @internal */
navigationId: number = 0;

/**
* The id of the currently active page in the router.
Expand Down Expand Up @@ -761,7 +762,8 @@ export class Router {
tap(t => {
if (isUrlTree(t.guardsResult)) {
const error: Error&{url?: UrlTree} = navigationCancelingError(
`Redirecting to "${this.serializeUrl(t.guardsResult)}"`);
REDIRECTING_CANCELLATION_REASON +
`"${this.serializeUrl(t.guardsResult)}"`);
error.url = t.guardsResult;
throw error;
}
Expand Down Expand Up @@ -820,8 +822,6 @@ export class Router {
return undefined;
}),

switchTap(() => this.afterPreactivation()),

// --- LOAD COMPONENTS ---
switchTap((t: NavigationTransition) => {
const loadComponents =
Expand All @@ -846,6 +846,8 @@ export class Router {
.pipe(defaultIfEmpty(), take(1));
}),

switchTap(() => this.afterPreactivation()),

map((t: NavigationTransition) => {
const targetRouterState = createRouterState(
this.routeReuseStrategy, t.targetSnapshot!, t.currentRouterState);
Expand Down
57 changes: 52 additions & 5 deletions packages/router/src/router_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
import {HashLocationStrategy, Location, LOCATION_INITIALIZED, LocationStrategy, PathLocationStrategy, ViewportScroller} from '@angular/common';
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Compiler, ComponentRef, ENVIRONMENT_INITIALIZER, Inject, inject, InjectFlags, InjectionToken, Injector, ModuleWithProviders, NgModule, NgProbeToken, Optional, Provider, SkipSelf, Type} from '@angular/core';
import {of, Subject} from 'rxjs';
import {filter, map, take} from 'rxjs/operators';

import {EmptyOutletComponent} from './components/empty_outlet';
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
import {RouterLinkActive} from './directives/router_link_active';
import {RouterOutlet} from './directives/router_outlet';
import {Event, stringifyEvent} from './events';
import {Event, NavigationCancel, NavigationEnd, NavigationError, stringifyEvent} from './events';
import {Route, Routes} from './models';
import {DefaultTitleStrategy, TitleStrategy} from './page_title_strategy';
import {RouteReuseStrategy} from './route_reuse_strategy';
Expand All @@ -24,6 +25,7 @@ import {ChildrenOutletContexts} from './router_outlet_context';
import {PreloadingStrategy, RouterPreloader} from './router_preloader';
import {ROUTER_SCROLLER, RouterScroller} from './router_scroller';
import {ActivatedRoute} from './router_state';
import {REDIRECTING_CANCELLATION_REASON} from './shared';
import {UrlHandlingStrategy} from './url_handling_strategy';
import {DefaultUrlSerializer, UrlSerializer, UrlTree} from './url_tree';
import {flatten} from './utils/collection';
Expand Down Expand Up @@ -588,18 +590,63 @@ function provideEnabledBlockingInitialNavigation(): Provider {
injector.get(LOCATION_INITIALIZED, Promise.resolve(null));
let initNavigation = false;

/**
* Performs the given action once the router finishes its next/current navigation.
*
* If the navigation is canceled or errors without a redirect, the navigation is considered
* complete. If the `NavigationEnd` event emits, the navigation is also considered complete.
*/
function afterNextNavigation(action: () => void) {
const router = injector.get(Router);
router.events
.pipe(
filter(
(e): e is NavigationEnd|NavigationCancel|NavigationError =>
e instanceof NavigationEnd || e instanceof NavigationCancel ||
e instanceof NavigationError),
map(e => {
if (e instanceof NavigationEnd) {
// Navigation assumed to succeed if we get `ActivationStart`
return true;
}
const newNavigationStarted = router.navigationId !== e.id;
// TODO(atscott): Do not rely on the string reason to determine if cancelation
// is redirecting
const redirectingWithUrlTree = e instanceof NavigationCancel ?
e.reason.indexOf(REDIRECTING_CANCELLATION_REASON) !== -1 :
false;
// Navigation failed, but if we already have a new navigation, wait for the
// result of that one instead.
return newNavigationStarted || redirectingWithUrlTree ? null : false;
}),
filter((result): result is boolean => result !== null),
take(1),
)
.subscribe(() => {
action();
});
}

return () => {
return locationInitialized.then(() => {
return new Promise(resolve => {
const router = injector.get(Router);
const bootstrapDone = injector.get(BOOTSTRAP_DONE);
afterNextNavigation(() => {
// Unblock APP_INITIALIZER in case the initial navigation was canceled or errored
// without a redirect.
resolve(true);
initNavigation = true;
});

router.afterPreactivation = () => {
// only the initial navigation should be delayed
// Unblock APP_INITIALIZER once we get to `afterPreactivation`. At this point, we
// assume activation will complete successfully (even though this is not
// guaranteed).
resolve(true);
// only the initial navigation should be delayed until bootstrapping is done.
if (!initNavigation) {
initNavigation = true;
resolve(true);
return bootstrapDone;
return bootstrapDone.closed ? of(void 0) : bootstrapDone;
// subsequent navigations should not be delayed
} else {
return of(void 0);
Expand Down
1 change: 1 addition & 0 deletions packages/router/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ 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) {
Expand Down
66 changes: 33 additions & 33 deletions packages/router/test/bootstrap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,40 +77,40 @@ describe('bootstrap', () => {
}
}));

// TODO(#44355): bootstrapping fails when the initial navigation fails
xit('should complete resolvers when initial navigation fails and initialNavigation = enabledBlocking',
async () => {
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(
[{
matcher: () => {
throw new Error('error in matcher');
},
children: []
}],
{useHash: true, initialNavigation: 'enabledBlocking'})
],
declarations: [RootCmp],
bootstrap: [RootCmp],
providers: [...testProviders],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
class TestModule {
constructor() {
log.push('TestModule');
}
}
it('should complete resolvers when initial navigation fails and initialNavigation = enabledBlocking',
async () => {
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(
[{
matcher: () => {
throw new Error('error in matcher');
},
children: []
}],
{useHash: true, initialNavigation: 'enabledBlocking'})
],
declarations: [RootCmp],
bootstrap: [RootCmp],
providers: [...testProviders],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
class TestModule {
constructor(router: Router) {
log.push('TestModule');
router.events.subscribe(e => log.push(e.constructor.name));
}
}

await platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => {
const router = res.injector.get(Router);
expect(router.navigated).toEqual(false);
expect(router.getCurrentNavigation()).toBeNull();
expect(log).toContain('TestModule');
expect(log).toContain('NavigationError');
});
});
await platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => {
const router = res.injector.get(Router);
expect(router.navigated).toEqual(false);
expect(router.getCurrentNavigation()).toBeNull();
expect(log).toContain('TestModule');
expect(log).toContain('NavigationError');
});
});

it('should wait for redirect when initialNavigation = enabledBlocking', async () => {
@Injectable({providedIn: 'root'})
Expand Down