# 任务 3:航班预订
¥Task 3: Flight Booker
这些 XState v4 文档不再维护
XState v5 现已推出!阅读有关 XState v5 的更多信息 (opens new window) 和 查看 XState v5 文档 (opens new window)。
¥XState v5 is out now! Read more about XState v5 (opens new window) and check out the XState v5 docs (opens new window).
这是 7GUI 的 7 项任务 (opens new window) 的第三个:
¥This is the third of The 7 Tasks from 7GUIs (opens new window):
挑战:限制。
¥Challenges: constraints.
任务是构建一个框架,其中包含一个组合框 C,其中包含“单程航班”和“返程航班”两个选项,两个文本字段 T1 和 T2 分别代表开始和返回日期,以及一个用于提交所选航班的按钮 B 。当 C 的值为“回程航班”时,T2 启用。当 C 的值为“回程航班”并且 T2 的日期严格早于 T1 的日期时,B 被禁用。当非禁用文本字段 T 的日期格式不正确时,T 会显示为红色,并且 B 会被禁用。单击 B 时,会显示一条消息,通知用户其选择(例如“你已预订 2014 年 4 月 4 日的单程航班。”)。最初,C 的值为“单程航班”,T1 和 T2 具有相同的(任意)日期(这意味着 T2 被禁用)。
¥The task is to build a frame containing a combobox C with the two options “one-way flight” and “return flight”, two textfields T1 and T2 representing the start and return date, respectively, and a button B for submitting the selected flight. T2 is enabled iff C’s value is “return flight”. When C has the value “return flight” and T2’s date is strictly before T1’s then B is disabled. When a non-disabled textfield T has an ill-formatted date then T is colored red and B is disabled. When clicking B a message is displayed informing the user of his selection (e.g. “You have booked a one-way flight on 04.04.2014.”). Initially, C has the value “one-way flight” and T1 as well as T2 have the same (arbitrary) date (it is implied that T2 is disabled).
Flight Booker 的重点在于一方面对小部件之间的约束进行建模,另一方面对小部件内的约束进行建模。此类限制在与 GUI 应用的日常交互中非常常见。Flight Booker 的一个好的解决方案将使约束在源代码中清晰、简洁和明确,而不是隐藏在大量脚手架后面。
¥The focus of Flight Booker lies on modelling constraints between widgets on the one hand and modelling constraints within a widget on the other hand. Such constraints are very common in everyday interactions with GUI applications. A good solution for Flight Booker will make the constraints clear, succinct and explicit in the source code and not hidden behind a lot of scaffolding.
Flight Booker 直接受到 Sodium 中 Flight Booking Java 示例的启发,简化了使用文本字段进行日期输入,而不是专门的日期选择小部件,因为 Flight Booker 的重点不是专门/自定义小部件。
¥Flight Booker is directly inspired by the Flight Booking Java example in Sodium with the simplification of using textfields for date input instead of specialized date picking widgets as the focus of Flight Booker is not on specialized/custom widgets.
# 建模
¥Modeling
总的来说,该表单有两种可能的状态:editing
或 submitted
。我们可以使用事件对 startDate
、returnDate
和 trip
字段的更新进行建模,但这些字段只能在处于 editing
状态时才能编辑。此外,只有当 trip
是 "roundTrip"
时,才能编辑 returnDate
,我们可以通过使用防护来强制执行该约束。
¥Overall, there's two possible states this form can be in: editing
or submitted
. We can model updating the startDate
, returnDate
, and trip
fields by using events, with the constraint that these fields can only be edited when in the editing
state. Additionally, the returnDate
can only be edited when trip
is "roundTrip"
, and we can enforce that constraint by using a guard.
SET_TRIP
事件控制 trip
字段,并且只能在 editing
状态下分配(尝试在 submitted
中编辑它 - 即使它没有禁用,它也不会改变)。我们可以添加附加约束,即它必须是 "oneWay"
或 "roundTrip"
。
¥A SET_TRIP
event controls the trip
field, and can only be assigned in the editing
state (try editing it in submitted
- even if it's not disabled, it will not change). We can add the additional constraint that it must either be "oneWay"
or "roundTrip"
.
要从 editing
转换到 submitted
,需要发送 SUBMIT
事件。在此转换中,通过使用防护来确保存在 startDate
(如果 trip
是 "oneWay"
)或存在 startDate
和 returnDate
(如果 trip
是 "roundTrip"
)。
¥To transition from editing
to submitted
, a SUBMIT
event needs to be sent. Validation occurs in this transition by using a guard to ensure that there is a startDate
if the trip
is "oneWay"
, or that there is a startDate
and returnDate
if the trip
is "roundTrip"
.
TIP:环境与状态
请注意,我们决定不使用 trip
的嵌套状态(例如 editing.oneWay
或 editing.roundTrip
)对机器进行建模。原因很简单,即使这在技术上是一个有限状态(并且你可以自由地以这种方式建模),它也是我们需要读取的上下文值,以便在 trip
选择输入中显示该值:context.trip
。
¥Notice that we decided not to model the machine with nested states for the trip
, such as editing.oneWay
or editing.roundTrip
. The reason is simply that even though this is technically a finite state (and you are free to model it this way), it is also a contextual value that we need to read from in order to display the value in the trip
select input: context.trip
.
但是,你可以使用嵌套状态对此进行建模,并且亲自尝试是一个很好的练习;它甚至可能简化 SUBMIT
转换中的一些保护逻辑。试试看:
¥However, you can model this using nested states, and it's a good exercise to try it on your own; it might even simplify some of the guard logic in the SUBMIT
transition. Try it out:
// ...
initial: 'oneWay',
states: {
oneWay: {
entry: assign({ trip: 'oneWay' }),
// ...
},
roundTrip: {
entry: assign({ trip: 'roundTrip' }),
// ...
}
},
// ...
状态:
¥States:
"editing"
- 正在编辑航班预订信息的状态¥
"editing"
- the state where the flight booking information is being edited"submitted"
- 航班预订信息已提交成功的状态,不可再更改¥
"submitted"
- the state where the flight booking information has been submitted successfully, and no further changes can be made
上下文:
¥Context:
interface FlightContext {
startDate?: string;
returnDate?: string;
trip: 'oneWay' | 'roundTrip';
}
事件:
¥Events:
type FlightEvent =
| {
type: 'SET_TRIP';
value: 'oneWay' | 'roundTrip';
}
| {
type: 'startDate.UPDATE';
value: string;
}
| {
type: 'returnDate.UPDATE';
value: string;
}
| { type: 'SUBMIT' };
# 编码
¥Coding
import { createMachine, assign } from 'xstate';
export const flightMachine = createMachine({
id: 'flight',
initial: 'editing',
context: {
startDate: undefined,
returnDate: undefined,
trip: 'oneWay' // or 'roundTrip'
},
states: {
editing: {
on: {
'startDate.UPDATE': {
actions: assign({
startDate: (_, event) => event.value
})
},
'returnDate.UPDATE': {
actions: assign({
returnDate: (_, event) => event.value
}),
cond: (context) => context.trip === 'roundTrip'
},
SET_TRIP: {
actions: assign({
trip: (_, event) => event.value
}),
cond: (_, event) =>
event.value === 'oneWay' || event.value === 'roundTrip'
},
SUBMIT: {
target: 'submitted',
cond: (context) => {
if (context.trip === 'oneWay') {
return !!context.startDate;
} else {
return (
!!context.startDate &&
!!context.returnDate &&
context.returnDate > context.startDate
);
}
}
}
}
},
submitted: {
type: 'final'
}
}
});
# 结果
¥Result