@koordinates/xstate-tree 2.0.10 → 3.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,82 +15,113 @@ While xstate-tree manages your application state, it does not have a mechanism f
15
15
 
16
16
  At Koordinates we use xstate-tree for all new UI development. Our desktop application, built on top of [Kart](https://kartproject.org/) our Geospatial version control system, is built entirely with xstate-tree using GraphQL for global state.
17
17
 
18
- A minimal example of a single machine tree
18
+ A minimal example of a single machine tree ([CodeSandbox](https://codesandbox.io/s/xstate-tree-b0el6e-forked-4i6txh?file=/src/index.tsx)):
19
19
 
20
20
  ```tsx
21
21
  import React from "react";
22
22
  import { createRoot } from "react-dom/client";
23
23
  import { createMachine } from "xstate";
24
24
  import { assign } from "@xstate/immer";
25
- import { buildSelectors, buildActions, buildView, buildXstateTreeMachine, buildRootComponent } from "@koordinates/xstate-tree";
26
-
27
- type Events = { type: "SWITCH_CLICKED" } | { type: "INCREMENT", amount: number };
25
+ import {
26
+ buildSelectors,
27
+ buildActions,
28
+ buildView,
29
+ buildXStateTreeMachine,
30
+ buildRootComponent
31
+ } from "@koordinates/xstate-tree";
32
+
33
+ type Events =
34
+ | { type: "SWITCH_CLICKED" }
35
+ | { type: "INCREMENT"; amount: number };
28
36
  type Context = { incremented: number };
29
37
 
30
38
  // If this tree had more than a single machine the slots to render child machines into would be defined here
31
39
  const slots = [];
32
40
 
33
41
  // A standard xstate machine, nothing extra is needed for xstate-tree
34
- const machine = createMachine<Context, Events>({
35
- id: "root",
36
- initial: "inactive",
37
- context: {
38
- incremented: 0,
39
- },
40
- states: {
41
- inactive: {
42
- on: {
43
- SWITCH_CLICKED: "active",
44
- },
42
+ const machine = createMachine<Context, Events>(
43
+ {
44
+ id: "root",
45
+ initial: "inactive",
46
+ context: {
47
+ incremented: 0
45
48
  },
46
- active: {
47
- on: {
48
- SWITCH_CLICKED: "idle",
49
- INCREMENT: { actions: "increment" },
49
+ states: {
50
+ inactive: {
51
+ on: {
52
+ SWITCH_CLICKED: "active"
53
+ }
50
54
  },
51
- },
55
+ active: {
56
+ on: {
57
+ SWITCH_CLICKED: "inactive",
58
+ INCREMENT: { actions: "increment" }
59
+ }
60
+ }
61
+ }
52
62
  },
53
- }, {
54
- actions: {
55
- increment: assign((context, event) => {
56
- context.incremented += event.amount;
57
- }),
58
- },
59
- });
63
+ {
64
+ actions: {
65
+ increment: assign((context, event) => {
66
+ if (event.type !== "INCREMENT") {
67
+ return;
68
+ }
69
+
70
+ context.incremented += event.amount;
71
+ })
72
+ }
73
+ }
74
+ );
60
75
 
61
76
  // Selectors to transform the machines state into a representation useful for the view
62
77
  const selectors = buildSelectors(machine, (ctx, canHandleEvent) => ({
63
- canIncrement: canHandleEvent({type: "INCREMENT", count: 1 }),
78
+ canIncrement: canHandleEvent({ type: "INCREMENT", amount: 1 }),
64
79
  showSecret: ctx.incremented > 10,
65
- count: ctx.incremented,
80
+ count: ctx.incremented
66
81
  }));
67
82
 
68
83
  // Actions to abstract away the details of sending events to the machine
69
- const actions = buildActions(machine, actions, (send, selectors) => ({
84
+ const actions = buildActions(machine, selectors, (send, selectors) => ({
70
85
  increment(amount: number) {
71
- send({ type: "INCREMENT", amount: selectors.count > 4 ? amount * 2 : amount });
86
+ send({
87
+ type: "INCREMENT",
88
+ amount: selectors.count > 4 ? amount * 2 : amount
89
+ });
72
90
  },
73
91
  switch() {
74
92
  send({ type: "SWITCH_CLICKED" });
75
- },
93
+ }
76
94
  }));
77
95
 
78
96
  // A view to bring it all together
79
97
  // the return value is a plain React view that can be rendered anywhere by passing in the needed props
80
98
  // the view has no knowledge of the machine it's bound to
81
- const view = buildView(machine, actions, selectors, slots, ({ actions, selectors, inState }) => {
82
- return (
83
- <div>
84
- <button onClick={() => actions.switch()}>{inState("active") ? "Deactivate" : "Activate"}</button>
85
- <p>Count: {selectors.count}</p>
86
- <button onClick={() => actions.increment(1)} disabled={!selectors.canIncrement}>Increment</button>
87
- {selectors.showSecret && <p>The secret password is hunter2</p>}
88
- </div>
89
- );
90
- });
99
+ const view = buildView(
100
+ machine,
101
+ selectors,
102
+ actions,
103
+ slots,
104
+ ({ actions, selectors, inState }) => {
105
+ return (
106
+ <div>
107
+ <button onClick={() => actions.switch()}>
108
+ {inState("active") ? "Deactivate" : "Activate"}
109
+ </button>
110
+ <p>Count: {selectors.count}</p>
111
+ <button
112
+ onClick={() => actions.increment(1)}
113
+ disabled={!selectors.canIncrement}
114
+ >
115
+ Increment
116
+ </button>
117
+ {selectors.showSecret && <p>The secret password is hunter2</p>}
118
+ </div>
119
+ );
120
+ }
121
+ );
91
122
 
92
123
  // Stapling the machine, selectors, actions, view, and slots together
93
- const RootMachine = buildXstateTreeMachine(machine, {
124
+ const RootMachine = buildXStateTreeMachine(machine, {
94
125
  selectors,
95
126
  actions,
96
127
  view,
@@ -100,12 +131,13 @@ const RootMachine = buildXstateTreeMachine(machine, {
100
131
  // Build the React host for the tree
101
132
  const XstateTreeRoot = buildRootComponent(RootMachine);
102
133
 
103
-
104
134
  // Rendering it with React
105
135
  const ReactRoot = createRoot(document.getElementById("root"));
106
136
  ReactRoot.render(<XstateTreeRoot />);
107
137
  ```
108
138
 
139
+ A more complicated todomvc [example](https://github.com/koordinates/xstate-tree/tree/master/examples/todomvc)
140
+
109
141
  ## Overview
110
142
 
111
143
  Each machine that forms the tree representing your UI has an associated set of selector, action, view functions, and "slots"
package/lib/builders.js CHANGED
@@ -45,8 +45,8 @@ export function buildSelectors(__machine, selectors) {
45
45
  * - `send` - the interpreters send function, which can be used to send events to the machine
46
46
  * - `selectors` - the output of the selectors function from {@link buildSelectors}
47
47
  *
48
- * The resulting action function has memoization. It will return the same value until the
49
- * selectors reference changes or the send reference changes
48
+ * The resulting action function will only be called once per invocation of a machine.
49
+ * The selectors are passed in as a proxy to always read the latest selector value
50
50
  *
51
51
  * @param machine - The machine to create the actions for
52
52
  * @param selectors - The selectors function
@@ -54,20 +54,7 @@ export function buildSelectors(__machine, selectors) {
54
54
  * @returns The actions function - ready to be passed to {@link buildView}
55
55
  * */
56
56
  export function buildActions(__machine, __selectors, actions) {
57
- let lastSelectorResult = undefined;
58
- let lastCachedResult = undefined;
59
- let lastSendReference = undefined;
60
- return (send, selectors) => {
61
- if (lastSelectorResult === selectors &&
62
- lastCachedResult !== undefined &&
63
- lastSendReference === send) {
64
- return lastCachedResult;
65
- }
66
- lastCachedResult = actions(send, selectors);
67
- lastSelectorResult = selectors;
68
- lastSendReference = send;
69
- return lastCachedResult;
70
- };
57
+ return actions;
71
58
  }
72
59
  /**
73
60
  * @public
@@ -1,6 +1,5 @@
1
1
  import { match, compile } from "path-to-regexp";
2
2
  import { parse, stringify } from "query-string";
3
- import { isNil } from "../../utils";
4
3
  import { joinRoutes } from "../joinRoutes";
5
4
  /**
6
5
  * @public
@@ -19,163 +18,136 @@ export function buildCreateRoute(history, basePath) {
19
18
  });
20
19
  }
21
20
  return {
22
- /**
23
- * Creates a dynamic Route using the supplied options
24
- *
25
- * The return value of dynamicRoute is a function that accepts the routes "dynamic" options
26
- * The argument to dynamicRoute itself is the params/query/meta schemas defining the route
27
- *
28
- * The returned function accepts a singular option object with the following fields
29
- *
30
- * `event`, the string constant for the routes event
31
- * `matches`, a function that is passed a url/query string and determines if the route matches
32
- * if the route is matched it returns the extracted params/query objects
33
- * `reverse`, a function that is passed params/query objects and turns them into a URL
34
- *
35
- * The params and query schemas are ZodSchemas, they both need to be an object (ie Z.object())
36
- */
37
- dynamicRoute: function createDynamicRoute(opts) {
38
- return ({ event, matches, reverse, }) => {
39
- return {
40
- paramsSchema: opts === null || opts === void 0 ? void 0 : opts.params,
41
- querySchema: opts === null || opts === void 0 ? void 0 : opts.query,
42
- event,
43
- history,
44
- basePath,
45
- parent: undefined,
46
- // @ts-ignore the usual
47
- getEvent({ params, query, meta } = {}) {
48
- return { type: event, params, query, meta };
49
- },
50
- // @ts-ignore not sure how to type this
51
- matches(url, search) {
52
- const query = parse(search);
53
- const match = matches(url, query);
21
+ simpleRoute(baseRoute) {
22
+ return ({ url, paramsSchema, querySchema, ...args }) => {
23
+ const matcher = match(url, { end: false });
24
+ const reverser = compile(url);
25
+ return this.route(baseRoute)({
26
+ ...args,
27
+ paramsSchema,
28
+ querySchema,
29
+ // @ts-ignore :cry:
30
+ matcher: (url, query) => {
31
+ const match = matcher(url);
54
32
  if (match === false) {
55
- return undefined;
33
+ return false;
56
34
  }
57
- if ((opts === null || opts === void 0 ? void 0 : opts.params) && "params" in match) {
58
- opts.params.parse(match.params);
35
+ const params = match.params;
36
+ if (params && paramsSchema) {
37
+ paramsSchema.parse(params);
59
38
  }
60
- if ((opts === null || opts === void 0 ? void 0 : opts.query) && "query" in match) {
61
- opts.query.parse(match.query);
39
+ if (query && querySchema) {
40
+ querySchema.parse(query);
62
41
  }
63
- return { type: event, originalUrl: `${url}${search}`, ...match };
64
- },
65
- // @ts-ignore not sure how to type this correctly
66
- // The types from external to this function are correct however
67
- reverse({ params, query } = {}) {
68
- return reverse({ params, query });
42
+ return {
43
+ matchLength: match.path.length,
44
+ params,
45
+ query,
46
+ };
69
47
  },
70
- // @ts-ignore not sure how to type this correctly
71
- // The types from external to this function are correct however
72
- navigate({ params, query, meta } = {}) {
73
- // @ts-ignore same problem
74
- const url = this.reverse({ params, query });
75
- navigate({
76
- url: joinRoutes(this.basePath, url),
77
- meta,
78
- history: this.history,
79
- });
48
+ // @ts-ignore :cry:
49
+ reverser: (args) => {
50
+ const url = reverser(args.params);
51
+ if (args.query) {
52
+ return `${url}?${stringify(args.query)}`;
53
+ }
54
+ return url;
80
55
  },
81
- };
56
+ });
82
57
  };
83
58
  },
84
- /**
85
- * Creates a static Route using the supplied options
86
- *
87
- * The return value of staticRoute is a function that accepts the routes options
88
- * The only argument to staticRoute itself is an optional parent route
89
- *
90
- * The returned function accepts 3 arguments
91
- *
92
- * 1. URL of the route
93
- * 2. The event type of the route
94
- * 3. The routes options, params schema, query schema and meta type
95
- *
96
- * The params and query schemas are ZodSchemas, they both need to be an object (ie Z.object())
97
- *
98
- * When creating a route that has a parent route, the following happens
99
- *
100
- * 1. The parent routes url is prepended to the routes URL
101
- * 2. The parents params schema is merged with the routes schema
102
- * 3. The parents meta type is merged with the routes meta type
103
- */
104
- staticRoute: function createStaticRoute(baseRoute) {
105
- return (url, event, opts) => {
106
- if (baseRoute && isNil(baseRoute.url)) {
107
- throw new Error("Somehow constructing a route with a base route missing a URL, did you pass a dynamic route?");
59
+ route(baseRoute) {
60
+ function getParentArray() {
61
+ const parentRoutes = [];
62
+ let currentParent = baseRoute;
63
+ while (currentParent) {
64
+ parentRoutes.unshift(currentParent);
65
+ currentParent = currentParent.parent;
66
+ }
67
+ return parentRoutes;
68
+ }
69
+ return ({ event, matcher, reverser, paramsSchema, querySchema, }) => {
70
+ let fullParamsSchema = paramsSchema;
71
+ let parentRoute = baseRoute;
72
+ while (fullParamsSchema && parentRoute) {
73
+ if (parentRoute.paramsSchema) {
74
+ fullParamsSchema = fullParamsSchema.merge(parentRoute.paramsSchema);
75
+ }
76
+ parentRoute = parentRoute.parent;
108
77
  }
109
- const urlWithTrailingSlash = url.endsWith("/") ? url : `${url}/`;
110
- const fullUrl = baseRoute
111
- ? joinRoutes(baseRoute.url, urlWithTrailingSlash)
112
- : urlWithTrailingSlash;
113
- const matcher = match(fullUrl, {});
114
- const reverser = compile(fullUrl);
115
- const paramsSchema = (baseRoute === null || baseRoute === void 0 ? void 0 : baseRoute.paramsSchema)
116
- ? (opts === null || opts === void 0 ? void 0 : opts.params)
117
- ? baseRoute.paramsSchema.merge(opts.params)
118
- : baseRoute.paramsSchema
119
- : (opts === null || opts === void 0 ? void 0 : opts.params)
120
- ? opts.params
121
- : undefined;
122
78
  return {
123
- paramsSchema,
124
- querySchema: opts === null || opts === void 0 ? void 0 : opts.query,
79
+ basePath,
125
80
  event,
126
81
  history,
127
- basePath,
128
- url: fullUrl,
82
+ paramsSchema,
83
+ querySchema,
129
84
  parent: baseRoute,
130
- // @ts-ignore the usual
131
- getEvent({ params, query, meta } = {}) {
85
+ matcher: matcher,
86
+ reverser: reverser,
87
+ // @ts-ignore :cry:
88
+ getEvent(args) {
89
+ const { params, query, meta } = args !== null && args !== void 0 ? args : {};
132
90
  return { type: event, params, query, meta };
133
91
  },
134
- // @ts-ignore not sure how to type this
135
- matches(url, search) {
136
- const fullUrl = url.endsWith("/") ? url : `${url}/`;
137
- const matches = matcher(fullUrl);
138
- if (matches === false) {
139
- return undefined;
92
+ // @ts-ignore :cry:
93
+ matches(suppliedUrl, search) {
94
+ var _a, _b, _c;
95
+ const fullUrl = suppliedUrl.endsWith("/")
96
+ ? suppliedUrl
97
+ : suppliedUrl + "/";
98
+ let url = fullUrl;
99
+ const parentRoutes = getParentArray();
100
+ let params = {};
101
+ while (parentRoutes.length) {
102
+ const parentRoute = parentRoutes.shift();
103
+ const parentMatch = parentRoute.matcher(url, undefined);
104
+ if (parentMatch === false) {
105
+ return false;
106
+ }
107
+ url = url.slice(parentMatch.matchLength);
108
+ // All routes assume the url starts with a /
109
+ // so if the parent route matches the / in the url, which consumes it
110
+ // need to re-add it for the next route to match against
111
+ if (!url.startsWith("/")) {
112
+ url = "/" + url;
113
+ }
114
+ params = { ...params, ...((_a = parentMatch.params) !== null && _a !== void 0 ? _a : {}) };
140
115
  }
141
- const params = matches.params;
142
- if (params && paramsSchema) {
143
- paramsSchema.parse(params);
116
+ const matches = matcher(url, parse(search));
117
+ // if there is any URL left after matching this route, the last to match
118
+ // that means the match isn't actually a match
119
+ if (matches === false || matches.matchLength !== url.length) {
120
+ return false;
144
121
  }
145
- const query = parse(search);
146
- if (opts === null || opts === void 0 ? void 0 : opts.query) {
147
- opts.query.parse(query);
122
+ const fullParams = {
123
+ ...params,
124
+ ...((_b = matches.params) !== null && _b !== void 0 ? _b : {}),
125
+ };
126
+ if (fullParamsSchema) {
127
+ fullParamsSchema.parse(fullParams);
128
+ }
129
+ if (querySchema) {
130
+ querySchema.parse(matches.query);
148
131
  }
149
132
  return {
150
- type: event,
151
133
  originalUrl: `${fullUrl}${search}`,
152
- params,
153
- query,
134
+ type: event,
135
+ params: fullParams,
136
+ query: (_c = matches.query) !== null && _c !== void 0 ? _c : {},
154
137
  };
155
138
  },
156
- // @ts-ignore not sure how to type this correctly
157
- // The types from external to this function are correct however
158
- reverse({ params, query } = {}) {
159
- const url = (() => {
160
- if (params) {
161
- // @ts-ignore same problem
162
- return reverser(params);
163
- }
164
- else {
165
- return reverser();
166
- }
167
- })();
168
- if (!isNil(query)) {
169
- return `${url}?${stringify(query)}`;
170
- }
171
- else {
172
- return url;
173
- }
139
+ // @ts-ignore :cry:
140
+ reverse(args) {
141
+ const { params, query } = args !== null && args !== void 0 ? args : {};
142
+ const parentRoutes = getParentArray();
143
+ const baseUrl = parentRoutes
144
+ .map((route) => route.reverser({ params }))
145
+ .join("");
146
+ return `${joinRoutes(baseUrl, reverser({ params, query }))}`;
174
147
  },
175
- // @ts-ignore not sure how to type this correctly
176
- // The types from external to this function are correct however
177
- navigate({ params, query, meta } = {}) {
178
- // @ts-ignore same problem
148
+ // @ts-ignore :cry:
149
+ navigate(args) {
150
+ const { params, query, meta } = args !== null && args !== void 0 ? args : {};
179
151
  const url = this.reverse({ params, query });
180
152
  navigate({
181
153
  url: joinRoutes(this.basePath, url),
@@ -1,5 +1,15 @@
1
1
  export function joinRoutes(base, route) {
2
2
  const realBase = base.endsWith("/") ? base.slice(0, -1) : base;
3
3
  const realRoute = route.startsWith("/") ? route : `/${route}`;
4
- return realBase + realRoute;
4
+ const joinedUrl = realBase + realRoute;
5
+ if (!joinedUrl.endsWith("/")) {
6
+ if (!joinedUrl.includes("?")) {
7
+ return `${joinedUrl}/`;
8
+ }
9
+ if (!joinedUrl.includes("/?")) {
10
+ return joinedUrl.replace("?", "/?");
11
+ }
12
+ return joinedUrl;
13
+ }
14
+ return joinedUrl;
5
15
  }
@@ -32,12 +32,13 @@ export declare type AnyRoute = {
32
32
  navigate: any;
33
33
  getEvent: any;
34
34
  event: string;
35
- url?: string;
36
35
  basePath: string;
37
36
  history: XstateTreeHistory;
38
37
  parent?: AnyRoute;
39
38
  paramsSchema?: Z.ZodObject<any>;
40
39
  querySchema?: Z.ZodObject<any>;
40
+ matcher: (url: string, query: ParsedQuery<string> | undefined) => any;
41
+ reverser: any;
41
42
  };
42
43
 
43
44
  /**
@@ -71,8 +72,8 @@ export declare function broadcast(event: GlobalEvents): void;
71
72
  * - `send` - the interpreters send function, which can be used to send events to the machine
72
73
  * - `selectors` - the output of the selectors function from {@link buildSelectors}
73
74
  *
74
- * The resulting action function has memoization. It will return the same value until the
75
- * selectors reference changes or the send reference changes
75
+ * The resulting action function will only be called once per invocation of a machine.
76
+ * The selectors are passed in as a proxy to always read the latest selector value
76
77
  *
77
78
  * @param machine - The machine to create the actions for
78
79
  * @param selectors - The selectors function
@@ -90,79 +91,50 @@ export declare function buildActions<TMachine extends AnyStateMachine, TActions,
90
91
  * @param basePath - the base path for this route factory
91
92
  */
92
93
  export declare function buildCreateRoute(history: XstateTreeHistory, basePath: string): {
93
- /**
94
- * Creates a dynamic Route using the supplied options
95
- *
96
- * The return value of dynamicRoute is a function that accepts the routes "dynamic" options
97
- * The argument to dynamicRoute itself is the params/query/meta schemas defining the route
98
- *
99
- * The returned function accepts a singular option object with the following fields
100
- *
101
- * `event`, the string constant for the routes event
102
- * `matches`, a function that is passed a url/query string and determines if the route matches
103
- * if the route is matched it returns the extracted params/query objects
104
- * `reverse`, a function that is passed params/query objects and turns them into a URL
105
- *
106
- * The params and query schemas are ZodSchemas, they both need to be an object (ie Z.object())
107
- */
108
- dynamicRoute: <TOpts extends Options<Z.ZodObject<any, "strip", Z.ZodTypeAny, {
109
- [x: string]: any;
110
- }, {
111
- [x: string]: any;
112
- }>, Z.ZodObject<any, "strip", Z.ZodTypeAny, {
94
+ simpleRoute<TBaseRoute extends AnyRoute>(baseRoute?: TBaseRoute | undefined): <TEvent extends string, TParamsSchema extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
113
95
  [x: string]: any;
114
96
  }, {
115
97
  [x: string]: any;
116
- }>, any>>(opts?: TOpts | undefined) => <TEvent extends string, TParamsSchema = Params<TOpts>, TQuerySchema = Query<TOpts>, TMeta = Meta<TOpts>, TParams = TParamsSchema extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
98
+ }> | undefined, TQuerySchema extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
117
99
  [x: string]: any;
118
100
  }, {
119
101
  [x: string]: any;
120
- }> ? Z.TypeOf<TParamsSchema> : undefined, TQuery = TQuerySchema extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
121
- [x: string]: any;
122
- }, {
123
- [x: string]: any;
124
- }> ? Z.TypeOf<TQuerySchema> : undefined, TFullMeta = TMeta extends undefined ? SharedMeta : TMeta & SharedMeta>({ event, matches, reverse, }: {
102
+ }> | undefined, TMeta extends Record<string, unknown>>({ url, paramsSchema, querySchema, ...args }: {
125
103
  event: TEvent;
126
- matches: (url: string, query: ParsedQuery<string>) => false | RouteArguments<TParams, TQuery, TFullMeta>;
127
- reverse: RouteArgumentFunctions<string, TParams, TQuery, TFullMeta, RouteArguments<TParams, TQuery, TFullMeta>>;
128
- }) => Route<TParams, TQuery, TEvent, TFullMeta>;
129
- /**
130
- * Creates a static Route using the supplied options
131
- *
132
- * The return value of staticRoute is a function that accepts the routes options
133
- * The only argument to staticRoute itself is an optional parent route
134
- *
135
- * The returned function accepts 3 arguments
136
- *
137
- * 1. URL of the route
138
- * 2. The event type of the route
139
- * 3. The routes options, params schema, query schema and meta type
140
- *
141
- * The params and query schemas are ZodSchemas, they both need to be an object (ie Z.object())
142
- *
143
- * When creating a route that has a parent route, the following happens
144
- *
145
- * 1. The parent routes url is prepended to the routes URL
146
- * 2. The parents params schema is merged with the routes schema
147
- * 3. The parents meta type is merged with the routes meta type
148
- */
149
- staticRoute: <TBaseRoute extends AnyRoute | undefined = undefined, TBaseParams = RouteParams<TBaseRoute>, TBaseMeta = RouteMeta<TBaseRoute>>(baseRoute?: TBaseRoute | undefined) => <TOpts_1 extends Options<Z.ZodObject<any, "strip", Z.ZodTypeAny, {
150
- [x: string]: any;
151
- }, {
152
- [x: string]: any;
153
- }>, Z.ZodObject<any, "strip", Z.ZodTypeAny, {
154
- [x: string]: any;
155
- }, {
156
- [x: string]: any;
157
- }>, any>, TEvent_1 extends string, TParamsSchema_1 = Params<TOpts_1>, TQuerySchema_1 = Query<TOpts_1>, TMeta_1 = Meta<TOpts_1>, TParams_1 = TParamsSchema_1 extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
104
+ url: string;
105
+ paramsSchema?: TParamsSchema | undefined;
106
+ querySchema?: TQuerySchema | undefined;
107
+ meta?: TMeta | undefined;
108
+ }) => Route<MergeRouteTypes<RouteParams<TBaseRoute>, ResolveZodType<TParamsSchema>>, ResolveZodType<TQuerySchema>, TEvent, MergeRouteTypes<RouteMeta<TBaseRoute>, TMeta> & SharedMeta>;
109
+ route<TBaseRoute_1 extends AnyRoute>(baseRoute?: TBaseRoute_1 | undefined): <TEvent_1 extends string, TParamsSchema_1 extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
158
110
  [x: string]: any;
159
111
  }, {
160
112
  [x: string]: any;
161
- }> ? Z.TypeOf<TParamsSchema_1> : undefined, TQuery_1 = TQuerySchema_1 extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
113
+ }> | undefined, TQuerySchema_1 extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
162
114
  [x: string]: any;
163
115
  }, {
164
116
  [x: string]: any;
165
- }> ? Z.TypeOf<TQuerySchema_1> : undefined, TFullParams = TParams_1 extends undefined ? TBaseParams extends undefined ? undefined : TBaseParams : TParams_1 & (TBaseParams extends undefined ? {} : TBaseParams), TFullMeta_1 = TMeta_1 extends undefined ? TBaseMeta extends undefined ? SharedMeta : TBaseMeta & SharedMeta : TMeta_1 & (TBaseMeta extends undefined ? {} : TBaseMeta) & SharedMeta>(url: string, event: TEvent_1, opts?: TOpts_1 | undefined) => Route<TFullParams, TQuery_1, TEvent_1, TFullMeta_1>;
117
+ }> | undefined, TMeta_1 extends Record<string, unknown>>({ event, matcher, reverser, paramsSchema, querySchema, }: {
118
+ event: TEvent_1;
119
+ paramsSchema?: TParamsSchema_1 | undefined;
120
+ querySchema?: TQuerySchema_1 | undefined;
121
+ meta?: TMeta_1 | undefined;
122
+ /**
123
+ * Determines if the route matches the given url and query
124
+ *
125
+ * If there is no match, return false
126
+ * If there is a match, return the parsed params and query as well as the length of the matched path in the URL
127
+ */
128
+ matcher: (url: string, query: ParsedQuery<string> | undefined) => false | (RouteArguments<MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1>> & {
129
+ matchLength: number;
130
+ });
131
+ /**
132
+ * Reverses the route to a URL
133
+ *
134
+ * Supplied with params/query objects and constructs the correct URL based on them
135
+ */
136
+ reverser: RouteArgumentFunctions<string, MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1>, RouteArguments<MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1>>>;
137
+ }) => Route<MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, TEvent_1, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1> & SharedMeta>;
166
138
  };
167
139
 
168
140
  /**
@@ -311,9 +283,9 @@ declare type InferViewProps<T> = T extends ViewProps<infer TSelectors, infer TAc
311
283
  inState: (state: Parameters<TMatches>[0]) => TMatches;
312
284
  } : never;
313
285
 
314
- declare type IsEmptyObject<Obj, ExcludeOptional extends boolean = false> = [
315
- keyof (ExcludeOptional extends true ? OmitOptional<Obj> : Obj)
316
- ] extends [never] ? true : false;
286
+ declare type IsEmptyObject<Obj, ExcludeOptional extends boolean = false> = undefined extends Obj ? true : [keyof (ExcludeOptional extends true ? OmitOptional<Obj> : Obj)] extends [
287
+ never
288
+ ] ? true : false;
317
289
 
318
290
  /**
319
291
  * @public
@@ -325,7 +297,7 @@ keyof (ExcludeOptional extends true ? OmitOptional<Obj> : Obj)
325
297
  * @param options - configure loading component and context to invoke machine with
326
298
  * @returns an xstate-tree machine that wraps the promise, invoking the resulting machine when it resolves
327
299
  */
328
- export declare function lazy<TMachine extends AnyStateMachine>(factory: () => Promise<TMachine>, { Loader, withContext, }?: Options_2<TMachine["context"]>): StateMachine<Context, any, Events, States, any, any, any>;
300
+ export declare function lazy<TMachine extends AnyStateMachine>(factory: () => Promise<TMachine>, { Loader, withContext, }?: Options<TMachine["context"]>): StateMachine<Context, any, Events, States, any, any, any>;
329
301
 
330
302
  /**
331
303
  * @public
@@ -372,6 +344,8 @@ export declare type MatchesFrom<T extends AnyStateMachine> = StateFrom<T>["match
372
344
  */
373
345
  export declare function matchRoute<TRoutes extends Route<any, any, any, any>[]>(routes: TRoutes, basePath: string, path: string, search: string): Return<TRoutes>;
374
346
 
347
+ declare type MergeRouteTypes<TBase, TSupplied> = undefined extends TBase ? TSupplied : undefined extends TSupplied ? TBase : TBase & TSupplied;
348
+
375
349
  /**
376
350
  * @public
377
351
  *
@@ -408,16 +382,7 @@ declare type OmitOptional<T> = {
408
382
  */
409
383
  export declare function onBroadcast(handler: (event: GlobalEvents) => void): () => void;
410
384
 
411
- /**
412
- * @public
413
- */
414
- export declare type Options<TParamsSchema extends Z.ZodObject<any>, TQuerySchema extends Z.ZodObject<any>, TMetaSchema> = {
415
- params?: TParamsSchema;
416
- query?: TQuerySchema;
417
- meta?: TMetaSchema;
418
- };
419
-
420
- declare type Options_2<TContext> = {
385
+ declare type Options<TContext> = {
421
386
  /**
422
387
  * Displayed while the promise is resolving, defaults to returning null
423
388
  */
@@ -465,6 +430,8 @@ export declare type Query<T> = T extends {
465
430
  query: infer TQuery;
466
431
  } ? TQuery : undefined;
467
432
 
433
+ declare type ResolveZodType<T extends Z.ZodType<any> | undefined> = undefined extends T ? undefined : Z.TypeOf<Exclude<T, undefined>>;
434
+
468
435
  declare type Return<TRoutes extends Route<any, any, any, any>[]> = {
469
436
  type: "matched";
470
437
  route: TRoutes[number];
@@ -498,7 +465,7 @@ export declare type Route<TParams, TQuery, TEvent, TMeta> = {
498
465
  matches: (url: string, search: string) => ({
499
466
  type: TEvent;
500
467
  originalUrl: string;
501
- } & RouteArguments<TParams, TQuery, TMeta>) | undefined;
468
+ } & RouteArguments<TParams, TQuery, TMeta>) | false;
502
469
  /**
503
470
  * Takes in query/params objects as required by the route and returns a URL for that route
504
471
  *
@@ -520,11 +487,14 @@ export declare type Route<TParams, TQuery, TEvent, TMeta> = {
520
487
  getEvent: RouteArgumentFunctions<{
521
488
  type: TEvent;
522
489
  } & RouteArguments<TParams, TQuery, TMeta>, TParams, TQuery, TMeta>;
490
+ matcher: (url: string, query: ParsedQuery<string> | undefined) => (RouteArguments<TParams, TQuery, TMeta> & {
491
+ matchLength: number;
492
+ }) | false;
493
+ reverser: RouteArgumentFunctions<string, TParams, TQuery, TMeta>;
523
494
  /**
524
495
  * Event type for this route
525
496
  */
526
497
  event: TEvent;
527
- url?: string;
528
498
  history: XstateTreeHistory;
529
499
  basePath: string;
530
500
  parent?: AnyRoute;
package/lib/xstateTree.js CHANGED
@@ -107,6 +107,7 @@ export function XstateTreeView({ interpreter }) {
107
107
  const [current] = useService(interpreter);
108
108
  const currentRef = useRef(current);
109
109
  currentRef.current = current;
110
+ const selectorsRef = useRef(undefined);
110
111
  const { view: View, actions: actionsFactory, selectors: selectorsFactory, slots: interpreterSlots, } = interpreter.machine.meta;
111
112
  const slots = useSlots(interpreter, interpreterSlots.map((x) => x.name));
112
113
  const canHandleEvent = useCallback((e) => {
@@ -121,11 +122,22 @@ export function XstateTreeView({ interpreter }) {
121
122
  // current state the machine is in changes. But _only_ then
122
123
  // eslint-disable-next-line react-hooks/exhaustive-deps
123
124
  [current.value]);
125
+ const selectorsProxy = useConstant(() => {
126
+ return new Proxy({}, {
127
+ get: (_target, prop) => {
128
+ var _a;
129
+ return (_a = selectorsRef.current) === null || _a === void 0 ? void 0 : _a[prop];
130
+ },
131
+ });
132
+ });
133
+ const actions = useConstant(() => {
134
+ return actionsFactory(interpreter.send, selectorsProxy);
135
+ });
124
136
  if (!current) {
125
137
  return null;
126
138
  }
127
139
  const selectors = selectorsFactory(current.context, canHandleEvent, inState, current.value);
128
- const actions = actionsFactory(interpreter.send, selectors);
140
+ selectorsRef.current = selectors;
129
141
  return (React.createElement(View, { selectors: selectors, actions: actions, slots: slots, inState: inState }));
130
142
  }
131
143
  /**
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@koordinates/xstate-tree",
3
3
  "main": "lib/index.js",
4
4
  "types": "lib/xstate-tree.d.ts",
5
- "version": "2.0.10",
5
+ "version": "3.0.0-beta.1",
6
6
  "license": "MIT",
7
7
  "description": "Build UIs with Actors using xstate and React",
8
8
  "keywords": [
@@ -41,6 +41,8 @@
41
41
  "@types/react-dom": "^18.0.6",
42
42
  "@types/testing-library__jest-dom": "^5.14.1",
43
43
  "@typescript-eslint/eslint-plugin": "^5.30.5",
44
+ "@vitejs/plugin-react": "^2.1.0",
45
+ "@xstate/immer": "^0.3.1",
44
46
  "@xstate/react": "^3.0.0",
45
47
  "classnames": "^2.3.1",
46
48
  "cz-conventional-changelog": "^3.3.0",
@@ -51,16 +53,22 @@
51
53
  "eslint-plugin-react-hooks": "^4.3.0",
52
54
  "history": "^4.10.1",
53
55
  "husky": "^8.0.1",
56
+ "immer": "^9.0.15",
54
57
  "jest": "^28.0.3",
55
58
  "jest-environment-jsdom": "^28.0.1",
56
59
  "react": "^18.1.0",
57
60
  "react-dom": "^18.1.0",
58
61
  "rimraf": "^3.0.2",
62
+ "rxjs": "^7.5.6",
59
63
  "semantic-release": "^19.0.3",
60
64
  "semantic-release-npm-github-publish": "^1.5.1",
65
+ "todomvc-app-css": "^2.4.2",
66
+ "todomvc-common": "^1.0.5",
61
67
  "ts-jest": "^28.0.5",
62
68
  "typescript": "^4.7.3",
63
- "xstate": "^4.32.0"
69
+ "vite": "^3.1.3",
70
+ "vite-tsconfig-paths": "^3.5.0",
71
+ "xstate": "^4.33.0"
64
72
  },
65
73
  "peerDependencies": {
66
74
  "@xstate/react": "^3.x",
@@ -70,8 +78,10 @@
70
78
  "scripts": {
71
79
  "lint": "eslint 'src/**/*'",
72
80
  "test": "jest",
73
- "build": "rimraf dist && tsc -p tsconfig.build.json",
74
- "build:watch": "tsc -p tsconfig.build.json -w",
81
+ "test-examples": "tsc --noEmit",
82
+ "todomvc": "vite dev",
83
+ "build": "rimraf lib && rimraf out && tsc -p tsconfig.build.json",
84
+ "build:watch": "tsc -p tsconfig.json -w",
75
85
  "api-extractor": "api-extractor run",
76
86
  "release": "semantic-release",
77
87
  "commitlint": "commitlint --edit"