Drupal 9多步骤表单如何构建提交前信息预览复核步骤
Drupal 9多步骤表单复核页实现方案
现有前端绑定方案失效的核心原因
你之前参考的JS值绑定方案没生效,基本是两个问题:
- 没有适配单选框这类非文本输入元素的取值逻辑,很多教程只覆盖了普通input输入框,漏了radio/select/checkbox的取值
- 没有在切换到复核步骤的时机主动触发预览刷新,且你当前模板里
data-bind的自定义键名和表单字段的name属性不一致,导致取值映射失败
方案1:适配现有Twig结构的前端修复
不需要大幅改动你现有的模板代码,只要修正属性映射、补全JS逻辑即可:
先修改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保持一致
- 把所有预约类型单选框的
替换原有的自定义绑定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
相关产品推荐
相关产品推荐

