1

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

第 1 步 - 盘子

  • 从 ["Salad", "Pasta", "Pizza"] 中选择一道菜

第 2 步 - 制备方法

  • 从 ["Oven", "Microwave"] 中选择一种制备方法

第 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:我更新了示例以响应 Davids 的回答。(见我对他的回答的评论)

4

2 回答 2

1

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

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

沙盒示例链接

  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>
    ))}
  </>
);
于 2020-07-06T23:15:06.737 回答
1

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

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': {
      // ...
    }
  }
});

并且您可以使用 Matin 答案中的数据驱动配置来基于context.dish和动态显示成分context.method

于 2020-07-07T14:40:57.223 回答