Files
archived-symfony-docs/form/form_themes.rst
Timo Bakx 3849004fcc Replaced caution blocks with warning
Fixes #20371

Both blocks are currently rendered identically. Keeping only one of the two makes it easier to contribute.

Some blocks were elevated to danger.
2024-12-07 12:02:55 +01:00

647 lines
25 KiB
ReStructuredText

How to Work with Form Themes
============================
This article explains how to use in your app any of the form themes provided by
Symfony and how to create your own custom form theme.
.. _symfony-builtin-forms:
Symfony Built-In Form Themes
----------------------------
Symfony comes with several **built-in form themes** that make your forms look
great when using some of the most popular CSS frameworks. Each theme is defined
in a single Twig template and they are enabled in the
:ref:`twig.form_themes <config-twig-form-themes>` option:
* `form_div_layout.html.twig`_, wraps each form field inside a ``<div>`` element
and it's the theme used by default in Symfony applications unless you configure
it as explained later in this article.
* `form_table_layout.html.twig`_, wraps the entire form inside a ``<table>``
element and each form field inside a ``<tr>`` element.
* `bootstrap_3_layout.html.twig`_, wraps each form field inside a ``<div>``
element with the appropriate CSS classes to apply the styles used by the
`Bootstrap 3 CSS framework`_.
* `bootstrap_3_horizontal_layout.html.twig`_, it's similar to the previous
theme, but the CSS classes applied are the ones used to display the forms
horizontally (i.e. the label and the widget in the same row).
* `bootstrap_4_layout.html.twig`_, same as ``bootstrap_3_layout.html.twig``, but
updated for `Bootstrap 4 CSS framework`_ styles.
* `bootstrap_4_horizontal_layout.html.twig`_, same as
``bootstrap_3_horizontal_layout.html.twig`` but updated for Bootstrap 4 styles.
* `bootstrap_5_layout.html.twig`_, same as ``bootstrap_4_layout.html.twig``, but
updated for `Bootstrap 5 CSS framework`_ styles.
* `bootstrap_5_horizontal_layout.html.twig`_, same as
``bootstrap_4_horizontal_layout.html.twig`` but updated for Bootstrap 5 styles.
* `foundation_5_layout.html.twig`_, wraps each form field inside a ``<div>``
element with the appropriate CSS classes to apply the default styles of the
version 5 of `Foundation CSS framework`_.
* `foundation_6_layout.html.twig`_, wraps each form field inside a ``<div>``
element with the appropriate CSS classes to apply the default styles of the
version 6 of `Foundation CSS framework`_.
* `tailwind_2_layout.html.twig`_, wraps each form field inside a ``<div>``
element with the absolute minimum styles to make them usable. It is based on the
`Tailwind CSS form plugin`_.
.. tip::
Read the articles about :doc:`Bootstrap 4 Symfony form theme </form/bootstrap4>` and :doc:`Bootstrap 5 Symfony form theme </form/bootstrap5>`
to learn more about them.
.. _forms-theming-global:
.. _forms-theming-twig:
Applying Themes to all Forms
----------------------------
Symfony forms use by default the ``form_div_layout.html.twig`` theme. If you
want to use another theme for all the forms of your app, configure it in the
``twig.form_themes`` option:
.. configuration-block::
.. code-block:: yaml
# config/packages/twig.yaml
twig:
form_themes: ['bootstrap_5_horizontal_layout.html.twig']
# ...
.. code-block:: xml
<!-- config/packages/twig.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:twig="http://symfony.com/schema/dic/twig"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd">
<twig:config>
<twig:form-theme>bootstrap_5_horizontal_layout.html.twig</twig:form-theme>
<!-- ... -->
</twig:config>
</container>
.. code-block:: php
// config/packages/twig.php
use Symfony\Config\TwigConfig;
return static function (TwigConfig $twig): void {
$twig->formThemes([
'bootstrap_5_horizontal_layout.html.twig',
]);
// ...
};
You can pass multiple themes to this option because sometimes form themes only
redefine a few elements. This way, if some theme doesn't override some element,
Symfony looks up in the other themes.
The order of the themes in the ``twig.form_themes`` option is important. Each
theme overrides all the previous themes, so you must put the most important
themes at the end of the list.
Applying Themes to Single Forms
-------------------------------
Although most of the times you'll apply form themes globally, you may need to
apply a theme only to some specific form. You can do that with the
:ref:`form_theme Twig tag <reference-twig-tag-form-theme>`:
.. code-block:: twig
{# 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) }}
The first argument of the ``form_theme`` tag (``form`` in this example) is the
name of the variable that stores the form view object. The second argument is
the path of the Twig template that defines the form theme.
Applying Multiple Themes to Single Forms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A form can also be customized by applying several themes. To do this, pass the
path of all the Twig templates as an array using the ``with`` keyword (their
order is important, because each theme overrides all the previous ones):
.. code-block:: twig
{# 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'
] %}
{# ... #}
Applying Different Themes to Child Forms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can also apply a form theme to a specific child of your form:
.. code-block:: twig
{% form_theme form.a_child_form 'form/my_custom_theme.html.twig' %}
This is useful when you want to have a custom theme for a nested form that's
different from the one of your main form. Specify both your themes:
.. code-block:: twig
{% form_theme form 'form/my_custom_theme.html.twig' %}
{% form_theme form.a_child_form 'form/my_other_theme.html.twig' %}
.. _disabling-global-themes-for-single-forms:
Disabling Global Themes for Single Forms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Global form themes defined in the app are always applied to all forms, even
those which use the ``form_theme`` tag to apply their own themes. You may want
to disable this for example when creating an admin interface for a bundle which
can be installed on different Symfony applications (and so you can't control what
themes are enabled globally). To do that, add the ``only`` keyword after the list
of form themes:
.. code-block:: twig
{% form_theme form with ['foundation_5_layout.html.twig'] only %}
{# ... #}
.. warning::
When using the ``only`` keyword, none of Symfony's built-in form themes
(``form_div_layout.html.twig``, etc.) will be applied. In order to render
your forms correctly, you need to either provide a fully-featured form theme
yourself, or extend one of the built-in form themes with Twig's ``use``
keyword instead of ``extends`` to re-use the original theme contents.
.. code-block:: twig
{# templates/form/common.html.twig #}
{% use "form_div_layout.html.twig" %}
{# ... #}
.. _create-your-own-form-theme:
Creating your Own Form Theme
----------------------------
Symfony uses Twig blocks to render each part of a form - field labels, errors,
``<input>`` text fields, ``<select>`` tags, etc. A *theme* is a Twig template
with one or more of those blocks that you want to use when rendering a form.
Consider for example a form field that represents an integer property called
``age``. If you add this to the template:
.. code-block:: twig
{{ form_widget(form.age) }}
The generated HTML content will be something like this (it will vary depending
upon the form themes enabled in your app):
.. code-block:: html
<input type="number" id="form_age" name="form[age]" required="required" value="33">
Symfony uses a Twig block called ``integer_widget`` to render that field. This
is because the field type is ``integer`` and you're rendering its ``widget`` (as
opposed to its ``label`` or ``errors`` or ``help``). The first step to create a
form theme is to know which Twig block to override, as explained in the
following section.
.. _form-customization-sidebar:
.. _form-fragment-naming:
Form Fragment Naming
~~~~~~~~~~~~~~~~~~~~
The naming of form fragments varies depending on your needs:
* If you want to customize **all fields of the same type** (e.g. all ``<textarea>``)
use the ``field-type_field-part`` pattern (e.g. ``textarea_widget``).
* If you want to customize **only one specific field** (e.g. the ``<textarea>``
used for the ``description`` field of the form that edits products) use the
``_field-id_field-part`` pattern (e.g. ``_product_description_widget``).
In both cases, the ``field-part`` can be any of these valid form field parts:
.. raw:: html
<object data="../_images/form/form-field-parts.svg" type="image/svg+xml"
alt="A wireframe showing all form field parts: form_row, form_label, form_widget, form_help and form_errors."
></object>
Fragment Naming for All Fields of the Same Type
...............................................
These fragment names follow the ``type_part`` pattern, where the ``type``
corresponds to the field *type* being rendered (e.g. ``textarea``, ``checkbox``,
``date``, etc) and the ``part`` corresponds to *what* is being rendered (e.g.
``label``, ``widget``, etc.)
A few examples of fragment names are:
* ``form_row`` - used by :ref:`form_row() <reference-forms-twig-row>` to render
most fields;
* ``textarea_widget`` - used by :ref:`form_widget() <reference-forms-twig-widget>`
to render a ``textarea`` field type;
* ``form_errors`` - used by :ref:`form_errors() <reference-forms-twig-errors>`
to render errors for a field;
Fragment Naming for Individual Fields
.....................................
These fragment names follow the ``_id_part`` pattern, where the ``id``
corresponds to the field ``id`` attribute (e.g. ``product_description``,
``user_age``, etc) and the ``part`` corresponds to *what* is being rendered
(e.g. ``label``, ``widget``, etc.)
The ``id`` attribute contains both the form name and the field name (e.g.
``product_price``). The form name can be set manually or generated automatically
based on your form type name (e.g. ``ProductType`` equates to ``product``). If
you're not sure what your form name is, look at the HTML code rendered for your
form. You can also define this value explicitly with the ``block_name`` option::
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',
]);
}
In this example, the fragment name will be ``_product_custom_name_widget``
instead of the default ``_product_name_widget``.
.. _form-fragment-custom-naming:
Custom Fragment Naming for Individual Fields
............................................
The ``block_prefix`` option allows form fields to define their own custom
fragment name. This is mostly useful to customize some instances of the same
field without having to :doc:`create a custom form type </form/create_custom_field_type>`::
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',
]);
}
Now you can use ``wrapped_text_row``, ``wrapped_text_widget``, etc. as the block
names.
.. _form-custom-prototype:
Fragment Naming for Collections
...............................
When using a :doc:`collection of forms </form/form_collections>`, you have
several ways of customizing the collection and each of its entries. First,
use the following blocks to customize each part of all form collections:
.. code-block:: twig
{% block collection_row %} ... {% endblock %}
{% block collection_label %} ... {% endblock %}
{% block collection_widget %} ... {% endblock %}
{% block collection_help %} ... {% endblock %}
{% block collection_errors %} ... {% endblock %}
You can also customize each entry of all collections with the following blocks:
.. code-block:: twig
{% 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 %}
Finally, you can customize specific form collections instead of all of them.
For example, consider the following complex example where a ``TaskManagerType``
has a collection of ``TaskListType`` which in turn has a collection of
``TaskType``::
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');
// ...
}
}
Then you get all the following customizable blocks (where ``*`` can be replaced
by ``row``, ``widget``, ``label``, or ``help``):
.. code-block:: twig
{% 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 %}
Template Fragment Inheritance
.............................
Each field type has a *parent* type (e.g. the parent type of ``textarea`` is
``text``, and the parent type of ``text`` is ``form``) and Symfony uses the
fragment for the parent type if the base fragment doesn't exist.
When Symfony renders for example the errors for a textarea type, it looks first
for a ``textarea_errors`` fragment before falling back to the ``text_errors``
and ``form_errors`` fragments.
.. tip::
The "parent" type of each field type is available in the
:doc:`form type reference </reference/forms/types>` for each field type.
Creating a Form Theme in the same Template as the Form
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is recommended when doing customizations specific to a single form in your
app, such as changing all ``<textarea>`` elements of a form or customizing a
very special form field which will be handled with JavaScript.
You only need to add the special ``{% form_theme form _self %}`` tag to the same
template where the form is rendered. This causes Twig to look inside the template
for any overridden form blocks:
.. code-block:: html+twig
{% 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 ... #}
The main disadvantage of this method is that it only works if your template
extends another (``'base.html.twig'`` in the previous example). If your template
does not, you must point ``form_theme`` to a separate template, as explained in
the next section.
Another disadvantage is that the customized form blocks can't be reused when
rendering other forms in other templates. If that's what you need, create a form
theme in a separate template as explained in the next section.
Creating a Form Theme in a Separate Template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is recommended when creating form themes that are used in your entire app
or even reused in different Symfony applications. You only need to create a Twig
template somewhere and follow the :ref:`form fragment naming <form-fragment-naming>`
rules to know which Twig blocks to define.
For example, if your form theme is simple and you only want to override the
``<input type="integer">`` elements, create this template:
.. code-block:: twig
{# templates/form/my_theme.html.twig #}
{% block integer_widget %}
{# ... add all the HTML, CSS and JavaScript needed to render this field #}
{% endblock %}
Now you need to tell Symfony to use this form theme instead of (or in addition
to) the default theme. As explained in the previous sections of this article, if
you want to apply the theme globally to all forms, define the
``twig.form_themes`` option:
.. configuration-block::
.. code-block:: yaml
# config/packages/twig.yaml
twig:
form_themes: ['form/my_theme.html.twig']
# ...
.. code-block:: xml
<!-- config/packages/twig.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:twig="http://symfony.com/schema/dic/twig"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd">
<twig:config>
<twig:form-theme>form/my_theme.html.twig</twig:form-theme>
<!-- ... -->
</twig:config>
</container>
.. code-block:: php
// config/packages/twig.php
use Symfony\Config\TwigConfig;
return static function (TwigConfig $twig): void {
$twig->formThemes([
'form/my_theme.html.twig',
]);
// ...
};
If you only want to apply it to some specific forms, use the ``form_theme`` tag:
.. code-block:: twig
{% form_theme form 'form/my_theme.html.twig' %}
{{ form_start(form) }}
{# ... #}
{{ form_end(form) }}
.. _referencing-base-form-blocks-twig-specific:
Reusing Parts of a Built-In Form Theme
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Creating a complete form theme takes a lot of work because there are too many
different form field types. Instead of defining all those Twig blocks, you can
define only the blocks you are interested in and then configure multiple form
themes in your app or template. This works because when rendering a block which
is not overridden in your custom theme, Symfony falls back to the other themes.
Another solution is to make your form theme template extend from one of the
built-in themes using the `Twig "use" tag`_ instead of the ``extends`` tag so
you can inherit all its blocks (if you are unsure, extend from the default
``form_div_layout.html.twig`` theme):
.. code-block:: twig
{# templates/form/my_theme.html.twig #}
{% use 'form_div_layout.html.twig' %}
{# ... override only the blocks you are interested in #}
Finally, you can also use the `Twig parent() function`_ to reuse the original
content of the built-in theme. This is useful when you only want to make minor
changes, such as wrapping the generated HTML with some element:
.. code-block:: html+twig
{# templates/form/my_theme.html.twig #}
{% use 'form_div_layout.html.twig' %}
{% block integer_widget %}
<div class="some-custom-class">
{{ parent() }}
</div>
{% endblock %}
This technique also works when defining the form theme in the same template that
renders the form. However, importing the blocks from the built-in themes is a
bit more complicated:
.. code-block:: html+twig
{% 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 ... #}
Customizing the Form Validation Errors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you define :doc:`validation rules </validation>` for your objects, you'll see
some validation error messages when the submitted data is not valid. These
messages are displayed with the :ref:`form_errors() <reference-forms-twig-errors>`
function and can be customized with the ``form_errors`` Twig block in any form
theme, as explained in the previous sections.
An important thing to consider is that certain errors are associated to the
entire form instead of a specific field. In order to differentiate between
global and local errors, use one of the
:ref:`variables available in forms <reference-form-twig-variables>` called
``compound``. If it is ``true``, it means that what's being currently rendered
is a collection of fields (e.g. a whole form), and not just an individual field:
.. code-block:: html+twig
{# 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 %}
.. _`form_div_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig
.. _`form_table_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig
.. _`bootstrap_3_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig
.. _`bootstrap_3_horizontal_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig
.. _`bootstrap_4_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig
.. _`bootstrap_4_horizontal_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig
.. _`bootstrap_5_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig
.. _`bootstrap_5_horizontal_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_horizontal_layout.html.twig
.. _`Bootstrap 3 CSS framework`: https://getbootstrap.com/docs/3.4/
.. _`Bootstrap 4 CSS framework`: https://getbootstrap.com/docs/4.6/
.. _`Bootstrap 5 CSS framework`: https://getbootstrap.com/docs/5.0/
.. _`foundation_5_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig
.. _`foundation_6_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_6_layout.html.twig
.. _`Foundation CSS framework`: https://get.foundation/
.. _`tailwind_2_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/tailwind_2_layout.html.twig
.. _`Tailwind CSS form plugin`: https://tailwindcss-forms.vercel.app/
.. _`Twig "use" tag`: https://twig.symfony.com/doc/3.x/tags/use.html
.. _`Twig parent() function`: https://twig.symfony.com/doc/3.x/functions/parent.html