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

Drupal自定义模块中嵌入与iframe交互的多步骤表单(禁止使用contrib模块)实现指引求助

Drupal自定义模块中嵌入与iframe交互的多步骤表单(禁止使用contrib模块)实现指引求助

嘿,能看出来你卡了一个多月,这个场景确实有点绕——既要跨域和iframe通信,又要做动态多步骤表单,还不能用 contrib 模块。咱们一步步把你的卡壳点逐个打通:


一、先把基础的iframe消息通信+Ajax提交掰正

你的JS代码有两个隐藏坑,先修正:

  1. 没验证消息来源,容易被XSS攻击
  2. 手动创建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回调即可
  }
}

五、关键注意事项(解决你之前的所有卡壳点)

  1. 为什么不能直接用实体表单路由?
    实体表单的默认提交逻辑是:验证失败会重定向到实体编辑页,因为它的表单是独立于你的模块上下文的。嵌入到自定义表单后,所有交互都在Ajax环境中,不会跳转。
  2. 多步骤状态怎么保持?
    用$form_state->setRebuild()可以让Drupal自动保持表单状态,不需要额外存储。
  3. 新增表单的嵌入逻辑
    以后要加新增选项时,只需要创建类似SecondStepAddForm的类,调用\Drupal::service('entity.form_builder')->getForm(Node::create(['type' => 'your_bundle']), 'default')即可,逻辑和编辑表单一致。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 08:23:08