Skip to content

Commit 951cea7

Browse files
authored
feat: .can() API (#74)
Matches xstate more closely and it's useful.
1 parent feb57f1 commit 951cea7

File tree

4 files changed

+86
-1
lines changed

4 files changed

+86
-1
lines changed

.changeset/fair-ligers-move.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"xstate-component-tree": minor
3+
---
4+
5+
Adding `.can()` API from XState
6+
7+
The `.can()` API is a simple passthrough to the interpreter for the root statechart being managed by `xstate-component-tree`, and is intended as a convenience function to make it easier to interact with a ComponentTree instance instead of a direct XState Interpreter reference.
8+
9+
From the XState docs on `.can()`:
10+
11+
> Determines whether sending the event will cause a non-forbidden transition to be selected, even if the transitions have no actions nor change the state value.

src/component-tree.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class ComponentTree {
8787
this._boundApis = {
8888
matches : this.matches.bind(this),
8989
hasTag : this.hasTag.bind(this),
90+
can : this.can.bind(this),
9091
broadcast : this.broadcast.bind(this),
9192
};
9293

@@ -501,6 +502,16 @@ class ComponentTree {
501502
return [ ...this._services.values() ].some(({ state }) => state.hasTag(tag));
502503
}
503504

505+
/**
506+
* Check if the current state or any child states can make a transition
507+
*
508+
* @param {Event | string} event
509+
* @returns boolean
510+
*/
511+
can(event) {
512+
return [ ...this._services.values() ].some(({ state }) => state.can(event));
513+
}
514+
504515
/**
505516
* Check if the current state or any child states match a path
506517
*

tests/api/can.test.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as assert from "uvu/assert";
2+
3+
import describe from "../util/describe.js";
4+
import { createTree } from "../util/trees.js";
5+
import { treeTeardown } from "../util/context.js";
6+
7+
import single from "./specimens/single.js";
8+
import child from "./specimens/child.js";
9+
import noComponents from "./specimens/no-components.js";
10+
11+
describe("can", (it) => {
12+
it.after.each(treeTeardown);
13+
14+
it("should check the root tree", async (context) => {
15+
const tree = context.tree = createTree(single);
16+
17+
let { extra } = await tree();
18+
19+
assert.ok(tree.builder.can("NEXT"));
20+
assert.ok(extra.can("NEXT"));
21+
22+
assert.not.ok(tree.builder.can("NOPE"));
23+
assert.not.ok(extra.can("NOPE"));
24+
25+
tree.service.send("NEXT");
26+
27+
({ extra } = await tree());
28+
29+
assert.ok(tree.builder.can("NEXT"));
30+
assert.ok(extra.can("NEXT"));
31+
32+
assert.not.ok(tree.builder.can("NOPE"));
33+
assert.not.ok(extra.can("NOPE"));
34+
});
35+
36+
it("should check child trees", async (context) => {
37+
const tree = context.tree = createTree(child);
38+
39+
const { extra } = await tree();
40+
41+
assert.ok(tree.builder.can("NEXT"));
42+
assert.ok(tree.builder.can("CHILD_NEXT"));
43+
assert.ok(extra.can("NEXT"));
44+
assert.ok(extra.can("CHILD_NEXT"));
45+
});
46+
47+
it("should work without components", async (context) => {
48+
const tree = context.tree = createTree(noComponents);
49+
50+
let { extra } = await tree();
51+
52+
assert.ok(tree.builder.can("NEXT"));
53+
assert.ok(extra.can("NEXT"));
54+
55+
tree.service.send("NEXT");
56+
57+
({ extra } = await tree());
58+
59+
assert.ok(tree.builder.can("NEXT"));
60+
assert.ok(extra.can("NEXT"));
61+
});
62+
});

tests/api/specimens/child.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ const child = createMachine({
1919
tags : "child-one",
2020

2121
on : {
22-
NEXT : "two",
22+
NEXT : "two",
23+
CHILD_NEXT : "two",
2324
},
2425
},
2526

0 commit comments

Comments
 (0)