Drupal自定义模块中嵌入与iframe交互的多步骤表单(禁止使用contrib模块)实现指引求助
Drupal自定义模块中嵌入与iframe交互的多步骤表单(禁止使用contrib模块)实现指引求助
嘿,能看出来你卡了一个多月,这个场景确实有点绕——既要跨域和iframe通信,又要做动态多步骤表单,还不能用 contrib 模块。咱们一步步把你的卡壳点逐个打通:
一、先把基础的iframe消息通信+Ajax提交掰正
你的JS代码有两个隐藏坑,先修正:
- 没验证消息来源,容易被XSS攻击
- 手动创建
Drupal.ajax时没带CSRF Token,大概率会触发403错误
修正后的JS:
// scripts.js window.addEventListener('message', function (event) { // 【必须做】只信任你的iframe域名,防止恶意消息 if (event.origin !== 'https://your-iframe-domain.com') return; // 规范Drupal Ajax初始化,自动带上CSRF Token const ajax = Drupal.ajax({ url: Drupal.url('my/controller/path'), base: '#form-target-container', // 绑定到页面提前加的空容器:<div id="form-target-container"></div> submit: { my_uuid: event.data.my_uuid, _token: Drupal.settings.csrf_token // 带上Drupal要求的CSRF Token }, success: function() { // 成功加载第一步表单后的可选回调 } }); ajax.execute(); });
二、重构控制器:不要提前渲染多步骤,只加载第一步
你之前在控制器里提前渲染两步表单是反逻辑的——第一步还没触发,完全不需要加载第二步。控制器的核心职责是:接收uuid、找到对应节点、返回Ajax命令把第一步表单插入页面。
修正后的控制器:
// src/Controller/MyFormController.php namespace Drupal\my_module\Controller; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\ReplaceCommand; use Symfony\Component\HttpFoundation\Request; class MyFormController extends ControllerBase { public function handler(Request $request) { $response = new AjaxResponse(); $my_uuid = $request->get('my_uuid'); // 用uuid找对应节点 $node = $this->findNodeByUuid($my_uuid); if (!$node) { $response->addCommand(new ReplaceCommand('#form-target-container', '<p class="error">无效的节点UUID</p>')); return $response; } // 只加载第一步表单(确认+操作选项) $first_step_form = \Drupal::formBuilder()->getForm('Drupal\my_module\Form\FirstStepForm', $node); $response->addCommand(new ReplaceCommand('#form-target-container', $first_step_form)); return $response; } // 你的uuid转node工具函数 private function findNodeByUuid($uuid) { $node_storage = \Drupal::entityTypeManager()->getStorage('node'); $nids = $node_storage->getQuery() ->condition('uuid', $uuid) ->execute(); return !empty($nids) ? $node_storage->load(reset($nids)) : NULL; } }
三、重构第一步表单:用Ajax回调动态加载第二步(核心破局点)
你之前尝试1的致命问题是直接跳转到实体编辑路由——这个路由的表单是独立的,验证失败会走默认流程跳转到实体编辑页。必须把实体表单嵌入到你自己的表单上下文里,这样所有交互都在你的模块环境中,不会跳转。
第一步表单代码:
// src/Form/FirstStepForm.php namespace Drupal\my_module\Form; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\ReplaceCommand; use Drupal\node\Entity\Node; class FirstStepForm extends FormBase { public function getFormId() { return 'my_module_first_step_form'; } public function buildForm(array $form, FormStateInterface $form_state, Node $node = NULL) { $form_state->setRebuild(); // 保持表单状态,不刷新页面 // 第一步:显示节点确认信息 $form['node_confirmation'] = [ '#type' => 'container', '#markup' => '<p>你将操作的节点:<strong>' . $node->getTitle() . '</strong>(ID:' . $node->id() . ')</p>', ]; // 操作选项(以后可以加新增) $form['action'] = [ '#type' => 'radios', '#title' => $this->t('请选择操作'), '#options' => [ 'edit' => $this->t('编辑该节点'), // 'add' => $this->t('新增关联节点') ], '#default_value' => 'edit', ]; // 触发Ajax加载第二步的按钮 $form['submit'] = [ '#type' => 'submit', '#value' => $this->t('确认进入下一步'), '#ajax' => [ 'callback' => '::loadSecondStep', 'wrapper' => 'form-target-container', 'method' => 'replace', ], ]; // 表单容器(确保Ajax替换目标存在) $form['#prefix'] = '<div id="form-target-container">'; $form['#suffix'] = '</div>'; return $form; } // Ajax回调:动态加载第二步表单 public function loadSecondStep(array $form, FormStateInterface $form_state) { $response = new AjaxResponse(); $action = $form_state->getValue('action'); $node = $form_state->getBuildInfo()['args'][0]; // 根据选项加载对应第二步表单 switch ($action) { case 'edit': $second_form = \Drupal::formBuilder()->getForm('Drupal\my_module\Form\SecondStepEditForm', $node->id()); break; // 以后加新增: // case 'add': // $second_form = \Drupal::formBuilder()->getForm('Drupal\my_module\Form\SecondStepAddForm'); // break; default: $second_form = ['#markup' => '<p class="error">无效的操作</p>']; } $response->addCommand(new ReplaceCommand('#form-target-container', $second_form)); return $response; } public function submitForm(array &$form, FormStateInterface $form_state) { // 无需处理,用Ajax回调即可 } }
四、第二步表单:嵌入实体编辑表单,解决验证不跳转问题
核心逻辑:把实体编辑表单作为子表单嵌入到自定义表单里,所有提交、验证都在你的Ajax环境中处理,失败了原地显示错误,不会跳转到实体编辑页。
第二步编辑表单代码:
// src/Form/SecondStepEditForm.php namespace Drupal\my_module\Form; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\ReplaceCommand; use Drupal\node\Entity\Node; class SecondStepEditForm extends FormBase { protected $nid; public function getFormId() { return 'my_module_second_step_edit_form'; } public function buildForm(array $form, FormStateInterface $form_state, $nid = NULL) { $this->nid = $nid; $form_state->setRebuild(); // 加载实体编辑表单作为子表单 $node = Node::load($nid); $edit_form = \Drupal::service('entity.form_builder')->getForm($node, 'default'); // 把实体表单嵌入当前表单 $form['node_edit_form'] = $edit_form; // 覆盖实体表单的提交按钮为Ajax提交 $form['node_edit_form']['actions']['submit']['#ajax'] = [ 'callback' => '::handleFormSubmit', 'wrapper' => 'form-target-container', 'method' => 'replace', ]; // 返回第一步的按钮 $form['back'] = [ '#type' => 'submit', '#value' => $this->t('返回第一步'), '#ajax' => [ 'callback' => '::backToFirstStep', 'wrapper' => 'form-target-container', ], '#limit_validation_errors' => [], // 跳过所有验证 ]; $form['#prefix'] = '<div id="form-target-container">'; $form['#suffix'] = '</div>'; return $form; } // 处理实体表单的Ajax提交 public function handleFormSubmit(array $form, FormStateInterface $form_state) { $response = new AjaxResponse(); $node = Node::load($this->nid); if (!$form_state->hasAnyErrors()) { // 提交成功:显示提示+返回第一步 $response->addCommand(new ReplaceCommand('#form-target-container', [ '#markup' => '<p class="success">节点已成功编辑!</p><button id="back-btn" class="button">返回操作界面</button>', ])); } else { // 验证失败:原地返回带错误的表单 $response->addCommand(new ReplaceCommand('#form-target-container', $form)); } return $response; } // 返回第一步的回调 public function backToFirstStep(array $form, FormStateInterface $form_state) { $response = new AjaxResponse(); $node = Node::load($this->nid); $first_step_form = \Drupal::formBuilder()->getForm('Drupal\my_module\Form\FirstStepForm', $node); $response->addCommand(new ReplaceCommand('#form-target-container', $first_step_form)); return $response; } public function submitForm(array &$form, FormStateInterface $form_state) { // 无需处理,用Ajax回调即可 } }
五、关键注意事项(解决你之前的所有卡壳点)
- 为什么不能直接用实体表单路由?
实体表单的默认提交逻辑是:验证失败会重定向到实体编辑页,因为它的表单是独立于你的模块上下文的。嵌入到自定义表单后,所有交互都在Ajax环境中,不会跳转。 - 多步骤状态怎么保持?
用$form_state->setRebuild()可以让Drupal自动保持表单状态,不需要额外存储。 - 新增表单的嵌入逻辑
以后要加新增选项时,只需要创建类似SecondStepAddForm的类,调用\Drupal::service('entity.form_builder')->getForm(Node::create(['type' => 'your_bundle']), 'default')即可,逻辑和编辑表单一致。
内容来源于stack exchange
相关产品推荐
相关产品推荐

