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

Posted

技术标签:

【中文标题】使用状态机时如何在向导中配置动态表单字段【英文标题】:how to configure dynamic form fields inside a wizard when using a state machine 【发布时间】:2020-10-26 19:58:40 【问题描述】:

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

第 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"]

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

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

    如果从第 1 步选择的菜品是“沙拉”,则应跳过第 2 步。什么是适当的声明方式?

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

编辑:我根据 Martins 的回答更新了示例。 (见我对他的回答的评论)

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

【问题讨论】:

【参考方案1】:

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

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

Sandbox example link

  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>
    ))
  </>
);

【讨论】:

感谢您的回答。这确实可以解决我描述的情况。然而,我所面临的实际情况更为复杂。在最后一步中显示哪些表单字段的决定取决于前面步骤的多个选择,这就是为什么我不能将其声明为数据的原因。我将更新我的示例以反映这一点。【参考方案2】:

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

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.dishcontext.method 动态显示成分。

【讨论】:

感谢您的回答。这解决了跳过该步骤的问题。关于成分,我更新了我的问题,以说明前两个步骤中的选择与最后一步中的成分之间的复杂映射。我不确定如何以声明性方式对此进行建模,尤其是在向导/表单添加更多步骤和成分的情况下。我意识到我的比喻不再有意义,但我希望我试图解决的问题是可以理解的。

以上是关于使用状态机时如何在向导中配置动态表单字段的主要内容,如果未能解决你的问题,请参考以下文章

步骤向导表单

如何使laravel中的动态表单字段可填写

动态表单 - 如何使用反应钩子更新“onChange”事件中多个表单字段的值?

动态添加字段到表单

使用 jQuery 动态添加 HTML 表单字段

如何创建“动态”表单 - 并在两个字段中存储数据和表单设计