Drupal 11中使用HTMX创建级联选择表单的详细指南

Drupal 11中使用HTMX创建级联选择表单的详细指南

Drupal 11: Cascading Select Forms With HTMX

这是一系列探讨Drupal中HTMX的文章的第四部分。在过去的两篇文章中,我们探讨了以不同方式在控制器中使用HTMX。这次,我将涉足HTMX和表单的领域。

几年前,我在这个网站上写了一篇关于 Drupal中级联Ajax选择表单 的文章,当我试图弄清楚与选择表单和Ajax相关的问题时,我经常会参考这篇文章。在那篇文章中,我使用了年、月和日选择字段,并将它们关联起来,以便在选择过程中相互影响。

我编写Drupal网站已经有很多年了,但在尝试在Drupal表单中实现Ajax之前,我仍然需要深吸一口气。为了让一切正常工作,我最终会在表单字段中添加包装元素或自定义属性。这似乎总是一个痛苦的过程。

当我学习HTMX和Drupal时,我坐下来重新实现这个级联选择表单,并在大约半小时内就完成了。大部分时间都花在了将表单元素添加到构建表单方法中。这与在Drupal表单中添加Ajax的旧方法形成了鲜明对比。

在本文中,我们将探讨创建一个包含多个选择元素的表单,然后使用HTMX(以及一点表单状态API)将它们关联起来,以便选择一个元素可以更新其他元素。

本文中包含的所有代码都可以在 GitHub上的Drupal HTMX示例项目 中找到,但在这里我们将详细介绍代码的功能以及它为生成内容所执行的操作。

就像其他关于HTMX的文章一样,我将从基础开始,定义路由。

一、路由

我们这里需要的路由只需将路径 /htmx-examples/cascading-select 指向我们的表单类。

drupal_htmx_examples_cascading_select_form:
  path: "/htmx-examples/cascading-select"
  defaults:
    _form: '\Drupal\drupal_htmx_examples\Form\CascadingSelectForm'
    _title: "HTMX Cascading Select Example Form"
  requirements:
    _permission: "access content"

这个路由没有什么特别之处,它只是一个常规的表单路由。

让我们为这个路由创建表单。

二、表单

表单类只是一个标准的Drupal表单,但在控制器中使用HTMX和在表单中使用HTMX之间存在差异。当我们定义一个将使用HTMX的控制器时,我们(有时)需要将 request_stack 服务注入到表单中,以便我们可以使用它来检测任何传入的HTMX请求。对于表单来说,这不是必需的,因为 request_stack 服务是核心FormBase类的一部分,我们在Drupal中创建所有表单时都会扩展这个类。

这是表单类的大纲,它将包含我们的级联选择表单。

<?php

namespace Drupal\drupal_htmx_examples\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Htmx\Htmx;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * 展示使用HTMX的级联选择的表单。
 */
class CascadingSelectForm extends FormBase {

  /**
   * {@inheritDoc}
   */
  public function getFormId() {
    return 'htmx_cascade_select_form';
  }

  /**
   * {@inheritDoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {}

  /**
   * {@inheritDoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    $year = $form_state->getValue('year');
    $month = $form_state->getValue('month');
    $day = $form_state->getValue('day');

    $args = [
      '%year' => $year,
      '%month' => $month,
      '%day' => $day,
    ];
    $this->messenger()->addMessage($this->t('Submitted form with values year: %year, month: %month, day: %day', $args));
  }

}

我还在这里添加了提交处理程序,它将把输入参数作为消息打印出来。

为了构建我们的表单,我们需要定义年、月和日选择字段,以便它们在用户交互过程中可以相互更新。

第一个元素是年份选择,它定义了一个从2019年到2050年的任意日期范围,并将其作为数组提供给选择字段。我们还从表单状态中获取当前年份的值,以防用户已经提交了表单,我们希望保持该状态。在这里,我们可以对日期范围进行更细致的处理,但这只是一个测试表单。

    $years = range(2019, 2050);
    $years = array_combine($years, $years);
    $year = $form_state->getValue('year');

    $form['year'] = [
      '#title' => $this->t('Year'),
      '#type' => 'select',
      '#empty_option' => $this->t('- Select -'),
      '#options' => $years,
      '#default_value' => $year,
    ];

为了定义月份选择,我们只需创建一个小的月份数组(毕竟只有12个值),并将其提供给月份选择元素。我们还从表单状态中添加当前月份的值,然后在这里设置表单的 '#states' API,以便只有当年份选择有值时才显示月份选择。

    $months = [
      1 => $this->t('Jan'),
      2 => $this->t('Feb'),
      3 => $this->t('Mar'),
      4 => $this->t('Apr'),
      5 => $this->t('May'),
      6 => $this->t('Jun'),
      7 => $this->t('Jul'),
      8 => $this->t('Aug'),
      9 => $this->t('Sep'),
      10 => $this->t('Oct'),
      11 => $this->t('Nov'),
      12 => $this->t('Dec'),
    ];
    $month = $form_state->getValue('month');

    $form['month'] = [
      '#title' => $this->t('Month'),
      '#type' => 'select',
      '#options' => $months,
      '#empty_option' => $this->t('- Select -'),
      '#default_value' => $month,
      '#states' => [
        '!visible' => [
          ':input[name="year"]' => ['value' => ''],
        ],
      ],
    ];

创建日期列表稍微复杂一些。有些月份的天数不同[需要引用来源],所以我们不能简单地添加一个从1到31的范围就完事了。相反,我们需要监听年份和月份的值,然后使用PHP内置函数 cal_days_in_month() 来加载天数。这意味着如果月份是闰年的二月,我们仍然会在选择列表中显示正确的天数。我们还添加了 #states API属性,以便只有当月份元素有值时才显示日期字段。

    $days = [];
    if ($month) {
      $number = cal_days_in_month(CAL_GREGORIAN, $month, $year);
      $days = range(1, $number);
      $days = array_combine($days, $days);
    }
    $day = $form_state->getValue('day');

    $form['day'] = [
      '#title' => $this->t('Day'),
      '#type' => 'select',
      '#options' => $days,
      '#empty_option' => $this->t('- Select -'),
      '#default_value' => $day,
      '#states' => [
        '!visible' => [
          ':input[name="month"]' => ['value' => ''],
        ],
      ],
    ];

最后,我们只需要添加一个提交按钮并返回表单。

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => 'Submit',
    ];

    return $form;

这个表单将根据状态API显示和隐藏元素,但它不会用正确的天数填充日期字段。所以让我们在表单中添加重要的HTMX。

三、添加HTMX

这个表单的HTMX需要在选择元素定义之后、表单末尾的返回语句之前添加。

我们需要在这个表单的两个字段中添加HTMX,以执行以下操作:

  • 当用户选择 月份 时,我们需要返回表单以计算该月的天数,并填充 日期 选择字段。
  • 当用户选择 年份 时,我们需要返回并重新填充 月份 日期 选择字段。这将涵盖选择闰年且用户恰好选择了2月29日的情况。

我们使用 Htmx Drupal类来创建我们需要的属性,并将它们应用到月份选择元素上。

    (new Htmx())
      ->post()
      ->select('*:has(>select[name="day"])')
      ->target('*:has(>select[name="day"])')
      ->swap('outerHTML')
      ->applyTo($form['month']);

这将以下HTMX属性添加到表单元素中。

  • data-hx-post - 这将向表单发送一个POST请求。由于我们没有为方法添加值,它将自动使用当前路由。
  • data-hx-select - 在处理Drupal中的HTMX表单时,选择属性至关重要。如果您阅读过我之前的文章,您会记得当我们发出HTMX请求时,我们会从服务器获取整个表单。因此,我们需要告诉HTMX从响应中只挑选出我们需要的元素。在这种情况下,我们使用选择器字符串 *:has(>select[name="day"]),它告诉HTMX我们要找到名称为 "day" 的选择元素的 父元素,即我们的日期选择元素。
  • data-hx-target - 这告诉HTMX将我们从响应中提取的元素放置在哪里。我们使用与data-hx-select属性相同的选择器字符串,我们想要替换页面上的日期选择元素。
  • data-hs-swap - 最后,我们将交换策略设置为outerHTML,这告诉HTMX用响应中选择的值完全替换目标元素。

创建另一个 Htmx Drupal对象,以将我们需要的属性添加到年份选择元素上。

    (new Htmx())
      ->post()
      ->select('*:has(>select[name="month"])')
      ->target('*:has(>select[name="month"])')
      // 我们还使用越界选择来针对edit-day ID(即选择元素),以替换日期选择。这可以处理选择2月29日且选择非闰年的边缘情况。
      ->selectOob('#edit-day')
      ->swap('outerHTML')
      ->applyTo($form['year']);

年份表单元素的主要新增内容是添加了 selectOob() 方法。这将 data-hx-select-oob 属性添加到年份字段,该属性在年份触发器的响应中针对日期字段。

如果您正在阅读这篇文章并想知道为什么我不能重用月份字段的 data-hx-selectdata-hx-target 属性的选择器字符串,那是因为该选择器在使用越界选择时似乎会导致HTMX出现问题。HTMX似乎希望属性中存在一个ID,所以如果我们传递一个以 "*" 开头的字符串,它会尝试将其作为一个id名称,如 "#*",这对于选择器来说是无效的语法。

四、HTMX工作流程

这些HTMX元素以以下方式工作。

请注意,我已经简化并删除了大量标记,以使内容更易于阅读和理解。

我们在这里要做的是选择日期 "2024年2月29日",这是一个闰年,然后选择2023年,这不是闰年。这会导致日期选择被取消选择,因为记录29不再存在。

表单被创建,其中包含以下元素。

<div class="js-form-item form-item form-type-select js-form-type-select form-item-year js-form-item-year">
<select data-hx-post="" data-hx-select="*:has(&gt;select[name=&quot;month&quot;])" data-hx-target="*:has(&gt;select[name=&quot;month&quot;])" data-hx-select-oob="edit-day" data-hx-swap="outerHTML ignoreTitle:true" data-drupal-selector="edit-year" id="edit-year" name="year" class="form-select form-element form-element--type-select">
  <option value="" selected="selected">- Select -</option>
  <option value="2019">2019</option>
  ...
  <option value="2024">2024</option>
  ...
  <option value="2050">2050</option>
</select>
</div>

<div class="js-form-item form-item form-type-select js-form-type-select form-item-month js-form-item-month" style="display: none;">
<select data-hx-post="" data-hx-select="*:has(&gt;select[name=&quot;day&quot;])

联系我们

提供基于Drupal的门户网站、电子商务网站、移动应用开发及托管服务

长按加微信
长风云微信
长按关注公众号
长风云公众号