From 413e3619f316ecb048ff341fb99c1351fda82086 Mon Sep 17 00:00:00 2001 From: Issa Date: Fri, 27 Mar 2020 12:21:17 +0000 Subject: [PATCH 1/3] Add interpreter options Add interpreter options to the `useMachine` and `interpret` functions --- src/index.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 6640ae1..bf6c2f6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { pathToRegexp } from 'path-to-regexp' import { useMachine } from '@xstate/react' -import { Machine, matchesState, StateSchema, EventObject, MachineConfig, MachineOptions, StateMachine, interpret } from 'xstate' +import { Machine, matchesState, StateSchema, EventObject, MachineConfig, MachineOptions, StateMachine, InterpreterOptions, interpret } from 'xstate' import { getStateNodes } from '@xstate/graph' import { assign } from 'xstate/lib/actions' import { createBrowserHistory } from 'history' @@ -136,9 +136,11 @@ export function routerMachine< options = ({} as MachineOptions), initialContext = {}, history = createBrowserHistory(), -}: RouterArgs) { +}: RouterArgs, + interpreterOptions?: Partial +) { const machine = createRouterMachine({config, options, initialContext, history}) - const service = interpret(machine) + const service = interpret(machine, interpreterOptions) service.start() handleTransitionEvents(service, history, getRoutes(config)) @@ -156,9 +158,10 @@ export function useRouterMachine options = ({} as MachineOptions), initialContext = {}, history = createBrowserHistory(), -}: RouterArgs) { +}: RouterArgs, + interpreterOptions?: Partial) { const machine = createRouterMachine({config, options, initialContext, history}) - const [state, send, service] = useMachine(machine); + const [state, send, service] = useMachine(machine, interpreterOptions); useEffect(() => { handleTransitionEvents(service, history, getRoutes(config)) From 281af1fd5f3760d9c08735b99c20614076ec840d Mon Sep 17 00:00:00 2001 From: Issa Shabo Date: Fri, 27 Mar 2020 12:22:28 +0000 Subject: [PATCH 2/3] add lib --- .gitignore | 1 - lib/index.d.ts | 23 ++++++ lib/index.js | 196 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 lib/index.d.ts create mode 100644 lib/index.js diff --git a/.gitignore b/.gitignore index dbd96b9..1556439 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ node_modules/ -lib/ .DS_Store diff --git a/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..a51f5d4 --- /dev/null +++ b/lib/index.d.ts @@ -0,0 +1,23 @@ +import { StateSchema, EventObject, MachineConfig, MachineOptions, StateMachine, InterpreterOptions } from 'xstate'; +export declare function matchURI(path: any, uri: any): any; +export declare function buildURI(path: string, match: any): string; +export declare function resolve(routes: any, location: any, handleError?: boolean): any; +export declare const routerEvent = "route-changed"; +export declare function getRoutes(config: any): any; +export declare function addRouterEvents(history: any, configObj: any, routes: any): any; +interface RouterArgs { + config: MachineConfig; + options: MachineOptions; + initialContext: TContext; + history?: any; +} +export declare function createRouterMachine({ config, options, initialContext, history, }: RouterArgs): StateMachine; +export declare function routerMachine({ config, options, initialContext, history, }: RouterArgs, interpreterOptions?: Partial): import("xstate").Interpreter; +export declare function useRouterMachine({ config, options, initialContext, history, }: RouterArgs, interpreterOptions?: Partial): { + state: import("xstate").State; + send: (event: any, payload?: import("xstate").EventData | undefined) => import("xstate").State; + service: import("xstate").Interpreter; +}; +export declare function handleTransitionEvents(service: any, history: any, routes: any): void; +export declare function findPathRecursive(stateNode: any): any; +export {}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..e61b2f5 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,196 @@ +"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; +} From ac440d07afa1ca41d133215b9edd2e6aa66bd640 Mon Sep 17 00:00:00 2001 From: Issa Shabo Date: Fri, 27 Mar 2020 12:41:18 +0000 Subject: [PATCH 3/3] remove lib --- .gitignore | 1 + lib/index.d.ts | 23 ------ lib/index.js | 196 ------------------------------------------------- package.json | 2 +- 4 files changed, 2 insertions(+), 220 deletions(-) delete mode 100644 lib/index.d.ts delete mode 100644 lib/index.js diff --git a/.gitignore b/.gitignore index 1556439..dbd96b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ +lib/ .DS_Store diff --git a/lib/index.d.ts b/lib/index.d.ts deleted file mode 100644 index a51f5d4..0000000 --- a/lib/index.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { StateSchema, EventObject, MachineConfig, MachineOptions, StateMachine, InterpreterOptions } from 'xstate'; -export declare function matchURI(path: any, uri: any): any; -export declare function buildURI(path: string, match: any): string; -export declare function resolve(routes: any, location: any, handleError?: boolean): any; -export declare const routerEvent = "route-changed"; -export declare function getRoutes(config: any): any; -export declare function addRouterEvents(history: any, configObj: any, routes: any): any; -interface RouterArgs { - config: MachineConfig; - options: MachineOptions; - initialContext: TContext; - history?: any; -} -export declare function createRouterMachine({ config, options, initialContext, history, }: RouterArgs): StateMachine; -export declare function routerMachine({ config, options, initialContext, history, }: RouterArgs, interpreterOptions?: Partial): import("xstate").Interpreter; -export declare function useRouterMachine({ config, options, initialContext, history, }: RouterArgs, interpreterOptions?: Partial): { - state: import("xstate").State; - send: (event: any, payload?: import("xstate").EventData | undefined) => import("xstate").State; - service: import("xstate").Interpreter; -}; -export declare function handleTransitionEvents(service: any, history: any, routes: any): void; -export declare function findPathRecursive(stateNode: any): any; -export {}; diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index e61b2f5..0000000 --- a/lib/index.js +++ /dev/null @@ -1,196 +0,0 @@ -"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; -} diff --git a/package.json b/package.json index 07fb7db..56e711a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xstate-router", - "version": "0.4.2", + "version": "0.4.3", "description": "XState Router. Add routes to your XState machine.", "main": "lib/index.js", "files": [