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 0fb3f769b8391..6a610d04b12d8 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 @@ -232,6 +232,18 @@ {%- endblock radio_row %} +{# Support #} + +{% block empty_row -%} + {%- set translation_domain = translation_domain ?: 'sf_form' -%} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %} +
+

+ {{- translation_domain is same as(false) ? empty_view : empty_view|trans({}, translation_domain) -}} +

+
+{%- endblock empty_row %} + {# Errors #} {% block form_errors -%} 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 63104def7d3eb..65319a00c1183 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,7 +2,12 @@ {%- block form_widget -%} {% if compound %} - {{- block('form_widget_compound') -}} + {%- if empty_view %} + {{- block('empty_row') -}} + {{ form_rest(form) }} + {%- else -%} + {{- block('form_widget_compound') -}} + {%- endif -%} {% else %} {{- block('form_widget_simple') -}} {% endif %} @@ -35,11 +40,15 @@ {%- endblock textarea_widget -%} {%- block choice_widget -%} - {% if expanded %} - {{- block('choice_widget_expanded') -}} - {% else %} - {{- block('choice_widget_collapsed') -}} - {% endif %} + {%- if empty_view is defined and empty_view -%} + {{- block('empty_row') -}} + {%- else -%} + {%- if expanded -%} + {{- block('choice_widget_expanded') -}} + {%- else -%} + {{- block('choice_widget_collapsed') -}} + {%- endif -%} + {%- endif -%} {%- endblock choice_widget -%} {%- block choice_widget_expanded -%} @@ -313,6 +322,13 @@ {% endfor %} {%- endblock form_rows -%} +{%- block empty_row -%} + {%- set translation_domain = translation_domain ?: 'sf_form' -%} +
+ {{- translation_domain is same as(false) ? empty_view : empty_view|trans({}, translation_domain) -}} +
+{%- endblock empty_row -%} + {%- block widget_attributes -%} id="{{ id }}" name="{{ full_name }}" {%- if disabled %} disabled="disabled"{% endif -%} 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 c7b3a4365b51b..5a676977fc331 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 @@ -42,3 +42,14 @@ {{- form_rest(form) -}} {%- endblock form_widget_compound -%} + +{%- block empty_row -%} + {%- set translation_domain = translation_domain ?: 'sf_form' -%} + + + + +
+ {{ translation_domain is same as(false) ? empty_view : empty_view|trans({}, translation_domain) }} +
+{%- endblock empty_row -%} 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..2e739f0f79cb0 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 @@ -184,6 +184,19 @@ {% endif %} {%- endblock choice_widget_expanded %} +{% block choice_widget_empty -%} + {%- if empty_choices is empty %} + {%- set empty_choices = 'No choice available' %} + {%- set choice_translation_domain = 'sf_form' %} + {%- endif -%} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %} +
+

+ {{- choice_translation_domain is same as(false) ? empty_choices : empty_choices|trans({}, choice_translation_domain) -}} +

+
+{%- endblock %} + {% block checkbox_widget -%} {% set parent_label_class = parent_label_class|default('') %} {% if errors|length > 0 -%} 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 13593a96f11ef..a5e6c410dba98 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,5 +1,9 @@ - -block($form, 'choice_widget_expanded') ?> + + block($form, 'empty_row')?> -block($form, 'choice_widget_collapsed') ?> + + block($form, 'choice_widget_expanded') ?> + + block($form, 'choice_widget_collapsed') ?> + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/empty_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/empty_row.html.php new file mode 100644 index 0000000000000..a794ca2204888 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/empty_row.html.php @@ -0,0 +1,9 @@ + +
block($form, 'widget_container_attributes') ?>> + + escape(false !== $translation_domain + ? $view['translator']->trans($empty_view, array(), $translation_domain) + : $empty_view + ); ?> + +
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 c5af39a5b6b06..479f6466ba645 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,5 +1,10 @@ -block($form, 'form_widget_compound')?> + + block($form, 'empty_row')?> + rest($form) ?> + + block($form, 'form_widget_compound')?> + block($form, 'form_widget_simple')?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/empty_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/empty_row.html.php new file mode 100644 index 0000000000000..05697705196de --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/empty_row.html.php @@ -0,0 +1,13 @@ + +block($form, 'widget_container_attributes') ?>> + + + +
+ + escape(false !== $translation_domain + ? $view['translator']->trans($empty_view, array(), $translation_domain) + : $empty_view + ); ?> + +
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index ca1e5b737b9ab..8302b128f0583 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -244,6 +244,18 @@ public function finishView(FormView $view, FormInterface $form, array $options) $childView->vars['full_name'] = $childName; } } + + // We have to override parent {@link FormType} condition of the empty_view option here, + // even if there is no choices the field view may not be empty because: + // + // - a select input is not "compound" but should be considered as any empty HTML tag, + // + // - when expanded it could hold a placeholder which has been counted as a children, + // we should ignore it in any case since its role is to be a "null" alternative to + // choices, not a choice itself. Think of "Choose something" with no choices. + if (array() === $view->vars['choices'] && array() === $view->vars['preferred_choices']) { + $view->vars['empty_view'] = $options['empty_view']; + } } /** @@ -323,6 +335,7 @@ public function configureOptions(OptionsResolver $resolver) 'preferred_choices' => array(), 'group_by' => null, 'empty_data' => $emptyData, + 'empty_view' => 'No choice available', 'placeholder' => $placeholderDefault, 'error_bubbling' => false, 'compound' => $compound, diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php index ff8d0b4fdecf7..89f21037f2ce9 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php @@ -113,6 +113,10 @@ public function finishView(FormView $view, FormInterface $form, array $options) } $view->vars['multipart'] = $multipart; + + if ($form->getConfig()->getCompound()) { + $view->vars['empty_view'] = (bool) $view->count() ? false : $options['empty_view']; + } } /** @@ -142,6 +146,12 @@ public function configureOptions(OptionsResolver $resolver) }; }; + // For any compound form which has no children, show some text + // instead of an empty HTML tag by default + $emptyView = function (Options $options) { + return $options['compound'] ? 'No fields' : false; + }; + // For any form that is not represented by a single HTML control, // errors should bubble up by default $errorBubbling = function (Options $options) { @@ -157,6 +167,7 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setDefaults(array( 'data_class' => $dataClass, 'empty_data' => $emptyData, + 'empty_view' => $emptyView, 'trim' => true, 'required' => true, 'property_path' => null, @@ -175,6 +186,7 @@ public function configureOptions(OptionsResolver $resolver) )); $resolver->setAllowedTypes('label_attr', 'array'); + $resolver->setAllowedTypes('empty_view', array('null', 'string', 'bool')); } /** diff --git a/src/Symfony/Component/Form/Resources/translations/sf_form.en.xlf b/src/Symfony/Component/Form/Resources/translations/sf_form.en.xlf new file mode 100644 index 0000000000000..ac2d89b83510c --- /dev/null +++ b/src/Symfony/Component/Form/Resources/translations/sf_form.en.xlf @@ -0,0 +1,11 @@ + + + + + + No choice available + No choice available + + + + diff --git a/src/Symfony/Component/Form/Resources/translations/sf_form.fr.xlf b/src/Symfony/Component/Form/Resources/translations/sf_form.fr.xlf new file mode 100644 index 0000000000000..8fa4757746db8 --- /dev/null +++ b/src/Symfony/Component/Form/Resources/translations/sf_form.fr.xlf @@ -0,0 +1,11 @@ + + + + + + No choice available + Aucun choix disponible + + + + diff --git a/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php index 2dbf871aba902..bb7606c6e0a09 100644 --- a/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php @@ -250,6 +250,27 @@ public function testSelectWithSizeBiggerThanOneCanBeRequired() ); } + public function testSingleChoiceWithoutChoices() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, array( + 'choices' => array(), + 'multiple' => false, + 'expanded' => false, + 'required' => true, + 'attr' => array('class' => 'my&class'), + )); + + $this->assertMatchesXpath($this->renderWidget($form->createView()), +'/div + [@class="my&class form-control"] + [not(@required)] + [ + ./p/em/.="[trans]No choice available[/trans]" + ] +' + ); + } + public function testSingleChoiceWithoutTranslation() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( @@ -813,6 +834,27 @@ public function testSingleChoiceExpandedWithLabelsSetFalseByCallable() ); } + public function testSingleChoiceExpandedWithoutChoices() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, array( + 'choices' => array(), + 'multiple' => false, + 'expanded' => true, + 'required' => true, + 'attr' => array('class' => 'my&class'), + )); + + $this->assertMatchesXpath($this->renderWidget($form->createView()), +'/div + [@class="my&class form-control"] + [not(@required)] + [ + ./p/em/.="[trans]No choice available[/trans]" + ] +' + ); + } + public function testSingleChoiceExpandedWithoutTranslation() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( diff --git a/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php index 44e2f1d72dee2..9d6fe0fe1ae35 100644 --- a/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php @@ -327,10 +327,15 @@ public function testEmptyCollection() 'entry_type' => 'Symfony\Component\Form\Extension\Core\Type\TextType', )); - $this->assertWidgetMatchesXpath($form->createView(), array(), + $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), '/div - [./input[@type="hidden"][@id="names__token"]] + [@id="my&id"] + [@class="my&class"] + [ + /em[.="[trans]No fields[/trans]"] + ] [count(./div)=0] +/following-sibling::input[@type="hidden"][@id="names__token"] ' ); } diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index 4ff4f5407b8cc..de0799a1ed63b 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -536,6 +536,23 @@ public function testSelectWithSizeBiggerThanOneCanBeRequired() ); } + public function testSingleChoiceWithoutChoices() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, array( + 'choices' => array(), + 'multiple' => false, + 'expanded' => false, + 'required' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [not(@required)] + /em[.="[trans]No choice available[/trans]"] +' + ); + } + public function testSingleChoiceWithoutTranslation() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( @@ -955,6 +972,23 @@ public function testSingleChoiceExpanded() ); } + public function testSingleChoiceExpandedWithoutChoices() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, array( + 'choices' => array(), + 'multiple' => false, + 'expanded' => true, + 'required' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [not(@required)] + /em[.="[trans]No choice available[/trans]"] +' + ); + } + public function testSingleChoiceExpandedWithoutTranslation() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( diff --git a/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php index 9902a6dc089f6..61749d71892b8 100644 --- a/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php @@ -216,10 +216,56 @@ public function testEmptyCollection() 'entry_type' => 'Symfony\Component\Form\Extension\Core\Type\TextType', )); + $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), +'/table + [ + ./tr + [ + ./td[@colspan="2"]/em[.="[trans]No fields[/trans]"] + ] + ] + [@id="my&id"] + [@class="my&class"] +/following-sibling::tr[@style="display: none"] + [ + ./td[@colspan="2"]/input[@type="hidden"][@id="names__token"] + ] +' + ); + } + + public function testSingleChoiceWithoutChoices() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, array( + 'choices' => array(), + 'multiple' => false, + 'expanded' => false, + 'required' => true, + )); + $this->assertWidgetMatchesXpath($form->createView(), array(), '/table - [./tr[@style="display: none"][./td[@colspan="2"]/input[@type="hidden"][@id="names__token"]]] - [count(./tr[./td/input])=1] + [ + ./tr/td[@colspan="2"]/em[.="[trans]No choice available[/trans]"] + ] +' + ); + } + + public function testSingleChoiceExpandedWithoutChoices() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, array( + 'choices' => array(), + 'multiple' => false, + 'expanded' => true, + 'required' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/table + [ + ./tr/td[@colspan="2"]/em[.="[trans]No choice available[/trans]"] + ] ' ); }