fix(router): ensure URL is updated after second redirect with UrlUpdateStrategy="eager" (#27523)
Navigating to a route such as `/users`, you may get redirected to `/login`. Previously, if you go then route to `/users` again the URL will end up showing `/users` after the second redirect. This only happened in `UrlUpdateStrategy="eager"`. This is now fixed so after the second redirect, the URL shows the correct page. Fixes #27116 PR Close #27523
This commit is contained in:
parent
880e8a5cfc
commit
07ada7f3d9
|
@ -291,6 +291,7 @@ function defaultRouterHook(snapshot: RouterStateSnapshot, runExtras: {
|
||||||
export class Router {
|
export class Router {
|
||||||
private currentUrlTree: UrlTree;
|
private currentUrlTree: UrlTree;
|
||||||
private rawUrlTree: UrlTree;
|
private rawUrlTree: UrlTree;
|
||||||
|
private browserUrlTree: UrlTree;
|
||||||
private readonly transitions: BehaviorSubject<NavigationTransition>;
|
private readonly transitions: BehaviorSubject<NavigationTransition>;
|
||||||
private navigations: Observable<NavigationTransition>;
|
private navigations: Observable<NavigationTransition>;
|
||||||
private lastSuccessfulNavigation: Navigation|null = null;
|
private lastSuccessfulNavigation: Navigation|null = null;
|
||||||
|
@ -400,6 +401,7 @@ export class Router {
|
||||||
this.resetConfig(config);
|
this.resetConfig(config);
|
||||||
this.currentUrlTree = createEmptyUrlTree();
|
this.currentUrlTree = createEmptyUrlTree();
|
||||||
this.rawUrlTree = this.currentUrlTree;
|
this.rawUrlTree = this.currentUrlTree;
|
||||||
|
this.browserUrlTree = this.parseUrl(this.location.path());
|
||||||
|
|
||||||
this.configLoader = new RouterConfigLoader(loader, compiler, onLoadStart, onLoadEnd);
|
this.configLoader = new RouterConfigLoader(loader, compiler, onLoadStart, onLoadEnd);
|
||||||
this.routerState = createEmptyState(this.currentUrlTree, this.rootComponentType);
|
this.routerState = createEmptyState(this.currentUrlTree, this.rootComponentType);
|
||||||
|
@ -461,7 +463,7 @@ export class Router {
|
||||||
return of (t).pipe(
|
return of (t).pipe(
|
||||||
switchMap(t => {
|
switchMap(t => {
|
||||||
const urlTransition =
|
const urlTransition =
|
||||||
!this.navigated || t.extractedUrl.toString() !== this.currentUrlTree.toString();
|
!this.navigated || t.extractedUrl.toString() !== this.browserUrlTree.toString();
|
||||||
const processCurrentUrl =
|
const processCurrentUrl =
|
||||||
(this.onSameUrlNavigation === 'reload' ? true : urlTransition) &&
|
(this.onSameUrlNavigation === 'reload' ? true : urlTransition) &&
|
||||||
this.urlHandlingStrategy.shouldProcessUrl(t.rawUrl);
|
this.urlHandlingStrategy.shouldProcessUrl(t.rawUrl);
|
||||||
|
@ -502,8 +504,12 @@ export class Router {
|
||||||
this.paramsInheritanceStrategy, this.relativeLinkResolution),
|
this.paramsInheritanceStrategy, this.relativeLinkResolution),
|
||||||
|
|
||||||
// Update URL if in `eager` update mode
|
// Update URL if in `eager` update mode
|
||||||
tap(t => this.urlUpdateStrategy === 'eager' && !t.extras.skipLocationChange &&
|
tap(t => {
|
||||||
this.setBrowserUrl(t.urlAfterRedirects, !!t.extras.replaceUrl, t.id)),
|
if (this.urlUpdateStrategy === 'eager' && !t.extras.skipLocationChange) {
|
||||||
|
this.setBrowserUrl(t.urlAfterRedirects, !!t.extras.replaceUrl, t.id);
|
||||||
|
this.browserUrlTree = t.urlAfterRedirects;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
// Fire RoutesRecognized
|
// Fire RoutesRecognized
|
||||||
tap(t => {
|
tap(t => {
|
||||||
|
@ -665,6 +671,7 @@ export class Router {
|
||||||
|
|
||||||
if (this.urlUpdateStrategy === 'deferred' && !t.extras.skipLocationChange) {
|
if (this.urlUpdateStrategy === 'deferred' && !t.extras.skipLocationChange) {
|
||||||
this.setBrowserUrl(this.rawUrlTree, !!t.extras.replaceUrl, t.id, t.extras.state);
|
this.setBrowserUrl(this.rawUrlTree, !!t.extras.replaceUrl, t.id, t.extras.state);
|
||||||
|
this.browserUrlTree = t.urlAfterRedirects;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/
|
||||||
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
||||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||||
import {fixmeIvy} from '@angular/private/testing';
|
import {fixmeIvy} from '@angular/private/testing';
|
||||||
import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, Navigation, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterEvent, RouterModule, 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, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterEvent, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlSerializer, UrlTree} from '@angular/router';
|
||||||
import {Observable, Observer, Subscription, of } from 'rxjs';
|
import {Observable, Observer, Subscription, of } from 'rxjs';
|
||||||
import {filter, first, map, tap} from 'rxjs/operators';
|
import {filter, first, map, tap} from 'rxjs/operators';
|
||||||
|
|
||||||
|
@ -575,64 +575,110 @@ describe('Integration', () => {
|
||||||
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
||||||
})));
|
})));
|
||||||
|
|
||||||
it('should eagerly update the URL with urlUpdateStrategy="eagar"',
|
describe('"eager" urlUpdateStrategy', () => {
|
||||||
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
beforeEach(() => {
|
||||||
const fixture = TestBed.createComponent(RootCmp);
|
const serializer = new DefaultUrlSerializer();
|
||||||
advance(fixture);
|
TestBed.configureTestingModule({
|
||||||
|
providers: [{
|
||||||
|
provide: 'authGuardFail',
|
||||||
|
useValue: (a: any, b: any) => {
|
||||||
|
return new Promise(res => { setTimeout(() => res(serializer.parse('/login')), 1); });
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
router.resetConfig([{path: 'team/:id', component: TeamCmp}]);
|
});
|
||||||
|
|
||||||
router.navigateByUrl('/team/22');
|
|
||||||
advance(fixture);
|
|
||||||
expect(location.path()).toEqual('/team/22');
|
|
||||||
|
|
||||||
expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]');
|
it('should eagerly update the URL with urlUpdateStrategy="eagar"',
|
||||||
|
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
|
const fixture = TestBed.createComponent(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
router.resetConfig([{path: 'team/:id', component: TeamCmp}]);
|
||||||
|
|
||||||
|
router.navigateByUrl('/team/22');
|
||||||
|
advance(fixture);
|
||||||
|
expect(location.path()).toEqual('/team/22');
|
||||||
|
|
||||||
router.urlUpdateStrategy = 'eager';
|
|
||||||
(router as any).hooks.beforePreactivation = () => {
|
|
||||||
expect(location.path()).toEqual('/team/33');
|
|
||||||
expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]');
|
expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]');
|
||||||
return of (null);
|
|
||||||
};
|
|
||||||
router.navigateByUrl('/team/33');
|
|
||||||
|
|
||||||
advance(fixture);
|
router.urlUpdateStrategy = 'eager';
|
||||||
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
(router as any).hooks.beforePreactivation = () => {
|
||||||
})));
|
expect(location.path()).toEqual('/team/33');
|
||||||
|
expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]');
|
||||||
|
return of (null);
|
||||||
|
};
|
||||||
|
router.navigateByUrl('/team/33');
|
||||||
|
|
||||||
it('should eagerly update URL after redirects are applied with urlUpdateStrategy="eagar"',
|
advance(fixture);
|
||||||
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
||||||
const fixture = TestBed.createComponent(RootCmp);
|
})));
|
||||||
advance(fixture);
|
it('should eagerly update the URL with urlUpdateStrategy="eagar"',
|
||||||
|
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
|
const fixture = TestBed.createComponent(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([{path: 'team/:id', component: TeamCmp}]);
|
router.urlUpdateStrategy = 'eager';
|
||||||
|
|
||||||
router.navigateByUrl('/team/22');
|
router.resetConfig([
|
||||||
advance(fixture);
|
{path: 'team/:id', component: SimpleCmp, canActivate: ['authGuardFail']},
|
||||||
expect(location.path()).toEqual('/team/22');
|
{path: 'login', component: AbsoluteSimpleLinkCmp}
|
||||||
|
]);
|
||||||
|
|
||||||
expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]');
|
router.navigateByUrl('/team/22');
|
||||||
|
advance(fixture);
|
||||||
|
expect(location.path()).toEqual('/team/22');
|
||||||
|
|
||||||
router.urlUpdateStrategy = 'eager';
|
// Redirects to /login
|
||||||
|
advance(fixture, 1);
|
||||||
|
expect(location.path()).toEqual('/login');
|
||||||
|
|
||||||
let urlAtNavStart = '';
|
// Perform the same logic again, and it should produce the same result
|
||||||
let urlAtRoutesRecognized = '';
|
router.navigateByUrl('/team/22');
|
||||||
router.events.subscribe(e => {
|
advance(fixture);
|
||||||
if (e instanceof NavigationStart) {
|
expect(location.path()).toEqual('/team/22');
|
||||||
urlAtNavStart = location.path();
|
|
||||||
}
|
|
||||||
if (e instanceof RoutesRecognized) {
|
|
||||||
urlAtRoutesRecognized = location.path();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/33');
|
// Redirects to /login
|
||||||
|
advance(fixture, 1);
|
||||||
|
expect(location.path()).toEqual('/login');
|
||||||
|
})));
|
||||||
|
|
||||||
advance(fixture);
|
it('should eagerly update URL after redirects are applied with urlUpdateStrategy="eagar"',
|
||||||
expect(urlAtNavStart).toBe('/team/22');
|
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
expect(urlAtRoutesRecognized).toBe('/team/33');
|
const fixture = TestBed.createComponent(RootCmp);
|
||||||
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
advance(fixture);
|
||||||
})));
|
|
||||||
|
router.resetConfig([{path: 'team/:id', component: TeamCmp}]);
|
||||||
|
|
||||||
|
router.navigateByUrl('/team/22');
|
||||||
|
advance(fixture);
|
||||||
|
expect(location.path()).toEqual('/team/22');
|
||||||
|
|
||||||
|
expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]');
|
||||||
|
|
||||||
|
router.urlUpdateStrategy = 'eager';
|
||||||
|
|
||||||
|
let urlAtNavStart = '';
|
||||||
|
let urlAtRoutesRecognized = '';
|
||||||
|
router.events.subscribe(e => {
|
||||||
|
if (e instanceof NavigationStart) {
|
||||||
|
urlAtNavStart = location.path();
|
||||||
|
}
|
||||||
|
if (e instanceof RoutesRecognized) {
|
||||||
|
urlAtRoutesRecognized = location.path();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.navigateByUrl('/team/33');
|
||||||
|
|
||||||
|
advance(fixture);
|
||||||
|
expect(urlAtNavStart).toBe('/team/22');
|
||||||
|
expect(urlAtRoutesRecognized).toBe('/team/33');
|
||||||
|
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
||||||
|
})));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
it('should navigate back and forward',
|
it('should navigate back and forward',
|
||||||
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
|
@ -4667,6 +4713,10 @@ class DummyLinkCmp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'link-cmp', template: `<a [routerLink]="['/simple']">link</a>`})
|
||||||
|
class AbsoluteSimpleLinkCmp {
|
||||||
|
}
|
||||||
|
|
||||||
@Component({selector: 'link-cmp', template: `<a [routerLink]="['../simple']">link</a>`})
|
@Component({selector: 'link-cmp', template: `<a [routerLink]="['../simple']">link</a>`})
|
||||||
class RelativeLinkCmp {
|
class RelativeLinkCmp {
|
||||||
}
|
}
|
||||||
|
@ -4847,8 +4897,8 @@ class ThrowingCmp {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function advance(fixture: ComponentFixture<any>): void {
|
function advance(fixture: ComponentFixture<any>, millis?: number): void {
|
||||||
tick();
|
tick(millis);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4876,6 +4926,7 @@ class LazyComponent {
|
||||||
StringLinkCmp,
|
StringLinkCmp,
|
||||||
DummyLinkCmp,
|
DummyLinkCmp,
|
||||||
AbsoluteLinkCmp,
|
AbsoluteLinkCmp,
|
||||||
|
AbsoluteSimpleLinkCmp,
|
||||||
RelativeLinkCmp,
|
RelativeLinkCmp,
|
||||||
DummyLinkWithParentCmp,
|
DummyLinkWithParentCmp,
|
||||||
LinkWithQueryParamsAndFragment,
|
LinkWithQueryParamsAndFragment,
|
||||||
|
@ -4904,6 +4955,7 @@ class LazyComponent {
|
||||||
StringLinkCmp,
|
StringLinkCmp,
|
||||||
DummyLinkCmp,
|
DummyLinkCmp,
|
||||||
AbsoluteLinkCmp,
|
AbsoluteLinkCmp,
|
||||||
|
AbsoluteSimpleLinkCmp,
|
||||||
RelativeLinkCmp,
|
RelativeLinkCmp,
|
||||||
DummyLinkWithParentCmp,
|
DummyLinkWithParentCmp,
|
||||||
LinkWithQueryParamsAndFragment,
|
LinkWithQueryParamsAndFragment,
|
||||||
|
@ -4934,6 +4986,7 @@ class LazyComponent {
|
||||||
StringLinkCmp,
|
StringLinkCmp,
|
||||||
DummyLinkCmp,
|
DummyLinkCmp,
|
||||||
AbsoluteLinkCmp,
|
AbsoluteLinkCmp,
|
||||||
|
AbsoluteSimpleLinkCmp,
|
||||||
RelativeLinkCmp,
|
RelativeLinkCmp,
|
||||||
DummyLinkWithParentCmp,
|
DummyLinkWithParentCmp,
|
||||||
LinkWithQueryParamsAndFragment,
|
LinkWithQueryParamsAndFragment,
|
||||||
|
|
Loading…
Reference in New Issue