Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add RouterHistoryStore #302

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e39929b
feat: add `RouterHistoryStore`
LayZeeDK Dec 23, 2022
ac97fb9
refactor: add notes on `NavigationCancel` and `NavigationError` events
LayZeeDK Dec 24, 2022
0c28854
test: cover `RouterHistoryStore`
LayZeeDK Dec 24, 2022
61db5b1
refactor: refactor `RouterHistoryStore` to only store the navigation …
LayZeeDK Dec 26, 2022
017e222
test: cover `RouterHistoryStore` with more test cases
LayZeeDK Dec 26, 2022
f0b3924
fix: initialize `RouterHistoryStore` on app initialization
LayZeeDK Dec 27, 2022
61e229c
refactor: add notes on `NavigationSkipped`
LayZeeDK Jan 2, 2023
7febcb5
refactor: remove TODO and notes
LayZeeDK Jan 4, 2023
6198488
refactor: rename router sequences
LayZeeDK Jan 4, 2023
70fccc3
docs: explain history key
LayZeeDK Jan 4, 2023
84805a3
refactor: refactor `#maxCompletedNavigationId$`
LayZeeDK Jan 4, 2023
8370281
refactor: use router navigated observable connected to a single updater
LayZeeDK Jan 4, 2023
9c694e4
refactor: extract `PopstateNavigationStart` type and type guard
LayZeeDK Jan 4, 2023
521cde1
fix: handle `NavigationCancel` and `NavigationError`
LayZeeDK Jan 5, 2023
a2f5bac
refactor: improve type annotations and inline documentation
LayZeeDK Jan 5, 2023
fa809ff
refactor: remove unnecessary selector debouncing
LayZeeDK Jan 5, 2023
654119b
refactor: extract router sequence types and type guard
LayZeeDK Jan 5, 2023
16875ea
refactor: rename `RouterHistory`
LayZeeDK Jan 5, 2023
caefc58
refactor: rename `RouterHistoryStore##findSourceNavigatedSequence`
LayZeeDK Jan 5, 2023
da9f557
docs: describe `RouterHistory`
LayZeeDK Jan 5, 2023
de60779
feat: add navigation effects
LayZeeDK Jan 5, 2023
6508ce1
feat: provide `RouterHistoryStore` as `EnvironmentProviders`
LayZeeDK May 22, 2023
23295c3
refactor: rename `RouterHistoryStore#routerEvents` to `RouterHistoryS…
LayZeeDK May 22, 2023
b284f43
refactor: remove Component Store lifecycle support
LayZeeDK May 22, 2023
a3bdc1d
feat: add WIP `RouterHistoryStore#nextUrl$` property
LayZeeDK May 22, 2023
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
Next Next commit
feat: add RouterHistoryStore
  • Loading branch information
LayZeeDK committed Jan 4, 2023
commit e39929b38a0c9fcefa0ff01d55df91dfa9f6440b
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { inject, Injectable, Provider } from '@angular/core';
import {
Navigation,
NavigationEnd,
NavigationStart,
Router,
} from '@angular/router';
import { ComponentStore, provideComponentStore } from '@ngrx/component-store';
import { concatMap, filter, Observable, take } from 'rxjs';

interface RouterHistoryRecord {
readonly id: number;
readonly url: string;
}

interface RouterHistoryState {
readonly currentIndex: number;
readonly event?: NavigationStart | NavigationEnd;
readonly history: readonly RouterHistoryRecord[];
readonly id: number;
readonly idToRestore?: number;
readonly trigger?: Navigation['trigger'];
}

export function provideRouterHistoryStore(): Provider[] {
return [provideComponentStore(RouterHistoryStore)];
}

@Injectable()
export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
#router = inject(Router);

#currentIndex$: Observable<number> = this.select(
(state) => state.currentIndex
);
#history$: Observable<readonly RouterHistoryRecord[]> = this.select(
(state) => state.history
);
#navigationEnd$: Observable<NavigationEnd> = this.#router.events.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd)
);
#navigationStart$: Observable<NavigationStart> = this.#router.events.pipe(
filter(
(event): event is NavigationStart => event instanceof NavigationStart
)
);
#imperativeNavigationEnd$: Observable<NavigationEnd> =
this.#navigationStart$.pipe(
filter((event) => event.navigationTrigger === 'imperative'),
concatMap(() => this.#navigationEnd$.pipe(take(1)))
);
#popstateNavigationEnd$: Observable<NavigationEnd> =
this.#navigationStart$.pipe(
filter((event) => event.navigationTrigger === 'popstate'),
concatMap(() => this.#navigationEnd$.pipe(take(1)))
);

currentUrl$: Observable<string> = this.select(
this.#navigationEnd$.pipe(
concatMap(() =>
this.select(
this.#currentIndex$,
this.#history$,
(currentIndex, history) => [currentIndex, history] as const
)
)
),
([currentIndex, history]) => history[currentIndex].url,
{
debounce: true,
}
);
previousUrl$: Observable<string | null> = this.select(
this.#navigationEnd$.pipe(
concatMap(() =>
this.select(
this.#currentIndex$,
this.#history$,
(currentIndex, history) => [currentIndex, history] as const
)
)
),
([currentIndex, history]) => history[currentIndex - 1]?.url ?? null,
{
debounce: true,
}
);

constructor() {
super(initialState);

this.#updateRouterHistoryOnNavigationStart(this.#navigationStart$);
this.#updateRouterHistoryOnImperativeNavigationEnd(
this.#imperativeNavigationEnd$
);
this.#updateRouterHistoryOnPopstateNavigationEnd(
this.#popstateNavigationEnd$
);
}

/**
* Update router history on imperative navigation end (`Router#navigate`,
* `Router#navigateByUrl`, or `RouterLink` click).
*/
#updateRouterHistoryOnImperativeNavigationEnd = this.updater<NavigationEnd>(
(state, event): RouterHistoryState => {
let currentIndex = state.currentIndex;
let history = state.history;
// remove all events in history that come after the current index
history = [
...history.slice(0, currentIndex + 1),
// add the new event to the end of the history
{
id: state.id,
url: event.urlAfterRedirects,
},
];
// set the new event as our current history index
currentIndex = history.length - 1;

return {
...state,
currentIndex,
event,
history,
};
}
);

#updateRouterHistoryOnNavigationStart = this.updater<NavigationStart>(
(state, event): RouterHistoryState => ({
...state,
id: event.id,
idToRestore: event.restoredState?.navigationId ?? undefined,
event,
trigger: event.navigationTrigger,
})
);

/**
* Update router history on browser navigation end (back, forward, and other
* `popstate` or `pushstate` events).
*/
#updateRouterHistoryOnPopstateNavigationEnd = this.updater<NavigationEnd>(
(state, event): RouterHistoryState => {
let currentIndex = 0;
let { history } = state;
// get the history item that references the idToRestore
const historyIndexToRestore = history.findIndex(
(historyRecord) => historyRecord.id === state.idToRestore
);

// if found, set the current index to that history item and update the id
if (historyIndexToRestore > -1) {
currentIndex = historyIndexToRestore;
history = [
...history.slice(0, historyIndexToRestore),
{
...history[historyIndexToRestore],
id: state.id,
},
...history.slice(historyIndexToRestore + 1),
];
}

return {
...state,
currentIndex,
event,
history,
};
}
);
}

export const initialState: RouterHistoryState = {
currentIndex: 0,
event: undefined,
history: [],
id: 0,
idToRestore: 0,
trigger: undefined,
};