You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Drupal 9多步骤表单如何构建提交前信息预览复核步骤

Drupal 9多步骤表单复核页实现方案

现有前端绑定方案失效的核心原因

你之前参考的JS值绑定方案没生效,基本是两个问题:

  • 没有适配单选框这类非文本输入元素的取值逻辑,很多教程只覆盖了普通input输入框,漏了radio/select/checkbox的取值
  • 没有在切换到复核步骤的时机主动触发预览刷新,且你当前模板里data-bind的自定义键名和表单字段的name属性不一致,导致取值映射失败

方案1:适配现有Twig结构的前端修复

不需要大幅改动你现有的模板代码,只要修正属性映射、补全JS逻辑即可:

  1. 先修改Twig里的属性映射,保证data-bind、data-update的值和对应字段的name属性完全一致:

    • 把所有预约类型单选框的data-bind="type"改成data-bind="reserveType",复核页对应的<div data-update="type"></div>改成<div data-update="reserveType"></div>
    • 把所有人数单选框的data-bind="occupancy"改成data-bind="reserveOccupancy",复核页对应的<div data-update="occupancy"></div>改成<div data-update="reserveOccupancy"></div>
    • 后续新增的所有字段都遵循这个规则,自定义绑定键和字段name保持一致
  2. 替换原有的自定义绑定JS,用统一的FormData API做值读取,避免不同表单元素的兼容问题:

document.addEventListener('DOMContentLoaded', function() {
  const form = document.querySelector('[data-multi-step]');
  if (!form) return;

  const bindFields = form.querySelectorAll('[data-bind]');
  const reviewNodes = form.querySelectorAll('[data-update]');
  const steps = Array.from(form.querySelectorAll('[data-step]'));
  const nextBtns = form.querySelectorAll('[data-next]');
  const prevBtns = form.querySelectorAll('[data-previous]');

  // 统一更新预览内容
  function renderReview() {
    const formData = new FormData(form);
    reviewNodes.forEach(node => {
      const key = node.dataset.update;
      node.textContent = formData.get(key) || '未填写';
    });
  }

  // 切换步骤逻辑
  function jumpToStep(targetIndex) {
    steps.forEach((step, idx) => {
      step.classList.toggle('active', idx === targetIndex);
      // 切到最后一步(复核页)时强制刷新预览
      if (idx === steps.length - 1 && idx === targetIndex) renderReview();
    });
  }

  // 绑定字段值变化事件
  bindFields.forEach(field => {
    field.addEventListener('change', renderReview);
    field.addEventListener('input', renderReview);
  });

  // 绑定下一步/上一步按钮事件
  nextBtns.forEach(btn => {
    btn.addEventListener('click', () => {
      const currentStep = btn.closest('[data-step]');
      const currentIdx = steps.indexOf(currentStep);
      if (currentIdx < steps.length - 1) jumpToStep(currentIdx + 1);
    });
  });

  prevBtns.forEach(btn => {
    btn.addEventListener('click', () => {
      const currentStep = btn.closest('[data-step]');
      const currentIdx = steps.indexOf(currentStep);
      if (currentIdx > 0) jumpToStep(currentIdx - 1);
    });
  });

  // 初始化渲染默认选中值
  renderReview();
});

注意:这个方案是纯前端实现,要额外做前端字段校验,避免用户跳过步骤直接提交,同时提交时要在后端做值校验,防止前端值被篡改。


方案2:Drupal原生多步骤表单实现(生产环境推荐)

如果你的表单还没做大量前端定制,优先用Drupal核心Form API实现多步骤逻辑,不需要自己写JS做值绑定,也天然兼容Drupal的校验、安全机制,维护成本更低:

  • 核心逻辑:用$form_state存储每一步的提交值,通过标记当前步骤数渲染不同的表单内容,最后一步直接从$form_state读取之前存的所有值渲染复核列表即可。
  • 简单实现示例:
<?php

namespace Drupal\your_custom_module\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class SpaceReserveForm extends FormBase {

  public function getFormId() {
    return 'space_reservation_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    // 初始化步骤,默认第一步
    $step = $form_state->get('step') ?: 1;
    $form_state->set('step', $step);
    $savedValues = $form_state->get('saved_values') ?: [];

    // 第一步:预约类型、人数选择
    if ($step == 1) {
      $form['reserve_type'] = [
        '#type' => 'radios',
        '#title' => $this->t('Reservation Type'),
        '#options' => [
          'class' => $this->t('class'),
          'cultural activity' => $this->t('cultural activity'),
          'conversation group/tutoring' => $this->t('conversation group/tutoring'),
          'faculty workshop' => $this->t('faculty workshop'),
          'other' => $this->t('other'),
        ],
        '#default_value' => $savedValues['reserve_type'] ?? 'class',
        '#required' => TRUE,
      ];

      // 读取空间实体的容量字段,渲染人数选项
      $space = $form_state->get('current_space');
      if ($space && !$space->field_occupancy->isEmpty()) {
        $maxOcc = (int)$space->field_occupancy->value;
        $occOptions = array_combine(range(1, $maxOcc), range(1, $maxOcc));
        $form['reserve_occupancy'] = [
          '#type' => 'radios',
          '#title' => $this->t('Number of People'),
          '#options' => $occOptions,
          '#default_value' => $savedValues['reserve_occupancy'] ?? 1,
          '#required' => TRUE,
        ];
      }

      $form['next'] = [
        '#type' => 'button',
        '#value' => $this->t('Next'),
        '#button_type' => 'primary',
        '#ajax' => [
          'callback' => '::refreshForm',
          'wrapper' => 'multi-step-form-wrap',
        ],
        '#submit' => ['::saveStep1'],
      ];
    }

    // 第二步:其他预约信息
    if ($step == 2) {
      // 这里放第二步的表单字段,逻辑和第一步一致
      // $form['xxx'] = [...];

      $form['back'] = [
        '#type' => 'button',
        '#value' => $this->t('Back'),
        '#ajax' => [
          'callback' => '::refreshForm',
          'wrapper' => 'multi-step-form-wrap',
        ],
        '#submit' => ['::prevStep'],
      ];
      $form['next'] = [
        '#type' => 'button',
        '#value' => $this->t('Next'),
        '#button_type' => 'primary',
        '#ajax' => [
          'callback' => '::refreshForm',
          'wrapper' => 'multi-step-form-wrap',
        ],
        '#submit' => ['::saveStep2'],
      ];
    }

    // 第三步:复核页
    if ($step == 3) {
      $form['review_content'] = [
        '#theme' => 'item_list',
        '#items' => [
          $this->t('Reservation Type: @val', ['@val' => $savedValues['reserve_type']]),
          $this->t('Number of People: @val', ['@val' => $savedValues['reserve_occupancy']]),
          // 其他字段按同样格式拼接即可
        ],
      ];

      $form['back'] = [
        '#type' => 'button',
        '#value' => $this->t('Back'),
        '#ajax' => [
          'callback' => '::refreshForm',
          'wrapper' => 'multi-step-form-wrap',
        ],
        '#submit' => ['::prevStep'],
      ];
      $form['submit'] = [
        '#type' => 'submit',
        '#value' => $this->t('Submit Reservation'),
        '#button_type' => 'primary',
      ];
    }

    $form['#prefix'] = '<div id="multi-step-form-wrap">';
    $form['#suffix'] = '</div>';
    return $form;
  }

  // 保存第一步值
  public function saveStep1(array &$form, FormStateInterface $form_state) {
    $saved = $form_state->get('saved_values') ?: [];
    $saved['reserve_type'] = $form_state->getValue('reserve_type');
    $saved['reserve_occupancy'] = $form_state->getValue('reserve_occupancy');
    $form_state->set('saved_values', $saved);
    $form_state->set('step', 2);
    $form_state->setRebuild(TRUE);
  }

  // 保存第二步值
  public function saveStep2(array &$form, FormStateInterface $form_state) {
    $saved = $form_state->get('saved_values') ?: [];
    // 保存第二步的字段值
    // $saved['xxx'] = $form_state->getValue('xxx');
    $form_state->set('saved_values', $saved);
    $form_state->set('step', 3);
    $form_state->setRebuild(TRUE);
  }

  // 返回上一步
  public function prevStep(array &$form, FormStateInterface $form_state) {
    $currentStep = $form_state->get('step');
    $form_state->set('step', $currentStep - 1);
    $form_state->setRebuild(TRUE);
  }

  // Ajax刷新表单
  public function refreshForm(array &$form, FormStateInterface $form_state) {
    return $form;
  }

  // 最终提交逻辑
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $allValues = $form_state->get('saved_values');
    // 这里写后续业务逻辑:创建预约实体、发送通知邮件等
    $this->messenger()->addStatus($this->t('Your reservation has been submitted successfully.'));
    $form_state->setRedirect('<front>');
  }

}

注意:使用这个方案不需要自己写前端步骤切换、值绑定逻辑,Drupal的Ajax API会自动处理步骤切换的内容刷新,所有值都存在服务端的$form_state里,不会出现前端值不同步的问题,也天然支持Drupal的字段验证、CSRF防护。


内容的提问来源于stack exchange,提问作者Parker McCrary

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 10:15:34