如何在使用状态机时在向导中配置动态表单字段



我正在尝试使用状态机实现多步骤向导,但不确定如何处理某些配置。为了说明这一点,我整理了一个向导的例子,帮助你准备一道菜。 假设以下示例,将此表单/向导行为建模为状态机的适当方法是什么?

第 1 步 - 菜

  • 从["沙拉","意大利面","比萨饼"]中选择一道菜

第 2 步 - 制备方法

  • 从["烤箱","微波炉"]中选择一种制备方法

第 3 步 - 成分

  • 在表格中添加和选择成分,根据菜肴和制备方法,表格看起来会有所不同
// ingredients based on previous selections
("Pizza", "Oven") => ["tomato", "cheese", "pepperoni"]
("Pizza", "Microwave") => ["cheese", "pepperoni", "mushrooms"]
("Pasta", "Oven") => ["parmesan", "butter", "creme fraiche"]
("Pasta", "Microwave") => ["parmesan", "creme fraiche"]
("Salad") => ["cucumber", "feta cheese", "lettuce"]

我试图尽可能地简化问题。以下是我的问题:

  1. 在步骤 3 中,我想显示一个包含不同类型的各种字段的表单。步骤 1 和 2 中的选择定义将在步骤 3 的表单中显示的字段。指定此表单配置的适当方法是什么?

  2. 如果步骤 1 中选择的菜肴是"沙拉",则应跳过第 2 步。声明这一点的适当方式是什么?

我计划使用 xstate 实现这一点,因为我正在处理的项目是用 react 编写的。

编辑:我更新了示例以回应马丁斯的答案。(见我对他的回答的评论(

编辑2:我更新了示例以回应David的答案。(见我对他的回答的评论(

对于整个流程,如果选择了"沙拉",则可以使用受保护的转换跳过方法步骤:

const machine = createMachine({
initial: 'pick a dish',
context: {
dish: null,
method: null
},
states: {
'pick a dish': {
on: {
'dish.select': [
{
target: 'ingredients',
cond: (_, e) => e.value === 'salad'
},
{
target: 'prep method',
actions: assign({ dish: (_, e) => e.value })
}
]
}
},
'prep method': {
on: {
'method.select': {
target: 'ingredients',
actions: assign({ method: (_, e) => e.value })
}
}
},
'ingredients': {
// ...
}
}
});

您可以使用 Martin 答案中的数据驱动配置,根据context.dishcontext.method动态显示成分。

您需要有一个保存数据以及它们之间关系的数据结构,然后您可以使用状态来存储所选项目,并具有显示/隐藏特定步骤的逻辑。

下面只是一个简单的例子来展示如何做到这一点:

沙盒示例链接

const data = [
{
// I recommend to use a unique id for any items that can be selective
dish: "Salad",
ingredients: ["ingredient-A", "ingredient-B", "ingredient-C"],
preparationMethods: []
},
{
dish: "Pasta",
ingredients: ["ingredient-E", "ingredient-F", "ingredient-G"],
preparationMethods: ["Oven", "Microwave"]
},
{
dish: "Pizza",
ingredients: ["ingredient-H", "ingredient-I", "ingredient-G"],
preparationMethods: ["Oven", "Microwave"]
}
];

export default function App() {
const [selectedDish, setSelectedDish] = useState(null);
const [selectedMethod, setSelectedMethod] = useState(null);
const [currentStep, setCurrentStep] = useState(1);
const onDishChange = event => {
const selecetedItem = data.filter(
item => item.dish === event.target.value
)[0];
setSelectedDish(selecetedItem);
setSelectedMethod(null);
setCurrentStep(selecetedItem.preparationMethods.length > 0 ? 2 : 3);
};
const onMethodChange = event => {
setSelectedMethod(event.target.value);
setCurrentStep(3);
};
const onBack = () => {
setCurrentStep(
currentStep === 3 && selectedMethod === null ? 1 : currentStep - 1
);
};
useEffect(() => {
switch (currentStep) {
case 1:
setSelectedDish(null);
setSelectedMethod(null);
break;
case 2:
setSelectedMethod(null);
break;
case 3:
default:
}
}, [currentStep]);
return (
<div className="App">
{currentStep === 1 && <Step1 onDishChange={onDishChange} />}
{currentStep === 2 && (
<Step2
onMethodChange={onMethodChange}
selectedMethod={selectedMethod}
selectedDish={selectedDish}
/>
)}
{currentStep === 3 && <Step3 selectedDish={selectedDish} />}
{selectedDish !== null && (
<>
<hr />
<div>Selected Dish: {selectedDish.dish}</div>
{selectedMethod !== null && (
<div>Selected Method: {selectedMethod}</div>
)}
</>
)}
<br />
{currentStep > 1 && <button onClick={onBack}> Back </button>}
</div>
);
}
const Step1 = ({ onDishChange }) => (
<>
<h5>Step 1:</h5>
<select onChange={onDishChange}>
<option value={null} disabled selected>
Select a dish
</option>
{data.map(item => (
<option key={item.dish} value={item.dish}>
{item.dish}
</option>
))}
</select>
</>
);
const Step2 = ({ onMethodChange, selectedMethod, selectedDish }) => (
<>
<h5>Step 2:</h5>
<div>
<select onChange={onMethodChange} value={selectedMethod}>
<option value={null} disabled selected>
Select a method
</option>
{selectedDish.preparationMethods.map(method => (
<option key={method} value={method}>
{method}
</option>
))}
</select>
</div>
</>
);
const Step3 = ({ selectedDish }) => (
<>
<h5>Step 3:</h5>
<h4>List of ingredient: </h4>
{selectedDish.ingredients.map(ingredient => (
<div key={ingredient}>{ingredient}</div>
))}
</>
);

最新更新