Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example: 7GUIs-flightBooker-react #4904

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
584 changes: 564 additions & 20 deletions examples/7guis-flight-booker-react/package-lock.json

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions examples/7guis-flight-booker-react/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "7guis-flight-booker-react",
"name": "fb",
"private": true,
"version": "0.0.0",
"type": "module",
Expand All @@ -10,13 +10,15 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.0.5",
"@xstate/react": "^4.1.0",
"date-fns": "^3.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"vite-tsconfig-paths": "^4.3.2",
"xstate": "^5.10.0"
"xstate": "^5.9.1"
},
"devDependencies": {
"@types/node": "^20.12.10",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
Expand All @@ -26,6 +28,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"typescript": "^5.2.2",
"vite": "^5.2.0"
"vite": "^5.2.0",
"vite-tsconfig-paths": "^4.3.2"
}
}
77 changes: 50 additions & 27 deletions examples/7guis-flight-booker-react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,80 @@
import FlightContext from './machines/flightMachine';
import { BookButton, Header } from './components';
import { DateSelector, TripSelector } from './components';
import { TODAY } from './utils';
import FlightContext, { TODAY } from "./machines/flightMachine";
import { BookButton, Header } from "./components";
import { DateSelector, TripSelector } from "./components";
import { format } from "date-fns";

const dateFormat = "EEEE MMMM do, yyyy";

export default function App() {
const { send } = FlightContext.useActorRef();
const state = FlightContext.useSelector((state) => state);
const { departDate, returnDate } = state.context;
const isRoundTrip = state.matches({ scheduling: 'roundTrip' });
const isBooking = state.matches('booking');
const isBooked = state.matches('booked');
const isRoundTrip = state.context.tripType === "roundTrip";
const isBooking = state.matches("booking");
const isBooked = state.matches("booked");

const isValidDepartDate = departDate >= TODAY;
const isValidReturnDate = returnDate >= departDate;

return (
<main>
const successMessage = (
<>
<Header>Booked!</Header>
<p>
You booked a <b>{isRoundTrip ? "round trip" : "one way"}</b> flight.
</p>
<p>
<b>Departs:</b> {format(departDate, dateFormat)}
</p>
{isRoundTrip && (
<p>
<b>Returns:</b> {format(returnDate, dateFormat)}
</p>
)}
</>
);

const ui = (
<>
<Header>Book Flight</Header>
<TripSelector
id="Trip Type"
isBooking={isBooking}
isBooked={isBooked}
tripType={isRoundTrip ? 'roundTrip' : 'oneWay'}
tripType={isRoundTrip ? "roundTrip" : "oneWay"}
/>
<DateSelector
id="Depart Date"
value={departDate}
isValidDate={isValidDepartDate}
disabled={isBooking || isBooked}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
send({
type: 'CHANGE_DEPART_DATE',
value: e.currentTarget.value
})
}
/>
<DateSelector
id="Return Date"
value={returnDate}
isValidDate={isValidReturnDate}
disabled={!isRoundTrip}
disabled={isBooking}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
send({
type: 'CHANGE_RETURN_DATE',
value: e.currentTarget.value
type: "CHANGE_DEPART_DATE",
value: e.currentTarget.value,
})
}
/>
{isRoundTrip && (
<DateSelector
id="Return Date"
value={returnDate}
isValidDate={isValidReturnDate}
disabled={!isRoundTrip || isBooking}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
send({
type: "CHANGE_RETURN_DATE",
value: e.currentTarget.value,
})
}
/>
)}
<BookButton
eventType={isRoundTrip ? 'BOOK_RETURN' : 'BOOK_DEPART'}
eventType={isRoundTrip ? "BOOK_RETURN" : "BOOK_DEPART"}
isBooking={isBooking}
isBooked={isBooked}
/>
</main>
</>
);

return <main>{isBooked ? successMessage : ui}</main>;
}
25 changes: 4 additions & 21 deletions examples/7guis-flight-booker-react/src/components/BookButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import FlightContext from '../machines/flightMachine';
import FlightContext from "../machines/flightMachine";

type Props = {
isBooking: boolean;
Expand All @@ -14,26 +14,9 @@ export default function BookButton({ eventType, isBooking, isBooked }: Props) {

const bookFlight = () => send({ type: eventType });

const successMessage = (
<>
<h2>You booked a flight!</h2>
<p>
<span>Departs:</span> {state.context.departDate}
</p>
<p>
<span>Returns:</span> {state.context.returnDate}
</p>
</>
);

return (
<>
<dialog open={isBooking || isBooked}>
{isBooking ? <p>Booking...</p> : successMessage}
</dialog>
<button onClick={bookFlight} disabled={!canBook}>
{isBooking ? 'Booking' : isBooked ? 'Booked!' : 'Book'}
</button>
</>
<button onClick={bookFlight} disabled={!canBook}>
{isBooking ? "Booking..." : "Book"}
</button>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function DateInput({ isValidDate, ...props }: Props) {
return (
<label>
<span className="visually-hidden">{props.id}</span>
<input type="date" {...props} className={isValidDate ? '' : 'error'} />
<input type="date" {...props} className={isValidDate ? "" : "error"} />
</label>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export default function TripSelector({
<select
disabled={isBooked || isBooking}
value={tripType}
onChange={() => {
send({ type: "CHANGE_TRIP_TYPE" });
onChange={(e) => {
const selectedValue = e.currentTarget.value as "oneWay" | "roundTrip";
send({ type: "CHANGE_TRIP_TYPE", tripType: selectedValue });
}}
{...props}
>
Expand Down
8 changes: 4 additions & 4 deletions examples/7guis-flight-booker-react/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Header from './Header';
import BookButton from './BookButton';
import DateSelector from './DateInput';
import TripSelector from './TripSelector';
import Header from "./Header";
import BookButton from "./BookButton";
import DateSelector from "./DateInput";
import TripSelector from "./TripSelector";

export { Header, BookButton, DateSelector, TripSelector };
125 changes: 72 additions & 53 deletions examples/7guis-flight-booker-react/src/machines/flightMachine.ts
Original file line number Diff line number Diff line change
@@ -1,107 +1,126 @@
import { setup, assign, assertEvent, fromPromise } from 'xstate';
import { createActorContext } from '@xstate/react';
import { TODAY, TOMORROW } from '../utils';
import { sleep } from '../utils';
import { setup, assign, assertEvent, fromPromise } from "xstate";
import { createActorContext } from "@xstate/react";
import { generateDate } from "../utils";
import { sleep } from "../utils";

export const TODAY = generateDate(0);
const TOMORROW = generateDate(1);

export const flightBookerMachine = setup({
types: {
context: {} as FlightData,
events: {} as
| { type: 'BOOK_DEPART' }
| { type: 'BOOK_RETURN' }
| { type: 'CHANGE_TRIP_TYPE' }
| { type: 'CHANGE_DEPART_DATE'; value: string }
| { type: 'CHANGE_RETURN_DATE'; value: string }
| { type: "BOOK_DEPART" }
| { type: "BOOK_RETURN" }
| { type: "CHANGE_TRIP_TYPE"; tripType: "oneWay" | "roundTrip" }
| { type: "CHANGE_DEPART_DATE"; value: string }
| { type: "CHANGE_RETURN_DATE"; value: string },
},
actions: {
setDepartDate: assign(({ event }) => {
assertEvent(event, 'CHANGE_DEPART_DATE');
assertEvent(event, "CHANGE_DEPART_DATE");
return { departDate: event.value };
}),
setReturnDate: assign(({ event }) => {
assertEvent(event, 'CHANGE_RETURN_DATE');
assertEvent(event, "CHANGE_RETURN_DATE");
return { returnDate: event.value };
})
}),
setTripType: assign(({ event }) => {
assertEvent(event, "CHANGE_TRIP_TYPE");
return { tripType: event.tripType };
}),
},
actors: {
Booker: fromPromise(() => {
return sleep(2000);
})
return sleep(1000);
}),
},
guards: {
'isValidDepartDate?': ({ context: { departDate } }) => {
"isValidDepartDate?": ({ context: { departDate } }) => {
return departDate >= TODAY;
},
'isValidReturnDate?': ({ context: { departDate, returnDate } }) => {
"isValidReturnDate?": ({ context: { departDate, returnDate } }) => {
return departDate >= TODAY && returnDate > departDate;
}
}
},
},
}).createMachine({
id: 'flightBookerMachine',
id: "flightBookerMachine",
context: {
departDate: TODAY,
returnDate: TOMORROW
returnDate: TOMORROW,
tripType: "oneWay",
},
initial: 'scheduling',
initial: "scheduling",
states: {
scheduling: {
initial: 'oneWay',
initial: "oneWay",
on: {
CHANGE_DEPART_DATE: {
actions: {
type: 'setDepartDate'
}
}
type: "setDepartDate",
},
},

BOOK_DEPART: {
target: "booking",
guard: {
type: "isValidDepartDate?",
},
},

BOOK_RETURN: {
target: "booking",
guard: {
type: "isValidReturnDate?",
},
},
},
states: {
oneWay: {
on: {
CHANGE_TRIP_TYPE: {
target: 'roundTrip'
target: "roundTrip",
actions: {
type: "setTripType",
tripType: "roundTrip",
},
},
BOOK_DEPART: {
target: '#flightBookerMachine.booking',
guard: {
type: 'isValidDepartDate?'
}
}
}
},
},
roundTrip: {
on: {
CHANGE_TRIP_TYPE: {
target: 'oneWay'
target: "oneWay",
actions: {
type: "setTripType",
tripType: "oneWay",
},
},

CHANGE_RETURN_DATE: {
actions: {
type: 'setReturnDate'
}
type: "setReturnDate",
},
},
BOOK_RETURN: {
target: '#flightBookerMachine.booking',
guard: {
type: 'isValidReturnDate?'
}
}
}
}
}
},
},
},
},
booking: {
invoke: {
src: 'Booker',
src: "Booker",
onDone: {
target: 'booked'
target: "booked",
},
onError: {
target: 'scheduling'
}
}
target: "scheduling",
},
},
},
booked: {
type: 'final'
}
}
type: "final",
},
},
});

export default createActorContext(flightBookerMachine);
Loading
Loading