Skip to content
Merged
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: 8 additions & 1 deletion packages/router/src/statemanager/navigation_state_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,15 @@ export class NavigationStateManager extends StateManager {
this.currentNavigation.rejectNavigateEvent?.();
const clearedState = {}; // Marker to detect if a new navigation started during async ops.
this.currentNavigation = clearedState;
// Do not reset state if we're redirecting or navigation is superseded by a new one.
// Do not reset browser history if we're redirecting or navigation is superseded by a new one.
if (isRedirectingEvent(cause)) {
if (
cause instanceof NavigationCancel &&
cause.code === NavigationCancellationCode.Redirect &&
this.routerState === transition.targetRouterState
) {
this.resetInternalState(transition.finalUrl, false);
}
return;
}
// Determine if the rollback should be a traversal to a specific previous entry
Expand Down
12 changes: 10 additions & 2 deletions packages/router/src/statemanager/state_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Event,
isRedirectingEvent,
NavigationCancel,
NavigationCancellationCode,
NavigationEnd,
NavigationError,
NavigationSkipped,
Expand Down Expand Up @@ -233,8 +234,15 @@ export class HistoryStateManager extends StateManager {
if (this.urlUpdateStrategy === 'deferred' && !currentTransition.extras.skipLocationChange) {
this.setBrowserUrl(this.createBrowserPath(currentTransition), currentTransition);
}
} else if (e instanceof NavigationCancel && !isRedirectingEvent(e)) {
this.restoreHistory(currentTransition);
} else if (e instanceof NavigationCancel) {
if (!isRedirectingEvent(e)) {
this.restoreHistory(currentTransition);
} else if (
e.code === NavigationCancellationCode.Redirect &&
this.routerState === currentTransition.targetRouterState
) {
this.resetInternalState(currentTransition);
}
} else if (e instanceof NavigationError) {
this.restoreHistory(currentTransition, true);
} else if (e instanceof NavigationEnd) {
Expand Down
30 changes: 28 additions & 2 deletions packages/router/test/integration/navigation_errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ export function navigationErrorsIntegrationSuite(browserAPI: 'history' | 'naviga
{path: 'error', component: BlankCmp},
],
{
resolveNavigationPromiseOnError: true,
errorHandler: () => new RedirectCommand(inject(Router).parseUrl('/error')),
},
),
Expand Down Expand Up @@ -171,7 +170,6 @@ export function navigationErrorsIntegrationSuite(browserAPI: 'history' | 'naviga
},
{path: 'error', component: BlankCmp},
],
withRouterConfig({resolveNavigationPromiseOnError: true}),
withNavigationErrorHandler(() => new RedirectCommand(inject(Router).parseUrl('/error'))),
),
],
Expand Down Expand Up @@ -219,6 +217,34 @@ export function navigationErrorsIntegrationSuite(browserAPI: 'history' | 'naviga
const router = TestBed.inject(Router);
});

it('can redirect from error handler when a component throws during activation alongside a secondary outlet', async () => {
let errors = 0;
TestBed.configureTestingModule({
providers: [
provideRouter(
[
{path: 'throwing', component: ThrowingCmp},
{path: 'user/:name', outlet: 'aux', component: UserCmp},
{path: 'error', component: SimpleCmp},
],
withNavigationErrorHandler(() => {
errors++;
return errors <= 3 ? new RedirectCommand(inject(Router).parseUrl('/error')) : undefined;
}),
),
],
});
const router = TestBed.inject(Router);
const fixture = await createRoot(router, RootCmp);

await router.navigateByUrl('/throwing(aux:user/victor)');
await advance(fixture);

expect(errors).toBe(1);
expect(router.url).toEqual('/error');
expect(fixture.nativeElement).toHaveText('simple');
});

// Errors should behave the same for both deferred and eager URL update strategies
(['deferred', 'eager'] as const).forEach((urlUpdateStrategy) => {
it(`should dispatch NavigationError after the url has been reset back (${urlUpdateStrategy})`, async () => {
Expand Down
Loading