diff --git a/packages/router-component-store/src/lib/router-history-store/router-history-service-in-angular.spec.ts b/packages/router-component-store/src/lib/router-history-store/router-history-service-in-angular.spec.ts new file mode 100644 index 0000000..24bc636 --- /dev/null +++ b/packages/router-component-store/src/lib/router-history-store/router-history-service-in-angular.spec.ts @@ -0,0 +1,77 @@ +import { + Event as NgRouterEvent, + NavigationEnd, + NavigationStart, +} from '@angular/router'; + +export const routerEvents: readonly NgRouterEvent[] = [ + // 1. Navigate to the root path ‘/’, which redirects me to the homepage + // Current: Home + // Previous: None + // Next: None + new NavigationStart(1, '/', 'imperative', null), + new NavigationEnd(1, '/', '/home'), + + // 2. Click a menu link to navigate to the About page + // Current: About + // Previous: Home + // Next: None + new NavigationStart(2, '/about', 'imperative', null), + new NavigationEnd(2, '/about', '/about'), + + // 3. Click a menu link to navigate to the Company page + // Current: Company + // Previous About + // Next: None + new NavigationStart(3, '/company', 'imperative', null), + new NavigationEnd(3, '/company', '/company'), + + // 4. Click the back button + // Current: About + // Previous: Home + // Next: Company + new NavigationStart(4, '/about', 'popstate', { navigationId: 2 }), + new NavigationEnd(4, '/about', '/about'), + + // 5. Click a menu link to navigate to the Products page + // Current: Products + // Previous: About + // Next: None + new NavigationStart(5, '/products', 'imperative', null), + new NavigationEnd(5, '/products', '/products'), + + // 6. Click a menu link to navigate to the Home page + // Current: Home + // Previous: Products + // Next: None + new NavigationStart(6, '/home', 'imperative', null), + new NavigationEnd(6, '/home', '/home'), + + // 7. Click a menu link to navigate to the About page + // Current: About + // Previous: Home + // Next: None + new NavigationStart(7, '/about', 'imperative', null), + new NavigationEnd(7, '/about', '/about'), + + // 8. Click the back button + // Current: Home + // Previous: Products + // Next: About + new NavigationStart(8, '/home', 'popstate', { navigationId: 6 }), + new NavigationEnd(8, '/home', '/home'), + + // 9. Click the forward button + // Current: About + // Previous: Home + // Next: None + new NavigationStart(9, '/about', 'popstate', { navigationId: 7 }), + new NavigationEnd(9, '/about', '/about'), + + // 10. Click the back button + // Current: Home + // Previous: Products + // Next: About + new NavigationStart(10, '/home', 'popstate', { navigationId: 8 }), + new NavigationEnd(10, '/home', '/home'), +]; diff --git a/packages/router-component-store/src/lib/router-history-store/router-history.store.spec.ts b/packages/router-component-store/src/lib/router-history-store/router-history.store.spec.ts index cd4d8d1..ea8fcd1 100644 --- a/packages/router-component-store/src/lib/router-history-store/router-history.store.spec.ts +++ b/packages/router-component-store/src/lib/router-history-store/router-history.store.spec.ts @@ -30,6 +30,14 @@ function createTestComponent(name: string, selector: string) { >< Back + > Next + Home About Company @@ -45,6 +53,11 @@ class TestAppComponent { event.preventDefault(); this.routerHistory.onNavigateBack(); } + + onNext(event: MouseEvent) { + event.preventDefault(); + this.routerHistory.onNavigateForward(); + } } describe(RouterHistoryStore.name, () => { @@ -98,55 +111,176 @@ describe(RouterHistoryStore.name, () => { } it('the URLs behave like the History API when navigating using links', async () => { - expect.assertions(2); + expect.assertions(3); const { click, routerHistory } = await setup(); // At Home + // Previous: None + // Next: None await click('#about-link'); // At About + // Previous: Home + // Next: None await click('#company-link'); // At Company + // Previous: About + // Next: None await click('#products-link'); // At Products + // Previous: Company + // Next: None expect(await firstValueFrom(routerHistory.currentUrl$)).toBe('/products'); expect(await firstValueFrom(routerHistory.previousUrl$)).toBe('/company'); + expect(await firstValueFrom(routerHistory.nextUrl$)).toBe(undefined); }); it('the URLs behave like the History API when navigating back', async () => { - expect.assertions(2); + expect.assertions(3); + + const { click, routerHistory } = await setup(); + + // At Home + // Previous: None + // Next: None + await click('#about-link'); + // At About + // Previous: Home + // Next: None + await click('#company-link'); + // At Company + // Previous: About + // Next: None + await click('#back-link'); + // At About + // Previous: Home + // Next: Company + + expect(await firstValueFrom(routerHistory.currentUrl$)).toBe('/about'); + expect(await firstValueFrom(routerHistory.previousUrl$)).toBe('/home'); + expect(await firstValueFrom(routerHistory.nextUrl$)).toBe('/company'); + }); + + it('the URLs behave like the History API when navigating back twice', async () => { + expect.assertions(3); + + const { click, routerHistory } = await setup(); + + // At Home + // Previous: None + // Next: None + await click('#about-link'); + // At About + // Previous: Home + // Next: None + await click('#company-link'); + // At Company + // Previous: About + // Next: None + await click('#back-link'); + // At About + // Previous: Home + // Next: Company + await click('#back-link'); + // At Home + // Previous: None + // Next: About + + expect(await firstValueFrom(routerHistory.currentUrl$)).toBe('/home'); + expect(await firstValueFrom(routerHistory.previousUrl$)).toBe(undefined); + expect(await firstValueFrom(routerHistory.nextUrl$)).toBe('/about'); + }); + + it('the URLs behave like the History API when navigating back twice then forward', async () => { + expect.assertions(3); const { click, routerHistory } = await setup(); // At Home + // Previous: None + // Next: None await click('#about-link'); // At About + // Previous: Home + // Next: None await click('#company-link'); // At Company + // Previous: About + // Next: None await click('#back-link'); // At About + // Previous: Home + // Next: Company + await click('#back-link'); + // At Home + // Previous: None + // Next: About + await click('#forward-link'); + // At About + // Previous: Home + // Next: Company expect(await firstValueFrom(routerHistory.currentUrl$)).toBe('/about'); expect(await firstValueFrom(routerHistory.previousUrl$)).toBe('/home'); + expect(await firstValueFrom(routerHistory.nextUrl$)).toBe('/company'); }); it('the URLs behave like the History API when navigating back then using links', async () => { - expect.assertions(2); + expect.assertions(3); const { click, routerHistory } = await setup(); // At Home + // Previous: None + // Next: None await click('#about-link'); // At About + // Previous: Home + // Next: None await click('#company-link'); // At Company + // Previous: About + // Next: None await click('#back-link'); // At About + // Previous: Home + // Next: Company await click('#products-link'); // At Products + // Previous: About + // Next: None expect(await firstValueFrom(routerHistory.currentUrl$)).toBe('/products'); expect(await firstValueFrom(routerHistory.previousUrl$)).toBe('/about'); + expect(await firstValueFrom(routerHistory.nextUrl$)).toBe(undefined); + }); + + it('the URLs behave like the History API when navigating back then forward', async () => { + expect.assertions(3); + + const { click, routerHistory } = await setup(); + + // At Home + await click('#about-link'); + // At About + // Previous: Home + // Next: None + await click('#company-link'); + // At Company + // Previous: About + // Next: None + await click('#back-link'); + // At About + // Previous: Home + // Next: Company + await click('#forward-link'); + // At Company + // Previous: About + // Next: None + + expect(await firstValueFrom(routerHistory.currentUrl$)).toBe('/company'); + expect(await firstValueFrom(routerHistory.previousUrl$)).toBe('/about'); + expect(await firstValueFrom(routerHistory.nextUrl$)).toBe(undefined); }); }); diff --git a/packages/router-component-store/src/lib/router-history-store/router-history.store.ts b/packages/router-component-store/src/lib/router-history-store/router-history.store.ts index 78499df..adb89fa 100644 --- a/packages/router-component-store/src/lib/router-history-store/router-history.store.ts +++ b/packages/router-component-store/src/lib/router-history-store/router-history.store.ts @@ -127,6 +127,38 @@ export class RouterHistoryStore extends ComponentStore { this.#latestRouterNavigatedSequence$, ([, navigationEnd]) => navigationEnd.urlAfterRedirects ); + /** + * The next URL when taking `popstate` events into account. + * + * `undefined` is emitted when the current navigation is the last in the + * navigation history. + */ + nextUrl$: Observable = this.select( + this.#history$, + this.#maxNavigatedId$, + (history, maxNavigatedId) => { + if (maxNavigatedId === 1) { + return undefined; + } + + const [sourceNavigationStart] = this.#findSourceNavigatedSequence( + maxNavigatedId, + history + ); + + if (sourceNavigationStart.id === maxNavigatedId) { + return undefined; + } + + const nextNavigationId = sourceNavigationStart.id + 1; + const [, nextNavigationEnd] = this.#findSourceNavigatedSequence( + nextNavigationId, + history + ); + + return nextNavigationEnd.urlAfterRedirects; + } + ); /** * The previous URL when taking `popstate` events into account. *