diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md index 29baddf4f6c15..f0b9d0046ccc8 100644 --- a/UPGRADE-2.1.md +++ b/UPGRADE-2.1.md @@ -102,7 +102,7 @@ ``` * The custom factories for the firewall configuration are now registered during the build method of bundles instead of being registered - by the end-user. This means that you will you need to remove the 'factories' + by the end-user. This means that you will you need to remove the 'factories' keys in your security configuration. * The Firewall listener is now registered after the Router listener. This @@ -313,29 +313,29 @@ return isset($options['widget']) && 'single_text' === $options['widget'] ? 'text' : 'choice'; } ``` - + * The methods `getDefaultOptions()` and `getAllowedOptionValues()` of form types no longer receive an option array. - + You can specify options that depend on other options using closures instead. - + Before: - + ``` public function getDefaultOptions(array $options) { $defaultOptions = array(); - + if ($options['multiple']) { $defaultOptions['empty_data'] = array(); } - + return $defaultOptions; } ``` - + After: - + ``` public function getDefaultOptions() { @@ -346,7 +346,7 @@ ); } ``` - + The second argument `$previousValue` does not have to be specified if not needed. @@ -366,29 +366,92 @@ (or any other of the BIND events). In case you used the CallbackValidator class, you should now pass the callback directly to `addEventListener`. - * simplified CSRF protection and removed the csrf type + * Since FormType and FieldType were merged, you need to adapt your form + themes. + + The "field_widget" and all references to it should be renamed to + "form_widget_single_control": + + Before: + + ``` + {% block url_widget %} + {% spaceless %} + {% set type = type|default('url') %} + {{ block('field_widget') }} + {% endspaceless %} + {% endblock url_widget %} + ``` + + After: + + ``` + {% block url_widget %} + {% spaceless %} + {% set type = type|default('url') %} + {{ block('form_widget_single_control') }} + {% endspaceless %} + {% endblock url_widget %} + ``` + + All other "field_*" blocks and references to them should be renamed to + "form_*". If you previously defined both a "field_*" and a "form_*" + block, you can merge them into a single "form_*" block and check the new + Boolean variable "single_control": + + Before: + + ``` + {% block form_errors %} + {% spaceless %} + ... form code ... + {% endspaceless %} + {% endblock form_errors %} - * deprecated FieldType and merged it into FormType + {% block field_errors %} + {% spaceless %} + ... field code ... + {% endspaceless %} + {% endblock field_errors %} + ``` + + After: + + ``` + {% block form_errors %} + {% spaceless %} + {% if single_control %} + ... field code ... + {% else %} + ... form code ... + {% endif %} + {% endspaceless %} + {% endblock form_errors %} + ``` + + Furthermore, the block "generic_label" was merged into "form_label". You + should now override "form_label" in order to customize labels. + + Last but not least, the block "widget_choice_options" was renamed to + "choice_widget_options" to be consistent with the rest of the default + theme. - * [BC BREAK] renamed "field_*" theme blocks to "form_*" and "field_widget" to - "input" - * The method `guessMinLength()` of FormTypeGuesserInterface was deprecated and will be removed in Symfony 2.3. You should use the new method `guessPattern()` instead which may return any regular expression that is inserted in the HTML5 attribute "pattern". - + Before: - + public function guessMinLength($class, $property) { if (/* condition */) { return new ValueGuess($minLength, Guess::LOW_CONFIDENCE); } } - + After: - + public function guessPattern($class, $property) { if (/* condition */) { @@ -470,7 +533,7 @@ `validate` and its return value was dropped. `ConstraintValidator` still contains the deprecated `isValid` method and - forwards `validate` calls to `isValid` by default. This BC layer will be + forwards `validate` calls to `isValid` by default. This BC layer will be removed in Symfony 2.3. You are advised to rename your methods. You should also remove the return values, which have never been used by the framework. @@ -500,7 +563,7 @@ $this->context->addViolation($constraint->message, array( '{{ value }}' => $value, )); - + return; } } 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 46420a71221fd..468423b66d945 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 @@ -2,17 +2,30 @@ {% block form_widget %} {% spaceless %} - {% if form.children|length > 0 %} -
- {{ block('form_rows') }} - {{ form_rest(form) }} -
+ {% if single_control %} + {{ block('form_widget_single_control') }} {% else %} - {{ block('input') }} + {{ block('form_widget_compound') }} {% endif %} {% endspaceless %} {% endblock form_widget %} +{% block form_widget_single_control %} +{% spaceless %} + {% set type = type|default('text') %} + +{% endspaceless %} +{% endblock form_widget_single_control %} + +{% block form_widget_compound %} +{% spaceless %} +
+ {{ block('form_rows') }} + {{ form_rest(form) }} +
+{% endspaceless %} +{% endblock form_widget_compound %} + {% block collection_widget %} {% spaceless %} {% if prototype is defined %} @@ -28,49 +41,61 @@ {% endspaceless %} {% endblock textarea_widget %} -{% block widget_choice_options %} +{% block choice_widget %} {% spaceless %} - {% for index, choice in options %} - {% if _form_is_choice_group(choice) %} - - {% for nested_choice in choice %} - - {% endfor %} - - {% else %} - - {% endif %} + {% if expanded %} + {{ block('choice_widget_expanded') }} + {% else %} + {{ block('choice_widget_collapsed') }} + {% endif %} +{% endspaceless %} +{% endblock choice_widget %} + +{% block choice_widget_expanded %} +{% spaceless %} +
+ {% for child in form %} + {{ form_widget(child) }} + {{ form_label(child) }} {% endfor %} +
{% endspaceless %} -{% endblock widget_choice_options %} +{% endblock choice_widget_expanded %} -{% block choice_widget %} +{% block choice_widget_collapsed %} {% spaceless %} - {% if expanded %} -
- {% for child in form %} - {{ form_widget(child) }} - {{ form_label(child) }} - {% endfor %} -
- {% else %} - {% endif %} {% endspaceless %} -{% endblock choice_widget %} +{% endblock choice_widget_collapsed %} + +{% block choice_widget_options %} +{% spaceless %} + {% for index, choice in options %} + {% if _form_is_choice_group(choice) %} + + {% for nested_choice in choice %} + + {% endfor %} + + {% else %} + + {% endif %} + {% endfor %} +{% endspaceless %} +{% endblock choice_widget_options %} {% block checkbox_widget %} {% spaceless %} @@ -87,7 +112,7 @@ {% block datetime_widget %} {% spaceless %} {% if widget == 'single_text' %} - {{ block('input') }} + {{ block('form_widget_single_control') }} {% else %}
{{ form_errors(form.date) }} @@ -102,7 +127,7 @@ {% block date_widget %} {% spaceless %} {% if widget == 'single_text' %} - {{ block('input') }} + {{ block('form_widget_single_control') }} {% else %}
{{ date_pattern|replace({ @@ -118,7 +143,7 @@ {% block time_widget %} {% spaceless %} {% if widget == 'single_text' %} - {{ block('input') }} + {{ block('form_widget_single_control') }} {% else %}
{{ form_widget(form.hour, { 'attr': { 'size': '1' } }) }}:{{ form_widget(form.minute, { 'attr': { 'size': '1' } }) }}{% if with_seconds %}:{{ form_widget(form.second, { 'attr': { 'size': '1' } }) }}{% endif %} @@ -131,80 +156,76 @@ {% spaceless %} {# type="number" doesn't work with floats #} {% set type = type|default('text') %} - {{ block('input') }} + {{ block('form_widget_single_control') }} {% endspaceless %} {% endblock number_widget %} {% block integer_widget %} {% spaceless %} {% set type = type|default('number') %} - {{ block('input') }} + {{ block('form_widget_single_control') }} {% endspaceless %} {% endblock integer_widget %} {% block money_widget %} {% spaceless %} - {{ money_pattern|replace({ '{{ widget }}': block('input') })|raw }} + {{ money_pattern|replace({ '{{ widget }}': block('form_widget_single_control') })|raw }} {% endspaceless %} {% endblock money_widget %} {% block url_widget %} {% spaceless %} {% set type = type|default('url') %} - {{ block('input') }} + {{ block('form_widget_single_control') }} {% endspaceless %} {% endblock url_widget %} {% block search_widget %} {% spaceless %} {% set type = type|default('search') %} - {{ block('input') }} + {{ block('form_widget_single_control') }} {% endspaceless %} {% endblock search_widget %} {% block percent_widget %} {% spaceless %} {% set type = type|default('text') %} - {{ block('input') }} % + {{ block('form_widget_single_control') }} % {% endspaceless %} {% endblock percent_widget %} {% block password_widget %} {% spaceless %} {% set type = type|default('password') %} - {{ block('input') }} + {{ block('form_widget_single_control') }} {% endspaceless %} {% endblock password_widget %} {% block hidden_widget %} +{% spaceless %} {% set type = type|default('hidden') %} - {{ block('input') }} + {{ block('form_widget_single_control') }} +{% endspaceless %} {% endblock hidden_widget %} {% block email_widget %} {% spaceless %} {% set type = type|default('email') %} - {{ block('input') }} + {{ block('form_widget_single_control') }} {% endspaceless %} {% endblock email_widget %} {# Labels #} -{% block generic_label %} -{% spaceless %} - {% if required %} - {% set attr = attr|merge({'class': attr.class|default('') ~ ' required'}) %} - {% endif %} - {{ label|trans({}, translation_domain) }} -{% endspaceless %} -{% endblock %} - {% block form_label %} {% spaceless %} - {% if form.children|length == 0 %} - {% set attr = attr|merge({'for': id}) %} + {% if single_control %} + {% set label_attr = label_attr|merge({'for': id}) %} {% endif %} - {{ block('generic_label') }} + {% if required %} + {% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %} + {% endif %} + {{ label|trans({}, translation_domain) }} {% endspaceless %} {% endblock form_label %} @@ -220,7 +241,11 @@ {% spaceless %}
{{ form_label(form, label|default(null)) }} - {% if form.children|length == 0 %} + {# + If the child is a compound form, the errors are rendered inside + the container. See also block form_rows. + #} + {% if single_control %} {{ form_errors(form) }} {% endif %} {{ form_widget(form) }} @@ -247,10 +272,8 @@ {% for error in errors %}
  • {{ error.messagePluralization is null - ? - error.messageTemplate|trans(error.messageParameters, 'validators') - : - error.messageTemplate|transchoice(error.messagePluralization, error.messageParameters, 'validators') + ? error.messageTemplate|trans(error.messageParameters, 'validators') + : error.messageTemplate|transchoice(error.messagePluralization, error.messageParameters, 'validators') }}
  • {% endfor %} @@ -279,13 +302,6 @@ {% endspaceless %} {% endblock form_rows %} -{% block input %} -{% spaceless %} - {% set type = type|default('text') %} - -{% endspaceless %} -{% endblock input %} - {% block widget_attributes %} {% spaceless %} id="{{ id }}" name="{{ full_name }}"{% if read_only %} readonly="readonly"{% endif %}{% if disabled %} disabled="disabled"{% endif %}{% if required %} required="required"{% endif %}{% if max_length %} maxlength="{{ max_length }}"{% endif %}{% if pattern %} pattern="{{ pattern }}"{% endif %} @@ -302,44 +318,12 @@ {# Deprecated in Symfony 2.1, to be removed in 2.3 #} -{% block field_widget %} -{% spaceless %} - {{ block('input') }} -{% endspaceless %} -{% endblock field_widget %} - -{% block field_label %} -{% spaceless %} - {{ block('form_label') }} -{% endspaceless %} -{% endblock field_label %} - -{% block field_row %} -{% spaceless %} - {{ block('form_row') }} -{% endspaceless %} -{% endblock field_row %} - -{% block field_enctype %} -{% spaceless %} - {{ block('form_enctype') }} -{% endspaceless %} -{% endblock field_enctype %} - -{% block field_errors %} -{% spaceless %} - {{ block('form_errors') }} -{% endspaceless %} -{% endblock field_errors %} - -{% block field_rest %} -{% spaceless %} - {{ block('form_rest') }} -{% endspaceless %} -{% endblock field_rest %} - -{% block field_rows %} -{% spaceless %} - {{ block('form_rows') }} -{% endspaceless %} -{% endblock field_rows %} \ No newline at end of file +{% block generic_label %}{{ block('form_label') }}{% endblock %} +{% block widget_choice_options %}{{ block('choice_widget_options') }}{% endblock %} +{% block field_widget %}{{ block('form_widget_single_control') }}{% endblock %} +{% block field_label %}{{ block('form_label') }}{% endblock %} +{% block field_row %}{{ block('form_row') }}{% endblock %} +{% block field_enctype %}{{ block('form_enctype') }}{% endblock %} +{% block field_errors %}{{ block('form_errors') }}{% endblock %} +{% block field_rest %}{{ block('form_rest') }}{% endblock %} +{% block field_rows %}{{ block('form_rows') }}{% endblock %} \ No newline at end of file diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig index b053c59e6f7e1..b3a95582839b6 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig @@ -7,7 +7,7 @@ {{ form_label(form, label|default(null)) }} - {% if form.children|length == 0 %} + {% if single_control %} {{ form_errors(form) }} {% endif %} {{ form_widget(form) }} @@ -18,7 +18,9 @@ {% block form_errors %} {% spaceless %} - {% if form.children|length > 0 %} + {% if single_control %} + {{ parent() }} + {% else %} {% if errors|length > 0 %} @@ -26,8 +28,6 @@ {% endif %} - {% else %} - {{ parent() }} {% endif %} {% endspaceless %} {% endblock form_errors %} @@ -42,15 +42,11 @@ {% endspaceless %} {% endblock hidden_row %} -{% block form_widget %} +{% block form_widget_compound %} {% spaceless %} - {% if form.children|length > 0 %} - - {{ block('form_rows') }} - {{ form_rest(form) }} -
    - {% else %} - {{ parent() }} - {% endif %} + + {{ block('form_rows') }} + {{ form_rest(form) }} +
    {% endspaceless %} -{% endblock form_widget %} +{% endblock form_widget_compound %} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/theme.html.twig b/src/Symfony/Bridge/Twig/Tests/Extension/theme.html.twig index ee5b19e0737d8..36f8636ad2e1b 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/theme.html.twig +++ b/src/Symfony/Bridge/Twig/Tests/Extension/theme.html.twig @@ -1,6 +1,6 @@ -{% block input %} +{% block form_widget_single_control %} {% spaceless %} {% set type = type|default('text') %} - + {% endspaceless %} -{% endblock input %} +{% endblock form_widget_single_control %} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/theme_extends.html.twig b/src/Symfony/Bridge/Twig/Tests/Extension/theme_extends.html.twig index f58e589498dae..12b9da872dd32 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/theme_extends.html.twig +++ b/src/Symfony/Bridge/Twig/Tests/Extension/theme_extends.html.twig @@ -1,8 +1,8 @@ {% extends 'form_div_layout.html.twig' %} -{% block input %} +{% block form_widget_single_control %} {% spaceless %} {% set type = type|default('text') %} - + {% endspaceless %} -{% endblock input %} +{% endblock form_widget_single_control %} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/theme_use.html.twig b/src/Symfony/Bridge/Twig/Tests/Extension/theme_use.html.twig index 9304e9dcfa94d..1b7f6b59cfd53 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/theme_use.html.twig +++ b/src/Symfony/Bridge/Twig/Tests/Extension/theme_use.html.twig @@ -1,8 +1,8 @@ {% use 'form_div_layout.html.twig' %} -{% block input %} +{% block form_widget_single_control %} {% spaceless %} {% set type = type|default('text') %} - + {% endspaceless %} -{% endblock input %} +{% endblock form_widget_single_control %} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/attributes.html.php index fe4afa01d2e61..12366d6e44655 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/attributes.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/attributes.html.php @@ -1,9 +1 @@ -id="escape($id) ?>" -name="escape($full_name) ?>" -readonly="readonly" -disabled="disabled" -required="required" -maxlength="escape($max_length) ?>" -pattern="escape($pattern) ?>" - $v) { printf('%s="%s" ', $view->escape($k), $view->escape($v)); } ?> - +renderBlock('widget_attributes') ?> \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/checkbox_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/checkbox_widget.html.php index 921f4af25bbff..fff427db11119 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/checkbox_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/checkbox_widget.html.php @@ -1,5 +1,5 @@ renderBlock('attributes') ?> + renderBlock('widget_attributes') ?> value="escape($value) ?>" checked="checked" /> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php index 66c664fb1d620..298df7b3001c0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php @@ -1,11 +1 @@ - $choice): ?> - isChoiceGroup($choice)): ?> - - - - - - - - - +renderBlock('choice_widget_options') ?> \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget.html.php index e54bdb4df8423..99db4acfae15b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget.html.php @@ -1,22 +1,5 @@ -
    renderBlock('container_attributes') ?>> - - widget($child) ?> - label($child) ?> - -
    +renderBlock('choice_widget_expanded') ?> - +renderBlock('choice_widget_collapsed') ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_collapsed.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_collapsed.html.php new file mode 100644 index 0000000000000..b0c601ea8f9a4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_collapsed.html.php @@ -0,0 +1,13 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_expanded.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_expanded.html.php new file mode 100644 index 0000000000000..58de38d646988 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_expanded.html.php @@ -0,0 +1,6 @@ +
    renderBlock('widget_container_attributes') ?>> + + widget($child) ?> + label($child) ?> + +
    diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php new file mode 100644 index 0000000000000..66c664fb1d620 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php @@ -0,0 +1,11 @@ + $choice): ?> + isChoiceGroup($choice)): ?> + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/container_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/container_attributes.html.php index a36cfbbdfc462..e132ed73c4c2a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/container_attributes.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/container_attributes.html.php @@ -1,2 +1 @@ -id="escape($id) ?>" - $v) { printf('%s="%s" ', $view->escape($k), $view->escape($v)); } ?> +renderBlock('widget_container_attributes') ?> \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/date_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/date_widget.html.php index bd2c2769af420..338e8edfed078 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/date_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/date_widget.html.php @@ -1,7 +1,7 @@ - renderBlock('input'); ?> + renderBlock('form_widget_single_control'); ?> -
    renderBlock('container_attributes') ?>> +
    renderBlock('widget_container_attributes') ?>> widget($form['year']), $view['form']->widget($form['month']), diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/datetime_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/datetime_widget.html.php index f71bd64e377f3..9d6aae60aaaca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/datetime_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/datetime_widget.html.php @@ -1,7 +1,7 @@ - renderBlock('input'); ?> + renderBlock('form_widget_single_control'); ?> -
    renderBlock('container_attributes') ?>> +
    renderBlock('widget_container_attributes') ?>> widget($form['date']).' '.$view['form']->widget($form['time']) ?>
    diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/email_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/email_widget.html.php index a00dda278e983..80409c0a3079c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/email_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/email_widget.html.php @@ -1 +1 @@ -renderBlock('input', array('type' => isset($type) ? $type : 'email')) ?> +renderBlock('form_widget_single_control', array('type' => isset($type) ? $type : 'email')) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_widget.html.php index f1ca2edadbfda..bad67a85e279c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_widget.html.php @@ -1 +1 @@ -renderBlock('input') ?> \ No newline at end of file +renderBlock('form_widget_single_control') ?> \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_label.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_label.html.php index 3ddc300fd4540..ac6bf646ce6ad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_label.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_label.html.php @@ -1,3 +1,3 @@ - -hasChildren()) { $attr['for'] = $id; } ?> - + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_row.html.php index 02fb9ae9b635c..977a272257d9f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_row.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_row.html.php @@ -1,6 +1,6 @@
    label($form, isset($label) ? $label : null) ?> - hasChildren()): ?> + errors($form) ?> widget($form) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget.html.php index 1c9368693b49c..98d1a80be7068 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget.html.php @@ -1,8 +1,5 @@ -hasChildren()): ?> -
    renderBlock('container_attributes') ?>> - renderBlock('form_rows') ?> - rest($form) ?> -
    + +renderBlock('form_widget_single_control')?> -renderBlock('input')?> +renderBlock('form_widget_compound')?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_compound.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_compound.html.php new file mode 100644 index 0000000000000..ee349d1bd7290 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_compound.html.php @@ -0,0 +1,4 @@ +
    renderBlock('widget_container_attributes') ?>> + renderBlock('form_rows') ?> + rest($form) ?> +
    \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_single_control.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_single_control.html.php new file mode 100644 index 0000000000000..29649cfbd6065 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_single_control.html.php @@ -0,0 +1,5 @@ +value="escape($value) ?>" + renderBlock('widget_attributes') ?> +/> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_widget.html.php index 50a42451aef26..d7395a48573fe 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_widget.html.php @@ -1 +1 @@ -renderBlock('input', array('type' => isset($type) ? $type : "hidden")) ?> +renderBlock('form_widget_single_control', array('type' => isset($type) ? $type : "hidden")) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/input.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/input.html.php deleted file mode 100644 index 0c8648338639a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/input.html.php +++ /dev/null @@ -1,5 +0,0 @@ -" - value="escape($value) ?>" - renderBlock('attributes') ?> -/> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/integer_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/integer_widget.html.php index 1fc6ace34b4fb..dc2866fd20605 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/integer_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/integer_widget.html.php @@ -1 +1 @@ -renderBlock('input', array('type' => isset($type) ? $type : "number")) ?> +renderBlock('form_widget_single_control', array('type' => isset($type) ? $type : "number")) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/money_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/money_widget.html.php index a68ad5ddacb10..17279721c5dfb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/money_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/money_widget.html.php @@ -1 +1 @@ -renderBlock('input'), $money_pattern) ?> +renderBlock('form_widget_single_control'), $money_pattern) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/number_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/number_widget.html.php index 7e1a2776a7d49..3d6fd62209321 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/number_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/number_widget.html.php @@ -1 +1 @@ -renderBlock('input', array('type' => isset($type) ? $type : "text")) ?> +renderBlock('form_widget_single_control', array('type' => isset($type) ? $type : "text")) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/password_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/password_widget.html.php index 7aff242ef43cc..716e46cf60162 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/password_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/password_widget.html.php @@ -1 +1 @@ -renderBlock('input', array('type' => isset($type) ? $type : "password")) ?> +renderBlock('form_widget_single_control', array('type' => isset($type) ? $type : "password")) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/percent_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/percent_widget.html.php index 328321f21fcae..5446968bafffd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/percent_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/percent_widget.html.php @@ -1 +1 @@ -renderBlock('input', array('type' => isset($type) ? $type : "text")) ?> % +renderBlock('form_widget_single_control', array('type' => isset($type) ? $type : "text")) ?> % diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/radio_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/radio_widget.html.php index daa16dbc6298f..3ecad14ca2ec8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/radio_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/radio_widget.html.php @@ -1,5 +1,5 @@ renderBlock('attributes') ?> + renderBlock('widget_attributes') ?> value="escape($value) ?>" checked="checked" /> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/search_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/search_widget.html.php index d8a773544e713..ca3b00917fbf1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/search_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/search_widget.html.php @@ -1 +1 @@ -renderBlock('input', array('type' => isset($type) ? $type : "search")) ?> +renderBlock('form_widget_single_control', array('type' => isset($type) ? $type : "search")) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/textarea_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/textarea_widget.html.php index 11a786d69c487..a82744b8e0684 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/textarea_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/textarea_widget.html.php @@ -1 +1 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/time_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/time_widget.html.php index 2178974c74c4c..8d34459b56aac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/time_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/time_widget.html.php @@ -1,7 +1,7 @@ - renderBlock('input'); ?> + renderBlock('form_widget_single_control'); ?> -
    renderBlock('container_attributes') ?>> +
    renderBlock('widget_container_attributes') ?>> renderBlock('input', array('type' => isset($type) ? $type : "url")) ?> +renderBlock('form_widget_single_control', array('type' => isset($type) ? $type : "url")) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_attributes.html.php new file mode 100644 index 0000000000000..fe4afa01d2e61 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_attributes.html.php @@ -0,0 +1,9 @@ +id="escape($id) ?>" +name="escape($full_name) ?>" +readonly="readonly" +disabled="disabled" +required="required" +maxlength="escape($max_length) ?>" +pattern="escape($pattern) ?>" + $v) { printf('%s="%s" ', $view->escape($k), $view->escape($v)); } ?> + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_container_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_container_attributes.html.php new file mode 100644 index 0000000000000..a36cfbbdfc462 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_container_attributes.html.php @@ -0,0 +1,2 @@ +id="escape($id) ?>" + $v) { printf('%s="%s" ', $view->escape($k), $view->escape($v)); } ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_errors.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_errors.html.php index 05d8c4aea89d3..1a889760ca869 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_errors.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_errors.html.php @@ -1,4 +1,26 @@ -hasChildren()): ?> + + +
      + +
    • getMessagePluralization()) { + echo $view['translator']->trans( + $error->getMessageTemplate(), + $error->getMessageParameters(), + 'validators' + ); + } else { + echo $view['translator']->transChoice( + $error->getMessageTemplate(), + $error->getMessagePluralization(), + $error->getMessageParameters(), + 'validators' + ); + }?>
    • + +
    + + 0): ?> @@ -26,26 +48,4 @@ - - -
      - -
    • getMessagePluralization()) { - echo $view['translator']->trans( - $error->getMessageTemplate(), - $error->getMessageParameters(), - 'validators' - ); - } else { - echo $view['translator']->transChoice( - $error->getMessageTemplate(), - $error->getMessagePluralization(), - $error->getMessageParameters(), - 'validators' - ); - }?>
    • - -
    - \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_row.html.php index 9262a1bae10b3..5526a03fe9cc5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_row.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_row.html.php @@ -3,7 +3,7 @@ label($form, isset($label) ? $label : null) ?> - hasChildren()): ?> + errors($form) ?> widget($form) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_widget.html.php deleted file mode 100644 index 171f1eb40c74e..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_widget.html.php +++ /dev/null @@ -1,8 +0,0 @@ -hasChildren()): ?> -renderBlock('container_attributes') ?>> - renderBlock('form_rows') ?> - rest($form) ?> -
    - -renderBlock('input')?> - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_widget_compound.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_widget_compound.html.php new file mode 100644 index 0000000000000..9c90f15e58fc9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_widget_compound.html.php @@ -0,0 +1,4 @@ +renderBlock('widget_container_attributes') ?>> + renderBlock('form_rows') ?> + rest($form) ?> +
    diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/input.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_widget_single_control.html.php similarity index 60% rename from src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/input.html.php rename to src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_widget_single_control.html.php index 64f43ddf6eb1b..6506f9140eeab 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/input.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_widget_single_control.html.php @@ -1,2 +1,2 @@ -renderBlock('attributes') ?> value="" rel="theme" /> +renderBlock('widget_attributes') ?> value="" rel="theme" /> diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index f6f32633e5f1d..818b43e969bca 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -25,16 +25,12 @@ CHANGELOG * [BC BREAK] removed EntitiesToArrayTransformer and EntityToIdTransformer. The former has been replaced by CollectionToArrayTransformer in combination with EntityChoiceList, the latter is not required in the core anymore. - * [BC BREAK] renamed - * ArrayToBooleanChoicesTransformer to ChoicesToBooleanArrayTransformer * ScalarToBooleanChoicesTransformer to ChoiceToBooleanArrayTransformer * ArrayToChoicesTransformer to ChoicesToValuesTransformer * ScalarToChoiceTransformer to ChoiceToValueTransformer - - to be consistent with the naming in ChoiceListInterface. - + to be consistent with the naming in ChoiceListInterface. * [BC BREAK] removed FormUtil::toArrayKey() and FormUtil::toArrayKeys(). They were merged into ChoiceList and have no public equivalent anymore. * choice fields now throw a FormException if neither the "choices" nor the @@ -62,8 +58,15 @@ CHANGELOG by event subscribers * simplified CSRF protection and removed the csrf type * deprecated FieldType and merged it into FormType - * [BC BREAK] renamed "field_*" theme blocks to "form_*" and "field_widget" to - "input" + * [BC BREAK] renamed theme blocks + * "field_*" to "form_*" + * "field_widget" to "form_widget_single_control" + * "widget_choice_options" to "choice_widget_options" + * "generic_label" to "form_label" + * added theme blocks "form_widget_compound", "choice_widget_expanded" and + "choice_widget_collapsed" to make theming more modular * ValidatorTypeGuesser now guesses "collection" for array type constraint * added method `guessPattern` to FormTypeGuesserInterface to guess which pattern to use in the HTML5 attribute "pattern" * deprecated method `guessMinLength` in favor of `guessPattern` + * labels don't display field attributes anymore. Label attributes can be + passed in the "label_attr" option/variable \ No newline at end of file diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php b/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php index 8ab2c6843d0ae..b99b5892aeb0b 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php @@ -46,11 +46,14 @@ public function buildView(FormView $view, FormInterface $form) */ public function getDefaultOptions() { + $emptyData = function (FormInterface $form, $clientData) { + return $clientData; + }; + return array( - 'value' => '1', - 'empty_data' => function (FormInterface $form, $clientData) { - return $clientData; - }, + 'value' => '1', + 'empty_data' => $emptyData, + 'single_control' => true, ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 1a8ba2d1be6a2..3e21af5ad1800 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -165,6 +165,10 @@ public function getDefaultOptions() return $options['required'] ? null : ''; }; + $singleControl = function (Options $options) { + return !$options['expanded']; + }; + return array( 'multiple' => false, 'expanded' => false, @@ -174,6 +178,7 @@ public function getDefaultOptions() 'empty_data' => $emptyData, 'empty_value' => $emptyValue, 'error_bubbling' => false, + 'single_control' => $singleControl, ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php index f428054edee74..2308059058b12 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php @@ -21,6 +21,7 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer; +use Symfony\Component\Form\Options; class DateTimeType extends AbstractType { @@ -130,6 +131,10 @@ public function buildView(FormView $view, FormInterface $form) */ public function getDefaultOptions() { + $singleControl = function (Options $options) { + return $options['widget'] === 'single_text'; + }; + return array( 'input' => 'datetime', 'data_timezone' => null, @@ -158,6 +163,7 @@ public function getDefaultOptions() // representation is not \DateTime, but an array, we need to unset // this option. 'data_class' => null, + 'single_control' => $singleControl, ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php index c506714c7ffd3..38b23e436ab01 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php @@ -11,18 +11,18 @@ namespace Symfony\Component\Form\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; - use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\Exception\CreationException; use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; use Symfony\Component\Form\ReversedTransformer; +use Symfony\Component\Form\Options; class DateType extends AbstractType { @@ -163,6 +163,10 @@ public function buildViewBottomUp(FormView $view, FormInterface $form) */ public function getDefaultOptions() { + $singleControl = function (Options $options) { + return $options['widget'] === 'single_text'; + }; + return array( 'years' => range(date('Y') - 5, date('Y') + 5), 'months' => range(1, 12), @@ -182,6 +186,7 @@ public function getDefaultOptions() // representation is not \DateTime, but an array, we need to unset // this option. 'data_class' => null, + 'single_control' => $singleControl, ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php index 468c65a0f359a..d57176ec672b1 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php @@ -38,6 +38,16 @@ public function buildViewBottomUp(FormView $view, FormInterface $form) ; } + /** + * {@inheritdoc} + */ + public function getDefaultOptions() + { + return array( + 'single_control' => true, + ); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php index ec5870acff7d3..0dea6d113c96c 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php @@ -40,8 +40,13 @@ public function buildForm(FormBuilder $builder, array $options) } else { $options['property_path'] = new PropertyPath($options['property_path']); } + if (!is_array($options['attr'])) { - throw new FormException('The "attr" option must be "array".'); + throw new FormException('The "attr" option must be an "array".'); + } + + if (!is_array($options['label_attr'])) { + throw new FormException('The "label_attr" option must be an "array".'); } $builder @@ -56,11 +61,13 @@ public function buildForm(FormBuilder $builder, array $options) ->setAttribute('max_length', $options['max_length']) ->setAttribute('pattern', $options['pattern']) ->setAttribute('label', $options['label'] ?: $this->humanize($builder->getName())) - ->setAttribute('attr', $options['attr'] ?: array()) + ->setAttribute('attr', $options['attr']) + ->setAttribute('label_attr', $options['label_attr']) ->setAttribute('invalid_message', $options['invalid_message']) ->setAttribute('invalid_message_parameters', $options['invalid_message_parameters']) ->setAttribute('translation_domain', $options['translation_domain']) ->setAttribute('virtual', $options['virtual']) + ->setAttribute('single_control', $options['single_control']) ->setData($options['data']) ->setDataMapper(new PropertyPathMapper($options['data_class'])) ->addEventSubscriber(new ValidationListener()) @@ -125,6 +132,8 @@ public function buildView(FormView $view, FormInterface $form) ->set('label', $form->getAttribute('label')) ->set('multipart', false) ->set('attr', $form->getAttribute('attr')) + ->set('label_attr', $form->getAttribute('label_attr')) + ->set('single_control', $form->getAttribute('single_control')) ->set('types', $types) ->set('translation_domain', $form->getAttribute('translation_domain')) ; @@ -183,7 +192,13 @@ public function getDefaultOptions() return ''; }; }; - + + // For any form that is not represented by a single HTML control, + // errors should bubble up by default + $errorBubbling = function (Options $options) { + return !$options['single_control']; + }; + return array( 'data' => null, 'data_class' => $dataClass, @@ -196,11 +211,13 @@ public function getDefaultOptions() 'pattern' => null, 'property_path' => null, 'by_reference' => true, - 'error_bubbling' => false, + 'error_bubbling' => $errorBubbling, 'error_mapping' => array(), 'label' => null, 'attr' => array(), + 'label_attr' => array(), 'virtual' => false, + 'single_control' => false, 'invalid_message' => 'This value is not valid.', 'invalid_message_parameters' => array(), 'translation_domain' => 'messages', diff --git a/src/Symfony/Component/Form/Extension/Core/Type/HiddenType.php b/src/Symfony/Component/Form/Extension/Core/Type/HiddenType.php index 71066250624af..49562d9fd87ef 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/HiddenType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/HiddenType.php @@ -25,6 +25,7 @@ public function getDefaultOptions() 'required' => false, // Pass errors to the parent 'error_bubbling' => true, + 'single_control' => true, ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php b/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php index 9beaed51029bf..293c01d3cd7cb 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php @@ -37,10 +37,11 @@ public function getDefaultOptions() { return array( // default precision is locale specific (usually around 3) - 'precision' => null, - 'grouping' => false, + 'precision' => null, + 'grouping' => false, // Integer cast rounds towards 0, so do the same when displaying fractions - 'rounding_mode' => \NumberFormatter::ROUND_DOWN, + 'rounding_mode' => \NumberFormatter::ROUND_DOWN, + 'single_control' => true, ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MoneyType.php b/src/Symfony/Component/Form/Extension/Core/Type/MoneyType.php index 1f87f3a2a6872..560cbe59b562d 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MoneyType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MoneyType.php @@ -51,10 +51,11 @@ public function buildView(FormView $view, FormInterface $form) public function getDefaultOptions() { return array( - 'precision' => 2, - 'grouping' => false, - 'divisor' => 1, - 'currency' => 'EUR', + 'precision' => 2, + 'grouping' => false, + 'divisor' => 1, + 'currency' => 'EUR', + 'single_control' => true, ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php b/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php index 21ea17046e760..993df32809561 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php @@ -36,9 +36,10 @@ public function getDefaultOptions() { return array( // default precision is locale specific (usually around 3) - 'precision' => null, - 'grouping' => false, - 'rounding_mode' => \NumberFormatter::ROUND_HALFUP, + 'precision' => null, + 'grouping' => false, + 'rounding_mode' => \NumberFormatter::ROUND_HALFUP, + 'single_control' => true, ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/PercentType.php b/src/Symfony/Component/Form/Extension/Core/Type/PercentType.php index ea6550869a244..0ee0af39e320d 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/PercentType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/PercentType.php @@ -31,8 +31,9 @@ public function buildForm(FormBuilder $builder, array $options) public function getDefaultOptions() { return array( - 'precision' => 0, - 'type' => 'fractional', + 'precision' => 0, + 'type' => 'fractional', + 'single_control' => true, ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TextType.php b/src/Symfony/Component/Form/Extension/Core/Type/TextType.php index 28003119340d1..f454160d3ec61 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TextType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TextType.php @@ -27,6 +27,16 @@ public function buildForm(FormBuilder $builder, array $options) ; } + /** + * {@inheritdoc} + */ + public function getDefaultOptions() + { + return array( + 'single_control' => true, + ); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php index 546220d0ab318..42cd7f42e536a 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php @@ -20,6 +20,7 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer; use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Options; class TimeType extends AbstractType { @@ -136,6 +137,10 @@ public function buildView(FormView $view, FormInterface $form) */ public function getDefaultOptions() { + $singleControl = function (Options $options) { + return $options['widget'] === 'single_text'; + }; + return array( 'hours' => range(0, 23), 'minutes' => range(0, 59), @@ -155,6 +160,7 @@ public function getDefaultOptions() // representation is not \DateTime, but an array, we need to unset // this option. 'data_class' => null, + 'single_control' => $singleControl, ); } diff --git a/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php b/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php index dd74d87b2eaea..da8a07d1af32c 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php +++ b/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php @@ -63,8 +63,8 @@ public function onBindClientData(FilterDataEvent $event) $form = $event->getForm(); $data = $event->getData(); - if ($form->isRoot() && $form->hasChildren() && isset($data[$this->fieldName])) { - if (!$this->csrfProvider->isCsrfTokenValid($this->intention, $data[$this->fieldName])) { + if ($form->isRoot() && !$form->getAttribute('single_control')) { + if (!isset($data[$this->fieldName]) || !$this->csrfProvider->isCsrfTokenValid($this->intention, $data[$this->fieldName])) { $form->addError(new FormError('The CSRF token is invalid. Please try to resubmit the form')); } diff --git a/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php index 8145e8e5755d6..c26324b41e2a1 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php +++ b/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php @@ -64,7 +64,7 @@ public function buildForm(FormBuilder $builder, array $options) */ public function buildViewBottomUp(FormView $view, FormInterface $form) { - if (!$view->hasParent() && $view->hasChildren() && $form->hasAttribute('csrf_field_name')) { + if (!$view->hasParent() && !$form->getAttribute('single_control') && $form->hasAttribute('csrf_field_name')) { $name = $form->getAttribute('csrf_field_name'); $csrfProvider = $form->getAttribute('csrf_provider'); $intention = $form->getAttribute('csrf_intention'); diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 5669e0ead4956..f75cd4b0a128e 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -225,10 +225,7 @@ public function __construct($name, EventDispatcherInterface $dispatcher, $this->validators = $validators; $this->required = (Boolean) $required; $this->disabled = (Boolean) $disabled; - // NULL is the default meaning: - // bubble up if the form has children (complex forms) - // don't bubble up if the form has no children (primitive fields) - $this->errorBubbling = null === $errorBubbling ? null : (Boolean) $errorBubbling; + $this->errorBubbling = (Boolean) $errorBubbling; $this->emptyData = $emptyData; $this->attributes = $attributes; @@ -665,7 +662,7 @@ public function addError(FormError $error) */ public function getErrorBubbling() { - return null === $this->errorBubbling ? $this->hasChildren() : $this->errorBubbling; + return $this->errorBubbling; } /** diff --git a/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php index 07813061886ba..6f0675cba5804 100644 --- a/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php @@ -277,6 +277,20 @@ public function testCollection() ); } + public function testEmptyCollection() + { + $form = $this->factory->createNamed('collection', 'name', array(), array( + 'type' => 'text', + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [./input[@type="hidden"][@id="name__token"]] + [count(./div)=0] +' + ); + } + public function testCollectionRow() { $collection = $this->factory->createNamedBuilder( diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index c75ba7c7a4ff3..7be8193cfe39d 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -155,7 +155,7 @@ public function testLabelOnForm() $this->assertMatchesXpath($html, '/label - [@class=" required"] + [@class="required"] [.="[trans]Name[/trans]"] ' ); @@ -204,7 +204,7 @@ public function testLabelWithCustomTextPassedAsOptionAndDirectly() ); } - public function testLabelWithCustomOptionsPassedDirectly() + public function testLabelDoesNotRenderFieldAttributes() { $form = $this->factory->createNamed('text', 'name'); $html = $this->renderLabel($form->createView(), null, array( @@ -215,6 +215,23 @@ public function testLabelWithCustomOptionsPassedDirectly() $this->assertMatchesXpath($html, '/label + [@for="name"] + [@class="required"] +' + ); + } + + public function testLabelWithCustomOptionsPassedDirectly() + { + $form = $this->factory->createNamed('text', 'name'); + $html = $this->renderLabel($form->createView(), null, array( + 'label_attr' => array( + 'class' => 'my&class' + ), + )); + + $this->assertMatchesXpath($html, +'/label [@for="name"] [@class="my&class required"] ' @@ -225,7 +242,7 @@ public function testLabelWithCustomTextAndCustomOptionsPassedDirectly() { $form = $this->factory->createNamed('text', 'name'); $html = $this->renderLabel($form->createView(), 'Custom label', array( - 'attr' => array( + 'label_attr' => array( 'class' => 'my&class' ), )); diff --git a/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php index f6edbce25a440..9bf77d9c59ee4 100644 --- a/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php @@ -170,6 +170,20 @@ public function testCollection() ); } + public function testEmptyCollection() + { + $form = $this->factory->createNamed('collection', 'name', array(), array( + 'type' => 'text', + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/table + [./tr[@style="display: none"][./td[@colspan="2"]/input[@type="hidden"][@id="name__token"]]] + [count(./tr[./td/input])=1] +' + ); + } + public function testForm() { $view = $this->factory->createNamedBuilder('form', 'name') diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php index 8275c61455999..6df8130e383fd 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php @@ -568,4 +568,32 @@ public function testCreateViewDoNoMarkItAsRendered() $this->assertFalse($view->isRendered()); } + + public function testErrorBubblingIfNoSingleControl() + { + $form = $this->factory->create('form', null, array( + 'single_control' => false, + )); + + $this->assertTrue($form->getErrorBubbling()); + } + + public function testNoErrorBubblingIfSingleControl() + { + $form = $this->factory->create('form', null, array( + 'single_control' => true, + )); + + $this->assertFalse($form->getErrorBubbling()); + } + + public function testOverrideErrorBubbling() + { + $form = $this->factory->create('form', null, array( + 'single_control' => true, + 'error_bubbling' => true, + )); + + $this->assertTrue($form->getErrorBubbling()); + } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php index 4424f81e14140..0fdea7d438e5b 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php @@ -56,43 +56,42 @@ protected function getExtensions() )); } - public function testCsrfProtectionByDefaultIfRootAndChildren() + public function testCsrfProtectionByDefaultIfRootAndNotSingleControl() { $view = $this->factory - ->createBuilder('form', null, array( + ->create('form', null, array( 'csrf_field_name' => 'csrf', + 'single_control' => false, )) - ->add($this->factory->createNamedBuilder('form', 'child')) - ->getForm() ->createView(); $this->assertTrue($view->hasChild('csrf')); } - public function testNoCsrfProtectionByDefaultIfChildrenButNotRoot() + public function testNoCsrfProtectionByDefaultIfNotSingleControlButNotRoot() { $view = $this->factory ->createNamedBuilder('form', 'root') ->add($this->factory ->createNamedBuilder('form', 'form', null, array( 'csrf_field_name' => 'csrf', + 'single_control' => false, )) - ->add($this->factory->createNamedBuilder('form', 'child')) ) ->getForm() - ->get('form') - ->createView(); + ->createView() + ->getChild('form'); $this->assertFalse($view->hasChild('csrf')); } - public function testNoCsrfProtectionByDefaultIfRootButNoChildren() + public function testNoCsrfProtectionByDefaultIfRootButSingleControl() { $view = $this->factory - ->createBuilder('form', null, array( + ->create('form', null, array( 'csrf_field_name' => 'csrf', + 'single_control' => true, )) - ->getForm() ->createView(); $this->assertFalse($view->hasChild('csrf')); @@ -101,12 +100,11 @@ public function testNoCsrfProtectionByDefaultIfRootButNoChildren() public function testCsrfProtectionCanBeDisabled() { $view = $this->factory - ->createBuilder('form', null, array( + ->create('form', null, array( 'csrf_field_name' => 'csrf', 'csrf_protection' => false, + 'single_control' => false, )) - ->add($this->factory->createNamedBuilder('form', 'child')) - ->getForm() ->createView(); $this->assertFalse($view->hasChild('csrf')); @@ -120,13 +118,12 @@ public function testGenerateCsrfToken() ->will($this->returnValue('token')); $view = $this->factory - ->createBuilder('form', null, array( + ->create('form', null, array( 'csrf_field_name' => 'csrf', 'csrf_provider' => $this->csrfProvider, - 'intention' => '%INTENTION%' + 'intention' => '%INTENTION%', + 'single_control' => false, )) - ->add($this->factory->createNamedBuilder('form', 'child')) - ->getForm() ->createView(); $this->assertEquals('token', $view->getChild('csrf')->get('value')); @@ -143,7 +140,7 @@ public function provideBoolean() /** * @dataProvider provideBoolean */ - public function testValidateTokenOnBindIfRootAndChildren($valid) + public function testValidateTokenOnBindIfRootAndNotSingleControl($valid) { $this->csrfProvider->expects($this->once()) ->method('isCsrfTokenValid') @@ -151,13 +148,12 @@ public function testValidateTokenOnBindIfRootAndChildren($valid) ->will($this->returnValue($valid)); $form = $this->factory - ->createBuilder('form', null, array( + ->create('form', null, array( 'csrf_field_name' => 'csrf', 'csrf_provider' => $this->csrfProvider, - 'intention' => '%INTENTION%' - )) - ->add($this->factory->createNamedBuilder('form', 'child')) - ->getForm(); + 'intention' => '%INTENTION%', + 'single_control' => false, + )); $form->bind(array( 'child' => 'foobar', @@ -171,7 +167,32 @@ public function testValidateTokenOnBindIfRootAndChildren($valid) $this->assertSame($valid, $form->isValid()); } - public function testDontValidateTokenIfChildrenButNoRoot() + public function testFailIfRootAndNotSingleControlAndTokenMissing() + { + $this->csrfProvider->expects($this->never()) + ->method('isCsrfTokenValid'); + + $form = $this->factory + ->create('form', null, array( + 'csrf_field_name' => 'csrf', + 'csrf_provider' => $this->csrfProvider, + 'intention' => '%INTENTION%', + 'single_control' => false, + )); + + $form->bind(array( + 'child' => 'foobar', + // token is missing + )); + + // Remove token from data + $this->assertSame(array('child' => 'foobar'), $form->getData()); + + // Validate accordingly + $this->assertFalse($form->isValid()); + } + + public function testDontValidateTokenIfNotSingleControlButNoRoot() { $this->csrfProvider->expects($this->never()) ->method('isCsrfTokenValid'); @@ -182,9 +203,9 @@ public function testDontValidateTokenIfChildrenButNoRoot() ->createNamedBuilder('form', 'form', null, array( 'csrf_field_name' => 'csrf', 'csrf_provider' => $this->csrfProvider, - 'intention' => '%INTENTION%' + 'intention' => '%INTENTION%', + 'single_control' => false, )) - ->add($this->factory->createNamedBuilder('form', 'child')) ) ->getForm() ->get('form'); @@ -195,18 +216,18 @@ public function testDontValidateTokenIfChildrenButNoRoot() )); } - public function testDontValidateTokenIfRootButNoChildren() + public function testDontValidateTokenIfRootButSingleControl() { $this->csrfProvider->expects($this->never()) ->method('isCsrfTokenValid'); $form = $this->factory - ->createBuilder('form', null, array( + ->create('form', null, array( 'csrf_field_name' => 'csrf', 'csrf_provider' => $this->csrfProvider, - 'intention' => '%INTENTION%' - )) - ->getForm(); + 'intention' => '%INTENTION%', + 'single_control' => true, + )); $form->bind(array( 'csrf' => 'token', diff --git a/src/Symfony/Component/Form/Tests/FormFactoryTest.php b/src/Symfony/Component/Form/Tests/FormFactoryTest.php index 206fa5eccf396..4b8eb3f3a9329 100644 --- a/src/Symfony/Component/Form/Tests/FormFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/FormFactoryTest.php @@ -608,41 +608,8 @@ public function testCreateNamedBuilderFromParentBuilder() $this->assertEquals($parentBuilder, $builder->getParent()); } - public function testUnknownOptions() - { - $type = new \Symfony\Component\Form\Extension\Core\Type\TextType(); - - $factory = new FormFactory(array(new \Symfony\Component\Form\Extension\Core\CoreExtension())); - - $this->setExpectedException('Symfony\Component\Form\Exception\InvalidOptionException', - 'The options "invalid", "unknown" do not exist. Known options are: ' . - '"attr", "by_reference", "data", "data_class", "disabled", ' . - '"empty_data", "error_bubbling", "error_mapping", "invalid_message", ' . - '"invalid_message_parameters", "label", "max_length", "pattern", ' . - '"property_path", "read_only", "required", "translation_domain", ' . - '"trim"' - ); - $factory->createNamedBuilder($type, "text", "value", array("invalid" => "opt", "unknown" => "opt")); - } - - public function testUnknownOption() - { - $type = new \Symfony\Component\Form\Extension\Core\Type\TextType(); - - $factory = new FormFactory(array(new \Symfony\Component\Form\Extension\Core\CoreExtension())); - - $this->setExpectedException('Symfony\Component\Form\Exception\InvalidOptionException', - 'The option "unknown" does not exist. Known options are: "attr", ' . - '"by_reference", "data", "data_class", "disabled", "empty_data", ' . - '"error_bubbling", "error_mapping", "invalid_message", ' . - '"invalid_message_parameters", "label", "max_length", "pattern", ' . - '"property_path", "read_only", "required", "translation_domain", ' . - '"trim"' - ); - $factory->createNamedBuilder($type, "text", "value", array("unknown" => "opt")); - } - public function testFieldTypeCreatesDefaultValueForEmptyDataOption() + public function testFormTypeCreatesDefaultValueForEmptyDataOption() { $factory = new FormFactory(array(new \Symfony\Component\Form\Extension\Core\CoreExtension())); diff --git a/src/Symfony/Component/Form/Tests/FormTest.php b/src/Symfony/Component/Form/Tests/FormTest.php index 08ef363e4bcf4..b7b7cb0803db7 100644 --- a/src/Symfony/Component/Form/Tests/FormTest.php +++ b/src/Symfony/Component/Form/Tests/FormTest.php @@ -133,61 +133,6 @@ public function testDataIsInitializedEmpty() $this->assertSame('bar', $form->getClientData()); } - public function testErrorsBubbleUpIfEnabled() - { - $error = new FormError('Error!'); - $parent = $this->form; - $form = $this->getBuilder()->setErrorBubbling(true)->getForm(); - - $form->setParent($parent); - $form->addError($error); - - $this->assertEquals(array(), $form->getErrors()); - $this->assertEquals(array($error), $parent->getErrors()); - } - - public function testErrorsDontBubbleUpIfDisabled() - { - $error = new FormError('Error!'); - $parent = $this->form; - $form = $this->getBuilder()->setErrorBubbling(false)->getForm(); - - $form->setParent($parent); - $form->addError($error); - - $this->assertEquals(array($error), $form->getErrors()); - $this->assertEquals(array(), $parent->getErrors()); - } - - public function testErrorsBubbleUpIfNullAndChildren() - { - $error = new FormError('Error!'); - $parent = $this->form; - $form = $this->getBuilder() - ->setErrorBubbling(null) - ->add($this->getBuilder('child')) - ->getForm(); - - $form->setParent($parent); - $form->addError($error); - - $this->assertEquals(array(), $form->getErrors()); - $this->assertEquals(array($error), $parent->getErrors()); - } - - public function testErrorsDontBubbleUpIfNullAndNoChildren() - { - $error = new FormError('Error!'); - $parent = $this->form; - $form = $this->getBuilder()->setErrorBubbling(null)->getForm(); - - $form->setParent($parent); - $form->addError($error); - - $this->assertEquals(array($error), $form->getErrors()); - $this->assertEquals(array(), $parent->getErrors()); - } - public function testValidIfAllChildrenAreValid() { $this->form->add($this->getValidForm('firstName'));