如何使用表单主题
本文介绍了如何在你的应用中使用 Symfony 提供的任何表单主题,以及如何创建你自己的自定义表单主题。
Symfony 内置表单主题
Symfony 自带了几个内置表单主题,使用一些最流行的 CSS 框架时,它们能让你的表单看起来很棒。每个主题都在一个单独的 Twig 模板中定义,并且在 twig.form_themes 选项中启用
- form_div_layout.html.twig,将每个表单字段包裹在
<div>
元素中,并且它是 Symfony 应用程序中的默认主题,除非你按照本文稍后解释的方式进行配置。 - form_table_layout.html.twig,将整个表单包裹在
<table>
元素中,并将每个表单字段包裹在<tr>
元素中。 - bootstrap_3_layout.html.twig,将每个表单字段包裹在
<div>
元素中,并带有适当的 CSS 类,以应用 Bootstrap 3 CSS 框架 使用的样式。 - bootstrap_3_horizontal_layout.html.twig,与前一个主题类似,但应用的 CSS 类是用于水平显示表单的类(即标签和小部件在同一行)。
- bootstrap_4_layout.html.twig,与
bootstrap_3_layout.html.twig
相同,但为 Bootstrap 4 CSS 框架 样式更新。 - bootstrap_4_horizontal_layout.html.twig,与
bootstrap_3_horizontal_layout.html.twig
相同,但为 Bootstrap 4 样式更新。 - bootstrap_5_layout.html.twig,与
bootstrap_4_layout.html.twig
相同,但为 Bootstrap 5 CSS 框架 样式更新。 - bootstrap_5_horizontal_layout.html.twig,与
bootstrap_4_horizontal_layout.html.twig
相同,但为 Bootstrap 5 样式更新。 - foundation_5_layout.html.twig,将每个表单字段包裹在
<div>
元素中,并带有适当的 CSS 类,以应用 Foundation CSS 框架 版本 5 的默认样式。 - foundation_6_layout.html.twig,将每个表单字段包裹在
<div>
元素中,并带有适当的 CSS 类,以应用 Foundation CSS 框架 版本 6 的默认样式。 - tailwind_2_layout.html.twig,将每个表单字段包裹在
<div>
元素中,并带有使其可用的绝对最小样式。它基于 Tailwind CSS 表单插件。
提示
阅读关于 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
(而不是它的 label
或 errors
或 help
)。创建表单主题的第一步是知道要覆盖哪个 Twig 块,如下节所述。
表单片段命名
表单片段的命名根据你的需求而有所不同
- 如果你想自定义相同类型的所有字段(例如,所有
<textarea>
),请使用field-type_field-part
模式(例如,textarea_widget
)。 - 如果你想仅自定义一个特定字段(例如,用于编辑产品的表单的
description
字段的<textarea>
),请使用_field-id_field-part
模式(例如,_product_description_widget
)。
在这两种情况下,field-part
可以是以下任何有效的表单字段部分
相同类型的所有字段的片段命名
这些片段名称遵循 type_part
模式,其中 type
对应于正在渲染的字段类型(例如,textarea
、checkbox
、date
等),而 part
对应于正在渲染的内容(例如,label
、widget
等)
一些片段名称的示例包括
form_row
- 由 form_row() 用于渲染大多数字段;textarea_widget
- 由 form_widget() 用于渲染textarea
字段类型;form_errors
- 由 form_errors() 用于渲染字段的错误;
单个字段的片段命名
这些片段名称遵循 _id_part
模式,其中 id
对应于字段 id
属性(例如,product_description
、user_age
等),而 part
对应于正在渲染的内容(例如,label
、widget
等)
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_row
、wrapped_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');
// ...
}
}
然后,你将获得以下所有可自定义的块(其中 *
可以替换为 row
、widget
、label
或 help
)
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>
元素或自定义将使用 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 %}