[TwigBridge] Add form templates for Bootstrap 5

This commit is contained in:
Romain Monteil 2020-11-24 11:39:29 +01:00 committed by Fabien Potencier
parent 926f87ff51
commit d52d0969ab
10 changed files with 2966 additions and 6 deletions

View File

@ -2,10 +2,11 @@ CHANGELOG
=========
5.3
-----
---
* Add a new `markAsPublic` method on `NotificationEmail` to change the `importance` context option to null after creation
* Add a new `fragment_uri()` helper to generate the URI of a fragment
* Add support of Bootstrap 5 for form theming
5.3.0
-----

View File

@ -54,6 +54,11 @@
{%- endif -%}
{%- endblock radio_widget %}
{% block choice_widget_collapsed -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%}
{{- parent() -}}
{%- endblock choice_widget_collapsed %}
{# Labels #}
{% block form_label -%}

View File

@ -202,6 +202,11 @@
{%- endif -%}
{%- endblock radio_widget %}
{% block choice_widget_collapsed -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%}
{{- parent() -}}
{%- endblock choice_widget_collapsed %}
{% block choice_widget_expanded -%}
<div {{ block('widget_container_attributes') }}>
{%- for child in form %}

View File

@ -0,0 +1,130 @@
{% use "bootstrap_5_layout.html.twig" %}
{# Labels #}
{% block form_label -%}
{%- if label is same as(false) -%}
<div class="{{ block('form_label_class') }}"></div>
{%- else -%}
{%- set row_class = row_class|default(row_attr.class|default('')) -%}
{%- if 'form-floating' not in row_class and 'input-group' not in row_class -%}
{%- if expanded is not defined or not expanded -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-label')|trim}) -%}
{%- endif -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ block('form_label_class'))|trim}) -%}
{%- endif -%}
{{- parent() -}}
{%- endif -%}
{%- endblock form_label %}
{% block form_label_class -%}
col-sm-2
{%- endblock form_label_class %}
{# Rows #}
{% block form_row -%}
{%- if expanded is defined and expanded -%}
{{ block('fieldset_form_row') }}
{%- else -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
{%- set row_class = row_class|default(row_attr.class|default('mb-3')) -%}
{%- set is_form_floating = is_form_floating|default('form-floating' in row_class) -%}
{%- set is_input_group = is_input_group|default('input-group' in row_class) -%}
{#- Remove behavior class from the main container -#}
{%- set row_class = row_class|replace({'form-floating': '', 'input-group': ''}) -%}
<div{% with {attr: row_attr|merge({class: (row_class ~ ' row' ~ ((not compound or force_error|default(false)) and not valid ? ' is-invalid'))|trim})} %}{{ block('attributes') }}{% endwith %}>
{%- if is_form_floating or is_input_group -%}
<div class="{{ block('form_label_class') }}"></div>
<div class="{{ block('form_group_class') }}">
{%- if is_form_floating -%}
<div class="form-floating">
{{- form_widget(form, widget_attr) -}}
{{- form_label(form) -}}
</div>
{%- elseif is_input_group -%}
<div class="input-group">
{{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}}
{#- Hack to properly display help with input group -#}
{{- form_help(form) -}}
</div>
{%- endif -%}
{%- if not is_input_group -%}
{{- form_help(form) -}}
{%- endif -%}
{{- form_errors(form) -}}
</div>
{%- else -%}
{{- form_label(form) -}}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</div>
{%- endif -%}
{##}</div>
{%- endif -%}
{%- endblock form_row %}
{% block fieldset_form_row -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<fieldset{% with {attr: row_attr|merge({class: row_attr.class|default('mb-3')|trim})} %}{{ block('attributes') }}{% endwith %}>
<div class="row{% if (not compound or force_error|default(false)) and not valid %} is-invalid{% endif %}">
{{- form_label(form) -}}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</div>
</div>
</fieldset>
{%- endblock fieldset_form_row %}
{% block submit_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('mb-3') ~ ' row')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
</div>{#--#}
</div>
{%- endblock submit_row %}
{% block reset_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('mb-3') ~ ' row')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
</div>{#--#}
</div>
{%- endblock reset_row %}
{% block button_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('mb-3') ~ ' row')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
</div>{#--#}
</div>
{%- endblock button_row %}
{% block checkbox_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('mb-3') ~ ' row')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</div>{#--#}
</div>
{%- endblock checkbox_row %}
{% block form_group_class -%}
col-sm-10
{%- endblock form_group_class %}

View File

@ -0,0 +1,374 @@
{% use "bootstrap_base_layout.html.twig" %}
{# Widgets #}
{% block money_widget -%}
{%- set prepend = not (money_pattern starts with '{{') -%}
{%- set append = not (money_pattern ends with '}}') -%}
{%- if prepend or append -%}
<div class="input-group{{ group_class|default('') }}">
{%- if prepend -%}
<span class="input-group-text">{{ money_pattern|form_encode_currency }}</span>
{%- endif -%}
{{- block('form_widget_simple') -}}
{%- if append -%}
<span class="input-group-text">{{ money_pattern|form_encode_currency }}</span>
{%- endif -%}
</div>
{%- else -%}
{{- block('form_widget_simple') -}}
{%- endif -%}
{%- endblock money_widget %}
{% block date_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{% if not valid %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) -%}
{% set valid = true %}
{% endif %}
{%- if datetime is not defined or not datetime -%}
<div {{ block('widget_container_attributes') -}}>
{%- endif %}
{%- if label is not same as(false) -%}
<div class="visually-hidden">
{{- form_label(form.year) -}}
{{- form_label(form.month) -}}
{{- form_label(form.day) -}}
</div>
{%- endif -%}
<div class="input-group">
{{- date_pattern|replace({
'{{ year }}': form_widget(form.year),
'{{ month }}': form_widget(form.month),
'{{ day }}': form_widget(form.day),
})|raw -}}
</div>
{%- if datetime is not defined or not datetime -%}
</div>
{%- endif -%}
{%- endif -%}
{%- endblock date_widget %}
{% block time_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{% if not valid %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) -%}
{% set valid = true %}
{% endif %}
{%- if datetime is not defined or false == datetime -%}
<div {{ block('widget_container_attributes') -}}>
{%- endif -%}
{%- if label is not same as(false) -%}
<div class="visually-hidden">
{{- form_label(form.hour) -}}
{%- if with_minutes -%}{{ form_label(form.minute) }}{%- endif -%}
{%- if with_seconds -%}{{ form_label(form.second) }}{%- endif -%}
</div>
{%- endif -%}
{% if with_minutes or with_seconds %}
<div class="input-group">
{% endif %}
{{- form_widget(form.hour) -}}
{%- if with_minutes -%}
<span class="input-group-text">:</span>
{{- form_widget(form.minute) -}}
{%- endif -%}
{%- if with_seconds -%}
<span class="input-group-text">:</span>
{{- form_widget(form.second) -}}
{%- endif -%}
{% if with_minutes or with_seconds %}
</div>
{% endif %}
{%- if datetime is not defined or false == datetime -%}
</div>
{%- endif -%}
{%- endif -%}
{%- endblock time_widget %}
{% block datetime_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{% if not valid %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) -%}
{% set valid = true %}
{% endif %}
<div {{ block('widget_container_attributes') }}>
{{- form_widget(form.date, { datetime: true } ) -}}
{{- form_errors(form.date) -}}
{{- form_widget(form.time, { datetime: true } ) -}}
{{- form_errors(form.time) -}}
</div>
{%- endif -%}
{%- endblock datetime_widget %}
{% block dateinterval_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{% if not valid %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) -%}
{% set valid = true %}
{% endif %}
<div {{ block('widget_container_attributes') }}>
{%- if with_years -%}
<div class="col-auto mb-3">
{{ form_label(form.years) }}
{{ form_widget(form.years) }}
</div>
{%- endif -%}
{%- if with_months -%}
<div class="col-auto mb-3">
{{ form_label(form.months) }}
{{ form_widget(form.months) }}
</div>
{%- endif -%}
{%- if with_weeks -%}
<div class="col-auto mb-3">
{{ form_label(form.weeks) }}
{{ form_widget(form.weeks) }}
</div>
{%- endif -%}
{%- if with_days -%}
<div class="col-auto mb-3">
{{ form_label(form.days) }}
{{ form_widget(form.days) }}
</div>
{%- endif -%}
{%- if with_hours -%}
<div class="col-auto mb-3">
{{ form_label(form.hours) }}
{{ form_widget(form.hours) }}
</div>
{%- endif -%}
{%- if with_minutes -%}
<div class="col-auto mb-3">
{{ form_label(form.minutes) }}
{{ form_widget(form.minutes) }}
</div>
{%- endif -%}
{%- if with_seconds -%}
<div class="col-auto mb-3">
{{ form_label(form.seconds) }}
{{ form_widget(form.seconds) }}
</div>
{%- endif -%}
{%- if with_invert %}{{ form_widget(form.invert) }}{% endif -%}
</div>
{%- endif -%}
{%- endblock dateinterval_widget %}
{% block percent_widget -%}
{%- if symbol -%}
<div class="input-group">
{{- block('form_widget_simple') -}}
<span class="input-group-text">{{ symbol|default('%') }}</span>
</div>
{%- else -%}
{{- block('form_widget_simple') -}}
{%- endif -%}
{%- endblock percent_widget %}
{% block form_widget_simple -%}
{%- if type is not defined or type != 'hidden' %}
{%- set widget_class = ' form-control' %}
{%- if type|default('') == 'color' -%}
{%- set widget_class = widget_class ~ ' form-control-color' -%}
{%- elseif type|default('') == 'range' -%}
{%- set widget_class = ' form-range' -%}
{%- endif -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ widget_class)|trim}) -%}
{% endif -%}
{%- if type is defined and type in ['range', 'color'] %}
{# Attribute "required" is not supported #}
{% set required = false %}
{% endif -%}
{{- parent() -}}
{%- endblock form_widget_simple %}
{%- block widget_attributes -%}
{%- if not valid %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) %}
{% endif -%}
{{ parent() }}
{%- endblock widget_attributes -%}
{%- block button_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('btn-secondary') ~ ' btn')|trim}) -%}
{{- parent() -}}
{%- endblock button_widget %}
{%- block submit_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('btn-primary'))|trim}) -%}
{{- parent() -}}
{%- endblock submit_widget %}
{%- block checkbox_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-check-input')|trim}) -%}
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
{%- set row_class = 'form-check' -%}
{%- if 'checkbox-inline' in parent_label_class %}
{% set row_class = row_class ~ ' form-check-inline' %}
{% endif -%}
{%- if 'checkbox-switch' in parent_label_class %}
{% set row_class = row_class ~ ' form-switch' %}
{% endif -%}
<div class="{{ row_class }}">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- endblock checkbox_widget %}
{%- block radio_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-check-input')|trim}) -%}
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
{%- set row_class = 'form-check' -%}
{%- if 'radio-inline' in parent_label_class -%}
{%- set row_class = row_class ~ ' form-check-inline' -%}
{%- endif -%}
<div class="{{ row_class }}">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- endblock radio_widget %}
{%- block choice_widget_collapsed -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-select')|trim}) -%}
{{- parent() -}}
{%- endblock choice_widget_collapsed -%}
{%- block choice_widget_expanded -%}
<div {{ block('widget_container_attributes') }}>
{%- for child in form %}
{{- form_widget(child, {
parent_label_class: label_attr.class|default(''),
translation_domain: choice_translation_domain,
valid: valid,
}) -}}
{% endfor -%}
</div>
{%- endblock choice_widget_expanded %}
{# Labels #}
{%- block form_label -%}
{% if label is not same as(false) -%}
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
{%- if compound is defined and compound -%}
{%- set element = 'legend' -%}
{%- if 'col-form-label' not in parent_label_class -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-label' )|trim}) -%}
{%- endif -%}
{%- else -%}
{%- set row_class = row_class|default(row_attr.class|default('')) -%}
{%- set label_attr = label_attr|merge({for: id}) -%}
{%- if 'col-form-label' not in parent_label_class -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ('input-group' in row_class ? ' input-group-text' : ' form-label') )|trim}) -%}
{%- endif -%}
{%- endif -%}
{%- endif -%}
{{- parent() -}}
{%- endblock form_label %}
{%- block checkbox_radio_label -%}
{#- Do not display the label if widget is not defined in order to prevent double label rendering -#}
{%- if widget is defined -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' form-check-label')|trim}) -%}
{%- if not compound -%}
{% set label_attr = label_attr|merge({'for': id}) %}
{%- endif -%}
{%- if required -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' required')|trim}) -%}
{%- endif -%}
{%- if parent_label_class is defined -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ parent_label_class)|replace({'checkbox-inline': '', 'radio-inline': ''})|trim}) -%}
{%- endif -%}
{%- if label is not same as(false) and label is empty -%}
{%- if label_format is not empty -%}
{%- set label = label_format|replace({
'%name%': name,
'%id%': id,
}) -%}
{%- else -%}
{%- set label = name|humanize -%}
{%- endif -%}
{%- endif -%}
{{ widget|raw }}
<label{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}>
{%- if label is not same as(false) -%}
{%- if translation_domain is same as(false) -%}
{%- if label_html is same as(false) -%}
{{- label -}}
{%- else -%}
{{- label|raw -}}
{%- endif -%}
{%- else -%}
{%- if label_html is same as(false) -%}
{{- label|trans(label_translation_parameters, translation_domain) -}}
{%- else -%}
{{- label|trans(label_translation_parameters, translation_domain)|raw -}}
{%- endif -%}
{%- endif -%}
{%- endif -%}
</label>
{%- endif -%}
{%- endblock checkbox_radio_label %}
{# Rows #}
{%- block form_row -%}
{%- if compound is defined and compound -%}
{%- set element = 'fieldset' -%}
{%- endif -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
{%- set row_class = row_class|default(row_attr.class|default('mb-3')|trim) -%}
<{{ element|default('div') }}{% with {attr: row_attr|merge({class: row_class})} %}{{ block('attributes') }}{% endwith %}>
{%- if 'form-floating' in row_class -%}
{{- form_widget(form, widget_attr) -}}
{{- form_label(form) -}}
{%- else -%}
{{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}}
{%- endif -%}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</{{ element|default('div') }}>
{%- endblock form_row %}
{%- block button_row -%}
<div{% with {attr: row_attr|merge({class: row_attr.class|default('mb-3')|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_widget(form) -}}
</div>
{%- endblock button_row %}
{# Errors #}
{%- block form_errors -%}
{%- if errors|length > 0 -%}
{%- for error in errors -%}
<div class="invalid-feedback d-block">{{ error.message }}</div>
{%- endfor -%}
{%- endif %}
{%- endblock form_errors %}
{# Help #}
{%- block form_help -%}
{% set row_class = row_attr.class|default('') %}
{% set help_class = ' form-text' %}
{% if 'input-group' in row_class %}
{#- Hack to properly display help with input group -#}
{% set help_class = ' input-group-text' %}
{% endif %}
{%- if help is not empty -%}
{%- set help_attr = help_attr|merge({class: (help_attr.class|default('') ~ help_class ~ ' mb-0')|trim}) -%}
{%- endif -%}
{{- parent() -}}
{%- endblock form_help %}

View File

@ -143,11 +143,6 @@
{%- endif -%}
{%- endblock dateinterval_widget -%}
{% block choice_widget_collapsed -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%}
{{- parent() -}}
{%- endblock choice_widget_collapsed %}
{% block choice_widget_expanded -%}
{%- if '-inline' in label_attr.class|default('') -%}
{%- for child in form %}

View File

@ -0,0 +1,353 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Tests\Extension;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\RadioType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormError;
/**
* Abstract class providing test cases for the Bootstrap 5 horizontal Twig form theme.
*
* @author Romain Monteil <monteil.romain@gmail.com>
*/
abstract class AbstractBootstrap5HorizontalLayoutTest extends AbstractBootstrap5LayoutTest
{
public function testRow()
{
$form = $this->factory->createNamed('name', TextType::class);
$form->addError(new FormError('[trans]Error![/trans]'));
$html = $this->renderRow($form->createView());
$this->assertMatchesXpath($html,
'/div
[@class="mb-3 row"]
[
./label
[@for="name"]
[@class="col-form-label col-sm-2 required"]
/following-sibling::div
[@class="col-sm-10"]
[
./input[@id="name"]
/following-sibling::div
[@class="invalid-feedback d-block"]
[.="[trans]Error![/trans]"]
]
[count(./div)=1]
]
'
);
}
public function testRowWithCustomClass()
{
$form = $this->factory->createNamed('name', TextType::class);
$form->addError(new FormError('[trans]Error![/trans]'));
$html = $this->renderRow($form->createView(), [
'row_attr' => [
'class' => 'mb-5',
],
]);
$this->assertMatchesXpath($html,
'/div
[@class="mb-5 row"]
[
./label
[@for="name"]
[@class="col-form-label col-sm-2 required"]
/following-sibling::div
[@class="col-sm-10"]
[
./input[@id="name"]
/following-sibling::div
[@class="invalid-feedback d-block"]
[.="[trans]Error![/trans]"]
]
[count(./div)=1]
]
'
);
}
public function testLabelOnForm()
{
$form = $this->factory->createNamed('name', DateType::class);
$view = $form->createView();
$this->renderWidget($view, ['label' => 'foo']);
$html = $this->renderLabel($view);
$this->assertMatchesXpath($html,
'/legend
[@class="col-form-label col-sm-2 required"]
[.="[trans]Name[/trans]"]
'
);
}
public function testLabelDoesNotRenderFieldAttributes()
{
$form = $this->factory->createNamed('name', TextType::class);
$html = $this->renderLabel($form->createView(), null, [
'attr' => [
'class' => 'my&class',
],
]);
$this->assertMatchesXpath($html,
'/label
[@for="name"]
[@class="col-form-label col-sm-2 required"]
'
);
}
public function testLabelWithCustomAttributesPassedDirectly()
{
$form = $this->factory->createNamed('name', TextType::class);
$html = $this->renderLabel($form->createView(), null, [
'label_attr' => [
'class' => 'my&class',
],
]);
$this->assertMatchesXpath($html,
'/label
[@for="name"]
[@class="my&class col-form-label col-sm-2 required"]
'
);
}
public function testLabelWithCustomTextAndCustomAttributesPassedDirectly()
{
$form = $this->factory->createNamed('name', TextType::class);
$html = $this->renderLabel($form->createView(), 'Custom label', [
'label_attr' => [
'class' => 'my&class',
],
]);
$this->assertMatchesXpath($html,
'/label
[@for="name"]
[@class="my&class col-form-label col-sm-2 required"]
[.="[trans]Custom label[/trans]"]
'
);
}
public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly()
{
$form = $this->factory->createNamed('name', TextType::class, null, [
'label' => 'Custom label',
]);
$html = $this->renderLabel($form->createView(), null, [
'label_attr' => [
'class' => 'my&class',
],
]);
$this->assertMatchesXpath($html,
'/label
[@for="name"]
[@class="my&class col-form-label col-sm-2 required"]
[.="[trans]Custom label[/trans]"]
'
);
}
public function testLabelHtmlDefaultIsFalse()
{
$form = $this->factory->createNamed('name', TextType::class, null, [
'label' => '<b>Bolded label</b>',
]);
$html = $this->renderLabel($form->createView(), null, [
'label_attr' => [
'class' => 'my&class',
],
]);
$this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-form-label col-sm-2 required"][.="[trans]<b>Bolded label</b>[/trans]"]');
$this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-form-label col-sm-2 required"]/b[.="Bolded label"]', 0);
}
public function testLabelHtmlIsTrue()
{
$form = $this->factory->createNamed('name', TextType::class, null, [
'label' => '<b>Bolded label</b>',
'label_html' => true,
]);
$html = $this->renderLabel($form->createView(), null, [
'label_attr' => [
'class' => 'my&class',
],
]);
$this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-form-label col-sm-2 required"][.="[trans]<b>Bolded label</b>[/trans]"]', 0);
$this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-form-label col-sm-2 required"]/b[.="Bolded label"]');
}
public function testLegendOnExpandedType()
{
$form = $this->factory->createNamed('name', ChoiceType::class, null, [
'label' => 'Custom label',
'expanded' => true,
'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'],
]);
$view = $form->createView();
$this->renderWidget($view);
$html = $this->renderLabel($view);
$this->assertMatchesXpath($html,
'/legend
[@class="col-sm-2 col-form-label required"]
[.="[trans]Custom label[/trans]"]
'
);
}
public function testCheckboxRow()
{
$form = $this->factory->createNamed('name', CheckboxType::class);
$view = $form->createView();
$html = $this->renderRow($view, ['label' => 'foo']);
$this->assertMatchesXpath($html, '/div[@class="mb-3 row"]/div[@class="col-sm-2" or @class="col-sm-10"]', 2);
}
public function testCheckboxRowWithHelp()
{
$form = $this->factory->createNamed('name', CheckboxType::class);
$view = $form->createView();
$html = $this->renderRow($view, ['label' => 'foo', 'help' => 'really helpful text']);
$this->assertMatchesXpath($html,
'/div
[@class="mb-3 row"]
[
./div[@class="col-sm-2" or @class="col-sm-10"]
/following-sibling::div[@class="col-sm-2" or @class="col-sm-10"]
[
./p
[@class="form-text mb-0 help-text"]
[.="[trans]really helpful text[/trans]"]
]
]
'
);
}
public function testRadioRowWithHelp()
{
$form = $this->factory->createNamed('name', RadioType::class, false);
$html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']);
$this->assertMatchesXpath($html,
'/div
[@class="mb-3 row"]
[
./div[@class="col-sm-2" or @class="col-sm-10"]
/following-sibling::div[@class="col-sm-2" or @class="col-sm-10"]
[
./p
[@class="form-text mb-0 help-text"]
[.="[trans]really helpful text[/trans]"]
]
]
'
);
}
public function testFileWithGroup()
{
$form = $this->factory->createNamed('name', FileType::class);
$html = $this->renderRow($form->createView(), [
'id' => 'n/a',
'attr' => [
'class' => 'my&class',
],
'row_attr' => [
'class' => 'input-group mb-3',
],
]);
$this->assertMatchesXpath($html,
'/div
[@class="mb-3 row"]
[
./div
[@class="col-sm-2"]
/following-sibling::div
[@class="col-sm-10"]
[
./div
[@class="input-group"]
[
./label
[@class="input-group-text required"]
[.="[trans]Name[/trans]"]
/following-sibling::input
[@type="file"]
[@name="name"]
[@class="my&class form-control"]
]
]
]
'
);
}
public function testFloatingLabel()
{
$form = $this->factory->createNamed('name', TextType::class, null, [
'attr' => [
'placeholder' => 'name',
],
'row_attr' => [
'class' => 'form-floating mb-3',
],
]);
$html = $this->renderRow($form->createView());
$this->assertMatchesXpath($html,
'/div
[@class="mb-3 row"]
[
./div
[@class="col-sm-2"]
/following-sibling::div
[@class="col-sm-10"]
[
./div
[@class="form-floating"]
[
./input
[@id="name"]
[@placeholder="[trans]name[/trans]"]
/following-sibling::label
[@for="name"]
]
]
]
'
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,113 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Tests\Extension;
use Symfony\Bridge\Twig\Extension\FormExtension;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Bridge\Twig\Form\TwigRendererEngine;
use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Form\FormView;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
/**
* Class providing test cases for the Bootstrap 5 horizontal Twig form theme.
*
* @author Romain Monteil <monteil.romain@gmail.com>
*/
class FormExtensionBootstrap5HorizontalLayoutTest extends AbstractBootstrap5HorizontalLayoutTest
{
use RuntimeLoaderProvider;
protected $testableFeatures = [
'choice_attr',
];
private $renderer;
protected function setUp(): void
{
parent::setUp();
$loader = new FilesystemLoader([
__DIR__.'/../../Resources/views/Form',
__DIR__.'/Fixtures/templates/form',
]);
$environment = new Environment($loader, ['strict_variables' => true]);
$environment->addExtension(new TranslationExtension(new StubTranslator()));
$environment->addExtension(new FormExtension());
$rendererEngine = new TwigRendererEngine([
'bootstrap_5_horizontal_layout.html.twig',
'custom_widgets.html.twig',
], $environment);
$this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder(CsrfTokenManagerInterface::class)->getMock());
$this->registerTwigRuntimeLoader($environment, $this->renderer);
}
protected function renderForm(FormView $view, array $vars = []): string
{
return (string) $this->renderer->renderBlock($view, 'form', $vars);
}
protected function renderLabel(FormView $view, $label = null, array $vars = []): string
{
if (null !== $label) {
$vars += ['label' => $label];
}
return (string) $this->renderer->searchAndRenderBlock($view, 'label', $vars);
}
protected function renderHelp(FormView $view): string
{
return (string) $this->renderer->searchAndRenderBlock($view, 'help');
}
protected function renderErrors(FormView $view): string
{
return (string) $this->renderer->searchAndRenderBlock($view, 'errors');
}
protected function renderWidget(FormView $view, array $vars = []): string
{
return (string) $this->renderer->searchAndRenderBlock($view, 'widget', $vars);
}
protected function renderRow(FormView $view, array $vars = []): string
{
return (string) $this->renderer->searchAndRenderBlock($view, 'row', $vars);
}
protected function renderRest(FormView $view, array $vars = []): string
{
return (string) $this->renderer->searchAndRenderBlock($view, 'rest', $vars);
}
protected function renderStart(FormView $view, array $vars = []): string
{
return (string) $this->renderer->renderBlock($view, 'form_start', $vars);
}
protected function renderEnd(FormView $view, array $vars = []): string
{
return (string) $this->renderer->renderBlock($view, 'form_end', $vars);
}
protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true): void
{
$this->renderer->setTheme($view, $themes, $useDefaultThemes);
}
}

View File

@ -0,0 +1,165 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Tests\Extension;
use Symfony\Bridge\Twig\Extension\FormExtension;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Bridge\Twig\Form\TwigRendererEngine;
use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Form\FormView;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
/**
* Class providing test cases for the Bootstrap 5 Twig form theme.
*
* @author Romain Monteil <monteil.romain@gmail.com>
*/
class FormExtensionBootstrap5LayoutTest extends AbstractBootstrap5LayoutTest
{
use RuntimeLoaderProvider;
/**
* @var FormRenderer
*/
private $renderer;
protected function setUp(): void
{
parent::setUp();
$loader = new FilesystemLoader([
__DIR__.'/../../Resources/views/Form',
__DIR__.'/Fixtures/templates/form',
]);
$environment = new Environment($loader, ['strict_variables' => true]);
$environment->addExtension(new TranslationExtension(new StubTranslator()));
$environment->addExtension(new FormExtension());
$rendererEngine = new TwigRendererEngine([
'bootstrap_5_layout.html.twig',
'custom_widgets.html.twig',
], $environment);
$this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder(CsrfTokenManagerInterface::class)->getMock());
$this->registerTwigRuntimeLoader($environment, $this->renderer);
}
public function testStartTagHasNoActionAttributeWhenActionIsEmpty()
{
$form = $this->factory->create(FormType::class, null, [
'method' => 'get',
'action' => '',
]);
$html = $this->renderStart($form->createView());
self::assertSame('<form name="form" method="get">', $html);
}
public function testStartTagHasActionAttributeWhenActionIsZero()
{
$form = $this->factory->create(FormType::class, null, [
'method' => 'get',
'action' => '0',
]);
$html = $this->renderStart($form->createView());
self::assertSame('<form name="form" method="get" action="0">', $html);
}
public function testMoneyWidgetInIso()
{
$environment = new Environment(new FilesystemLoader([
__DIR__.'/../../Resources/views/Form',
__DIR__.'/Fixtures/templates/form',
]), ['strict_variables' => true]);
$environment->addExtension(new TranslationExtension(new StubTranslator()));
$environment->addExtension(new FormExtension());
$environment->setCharset('ISO-8859-1');
$rendererEngine = new TwigRendererEngine([
'bootstrap_5_layout.html.twig',
'custom_widgets.html.twig',
], $environment);
$this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder(CsrfTokenManagerInterface::class)->getMock());
$this->registerTwigRuntimeLoader($environment, $this->renderer);
$view = $this->factory
->createNamed('name', MoneyType::class)
->createView();
self::assertSame(<<<'HTML'
<div class="input-group"><span class="input-group-text">&euro; </span><input type="text" id="name" name="name" required="required" class="form-control" /></div>
HTML
, trim($this->renderWidget($view)));
}
protected function renderForm(FormView $view, array $vars = []): string
{
return (string) $this->renderer->renderBlock($view, 'form', $vars);
}
protected function renderLabel(FormView $view, $label = null, array $vars = []): string
{
if (null !== $label) {
$vars += ['label' => $label];
}
return (string) $this->renderer->searchAndRenderBlock($view, 'label', $vars);
}
protected function renderHelp(FormView $view): string
{
return (string) $this->renderer->searchAndRenderBlock($view, 'help');
}
protected function renderErrors(FormView $view): string
{
return (string) $this->renderer->searchAndRenderBlock($view, 'errors');
}
protected function renderWidget(FormView $view, array $vars = []): string
{
return (string) $this->renderer->searchAndRenderBlock($view, 'widget', $vars);
}
protected function renderRow(FormView $view, array $vars = []): string
{
return (string) $this->renderer->searchAndRenderBlock($view, 'row', $vars);
}
protected function renderRest(FormView $view, array $vars = []): string
{
return (string) $this->renderer->searchAndRenderBlock($view, 'rest', $vars);
}
protected function renderStart(FormView $view, array $vars = []): string
{
return (string) $this->renderer->renderBlock($view, 'form_start', $vars);
}
protected function renderEnd(FormView $view, array $vars = []): string
{
return (string) $this->renderer->renderBlock($view, 'form_end', $vars);
}
protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true): void
{
$this->renderer->setTheme($view, $themes, $useDefaultThemes);
}
}