@virtualstate/navigation
Version:
Native JavaScript [navigation](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API) implementation
131 lines (107 loc) • 4.12 kB
text/typescript
import {NavigateEvent, Navigation, NavigationHistoryEntry} from "../spec/navigation";
import {getNavigation} from "../get-navigation";
import {Push} from "@virtualstate/promise";
import {Event} from "../event-target";
import {isPromise, ok} from "../is";
export function setState<S>(state: S, navigation: Navigation<S> = getNavigation()): unknown {
const currentEntryChangePromise = new Promise(
resolve => {
navigation.addEventListener(
"currententrychange",
resolve,
{ once: true }
)
}
);
const returned = navigation.updateCurrentEntry({
state
});
const promises = [currentEntryChangePromise];
if (isPromise(returned)) {
promises.push(returned);
}
return Promise.all(promises);
}
export function getState<S>(navigation: Navigation<S> = getNavigation()) {
return navigation.currentEntry.getState();
}
const states = new WeakMap<Navigation, AsyncIterable<unknown>>();
export function state<S>(navigation: Navigation<S> = getNavigation()) {
const existing = states.get(navigation);
// Using a stable object for a navigation instance
// allows for this state object to be used in memo'd functions
if (isExisting(existing)) {
return existing;
}
// Returning an object like this allows callers to bind
// a navigation to the stateGenerator function, each time
// a new asyncIterator is created, the transition or currentEntry
// at that point will be used, meaning that this object
// too can be freely used as a static source across
// an application.
const result: AsyncIterable<S> = {
[Symbol.asyncIterator]() {
return stateGenerator(navigation);
}
}
states.set(navigation, result);
return result;
function isExisting(existing: AsyncIterable<unknown>): existing is AsyncIterable<S> {
return !!existing;
}
}
export async function * stateGenerator<S>(navigation: Navigation<S> = getNavigation()): AsyncIterableIterator<S> {
let lastState: S | undefined = undefined,
wasState = false;
let currentEntry: NavigationHistoryEntry<S> | undefined = navigation.currentEntry;
if (navigation.transition) {
currentEntry = await navigation.transition?.finished;
}
const push = new Push<S>();
ok(currentEntry, "Expected a currentEntry");
pushState();
navigation.addEventListener("navigate", onNavigate);
navigation.addEventListener("navigatesuccess", onNavigateSuccess);
navigation.addEventListener("navigateerror", onNavigateError, { once: true });
navigation.addEventListener("currententrychange", pushState);
currentEntry.addEventListener("dispose", close, { once: true });
yield * push;
function pushState() {
if (navigation.currentEntry.id !== currentEntry.id) {
return close();
}
const state = currentEntry.getState()
if (wasState || typeof state !== "undefined") {
if (lastState === state) {
return;
}
push.push(state);
lastState = state;
wasState = true;
}
}
function onNavigate(event: NavigateEvent) {
// Indicate that we have intercepted navigation
// and are using it as a state tracker
event.intercept?.({
handler: () => Promise.resolve()
});
}
function onNavigateSuccess() {
if (navigation.currentEntry.id !== currentEntry.id) {
close();
}
}
function onNavigateError(event: Event) {
const { error } = event;
push.throw(error);
}
function close() {
navigation.removeEventListener("navigate", onNavigate);
navigation.removeEventListener("navigatesuccess", onNavigateSuccess);
navigation.removeEventListener("navigateerror", onNavigateError);
navigation.removeEventListener("currententrychange", pushState);
currentEntry.removeEventListener("dispose", close);
push.close();
}
}