The UI element that I'm calling the Strong Confirmation Modal is a prompt to the user to doubly confirm a destructive action. I'll quickly discuss the idea behind this and then show how I implemented it with XState.
Strong Confirmation
"Yes, you clicked the delete button, but are you sure? Type the name of the thing you want to delete so that we can both be sure."
I've seen this UI element in plenty of places, but the one that stands out to me is GitHub's. Deleting a repository is definitely a destructive action and not one you'd like to do accidentally. Certainly not something you'd like your cat to be able to trigger by stepping on the keyboard. Here is what it looks like.
You have to type the name of the repository you want to delete in order to enable the button that confirms the deletion. It seems like a small thing, but a UI element like this can go a long way in helping users avoid huge headaches.
An XState Implementation
Here is the codesandbox if you want to dive right in. Below I'll talk about some of the XState concepts and features that stand out to me.
Nested States
State machines are hierarchical. They can be made up of simple and composite states. Composite states have sub-states nested within them. (See the stately viz)
Nested states are a great way to represent those concepts that feel like they need multiple booleans. This machine has an open
state which is composite. While the modal is open, the machine can be in different sub-states. If the input doesn't match the confirmation text, then it stays in {open: "idle"}
. Once the input matches the confirmation text, the machine will transition to {open: "confirmable"}
.
Validations
The piece of this machine that I found trickiest to implement was validation of the input. If the input matches some criteria, I want to move to some valid state. If the input doesn't match, then I need to stay in or move to the invalid state.
I achieved this with an invoked service.
{
services: {
checkInputConfirmText: (context) => (send) => {
console.log("Checking input confirm text: ", context.inputConfirmText);
if (context.doubleConfirmText === context.inputConfirmText) {
send("REPORT_MATCHING");
}
}
}
}
An invoked service can send an event to the machine that invoked it which seemed like the perfect way to trigger the transition I needed. I also took advantage of the fact that an invoked service that exits cleanly will trigger an onDone
action.
Each time this service is invoked, it will check the validation (do the two text strings match?) and then do one of two things.
The validation doesn't pass, it exits, and the
onDone
internally self-transitions back to theidle
state.The validation does pass, the
REPORT_MATCHING
event is sent, and the invoking machine transitions to theconfirmable
sub-state.
External Self-Transitions
The CHANGE
event that is sent each time the modal's input value changes triggers an external self-transition to the idle
state.
open: {
exit: ["clearErrorMessage"],
initial: "idle",
on: {
CANCEL: {
target: "#closed"
},
CHANGE: {
target: ".idle",
internal: false,
actions: "assignValueToContext"
}
},
states: {
idle: { /* ... */ },
confirmable: { /* ... */ }
}
}
A transition of { target: ".idle" }
would be an internal transition. That would prevent the validation service from being re-invoked. But I want that service to be invoked on each change, so I include internal: false
in there to make it an external transition.
Conclusion
There are lots of other interesting bits going on in this machine beyond what I highlighted. It is worth taking some time to read through it and see what stands out.
Implementing a machine like this was fun because it had a real-world use and I learned a lot while figuring it out. I learned new things about XState and I was pushed to think differently about how to model the problem as a state machine.
If you enjoy my writing, consider joining my newsletter or following me on twitter.
Top comments (0)