import * as React from "react"; import { ReactNode } from "react"; import { Route, RouteProps } from "react-router"; import { url } from "../strings"; interface RouteOptions extends Omit { layer?: string; /** * Generate a key to be added to the Component. * - `match_path`: Use the match path as the key - the Component will be recreated whenever the matched segment of the path changes. */ keyBy?: 'match_path' | ((props: any) => string); } interface RenderInfo { element: ReactNode; always_render?: boolean; } class RouteEntry { constructor(path: string, options: RouteOptions, parent?: RouteEntry) { this.relativePath = path; this.options = options; this.parent = parent; if (parent) parent.addEntry(this) } readonly relativePath: string; readonly options: RouteOptions; readonly parent: RouteEntry; private subEntries: RouteEntry[] = [] private addEntry(entry: RouteEntry) { this.subEntries.push(entry) } get layer() { return this.options.layer || (this.parent && this.parent.layer) || 'default'; } private _path: string; get path() { if (!this._path) { this._path = url`${this.parent ? this.parent.path : ''}/${this.relativePath}`; } return this._path; } get hasComponent() { return !!(this.options.children || this.options.component || this.options.render); } protected getAllLayers(layerSet = new Set()) { layerSet.add('default'); if (this.options.layer) layerSet.add(this.options.layer) for (let entry of this.subEntries) { entry.getAllLayers(layerSet); } return layerSet; } protected internalRenderLayer(target_layer: string): RenderInfo { const info: RenderInfo = { element: null, always_render: false, } if (typeof this.options.children == 'function') info.always_render = true; const { layer, keyBy, ...route_props } = this.options; const computeKey = (props) => { let key = null; if (keyBy == 'match_path') { key = props.match.url; if (!key.endsWith('/')) key += '/'; } else if (typeof keyBy == 'function') { key = keyBy(props); } return key; } if (keyBy) { if (route_props.component) { const Component = route_props.component; route_props.component = (props) => { return } } if (route_props.children) { if (typeof route_props.children != 'function') { const { children } = route_props; route_props.children = null; route_props.render = (props) => children; } else { throw new Error("Cannot use keyBy with children function"); } } if (route_props.render) { const { render } = route_props; route_props.render = (props) => { return {render(props)} } } } if (this.subEntries.length == 0) { if (this.layer != target_layer) return null; info.element = } else { const subItems: ReactNode[] = []; for (let sub of this.subEntries) { const subData = sub.internalRenderLayer(target_layer); if (subData == null) continue; if (subData.always_render) info.always_render = true; subItems.push(subData.element); } if (this.hasComponent && this.layer == target_layer) { subItems.push() } if (subItems.length == 0) return null; const childValue = info.always_render ? () => subItems : subItems; info.element = } return info; } renderLayerToRoutes(layer: string = 'default') { const routes = this.internalRenderLayer(layer).element; return routes; } renderRouteLayers(layers: string[]) { const layer_table: Record = {} for (let layer of layers) { layer_table[layer] = this.renderLayerToRoutes(layer); } return layer_table; } renderAllRouteLayers() { return this.renderRouteLayers([...this.getAllLayers()]) } } let route_table: RouteEntry; function normal_route(path: string, options: RouteOptions, block?: () => void) { const parent_table = route_table; const newEntry = new RouteEntry(path, options, parent_table) if (block) { route_table = newEntry; block(); route_table = parent_table; } return newEntry; } /** * Declaratively build React Router Routes. * * Adds the following features: * - If a Route on a branch uses `children` (indicating to React Router that it should _always_ render), * all parent routes will be converted to use `children` as well, so that, regardless of where it is used, * it will always render. Ideal for transitioning Modal, Trays, and such. * - Route "layers". Allows optionally specifying a layer when specifying a route. * This allows routes to be part of the same tree (and thus inherit relative paths), but to be used in different places. * An example of how this can be used for Modals is included below. * * **Example Usage:** * ```javascript * const ROUTES = route('/', {}, () => { * route('sub/', sub_routes) * route('test_page/', { component: TestPage }) * route('test_modal/', { component: TestModal, layer: 'modal' }) * }) * const sub_routes = () => { * route('new/', { component: CreatePage }); * } * ``` * * **Modal Example:** * ```javascript * const NamedLocationContext = createContext>({}) * export function useNamedLocation(name: string) { * const ctx = useContext(NamedLocationContext); * return useObserver(() => ctx[name]); * } * export const NamedLocationRoute: React.SFC = ({ name, ...pass }) => { * const loc = useNamedLocation(name); * return * } * export function ModalSwitch({ children }) { * const location = useLocation(); * const background = location.state && location.state.background; * * const backgroundLocation = background || location; * const namedLocations = useAsObservableSource({ * modal: background ? location : null, * }); * * return ( * * * {children} * * * ); * } * const ROUTE_LAYERS = ROUTES.renderAllRouteLayers(); * const WRAPPED_ROUTES = Object.entries(ROUTE_LAYERS).map(([layer, routes]) => { * if (name == 'default') return routes; * return * }) * const RoutesComponent = () => { * return * } * ``` * * @param path * @param block */ export function route(path: string, block: () => void): RouteEntry; export function route(path: string, options: RouteOptions, block?: () => void): RouteEntry; export function route(a, b, c?) { if (typeof b == 'function') { return normal_route(a, {}, b); } else { return normal_route(a, b, c); } }