跳到内容

如何使用表单主题

编辑此页

本文介绍了如何在你的应用中使用 Symfony 提供的任何表单主题,以及如何创建你自己的自定义表单主题。

Symfony 内置表单主题

Symfony 自带了几个内置表单主题,使用一些最流行的 CSS 框架时,它们能让你的表单看起来很棒。每个主题都在一个单独的 Twig 模板中定义,并且在 twig.form_themes 选项中启用

提示

阅读关于 Bootstrap 4 Symfony 表单主题Bootstrap 5 Symfony 表单主题 的文章,以了解更多信息。

将主题应用于所有表单

Symfony 表单默认使用 form_div_layout.html.twig 主题。如果你想为应用的所有表单使用另一个主题,请在 twig.form_themes 选项中配置它

1
2
3
4
# config/packages/twig.yaml
twig:
    form_themes: ['bootstrap_5_horizontal_layout.html.twig']
    # ...

你可以将多个主题传递给此选项,因为有时表单主题仅重新定义一些元素。这样,如果某些主题没有覆盖某些元素,Symfony 会在其他主题中查找。

twig.form_themes 选项中主题的顺序很重要。每个主题都会覆盖所有先前的主题,因此你必须将最重要的主题放在列表的末尾。

将主题应用于单个表单

尽管大多数时候你将全局应用表单主题,但你可能需要仅将主题应用于某些特定表单。你可以使用 form_theme Twig 标签 来做到这一点

1
2
3
4
5
6
{# this form theme will be applied only to the form of this template #}
{% form_theme form 'foundation_5_layout.html.twig' %}

{{ form_start(form) }}
    {# ... #}
{{ form_end(form) }}

form_theme 标签的第一个参数(本例中为 form)是存储表单视图对象的变量名称。第二个参数是定义表单主题的 Twig 模板的路径。

将多个主题应用于单个表单

一个表单也可以通过应用多个主题进行自定义。为此,请使用 with 关键字传递所有 Twig 模板的路径作为数组(它们的顺序很重要,因为每个主题都会覆盖所有先前的主题)

1
2
3
4
5
6
7
{# apply multiple form themes but only to the form of this template #}
{% form_theme form with [
    'foundation_5_layout.html.twig',
    'form/my_custom_theme.html.twig'
] %}

{# ... #}

将不同主题应用于子表单

你还可以将表单主题应用于表单的特定子项

1
{% form_theme form.a_child_form 'form/my_custom_theme.html.twig' %}

当你想要为嵌套表单设置与主表单不同的自定义主题时,这很有用。指定你的两个主题

1
2
{% form_theme form 'form/my_custom_theme.html.twig' %}
{% form_theme form.a_child_form 'form/my_other_theme.html.twig' %}

为单个表单禁用全局主题

在应用程序中定义的全局表单主题始终应用于所有表单,即使是那些使用 form_theme 标签来应用自己的主题的表单也是如此。你可能希望禁用此功能,例如在为可以安装在不同 Symfony 应用程序上的 bundle 创建管理界面时(因此你无法控制全局启用了哪些主题)。为此,在表单主题列表后添加 only 关键字

1
2
3
{% form_theme form with ['foundation_5_layout.html.twig'] only %}

{# ... #}

警告

当使用 only 关键字时,Symfony 的任何内置表单主题(form_div_layout.html.twig 等)都不会被应用。为了正确渲染你的表单,你需要自己提供一个功能齐全的表单主题,或者使用 Twig 的 use 关键字而不是 extends 扩展内置表单主题,以重用原始主题内容。

1
2
3
4
{# templates/form/common.html.twig #}
{% use "form_div_layout.html.twig" %}

{# ... #}

创建你自己的表单主题

Symfony 使用 Twig 块来渲染表单的每个部分 - 字段标签、错误、<input> 文本字段、<select> 标签等。主题是一个 Twig 模板,其中包含你想要在渲染表单时使用的一个或多个块。

例如,考虑一个表示名为 age 的整数属性的表单字段。如果你将其添加到模板中

1
{{ form_widget(form.age) }}

生成的 HTML 内容将类似于以下内容(它会根据你的应用程序中启用的表单主题而有所不同)

1
<input type="number" id="form_age" name="form[age]" required="required" value="33">

Symfony 使用名为 integer_widget 的 Twig 块来渲染该字段。这是因为字段类型是 integer,并且你正在渲染它的 widget(而不是它的 labelerrorshelp)。创建表单主题的第一步是知道要覆盖哪个 Twig 块,如下节所述。

表单片段命名

表单片段的命名根据你的需求而有所不同

  • 如果你想自定义相同类型的所有字段(例如,所有 <textarea>),请使用 field-type_field-part 模式(例如,textarea_widget)。
  • 如果你想仅自定义一个特定字段(例如,用于编辑产品的表单的 description 字段的 <textarea>),请使用 _field-id_field-part 模式(例如,_product_description_widget)。

在这两种情况下,field-part 可以是以下任何有效的表单字段部分

相同类型的所有字段的片段命名

这些片段名称遵循 type_part 模式,其中 type 对应于正在渲染的字段类型(例如,textareacheckboxdate 等),而 part 对应于正在渲染的内容(例如,labelwidget 等)

一些片段名称的示例包括

  • form_row - 由 form_row() 用于渲染大多数字段;
  • textarea_widget - 由 form_widget() 用于渲染 textarea 字段类型;
  • form_errors - 由 form_errors() 用于渲染字段的错误;

单个字段的片段命名

这些片段名称遵循 _id_part 模式,其中 id 对应于字段 id 属性(例如,product_descriptionuser_age 等),而 part 对应于正在渲染的内容(例如,labelwidget 等)

id 属性同时包含表单名称和字段名称(例如,product_price)。表单名称可以手动设置,也可以根据你的表单类型名称自动生成(例如,ProductType 等同于 product)。如果你不确定你的表单名称是什么,请查看为你的表单渲染的 HTML 代码。你还可以使用 block_name 选项显式定义此值

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options): void
{
    // ...

    $builder->add('name', TextType::class, [
        'block_name' => 'custom_name',
    ]);
}

在此示例中,片段名称将是 _product_custom_name_widget 而不是默认的 _product_name_widget

单个字段的自定义片段命名

block_prefix 选项允许表单字段定义它们自己的自定义片段名称。这主要用于自定义同一字段的一些实例,而无需创建自定义表单类型

1
2
3
4
5
6
7
8
9
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder->add('name', TextType::class, [
        'block_prefix' => 'wrapped_text',
    ]);
}

现在你可以使用 wrapped_text_rowwrapped_text_widget 等作为块名称。

集合的片段命名

当使用表单集合时,你有多种方法可以自定义集合及其每个条目。首先,使用以下块自定义所有表单集合的每个部分

1
2
3
4
5
{% block collection_row %} ... {% endblock %}
{% block collection_label %} ... {% endblock %}
{% block collection_widget %} ... {% endblock %}
{% block collection_help %} ... {% endblock %}
{% block collection_errors %} ... {% endblock %}

你还可以使用以下块自定义所有集合的每个条目

1
2
3
4
5
{% block collection_entry_row %} ... {% endblock %}
{% block collection_entry_label %} ... {% endblock %}
{% block collection_entry_widget %} ... {% endblock %}
{% block collection_entry_help %} ... {% endblock %}
{% block collection_entry_errors %} ... {% endblock %}

最后,你可以自定义特定的表单集合,而不是所有集合。例如,考虑以下复杂示例,其中 TaskManagerType 具有 TaskListType 的集合,而 TaskListType 又具有 TaskType 的集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class TaskManagerType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options = []): void
    {
        // ...
        $builder->add('taskLists', CollectionType::class, [
            'entry_type' => TaskListType::class,
            'block_name' => 'task_lists',
        ]);
    }
}

class TaskListType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options = []): void
    {
        // ...
        $builder->add('tasks', CollectionType::class, [
            'entry_type' => TaskType::class,
        ]);
    }
}

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options = []): void
    {
        $builder->add('name');
        // ...
    }
}

然后,你将获得以下所有可自定义的块(其中 * 可以替换为 rowwidgetlabelhelp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{% block _task_manager_task_lists_* %}
    {# the collection field of TaskManager #}
{% endblock %}

{% block _task_manager_task_lists_entry_* %}
    {# the inner TaskListType #}
{% endblock %}

{% block _task_manager_task_lists_entry_tasks_* %}
    {# the collection field of TaskListType #}
{% endblock %}

{% block _task_manager_task_lists_entry_tasks_entry_* %}
    {# the inner TaskType #}
{% endblock %}

{% block _task_manager_task_lists_entry_tasks_entry_name_* %}
    {# the field of TaskType #}
{% endblock %}

模板片段继承

每个字段类型都有一个类型(例如,textarea 的父类型是 text,而 text 的父类型是 form),如果基本片段不存在,Symfony 将使用父类型的片段。

例如,当 Symfony 渲染 textarea 类型的错误时,它首先查找 textarea_errors 片段,然后再回退到 text_errorsform_errors 片段。

提示

每个字段类型的“父”类型在每个字段类型的表单类型参考中可用。

在与表单相同的模板中创建表单主题

当在你的应用程序中进行特定于单个表单的自定义时,建议这样做,例如更改表单的所有 <textarea> 元素或自定义将使用 JavaScript 处理的非常特殊的表单字段。

你只需要将特殊的 {% form_theme form _self %} 标签添加到渲染表单的同一模板中。这会导致 Twig 在模板内查找任何被覆盖的表单块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{% extends 'base.html.twig' %}

{% form_theme form _self %}

{# this overrides the widget of any field of type integer, but only in the
   forms rendered inside this template #}
{% block integer_widget %}
    <div class="...">
        {# ... render the HTML element to display this field ... #}
    </div>
{% endblock %}

{# this overrides the entire row of the field whose "id" = "product_stock" (and whose
   "name" = "product[stock]") but only in the forms rendered inside this template #}
{% block _product_stock_row %}
    <div class="..." id="...">
        {# ... render the entire field contents, including its errors ... #}
    </div>
{% endblock %}

{# ... render the form ... #}

此方法的主要缺点是它仅在你的模板扩展了另一个模板时才有效(上例中为 'base.html.twig')。如果你的模板没有扩展任何模板,则必须将 form_theme 指向单独的模板,如下节所述。

另一个缺点是,在渲染其他模板中的其他表单时,无法重用自定义的表单块。如果这是你需要的,请在单独的模板中创建表单主题,如下节所述。

在单独的模板中创建表单主题

当创建在你的整个应用程序中使用甚至在不同的 Symfony 应用程序中重用的表单主题时,建议这样做。你只需要在某处创建一个 Twig 模板,并遵循表单片段命名规则,以了解要定义哪些 Twig 块。

例如,如果你的表单主题很简单,并且你只想覆盖 <input type="integer"> 元素,请创建此模板

1
2
3
4
5
6
{# templates/form/my_theme.html.twig #}
{% block integer_widget %}

    {# ... add all the HTML, CSS and JavaScript needed to render this field #}

{% endblock %}

现在你需要告诉 Symfony 使用此表单主题而不是(或除了)默认主题。如本文前面的部分所述,如果你想将主题全局应用于所有表单,请定义 twig.form_themes 选项

1
2
3
4
# config/packages/twig.yaml
twig:
    form_themes: ['form/my_theme.html.twig']
    # ...

如果你只想将其应用于某些特定表单,请使用 form_theme 标签

1
2
3
4
5
{% form_theme form 'form/my_theme.html.twig' %}

{{ form_start(form) }}
    {# ... #}
{{ form_end(form) }}

重用内置表单主题的部分内容

创建一个完整的表单主题需要大量工作,因为有太多不同的表单字段类型。你可以只定义你感兴趣的块,然后在你的应用程序或模板中配置多个表单主题,而不是定义所有这些 Twig 块。这是可行的,因为当渲染自定义主题中未覆盖的块时,Symfony 会回退到其他主题。

另一种解决方案是使你的表单主题模板从内置主题之一扩展,使用 Twig "use" 标签 而不是 extends 标签,这样你就可以继承其所有块(如果你不确定,请从默认的 form_div_layout.html.twig 主题扩展)。

1
2
3
4
{# templates/form/my_theme.html.twig #}
{% use 'form_div_layout.html.twig' %}

{# ... override only the blocks you are interested in #}

最后,你还可以使用 Twig parent() 函数 来重用内置主题的原始内容。当你只想进行小的更改时,例如用某个元素包装生成的 HTML,这非常有用。

1
2
3
4
5
6
7
8
{# templates/form/my_theme.html.twig #}
{% use 'form_div_layout.html.twig' %}

{% block integer_widget %}
    <div class="some-custom-class">
        {{ parent() }}
    </div>
{% endblock %}

当在渲染表单的同一模板中定义表单主题时,此技术也有效。但是,从内置主题导入块会稍微复杂一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
{% form_theme form _self %}

{# import a block from the built-in theme and rename it so it doesn't
   conflict with the same block defined in this template #}
{% use 'form_div_layout.html.twig' with integer_widget as base_integer_widget %}

{% block integer_widget %}
    <div class="some-custom-class">
        {{ block('base_integer_widget') }}
    </div>
{% endblock %}

{# ... render the form ... #}

自定义表单验证错误

如果你为你的对象定义了 验证规则,当提交的数据无效时,你将看到一些验证错误消息。这些消息通过 form_errors() 函数显示,并且可以使用任何表单主题中的 form_errors Twig 块进行自定义,如前面的章节中所述。

需要考虑的一个重要事项是,某些错误与整个表单而不是特定字段相关联。为了区分全局错误和局部错误,请使用表单中可用的变量之一,名为 compound。如果它是 true,则表示当前渲染的是字段集合(例如,整个表单),而不仅仅是一个单独的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{# templates/form/my_theme.html.twig #}
{% block form_errors %}
    {% if errors|length > 0 %}
        {% if compound %}
            {# ... display the global form errors #}
            <ul>
                {% for error in errors %}
                    <li>{{ error.message }}</li>
                {% endfor %}
            </ul>
        {% else %}
            {# ... display the errors for a single field #}
        {% endif %}
    {% endif %}
{% endblock form_errors %}
本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本