UNPKG

xstate-router

Version:

XState Router. Add routes to your XState machine.

197 lines (196 loc) 7.58 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const react_1 = require("react"); const path_to_regexp_1 = require("path-to-regexp"); const react_2 = require("@xstate/react"); const xstate_1 = require("xstate"); const graph_1 = require("@xstate/graph"); const actions_1 = require("xstate/lib/actions"); const history_1 = require("history"); function matchURI(path, uri) { if (path === undefined) { return {}; } const keys = []; const pattern = path_to_regexp_1.pathToRegexp(path, keys); const match = pattern.exec(uri); if (!match) return null; const params = Object.create(null); for (let i = 1; i < match.length; i++) { params[keys[i - 1].name] = match[i] !== undefined ? match[i] : undefined; } return params; } exports.matchURI = matchURI; function buildURI(path, match) { const keys = []; const pattern = path_to_regexp_1.pathToRegexp(path, keys); // TODO: Use caching const regexp = pattern.exec(path); if (!regexp) return path; let result = ''; var lastIndex = 0; for (let i = 1; i < regexp.length; i++) { const param = regexp[i]; // e.g. :whatever const paramName = param.substr(1); // e.g. whatever const pos = path.indexOf(param, lastIndex); result += path.substring(lastIndex, pos) + match[paramName]; lastIndex = pos + param.length; } result += path.substr(lastIndex); return result; } exports.buildURI = buildURI; function resolve(routes, location, handleError) { for (const route of routes) { const uri = location.pathname; const params = matchURI(route[1], uri); if (params) return params; } if (!handleError) { return resolve(routes, location, true); } } exports.resolve = resolve; exports.routerEvent = 'route-changed'; function getRoutes(config) { const nodes = graph_1.getStateNodes(xstate_1.Machine(config)); const routes = []; for (const node of nodes) { if (node.meta && node.meta.path) { routes.push([node.path, node.meta.path]); } } return routes; } exports.getRoutes = getRoutes; function addRouterEvents(history, configObj, routes) { const config = Object.assign({}, configObj); if (!config.on) { config.on = {}; } else { config.on = Object.assign({}, config.on); } const given = exports.routerEvent in config.on ? config.on[exports.routerEvent] : []; const on = given instanceof Array ? given : [given]; on.push({ cond: (context, event) => event.dueToStateTransition, actions: actions_1.assign(() => ({ location: history.location, match: resolve(routes, history.location) })) }); for (const route of routes) { on.push({ target: '#(machine).' + route[0].join('.'), cond: (context, event) => event.dueToStateTransition === false && event.route && event.route === route[1], actions: actions_1.assign(() => ({ location: history.location, match: matchURI(route[1], history.location.pathname) })) }); } config.on[exports.routerEvent] = on; return config; } exports.addRouterEvents = addRouterEvents; function createRouterMachine({ config, options = {}, initialContext = {}, history = history_1.createBrowserHistory(), }) { const routes = getRoutes(config); const enhancedConfig = addRouterEvents(history, config, routes); const currentLocation = history.location; const enhancedContext = Object.assign(Object.assign({}, initialContext), { match: resolve(routes, currentLocation), location: currentLocation, history }); return xstate_1.Machine(enhancedConfig, options, enhancedContext); } exports.createRouterMachine = createRouterMachine; function routerMachine({ config, options = {}, initialContext = {}, history = history_1.createBrowserHistory(), }, interpreterOptions) { const machine = createRouterMachine({ config, options, initialContext, history }); const service = xstate_1.interpret(machine, interpreterOptions); service.start(); handleTransitionEvents(service, history, getRoutes(config)); return service; } exports.routerMachine = routerMachine; function useRouterMachine({ config, options = {}, initialContext = {}, history = history_1.createBrowserHistory(), }, interpreterOptions) { const machine = createRouterMachine({ config, options, initialContext, history }); const [state, send, service] = react_2.useMachine(machine, interpreterOptions); react_1.useEffect(() => { handleTransitionEvents(service, history, getRoutes(config)); }, []); return { state, send, service }; } exports.useRouterMachine = useRouterMachine; function handleTransitionEvents(service, history, routes) { let debounceHistoryFlag = false; let debounceState = false; handleRouterTransition(history.location); service.onTransition(state => { const stateNode = getCurrentStateNode(service, state); const path = findPathRecursive(stateNode); if (debounceState // debounce only if no target for event was given e.g. in case of // fetching 'route-changed' events by the user && debounceState[1] === path) { debounceState = false; return; } if (!matchURI(path, history.location.pathname)) { debounceHistoryFlag = true; const uri = buildURI(path, state.context.match); history.push(uri); service.send({ type: exports.routerEvent, dueToStateTransition: true, route: path, service: service }); } }); history.listen(location => { if (!service.initialized) { service.start(); } if (debounceHistoryFlag) { debounceHistoryFlag = false; return; } handleRouterTransition(location, true); }); function handleRouterTransition(location, debounceHistory) { let matchingRoute; for (const route of routes) { const params = matchURI(route[1], location.pathname); if (params) { matchingRoute = route; break; } } if (matchingRoute) { debounceState = matchingRoute[1]; // debounce only for this route service.send({ type: exports.routerEvent, dueToStateTransition: false, route: matchingRoute[1], service: service }); const state = service.state.value; if (!xstate_1.matchesState(state, matchingRoute[0].join('.'))) { const stateNode = getCurrentStateNode(service, service.state); if (stateNode.meta && stateNode.meta.path) { if (debounceHistory) { debounceHistoryFlag = true; } history.replace(stateNode.meta.path); } } } } } exports.handleTransitionEvents = handleTransitionEvents; function findPathRecursive(stateNode) { let actual = stateNode; while (actual.parent) { if (actual.meta && actual.meta.path) { return actual.meta.path; } actual = actual.parent; } } exports.findPathRecursive = findPathRecursive; function getCurrentStateNode(service, state) { const strings = state.toStrings(); const stateNode = service.machine.getStateNodeByPath(strings[strings.length - 1]); return stateNode; }