From 4222d54a53123098e2402836d72e66df8a0f312b Mon Sep 17 00:00:00 2001 From: Hidde Wieringa Date: Fri, 24 Feb 2017 23:34:35 +0100 Subject: [PATCH 1/3] Initial commit for Bootstrap 4 form theme, based on Bootstrap 3 form theme Fixed Bootstrap 4 error output Updated form errors to look correctly Cranked Twig Bridge Composer version to ~3.2 Added @ author PHPdoc tag to BS 4 test classes Fixed small items, and added fieldset/legend support - Fixed form class for File type - Added fieldset element for expanded form types - Added legend element as label for expanded form types - Fixed horizontal form classes for labels Added test for legend form label Fixed form label coloring on validation errors Fixed concrete Bootstrap layout test classes to use new code Fixed tests for form-control-label class on form labels Fixed a typo in using old Bootstrap 4 concrete test code Processed proposed changes from stof Processed proposed changes in bootstrap base layout from stof Updated margin-top for error list in the alpha-5 version of BS 4 Fixed {% endblock ... %} and fixed BS error class in test Fixed TwigRenderer => FormRenderer code change Minor fixed for radio/checkboxes and fixed validation feedback Added ~3.4 require of symfony/form (and <3.4 conflict) to Twig Bridge composer.json Updated Boostrap 4 file input to have class .form-control-file --- .../bootstrap_3_horizontal_layout.html.twig | 20 - .../views/Form/bootstrap_3_layout.html.twig | 140 +-- .../bootstrap_4_horizontal_layout.html.twig | 69 ++ .../views/Form/bootstrap_4_layout.html.twig | 132 +++ .../Form/bootstrap_base_layout.html.twig | 183 ++++ .../views/Form/form_div_layout.html.twig | 2 +- .../views/Form/foundation_5_layout.html.twig | 4 +- ...xtensionBootstrap4HorizontalLayoutTest.php | 107 ++ .../FormExtensionBootstrap4LayoutTest.php | 129 +++ src/Symfony/Bridge/Twig/composer.json | 4 +- ...AbstractBootstrap4HorizontalLayoutTest.php | 181 ++++ .../Tests/AbstractBootstrap4LayoutTest.php | 978 ++++++++++++++++++ 12 files changed, 1786 insertions(+), 163 deletions(-) create mode 100644 src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig create mode 100644 src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig create mode 100644 src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig create mode 100644 src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4HorizontalLayoutTest.php create mode 100644 src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php create mode 100644 src/Symfony/Component/Form/Tests/AbstractBootstrap4HorizontalLayoutTest.php create mode 100644 src/Symfony/Component/Form/Tests/AbstractBootstrap4LayoutTest.php diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig index 5de20b1b8f181..478de622da435 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig @@ -34,26 +34,6 @@ col-sm-2 {##} {%- endblock form_row %} -{% block checkbox_row -%} - {{- block('checkbox_radio_row') -}} -{%- endblock checkbox_row %} - -{% block radio_row -%} - {{- block('checkbox_radio_row') -}} -{%- endblock radio_row %} - -{% block checkbox_radio_row -%} -{% spaceless %} -
-
-
- {{ form_widget(form) }} - {{ form_errors(form) }} -
-
-{% endspaceless %} -{%- endblock checkbox_radio_row %} - {% block submit_row -%} {% spaceless %}
diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig index f2fe8bf3d2588..b89a788041cae 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig @@ -1,4 +1,4 @@ -{% use "form_div_layout.html.twig" %} +{% use "bootstrap_base_layout.html.twig" %} {# Widgets #} @@ -9,146 +9,10 @@ {{- parent() -}} {%- endblock form_widget_simple %} -{% block textarea_widget -%} - {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %} - {{- parent() -}} -{%- endblock textarea_widget %} - {% block button_widget -%} {% set attr = attr|merge({class: (attr.class|default('btn-default') ~ ' btn')|trim}) %} {{- parent() -}} -{%- endblock %} - -{% block money_widget -%} -
- {% set append = money_pattern starts with '{{' %} - {% if not append %} - {{ money_pattern|replace({ '{{ widget }}':''}) }} - {% endif %} - {{- block('form_widget_simple') -}} - {% if append %} - {{ money_pattern|replace({ '{{ widget }}':''}) }} - {% endif %} -
-{%- endblock money_widget %} - -{% block percent_widget -%} -
- {{- block('form_widget_simple') -}} - % -
-{%- endblock percent_widget %} - -{% block datetime_widget -%} - {% if widget == 'single_text' %} - {{- block('form_widget_simple') -}} - {% else -%} - {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%} -
- {{- form_errors(form.date) -}} - {{- form_errors(form.time) -}} - {{- form_widget(form.date, { datetime: true } ) -}} - {{- form_widget(form.time, { datetime: true } ) -}} -
- {%- endif %} -{%- endblock datetime_widget %} - -{% block date_widget -%} - {% if widget == 'single_text' %} - {{- block('form_widget_simple') -}} - {% else -%} - {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%} - {% if datetime is not defined or not datetime -%} -
- {%- endif %} - {{- date_pattern|replace({ - '{{ year }}': form_widget(form.year), - '{{ month }}': form_widget(form.month), - '{{ day }}': form_widget(form.day), - })|raw -}} - {% if datetime is not defined or not datetime -%} -
- {%- endif -%} - {% endif %} -{%- endblock date_widget %} - -{% block time_widget -%} - {% if widget == 'single_text' %} - {{- block('form_widget_simple') -}} - {% else -%} - {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%} - {% if datetime is not defined or false == datetime -%} -
- {%- endif -%} - {{- form_widget(form.hour) }}{% if with_minutes %}:{{ form_widget(form.minute) }}{% endif %}{% if with_seconds %}:{{ form_widget(form.second) }}{% endif %} - {% if datetime is not defined or false == datetime -%} -
- {%- endif -%} - {% endif %} -{%- endblock time_widget %} - -{%- block dateinterval_widget -%} - {%- if widget == 'single_text' -%} - {{- block('form_widget_simple') -}} - {%- else -%} - {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%} -
- {{- form_errors(form) -}} -
- - - - {%- if with_years %}{% endif -%} - {%- if with_months %}{% endif -%} - {%- if with_weeks %}{% endif -%} - {%- if with_days %}{% endif -%} - {%- if with_hours %}{% endif -%} - {%- if with_minutes %}{% endif -%} - {%- if with_seconds %}{% endif -%} - - - - - {%- if with_years %}{% endif -%} - {%- if with_months %}{% endif -%} - {%- if with_weeks %}{% endif -%} - {%- if with_days %}{% endif -%} - {%- if with_hours %}{% endif -%} - {%- if with_minutes %}{% endif -%} - {%- if with_seconds %}{% endif -%} - - -
{{ form_label(form.years) }}{{ form_label(form.months) }}{{ form_label(form.weeks) }}{{ form_label(form.days) }}{{ form_label(form.hours) }}{{ form_label(form.minutes) }}{{ form_label(form.seconds) }}
{{ form_widget(form.years) }}{{ form_widget(form.months) }}{{ form_widget(form.weeks) }}{{ form_widget(form.days) }}{{ form_widget(form.hours) }}{{ form_widget(form.minutes) }}{{ form_widget(form.seconds) }}
-
- {%- if with_invert %}{{ form_widget(form.invert) }}{% endif -%} -
- {%- endif -%} -{%- endblock dateinterval_widget -%} - -{% block choice_widget_collapsed -%} - {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %} - {{- parent() -}} -{%- endblock %} - -{% block choice_widget_expanded -%} - {% if '-inline' in label_attr.class|default('') -%} - {%- for child in form %} - {{- form_widget(child, { - parent_label_class: label_attr.class|default(''), - translation_domain: choice_translation_domain, - }) -}} - {% endfor -%} - {%- else -%} -
- {%- for child in form %} - {{- form_widget(child, { - parent_label_class: label_attr.class|default(''), - translation_domain: choice_translation_domain, - }) -}} - {% endfor -%} -
- {%- endif %} -{%- endblock choice_widget_expanded %} +{%- endblock button_widget %} {% block checkbox_widget -%} {%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig new file mode 100644 index 0000000000000..046cf3cdd1703 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig @@ -0,0 +1,69 @@ +{% use "bootstrap_4_layout.html.twig" %} + +{# Labels #} + +{% block form_label -%} + {%- if label is same as(false) -%} +
+ {%- else -%} + {%- 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}) -%} + {{- 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 -%} +
+ {{- form_label(form) -}} +
+ {{- form_widget(form) -}} + {{- form_errors(form) -}} +
+ {##}
+ {%- endif -%} +{%- endblock form_row %} + +{% block fieldset_form_row -%} +
+
+ {{- form_label(form) -}} +
+ {{- form_widget(form) -}} + {{- form_errors(form) -}} +
+
+{##}
+{%- endblock fieldset_form_row %} + +{% block submit_row -%} +
+
+
+ {{- form_widget(form) -}} +
+
+{%- endblock submit_row %} + +{% block reset_row -%} +
+
+
+ {{- form_widget(form) -}} +
+
+{%- endblock reset_row %} + +{% block form_group_class -%} +col-sm-10 +{%- endblock form_group_class %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig new file mode 100644 index 0000000000000..b3c71a74ea5aa --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig @@ -0,0 +1,132 @@ +{% use "bootstrap_base_layout.html.twig" %} + +{# Widgets #} + +{% block form_widget_simple -%} + {% if type is not defined or type != 'hidden' %} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control' ~ (type|default('') == 'file' ? '-file' : ''))|trim}) -%} + {% 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 checkbox_widget -%} + {%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%} + {%- set attr = attr|merge({class: attr.class|default('form-check-input')}) -%} + {%- if 'checkbox-inline' in parent_label_class -%} + {{- form_label(form, null, { widget: parent() }) -}} + {%- else -%} +
+ {{- form_label(form, null, { widget: parent() }) -}} +
+ {%- endif -%} +{%- endblock checkbox_widget %} + +{% block radio_widget -%} + {%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%} + {%- set attr = attr|merge({class: attr.class|default('form-check-input')}) -%} + {%- if 'radio-inline' in parent_label_class -%} + {{- form_label(form, null, { widget: parent() }) -}} + {%- else -%} +
+ {{- form_label(form, null, { widget: parent() }) -}} +
+ {%- endif -%} +{%- endblock radio_widget %} + +{% block choice_widget_expanded -%} + {% if '-inline' in label_attr.class|default('') -%} + {%- for child in form %} + {{- form_widget(child, { + parent_label_class: label_attr.class|default(''), + translation_domain: choice_translation_domain, + valid: valid, + }) -}} + {% endfor -%} + {%- else -%} +
+ {%- for child in form %} + {{- form_widget(child, { + parent_label_class: label_attr.class|default(''), + translation_domain: choice_translation_domain, + valid: valid, + }) -}} + {% endfor -%} +
+ {%- endif %} +{%- endblock choice_widget_expanded %} + +{# Labels #} + +{% block form_label -%} + {%- if expanded is defined and expanded -%} + {%- set element = 'legend' -%} + {%- set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' col-form-legend')|trim}) -%} + {%- endif -%} + {%- set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' form-control-label')|trim}) -%} + {{- 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 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)|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 is not same as(false) ? (translation_domain is same as(false) ? label : label|trans({}, translation_domain)) -}} + + {%- endif -%} +{%- endblock checkbox_radio_label %} + +{# Rows #} + +{% block form_row -%} + {%- if expanded is defined and expanded -%} + {%- set element = 'fieldset' -%} + {%- endif -%} + <{{ element|default('div') }} class="form-group"> + {{- form_label(form) -}} + {{- form_widget(form) -}} + {{- form_errors(form) -}} + +{%- endblock form_row %} + +{# Errors #} + +{% block form_errors -%} + {%- if errors|length > 0 -%} +
+
    + {%- for error in errors -%} +
  • {{ error.message }}
  • + {%- endfor -%} +
+
+ {%- endif %} +{%- endblock form_errors %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig new file mode 100644 index 0000000000000..cdb9dbc88ef96 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig @@ -0,0 +1,183 @@ +{% use "form_div_layout.html.twig" %} + +{# Widgets #} + +{% block textarea_widget -%} + {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %} + {{- parent() -}} +{%- endblock textarea_widget %} + +{% block money_widget -%} +
+ {%- set append = money_pattern starts with '{{' -%} + {%- if not append -%} + {{ money_pattern|replace({ '{{ widget }}':''}) }} + {%- endif -%} + {{- block('form_widget_simple') -}} + {%- if append -%} + {{ money_pattern|replace({ '{{ widget }}':''}) }} + {%- endif -%} +
+{%- endblock money_widget %} + +{% block percent_widget -%} +
+ {{- block('form_widget_simple') -}} + % +
+{%- endblock percent_widget %} + +{% block datetime_widget -%} + {%- if widget == 'single_text' -%} + {{- block('form_widget_simple') -}} + {%- else -%} + {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%} +
+ {{- form_errors(form.date) -}} + {{- form_errors(form.time) -}} + {{- form_widget(form.date, { datetime: true } ) -}} + {{- form_widget(form.time, { datetime: true } ) -}} +
+ {%- endif -%} +{%- endblock datetime_widget %} + +{% block date_widget -%} + {%- if widget == 'single_text' -%} + {{- block('form_widget_simple') -}} + {%- else -%} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%} + {%- if datetime is not defined or not datetime -%} +
+ {%- endif %} + {{- date_pattern|replace({ + '{{ year }}': form_widget(form.year), + '{{ month }}': form_widget(form.month), + '{{ day }}': form_widget(form.day), + })|raw -}} + {%- if datetime is not defined or not datetime -%} +
+ {%- endif -%} + {%- endif -%} +{%- endblock date_widget %} + +{% block time_widget -%} + {%- if widget == 'single_text' -%} + {{- block('form_widget_simple') -}} + {%- else -%} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%} + {%- if datetime is not defined or false == datetime -%} +
+ {%- endif -%} + {{- form_widget(form.hour) }}{% if with_minutes %}:{{ form_widget(form.minute) }}{% endif %}{% if with_seconds %}:{{ form_widget(form.second) }}{% endif %} + {%- if datetime is not defined or false == datetime -%} +
+ {%- endif -%} + {%- endif -%} +{%- endblock time_widget %} + +{%- block dateinterval_widget -%} + {%- if widget == 'single_text' -%} + {{- block('form_widget_simple') -}} + {%- else -%} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%} +
+ {{- form_errors(form) -}} +
+ + + + {%- if with_years %}{% endif -%} + {%- if with_months %}{% endif -%} + {%- if with_weeks %}{% endif -%} + {%- if with_days %}{% endif -%} + {%- if with_hours %}{% endif -%} + {%- if with_minutes %}{% endif -%} + {%- if with_seconds %}{% endif -%} + + + + + {%- if with_years %}{% endif -%} + {%- if with_months %}{% endif -%} + {%- if with_weeks %}{% endif -%} + {%- if with_days %}{% endif -%} + {%- if with_hours %}{% endif -%} + {%- if with_minutes %}{% endif -%} + {%- if with_seconds %}{% endif -%} + + +
{{ form_label(form.years) }}{{ form_label(form.months) }}{{ form_label(form.weeks) }}{{ form_label(form.days) }}{{ form_label(form.hours) }}{{ form_label(form.minutes) }}{{ form_label(form.seconds) }}
{{ form_widget(form.years) }}{{ form_widget(form.months) }}{{ form_widget(form.weeks) }}{{ form_widget(form.days) }}{{ form_widget(form.hours) }}{{ form_widget(form.minutes) }}{{ form_widget(form.seconds) }}
+
+ {%- if with_invert %}{{ form_widget(form.invert) }}{% endif -%} +
+ {%- 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 %} + {{- form_widget(child, { + parent_label_class: label_attr.class|default(''), + translation_domain: choice_translation_domain, + }) -}} + {% endfor -%} + {%- else -%} +
+ {%- for child in form %} + {{- form_widget(child, { + parent_label_class: label_attr.class|default(''), + translation_domain: choice_translation_domain, + }) -}} + {%- endfor -%} +
+ {%- endif -%} +{%- endblock choice_widget_expanded %} + +{# Labels #} + +{% block choice_label -%} + {# remove the checkbox-inline and radio-inline class, it's only useful for embed labels #} + {%- set label_attr = label_attr|merge({class: label_attr.class|default('')|replace({'checkbox-inline': '', 'radio-inline': ''})|trim}) -%} + {{- block('form_label') -}} +{% endblock choice_label %} + +{% block checkbox_label -%} + {{- block('checkbox_radio_label') -}} +{%- endblock checkbox_label %} + +{% block radio_label -%} + {{- block('checkbox_radio_label') -}} +{%- endblock radio_label %} + +{# Rows #} + +{% block button_row -%} +
+ {{- form_widget(form) -}} +
+{%- endblock button_row %} + +{% block choice_row -%} + {%- set force_error = true -%} + {{- block('form_row') -}} +{%- endblock choice_row %} + +{% block date_row -%} + {%- set force_error = true -%} + {{- block('form_row') -}} +{%- endblock date_row %} + +{% block time_row -%} + {%- set force_error = true -%} + {{- block('form_row') -}} +{%- endblock time_row %} + +{% block datetime_row -%} + {%- set force_error = true -%} + {{- block('form_row') -}} +{%- endblock datetime_row %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index 3d8afe2cba602..20e5de35ee9f8 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -259,7 +259,7 @@ {% set label = name|humanize %} {%- endif -%} {%- endif -%} - {{ translation_domain is same as(false) ? label : label|trans({}, translation_domain) }} + <{{ element|default('label') }}{% if label_attr %}{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}>{{ translation_domain is same as(false) ? label : label|trans({}, translation_domain) }} {%- endif -%} {%- endblock form_label -%} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig index dc7bec9fb6ccd..d34161ad0c669 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig @@ -20,7 +20,7 @@ {% block button_widget -%} {% set attr = attr|merge({class: (attr.class|default('') ~ ' button')|trim}) %} {{- parent() -}} -{%- endblock %} +{%- endblock button_widget %} {% block money_widget -%}
@@ -228,7 +228,7 @@ {# remove the checkbox-inline and radio-inline class, it's only useful for embed labels #} {% set label_attr = label_attr|merge({class: label_attr.class|default('')|replace({'checkbox-inline': '', 'radio-inline': ''})|trim}) %} {{- block('form_label') -}} -{%- endblock %} +{%- endblock choice_label %} {% block checkbox_label -%} {{- block('checkbox_radio_label') -}} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4HorizontalLayoutTest.php new file mode 100644 index 0000000000000..842150dc5e0fa --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4HorizontalLayoutTest.php @@ -0,0 +1,107 @@ + + * + * 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\Form\TwigRendererEngine; +use Symfony\Bridge\Twig\Extension\TranslationExtension; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; +use Symfony\Component\Form\FormRenderer; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Tests\AbstractBootstrap4HorizontalLayoutTest; + +/** + * Class providing test cases for the Bootstrap 4 Twig form theme. + * + * @author Hidde Wieringa + */ +class FormExtensionBootstrap4HorizontalLayoutTest extends AbstractBootstrap4HorizontalLayoutTest +{ + use RuntimeLoaderProvider; + + protected $testableFeatures = array( + 'choice_attr', + ); + + private $renderer; + + protected function setUp() + { + parent::setUp(); + + $loader = new StubFilesystemLoader(array( + __DIR__.'/../../Resources/views/Form', + __DIR__.'/Fixtures/templates/form', + )); + + $environment = new \Twig_Environment($loader, array('strict_variables' => true)); + $environment->addExtension(new TranslationExtension(new StubTranslator())); + $environment->addExtension(new FormExtension()); + + $rendererEngine = new TwigRendererEngine(array( + 'bootstrap_4_horizontal_layout.html.twig', + 'custom_widgets.html.twig', + ), $environment); + $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock()); + $this->registerTwigRuntimeLoader($environment, $this->renderer); + } + + protected function renderForm(FormView $view, array $vars = array()) + { + return (string) $this->renderer->renderBlock($view, 'form', $vars); + } + + protected function renderLabel(FormView $view, $label = null, array $vars = array()) + { + if ($label !== null) { + $vars += array('label' => $label); + } + + return (string) $this->renderer->searchAndRenderBlock($view, 'label', $vars); + } + + protected function renderErrors(FormView $view) + { + return (string) $this->renderer->searchAndRenderBlock($view, 'errors'); + } + + protected function renderWidget(FormView $view, array $vars = array()) + { + return (string) $this->renderer->searchAndRenderBlock($view, 'widget', $vars); + } + + protected function renderRow(FormView $view, array $vars = array()) + { + return (string) $this->renderer->searchAndRenderBlock($view, 'row', $vars); + } + + protected function renderRest(FormView $view, array $vars = array()) + { + return (string) $this->renderer->searchAndRenderBlock($view, 'rest', $vars); + } + + protected function renderStart(FormView $view, array $vars = array()) + { + return (string) $this->renderer->renderBlock($view, 'form_start', $vars); + } + + protected function renderEnd(FormView $view, array $vars = array()) + { + return (string) $this->renderer->renderBlock($view, 'form_end', $vars); + } + + protected function setTheme(FormView $view, array $themes) + { + $this->renderer->setTheme($view, $themes); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php new file mode 100644 index 0000000000000..4650246cda4f7 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php @@ -0,0 +1,129 @@ + + * + * 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\Form\TwigRendererEngine; +use Symfony\Bridge\Twig\Extension\TranslationExtension; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; +use Symfony\Component\Form\FormRenderer; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Tests\AbstractBootstrap4LayoutTest; + +/** + * Class providing test cases for the Bootstrap 4 horizontal Twig form theme. + * + * @author Hidde Wieringa + */ +class FormExtensionBootstrap4LayoutTest extends AbstractBootstrap4LayoutTest +{ + use RuntimeLoaderProvider; + /** + * @var FormRenderer; + */ + private $renderer; + + protected function setUp() + { + parent::setUp(); + + $loader = new StubFilesystemLoader(array( + __DIR__.'/../../Resources/views/Form', + __DIR__.'/Fixtures/templates/form', + )); + + $environment = new \Twig_Environment($loader, array('strict_variables' => true)); + $environment->addExtension(new TranslationExtension(new StubTranslator())); + $environment->addExtension(new FormExtension()); + + $rendererEngine = new TwigRendererEngine(array( + 'bootstrap_4_layout.html.twig', + 'custom_widgets.html.twig', + ), $environment); + $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock()); + $this->registerTwigRuntimeLoader($environment, $this->renderer); + } + + public function testStartTagHasNoActionAttributeWhenActionIsEmpty() + { + $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, array( + 'method' => 'get', + 'action' => '', + )); + + $html = $this->renderStart($form->createView()); + + $this->assertSame('
', $html); + } + + public function testStartTagHasActionAttributeWhenActionIsZero() + { + $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, array( + 'method' => 'get', + 'action' => '0', + )); + + $html = $this->renderStart($form->createView()); + + $this->assertSame('', $html); + } + + protected function renderForm(FormView $view, array $vars = array()) + { + return (string) $this->renderer->renderBlock($view, 'form', $vars); + } + + protected function renderLabel(FormView $view, $label = null, array $vars = array()) + { + if ($label !== null) { + $vars += array('label' => $label); + } + + return (string) $this->renderer->searchAndRenderBlock($view, 'label', $vars); + } + + protected function renderErrors(FormView $view) + { + return (string) $this->renderer->searchAndRenderBlock($view, 'errors'); + } + + protected function renderWidget(FormView $view, array $vars = array()) + { + return (string) $this->renderer->searchAndRenderBlock($view, 'widget', $vars); + } + + protected function renderRow(FormView $view, array $vars = array()) + { + return (string) $this->renderer->searchAndRenderBlock($view, 'row', $vars); + } + + protected function renderRest(FormView $view, array $vars = array()) + { + return (string) $this->renderer->searchAndRenderBlock($view, 'rest', $vars); + } + + protected function renderStart(FormView $view, array $vars = array()) + { + return (string) $this->renderer->renderBlock($view, 'form_start', $vars); + } + + protected function renderEnd(FormView $view, array $vars = array()) + { + return (string) $this->renderer->renderBlock($view, 'form_end', $vars); + } + + protected function setTheme(FormView $view, array $themes) + { + $this->renderer->setTheme($view, $themes); + } +} diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 14f734cdc80ef..e1e86408cff33 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -23,7 +23,7 @@ "fig/link-util": "^1.0", "symfony/asset": "~2.8|~3.0|~4.0", "symfony/finder": "~2.8|~3.0|~4.0", - "symfony/form": "^3.2.10|^3.3.3|~4.0", + "symfony/form": "~3.4|~4.0", "symfony/http-kernel": "~3.2|~4.0", "symfony/polyfill-intl-icu": "~1.0", "symfony/routing": "~2.8|~3.0|~4.0", @@ -39,7 +39,7 @@ "symfony/web-link": "~3.3|~4.0" }, "conflict": { - "symfony/form": "<3.2.10|~3.3,<3.3.3", + "symfony/form": "<3.4", "symfony/console": "<3.4" }, "suggest": { diff --git a/src/Symfony/Component/Form/Tests/AbstractBootstrap4HorizontalLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractBootstrap4HorizontalLayoutTest.php new file mode 100644 index 0000000000000..990fffbe94689 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/AbstractBootstrap4HorizontalLayoutTest.php @@ -0,0 +1,181 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests; + +/** + * Abstract class providing test cases for the Bootstrap 4 horizontal Twig form theme. + * + * @author Hidde Wieringa + */ +abstract class AbstractBootstrap4HorizontalLayoutTest extends AbstractBootstrap4LayoutTest +{ + public function testLabelOnForm() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType'); + $view = $form->createView(); + $this->renderWidget($view, array('label' => 'foo')); + $html = $this->renderLabel($view); + + $this->assertMatchesXpath($html, +'/label + [@class="col-form-label col-sm-2 form-control-label required"] + [.="[trans]Name[/trans]"] +' + ); + } + + public function testLabelDoesNotRenderFieldAttributes() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); + $html = $this->renderLabel($form->createView(), null, array( + 'attr' => array( + 'class' => 'my&class', + ), + )); + + $this->assertMatchesXpath($html, +'/label + [@for="name"] + [@class="col-form-label col-sm-2 form-control-label required"] +' + ); + } + + public function testLabelWithCustomAttributesPassedDirectly() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); + $html = $this->renderLabel($form->createView(), null, array( + 'label_attr' => array( + 'class' => 'my&class', + ), + )); + + $this->assertMatchesXpath($html, +'/label + [@for="name"] + [@class="my&class col-form-label col-sm-2 form-control-label required"] +' + ); + } + + public function testLabelWithCustomTextAndCustomAttributesPassedDirectly() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); + $html = $this->renderLabel($form->createView(), 'Custom label', array( + 'label_attr' => array( + 'class' => 'my&class', + ), + )); + + $this->assertMatchesXpath($html, +'/label + [@for="name"] + [@class="my&class col-form-label col-sm-2 form-control-label required"] + [.="[trans]Custom label[/trans]"] +' + ); + } + + public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, array( + 'label' => 'Custom label', + )); + $html = $this->renderLabel($form->createView(), null, array( + 'label_attr' => array( + 'class' => 'my&class', + ), + )); + + $this->assertMatchesXpath($html, +'/label + [@for="name"] + [@class="my&class col-form-label col-sm-2 form-control-label required"] + [.="[trans]Custom label[/trans]"] +' + ); + } + + public function testLegendOnExpandedType() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, array( + 'label' => 'Custom label', + 'expanded' => true, + 'choices' => array('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-legend form-control-label required"] + [.="[trans]Custom label[/trans]"] +' + ); + } + + public function testStartTag() + { + $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, array( + 'method' => 'get', + 'action' => 'http://example.com/directory', + )); + + $html = $this->renderStart($form->createView()); + + $this->assertSame('', $html); + } + + public function testStartTagWithOverriddenVars() + { + $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, array( + 'method' => 'put', + 'action' => 'http://example.com/directory', + )); + + $html = $this->renderStart($form->createView(), array( + 'method' => 'post', + 'action' => 'http://foo.com/directory', + )); + + $this->assertSame('', $html); + } + + public function testStartTagForMultipartForm() + { + $form = $this->factory->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', null, array( + 'method' => 'get', + 'action' => 'http://example.com/directory', + )) + ->add('file', 'Symfony\Component\Form\Extension\Core\Type\FileType') + ->getForm(); + + $html = $this->renderStart($form->createView()); + + $this->assertSame('', $html); + } + + public function testStartTagWithExtraAttributes() + { + $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, array( + 'method' => 'get', + 'action' => 'http://example.com/directory', + )); + + $html = $this->renderStart($form->createView(), array( + 'attr' => array('class' => 'foobar'), + )); + + $this->assertSame('', $html); + } +} diff --git a/src/Symfony/Component/Form/Tests/AbstractBootstrap4LayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractBootstrap4LayoutTest.php new file mode 100644 index 0000000000000..35bd1203b3f80 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/AbstractBootstrap4LayoutTest.php @@ -0,0 +1,978 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests; + +use Symfony\Component\Form\FormError; + +/** + * Abstract class providing test cases for the Bootstrap 4 Twig form theme. + * + * @author Hidde Wieringa + */ +abstract class AbstractBootstrap4LayoutTest extends AbstractBootstrap3LayoutTest +{ + public function testLabelOnForm() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType'); + $view = $form->createView(); + $this->renderWidget($view, array('label' => 'foo')); + $html = $this->renderLabel($view); + + $this->assertMatchesXpath($html, +'/label + [@class="form-control-label required"] + [.="[trans]Name[/trans]"] +' + ); + } + + public function testLabelDoesNotRenderFieldAttributes() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); + $html = $this->renderLabel($form->createView(), null, array( + 'attr' => array( + 'class' => 'my&class', + ), + )); + + $this->assertMatchesXpath($html, +'/label + [@for="name"] + [@class="form-control-label required"] +' + ); + } + + public function testLabelWithCustomAttributesPassedDirectly() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); + $html = $this->renderLabel($form->createView(), null, array( + 'label_attr' => array( + 'class' => 'my&class', + ), + )); + + $this->assertMatchesXpath($html, +'/label + [@for="name"] + [@class="my&class form-control-label required"] +' + ); + } + + public function testLabelWithCustomTextAndCustomAttributesPassedDirectly() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); + $html = $this->renderLabel($form->createView(), 'Custom label', array( + 'label_attr' => array( + 'class' => 'my&class', + ), + )); + + $this->assertMatchesXpath($html, +'/label + [@for="name"] + [@class="my&class form-control-label required"] + [.="[trans]Custom label[/trans]"] +' + ); + } + + public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, array( + 'label' => 'Custom label', + )); + $html = $this->renderLabel($form->createView(), null, array( + 'label_attr' => array( + 'class' => 'my&class', + ), + )); + + $this->assertMatchesXpath($html, +'/label + [@for="name"] + [@class="my&class form-control-label required"] + [.="[trans]Custom label[/trans]"] +' + ); + } + + public function testLegendOnExpandedType() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, array( + 'label' => 'Custom label', + 'expanded' => true, + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + )); + $view = $form->createView(); + $this->renderWidget($view); + $html = $this->renderLabel($view); + + $this->assertMatchesXpath($html, +'/legend + [@class="col-form-legend form-control-label required"] + [.="[trans]Custom label[/trans]"] +' + ); + } + + public function testErrors() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); + $form->addError(new FormError('[trans]Error 1[/trans]')); + $form->addError(new FormError('[trans]Error 2[/trans]')); + $view = $form->createView(); + $html = $this->renderErrors($view); + + $this->assertMatchesXpath($html, +'/div + [@class="alert alert-danger"] + [ + ./ul + [@class="list-unstyled mb-0"] + [ + ./li + [.="[trans]Error 1[/trans]"] + /following-sibling::li + [.="[trans]Error 2[/trans]"] + ] + [count(./li)=2] + ] +' + ); + } + + public function testCheckedCheckbox() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', true); + + $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), +'/div + [@class="form-check"] + [ + ./label + [.=" [trans]Name[/trans]"] + [@class="form-check-label required"] + [ + ./input[@type="checkbox"][@name="name"][@id="my&id"][@class="my&class"][@checked="checked"][@value="1"] + ] + ] +' + ); + } + + public function testSingleChoiceAttributesWithMainAttributes() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'multiple' => false, + 'expanded' => false, + 'attr' => array('class' => 'bar&baz'), + )); + + $this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'bar&baz')), +'/select + [@name="name"] + [@class="bar&baz form-control"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testSingleExpandedChoiceAttributesWithMainAttributes() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'multiple' => false, + 'expanded' => true, + 'attr' => array('class' => 'bar&baz'), + )); + + $this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'bar&baz')), +'/div + [@class="bar&baz"] + [ + ./div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testUncheckedCheckbox() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', false); + + $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), +'/div + [@class="form-check"] + [ + ./label + [.=" [trans]Name[/trans]"] + [ + ./input[@type="checkbox"][@name="name"][@id="my&id"][@class="my&class"][not(@checked)] + ] + ] +' + ); + } + + public function testCheckboxWithValue() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', false, array( + 'value' => 'foo&bar', + )); + + $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), +'/div + [@class="form-check"] + [ + ./label + [.=" [trans]Name[/trans]"] + [ + ./input[@type="checkbox"][@name="name"][@id="my&id"][@class="my&class"][@value="foo&bar"] + ] + ] +' + ); + } + + public function testSingleChoiceExpanded() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'multiple' => false, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testSingleChoiceExpandedWithLabelsAsFalse() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'choice_label' => false, + 'multiple' => false, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testSingleChoiceExpandedWithLabelsSetByCallable() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'), + 'choice_label' => function ($choice, $label, $value) { + if ('&b' === $choice) { + return false; + } + + return 'label.'.$value; + }, + 'multiple' => false, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [.=" [trans]label.&a[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" [trans]label.&c[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_2"][@value="&c"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testSingleChoiceExpandedWithLabelsSetFalseByCallable() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'choice_label' => function () { + return false; + }, + 'multiple' => false, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testSingleChoiceExpandedWithoutTranslation() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'multiple' => false, + 'expanded' => true, + 'choice_translation_domain' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [.=" Choice&A"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" Choice&B"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testSingleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => false, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)][@class="foo&bar"] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testSingleChoiceExpandedWithPlaceholder() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'multiple' => false, + 'expanded' => true, + 'placeholder' => 'Test&Me', + 'required' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [.=" [trans]Test&Me[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_placeholder"][not(@checked)] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testSingleChoiceExpandedWithPlaceholderWithoutTranslation() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'multiple' => false, + 'expanded' => true, + 'required' => false, + 'choice_translation_domain' => false, + 'placeholder' => 'Placeholder&Not&Translated', + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [.=" Placeholder&Not&Translated"] + [ + ./input[@type="radio"][@name="name"][@id="name_placeholder"][not(@checked)] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" Choice&A"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" Choice&B"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testSingleChoiceExpandedWithBooleanValue() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', true, array( + 'choices' => array('Choice&A' => '1', 'Choice&B' => '0'), + 'multiple' => false, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testMultipleChoiceExpanded() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a', '&c'), array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'), + 'multiple' => true, + 'expanded' => true, + 'required' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&C[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testMultipleChoiceExpandedWithLabelsAsFalse() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a'), array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'choice_label' => false, + 'multiple' => true, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_1"][@value="&b"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testMultipleChoiceExpandedWithLabelsSetByCallable() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a'), array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'), + 'choice_label' => function ($choice, $label, $value) { + if ('&b' === $choice) { + return false; + } + + return 'label.'.$value; + }, + 'multiple' => true, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), + '/div + [ + ./div + [@class="form-check"] + [ + ./label + [.=" [trans]label.&a[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_1"][@value="&b"][not(@checked)] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" [trans]label.&c[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_2"][@value="&c"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testMultipleChoiceExpandedWithLabelsSetFalseByCallable() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a'), array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'choice_label' => function () { + return false; + }, + 'multiple' => true, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_1"][@value="&b"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testMultipleChoiceExpandedWithoutTranslation() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a', '&c'), array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'), + 'multiple' => true, + 'expanded' => true, + 'required' => true, + 'choice_translation_domain' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [.=" Choice&A"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" Choice&B"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" Choice&C"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testMultipleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a', '&c'), array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => true, + 'expanded' => true, + 'required' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)][@class="foo&bar"] + ] + ] + /following-sibling::div + [@class="form-check"] + [ + ./label + [.=" [trans]Choice&C[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testCheckedRadio() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', true); + + $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), +'/div + [@class="form-check"] + [ + ./label + [@class="form-check-label required"] + [ + ./input + [@id="my&id"] + [@type="radio"] + [@name="name"] + [@class="my&class"] + [@checked="checked"] + [@value="1"] + ] + ] +' + ); + } + + public function testUncheckedRadio() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false); + + $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), +'/div + [@class="form-check"] + [ + ./label + [@class="form-check-label required"] + [ + ./input + [@id="my&id"] + [@type="radio"] + [@name="name"] + [@class="my&class"] + [not(@checked)] + ] + ] +' + ); + } + + public function testRadioWithValue() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false, array( + 'value' => 'foo&bar', + )); + + $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), +'/div + [@class="form-check"] + [ + ./label + [@class="form-check-label required"] + [ + ./input + [@id="my&id"] + [@type="radio"] + [@name="name"] + [@class="my&class"] + [@value="foo&bar"] + ] + ] +' + ); + } + + public function testButtonAttributeNameRepeatedIfTrue() + { + $form = $this->factory->createNamed('button', 'Symfony\Component\Form\Extension\Core\Type\ButtonType', null, array( + 'attr' => array('foo' => true), + )); + + $html = $this->renderWidget($form->createView()); + + // foo="foo" + $this->assertSame('', $html); + } + + public function testFile() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\FileType'); + + $this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'my&class form-control-file')), +'/input + [@type="file"] +' + ); + } +} From 709f134dc8f5c62a1f116d922e7ce3ee66c92f39 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 25 Sep 2017 17:11:48 +0200 Subject: [PATCH 2/3] Removed unneeded wrapping quotes around a Twig key --- .../views/Form/bootstrap_4_horizontal_layout.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig index 046cf3cdd1703..32b4944bab3d0 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig @@ -7,7 +7,7 @@
{%- else -%} {%- if expanded is not defined or not expanded -%} - {%- set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' col-form-label')|trim}) -%} + {%- 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}) -%} {{- parent() -}} From f12e5887ff51266519226c8eea9ffb268f1273d0 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 25 Sep 2017 17:12:23 +0200 Subject: [PATCH 3/3] Removed unneeded wrapping quotes around a Twig key --- .../Twig/Resources/views/Form/bootstrap_4_layout.html.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig index b3c71a74ea5aa..7dda32ace2fc4 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig @@ -72,9 +72,9 @@ {% block form_label -%} {%- if expanded is defined and expanded -%} {%- set element = 'legend' -%} - {%- set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' col-form-legend')|trim}) -%} + {%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-legend')|trim}) -%} {%- endif -%} - {%- set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' form-control-label')|trim}) -%} + {%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' form-control-label')|trim}) -%} {{- parent() -}} {%- endblock form_label %}