<div id="app"></div>
* {
box-sizing: border-box;
}
body {
align-items: center;
background-color: #EDF1F4;
color: #153D66;
display: flex;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
justify-content: center;
min-height: 100vh;
}
h2 {
margin: 0;
}
.form-container {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.125);
}
.form-header {
padding: 1.75rem 1.75rem 0;
}
.form-body {
padding: 1.75rem;
}
label {
display: block;
margin-bottom: 4px;
width: 100%;
}
input {
border-radius: 4px;
border: 1px solid #A4B6C0;
box-shadow: none;
display: block;
margin-bottom: 16px;
padding: 10px;
width: 100%;
}
button:focus,
input:focus {
border-color: rgb(102, 175, 233);
box-shadow: rgba(0, 0, 0, 0.075) 0px 1px 1px inset, rgba(102, 175, 233, 0.6) 0px 0px 8px;
outline: none;
}
button {
-webkit-appearance: none;
border-radius: 4px;
border: 0;
cursor: pointer;
display: block;
font-size: 14px;
font-weight: bold;
margin: 1.75rem auto 0;
padding: 12px 20px;
}
.btn-success {
background-color: #A6E9DC;
color: #004C4A;
}
.btn-reset {
background-color: #FFDFDF;
color: #711117;
}
.pill-container {
text-align: center;
}
.state-pill {
background: #fff;
border-radius: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.125);
display: inline-block;
font-weight: bold;
margin-bottom: 20px;
padding: 12px 20px;
text-align:center;
width: auto;
}
.alert {
border-radius: 4px;
margin-top: 12px;
margin: 12px auto 0;
padding: 8px 12px;
text-align: center;
width: 85%;
}
.alert.error {
border: 1px solid #ebcccc;
color: #a94442;
background-color: #f2dede;
}
.alert.success {
background-color: #dff0d8;
border-color: #d0e9c6;
color: #3c763d;
}
const { Machine } = XState;
/**
* The state machine that will power our payment form.
*
* This machine can be visualized copying the object we
* pass to `Machine()` through xviz
*
* @see https://bit.ly/xstate-viz
*/
const stateMachine = Machine({
initial: "idle",
states: {
idle: {
on: {
SUBMIT: [
{
target: "loading",
cond: ({ state }) => state.name !== "" && state.card !== ""
},
{ target: "error" }
]
}
},
loading: {
onEntry: ["doPayment"],
on: {
PAYMENT_RECEIVED: "success",
PAYMENT_FAILED: "error"
}
},
error: {
on: {
SUBMIT: {
target: "loading",
cond: ({ state }) => state.name !== "" && state.card !== ""
}
}
},
success: {
on: {
RESET: "idle"
}
}
}
});
/**
* We don't actually have a backend service for this
* payment system. So, instead we're rolling a Math.random
* dice and resolving or rejecting a fake payment promise.
*/
function fakePayment() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const dice = Math.floor(Math.random() * Math.floor(2));
if (dice === 0) return resolve("Payment succeeded.");
return reject("Payment failed.");
}, 1000);
});
}
class App extends React.Component {
state = {
machine: {},
name: "",
card: "",
msg: ""
};
// Initialize the state machine
componentDidMount() {
this.setState({ machine: stateMachine.initialState });
}
/**
* When our machine enters the loading state, fire off
* a call to our fakePayment() promise.
*/
doPayment = () => {
return fakePayment()
.then(msg => this.transition("PAYMENT_RECEIVED", { msg }))
.catch(msg => this.transition("PAYMENT_FAILED", { msg }));
};
/**
* We need a way to call the function we define as a string
* in our onEntry action in the loading state.
*
* If you're going to leverage any of the other action types,
* check out what Michele Bertoli has done in react-automata.
*
* @see https://github.com/MicheleBertoli/react-automata/blob/master/src/withStateMachine.js#L79
*/
runActions = state => {
if (state.actions.length > 0) {
state.actions.forEach(f => this[f]());
}
};
// Transition based on current state and extState
transition = (eventType, extState) => {
// Determine the next state
const newState = stateMachine.transition(
this.state.machine.value,
eventType,
{
state: this.state
}
);
// Perform any actions for the new state
this.runActions(newState);
// Update our component state
this.setState({
machine: newState,
msg: extState && extState.msg ? extState.msg : ""
});
};
// Prevent the form from refreshing the page
submitForm = e => {
e.preventDefault();
this.transition("SUBMIT");
};
handleInput = e => {
this.setState({ [e.target.name]: e.target.value });
};
render() {
const { machine, msg } = this.state;
return (
<div>
<div className="pill-container">
<div className="state-pill">current state: {machine.value}</div>
</div>
<div className="form-container">
<div className="form-header">
<h2>State Machine Payment Form</h2>
</div>
{machine.value === "error" ? (
<div className="alert error">
{msg ? msg : "You must fill out all the form fields."}
</div>
) : null}
{machine.value === "success" ? (
<div className="alert success">{msg ? msg : null}</div>
) : null}
<div className="form-body">
<form onSubmit={e => this.submitForm(e)}>
<div className="form-group">
<label htmlFor="NameOnCard">Name on card</label>
<input
id="NameOnCard"
name="name"
className="form-control"
type="text"
maxlength="255"
value={this.state.name}
onChange={this.handleInput}
/>
</div>
<div className="form-group">
<label htmlFor="CreditCardNumber">Card number</label>
<input
id="CreditCardNumber"
name="card"
className="null card-image form-control"
type="text"
value={this.state.card}
onChange={this.handleInput}
/>
</div>
<button id="PayButton" className="btn-success" type="submit">
Pay Now
</button>
</form>
</div>
</div>
{machine.value === "success" ? (
<button
id="ResetButton"
className="btn-reset"
type="button"
onClick={() => this.transition("RESET")}
>
Reset
</button>
) : null}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("app"));
View Compiled
This Pen doesn't use any external CSS resources.