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

Parse and stringify location #4

Merged
merged 3 commits into from
Aug 18, 2024
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
4 changes: 4 additions & 0 deletions src/main/Outlet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ChildSlotControllerContext, Slot } from './Slot';

/**
* Props of the {@link Outlet} component.
*
* @group Components
*/
export interface OutletProps {
/**
Expand All @@ -13,6 +15,8 @@ export interface OutletProps {

/**
* Renders a route provided by an enclosing {@link Router}.
*
* @group Components
*/
export function Outlet(props: OutletProps): ReactNode {
const controller = useContext(ChildSlotControllerContext);
Expand Down
4 changes: 4 additions & 0 deletions src/main/PathnameTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Dict } from './types';

/**
* A result returned by {@link PathnameTemplate.match} on a successful pathname match.
*
* @group Routing
*/
export interface PathnameMatch {
/**
Expand All @@ -22,6 +24,8 @@ export interface PathnameMatch {

/**
* A template of a pathname pattern.
*
* @group Routing
*/
export class PathnameTemplate {
/**
Expand Down
1 change: 1 addition & 0 deletions src/main/Route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type PartialToVoid<T> = Partial<T> extends T ? T | void : T;
* @template Params Route params.
* @template Data Data loaded by a route.
* @template Context A context required by a data loader.
* @group Routing
*/
export class Route<
Parent extends Route<any, any, Context> | null = any,
Expand Down
4 changes: 4 additions & 0 deletions src/main/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { InternalRouter } from './InternalRouter';
* Props of the {@link Router} component.
*
* @template Context A context provided by a {@link Router} for a {@link RouteOptions.loader}.
* @group Components
*/
export interface RouterProps<Context> {
/**
Expand Down Expand Up @@ -92,13 +93,16 @@ export interface RouterProps<Context> {

/**
* A router that renders a route that matches the provided location.
*
* @group Components
*/
export function Router(props: Omit<RouterProps<void>, 'context'>): ReactElement;

/**
* A router that renders a route that matches the provided location.
*
* @template Context A context provided by a {@link Router} for a {@link RouteOptions.loader}.
* @group Components
*/
export function Router<Context>(props: RouterProps<Context>): ReactElement;

Expand Down
2 changes: 2 additions & 0 deletions src/main/createNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { toLocation } from './utils';

/**
* Provides components a way to trigger router navigation.
*
* @group Routing
*/
export interface Navigation {
/**
Expand Down
4 changes: 4 additions & 0 deletions src/main/createRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { RouteOptions } from './types';
* @template Params Route params.
* @template Data Data loaded by a route.
* @template Context A context provided by a {@link Router} for a {@link RouteOptions.loader}.
* @group Routing
*/
export function createRoute<Params extends object | void = object | void, Data = void, Context = any>(
options?: RouteOptions<Params, Data, Context>
Expand All @@ -23,6 +24,7 @@ export function createRoute<Params extends object | void = object | void, Data =
* @template Params Route params.
* @template Data Data loaded by a route.
* @template Context A context provided by a {@link Router} for a {@link RouteOptions.loader}.
* @group Routing
*/
export function createRoute<Parent extends Route, Params extends object | void = object | void, Data = void>(
parent: Parent,
Expand All @@ -35,6 +37,7 @@ export function createRoute<Parent extends Route, Params extends object | void =
* @param pathname A URL {@link RouteOptions.pathname pathname} pattern.
* @param component A component that is rendered by a route.
* @template Params Route params.
* @group Routing
*/
export function createRoute<Params extends object | void = object | void>(
pathname: string,
Expand All @@ -49,6 +52,7 @@ export function createRoute<Params extends object | void = object | void>(
* @param component A component that is rendered by a route.
* @template Parent A parent route.
* @template Params Route params.
* @group Routing
*/
export function createRoute<Parent extends Route, Params extends object | void = object | void>(
parent: Parent,
Expand Down
4 changes: 4 additions & 0 deletions src/main/history/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { HistoryContext } from './useHistory';

/**
* Props of the {@link Link} component.
*
* @group Components
*/
export interface LinkProps extends Omit<HTMLAttributes<HTMLAnchorElement>, 'href'> {
/**
Expand Down Expand Up @@ -33,6 +35,8 @@ export interface LinkProps extends Omit<HTMLAttributes<HTMLAnchorElement>, 'href
*
* If there's no enclosing {@link HistoryProvider} the {@link Link} component renders `#`
* as an {@link !HTMLAnchorElement.href a.href}.
*
* @group Components
*/
export const Link = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
const { to, prefetch, replace, onClick, ...anchorProps } = props;
Expand Down
25 changes: 15 additions & 10 deletions src/main/history/createBrowserHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,51 @@ import { Location } from '../types';
import { toLocation } from '../utils';
import { History, HistoryOptions } from './types';
import { urlSearchParamsAdapter } from './urlSearchParamsAdapter';
import { parseURL, toURL } from './utils';
import { debasePathname, parseLocation, rebasePathname, stringifyLocation } from './utils';

/**
* Create the history adapter that reads and writes location to a browser's session history.
*
* @param options History options.
* @group History
*/
export function createBrowserHistory(options: HistoryOptions = {}): History {
const { base, searchParamsAdapter = urlSearchParamsAdapter } = options;
const { basePathname, searchParamsAdapter = urlSearchParamsAdapter } = options;
const pubSub = new PubSub();
const handlePopstate = () => pubSub.publish();
const baseURL = base === undefined ? undefined : new URL(base);

let prevHref: string;
let location: Location;

return {
get location() {
const { href } = window.location;
const { pathname, search, hash } = window.location;
const href = pathname + search + hash;

if (prevHref !== href) {
prevHref = href;
location = parseURL(href, searchParamsAdapter, baseURL);
location = parseLocation(debasePathname(basePathname, href), searchParamsAdapter);
}
return location;
},

toURL(to) {
return toURL(toLocation(to), searchParamsAdapter, baseURL);
return rebasePathname(basePathname, typeof to === 'string' ? to : stringifyLocation(to, searchParamsAdapter));
},

push(to) {
location = toLocation(to);
window.history.pushState(location.state, '', toURL(location, searchParamsAdapter, baseURL));
location = typeof to === 'string' ? parseLocation(to, searchParamsAdapter) : toLocation(to);

to = rebasePathname(basePathname, stringifyLocation(location, searchParamsAdapter));
window.history.pushState(location.state, '', to);
pubSub.publish();
},

replace(to) {
location = toLocation(to);
window.history.replaceState(location.state, '', toURL(location, searchParamsAdapter, baseURL));
location = typeof to === 'string' ? parseLocation(to, searchParamsAdapter) : toLocation(to);

to = rebasePathname(basePathname, stringifyLocation(location, searchParamsAdapter));
window.history.replaceState(location.state, '', to);
pubSub.publish();
},

Expand Down
27 changes: 16 additions & 11 deletions src/main/history/createHashHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import { Location } from '../types';
import { toLocation } from '../utils';
import { History, HistoryOptions } from './types';
import { urlSearchParamsAdapter } from './urlSearchParamsAdapter';
import { parseURL, toURL } from './utils';
import { parseLocation, rebasePathname, stringifyLocation } from './utils';

/**
* Create the history adapter that reads and writes location to a browser's session history using only URL hash.
*
* @param options History options.
* @group History
*/
export function createHashHistory(options: HistoryOptions = {}): History {
const { base, searchParamsAdapter = urlSearchParamsAdapter } = options;
const { basePathname, searchParamsAdapter = urlSearchParamsAdapter } = options;
const pubSub = new PubSub();
const handlePopstate = () => pubSub.publish();
const baseURL = base === undefined ? undefined : new URL(base);

let prevHref: string;
let location: Location;
Expand All @@ -25,26 +25,31 @@ export function createHashHistory(options: HistoryOptions = {}): History {

if (prevHref !== href) {
prevHref = href;
location = parseURL(href, searchParamsAdapter);
location = parseLocation(href, searchParamsAdapter);
}
return location;
},

toURL(to) {
const url = '#' + encodeURIComponent(toURL(toLocation(to), searchParamsAdapter));

return baseURL === undefined ? url : new URL(url, baseURL).toString();
return rebasePathname(
basePathname,
'#' + encodeURIComponent(typeof to === 'string' ? to : stringifyLocation(to, searchParamsAdapter))
);
},

push(to) {
location = toLocation(to);
window.history.pushState(location.state, '', '#' + encodeURIComponent(toURL(location, searchParamsAdapter)));
location = typeof to === 'string' ? parseLocation(to, searchParamsAdapter) : toLocation(to);

to = stringifyLocation(location, searchParamsAdapter);
window.history.pushState(location.state, '', '#' + encodeURIComponent(to));
pubSub.publish();
},

replace(to) {
location = toLocation(to);
window.history.replaceState(location.state, '', '#' + encodeURIComponent(toURL(location, searchParamsAdapter)));
location = typeof to === 'string' ? parseLocation(to, searchParamsAdapter) : toLocation(to);

to = stringifyLocation(location, searchParamsAdapter);
window.history.replaceState(location.state, '', '#' + encodeURIComponent(to));
pubSub.publish();
},

Expand Down
17 changes: 11 additions & 6 deletions src/main/history/createMemoryHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Location } from '../types';
import { toLocation } from '../utils';
import { History, HistoryOptions } from './types';
import { urlSearchParamsAdapter } from './urlSearchParamsAdapter';
import { toURL } from './utils';
import { parseLocation, rebasePathname, stringifyLocation } from './utils';

/**
* Options of {@link createMemoryHistory}.
*
* @group History
*/
export interface MemoryHistoryOptions extends HistoryOptions {
/**
Expand All @@ -19,12 +21,12 @@ export interface MemoryHistoryOptions extends HistoryOptions {
* Create the history adapter that reads and writes location to an in-memory stack.
*
* @param options History options.
* @group History
*/
export function createMemoryHistory(options: MemoryHistoryOptions): History {
const { base, searchParamsAdapter = urlSearchParamsAdapter } = options;
const { basePathname, searchParamsAdapter = urlSearchParamsAdapter } = options;
const pubSub = new PubSub();
const entries = options.initialEntries.slice(0);
const baseURL = base === undefined ? undefined : new URL(base);

if (entries.length === 0) {
throw new Error('Expected at least one initial entry');
Expand All @@ -38,17 +40,20 @@ export function createMemoryHistory(options: MemoryHistoryOptions): History {
},

toURL(to) {
return toURL(toLocation(to), searchParamsAdapter, baseURL);
return rebasePathname(basePathname, typeof to === 'string' ? to : stringifyLocation(to, searchParamsAdapter));
},

push(to) {
cursor++;
entries.splice(cursor, entries.length, toLocation(to));

to = typeof to === 'string' ? parseLocation(to, searchParamsAdapter) : toLocation(to);
entries.splice(cursor, entries.length, to);
pubSub.publish();
},

replace(to) {
entries.splice(cursor, entries.length, toLocation(to));
to = typeof to === 'string' ? parseLocation(to, searchParamsAdapter) : toLocation(to);
entries.splice(cursor, entries.length, to);
pubSub.publish();
},

Expand Down
32 changes: 26 additions & 6 deletions src/main/history/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Dict, Location, To } from '../types';

/**
* @group History
*/
export interface HistoryOptions {
/**
* A base URL.
* A base pathname.
*/
base?: URL | string;
basePathname?: string;

/**
* An adapter that extracts params from a URL search string and stringifies them back. By default, an adapter that
Expand All @@ -15,6 +18,8 @@ export interface HistoryOptions {

/**
* A history abstraction.
*
* @group History
*/
export interface History {
/**
Expand All @@ -23,25 +28,38 @@ export interface History {
readonly location: Location;

/**
* Creates a URL for a given location.
* Creates a pathname-search-hash string for a given location.
*
* If history was initialized with a {@link HistoryOptions.basePathname basePathname} then it is prepended to the
* returned URL.
*
* @param to A location to create a URL for.
*/
toURL(to: To): string;
toURL(to: To | string): string;

/**
* Adds an entry to the history stack.
*
* @param to A location to navigate to.
* @example
* const userRoute = createRoute('/users/:userId');
* history.push(userRoute.getLocation({ userId: 42 }));
* // or
* history.push('/users/42');
*/
push(to: To): void;
push(to: To | string): void;

/**
* Modifies the current history entry, replacing it with the state object and URL passed in the method parameters.
*
* @param to A location to navigate to.
* @example
* const userRoute = createRoute('/users/:userId');
* history.replace(userRoute.getLocation({ userId: 42 }));
* // or
* history.replace('/users/42');
*/
replace(to: To): void;
replace(to: To | string): void;

/**
* Move back to the previous history entry.
Expand All @@ -59,6 +77,8 @@ export interface History {

/**
* Extracts params from a URL search string and stringifies them back.
*
* @group History
*/
export interface SearchParamsAdapter {
/**
Expand Down
2 changes: 2 additions & 0 deletions src/main/history/urlSearchParamsAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { SearchParamsAdapter } from './types';

/**
* Parses URL search params using {@link !URLSearchParams}.
*
* @group History
*/
export const urlSearchParamsAdapter: SearchParamsAdapter = {
parse(search) {
Expand Down
Loading