From db2c3afeabb4e0f6e06db026fd9774c1bcf7f506 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Mon, 22 Oct 2012 01:59:48 +0100 Subject: [PATCH] [Form] Enhanced DateType and TimeType to support different widgets types per date/time part --- .../Form/Extension/Core/Type/DateType.php | 84 +++++++++-- .../Form/Extension/Core/Type/TimeType.php | 130 ++++++++++++++---- .../Extension/Core/Type/DateTypeTest.php | 100 ++++++++++++++ 3 files changed, 275 insertions(+), 39 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php index c659a77f0498b..4dcc6a1d2a864 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php @@ -37,6 +37,23 @@ class DateType extends AbstractType \IntlDateFormatter::SHORT, ); + private static $allowedSingleWidgets = array( + 'single_text', + 'text', + 'choice' + ); + + private static $allowedPartWidgets = array( + 'text', + 'choice', + ); + + private static $allowedParts = array( + 'year', + 'month', + 'day', + ); + /** * {@inheritdoc} */ @@ -79,12 +96,21 @@ public function buildForm(FormBuilderInterface $builder, array $options) ); $formatter->setLenient(false); - if ('choice' === $options['widget']) { + if ('choice' === $options['widget']['year']) { // Only pass a subset of the options to children + $yearOptions = array_merge($options['year_options'], $yearOptions); $yearOptions['choices'] = $this->formatTimestamps($formatter, '/y+/', $this->listYears($options['years'])); $yearOptions['empty_value'] = $options['empty_value']['year']; + } + + if ('choice' === $options['widget']['month']) { + $monthOptions = array_merge($options['month_options'], $monthOptions); $monthOptions['choices'] = $this->formatTimestamps($formatter, '/M+/', $this->listMonths($options['months'])); $monthOptions['empty_value'] = $options['empty_value']['month']; + } + + if ('choice' === $options['widget']['day']) { + $dayOptions = array_merge($options['day_options'], $dayOptions); $dayOptions['choices'] = $this->formatTimestamps($formatter, '/d+/', $this->listDays($options['days'])); $dayOptions['empty_value'] = $options['empty_value']['day']; } @@ -95,9 +121,9 @@ public function buildForm(FormBuilderInterface $builder, array $options) } $builder - ->add('year', $options['widget'], $yearOptions) - ->add('month', $options['widget'], $monthOptions) - ->add('day', $options['widget'], $dayOptions) + ->add('year', $options['widget']['year'], $yearOptions) + ->add('month', $options['widget']['month'], $monthOptions) + ->add('day', $options['widget']['day'], $dayOptions) ->addViewTransformer(new DateTimeToArrayTransformer( $options['model_timezone'], $options['view_timezone'], array('year', 'month', 'day') )) @@ -159,6 +185,45 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) return $options['widget'] !== 'single_text'; }; + $widgetNormalizer = function (Options $options, $widget) { + if ("single_text" === $widget) { + return $widget; + } + + if (is_array($widget)) { + if (0 < count(array_diff(array_keys($widget), self::$allowedParts))) { + throw new InvalidOptionsException(sprintf('The "widget" option can only be used to define the ' . + 'following date parts: "%s"', implode('", "', self::$allowedParts))); + + } + + if (0 < count(array_diff($widget, self::$allowedPartWidgets))) { + throw new InvalidOptionsException(sprintf( + 'The "widget" option date part widgets can only be one of "%s"', + implode('", "', self::$allowedPartWidgets) + )); + } + + return array_merge(array( + 'year' => 'choice', + 'month' => 'choice', + 'day' => 'choice', + ), $widget); + } + + if (!in_array($widget, self::$allowedSingleWidgets, true)) { + throw new InvalidOptionsException(sprintf('The "widget" option must be one of "%s" or individually' + . ' defined for each date part ("%s")', implode('", "', self::$allowedSingleWidgets), + implode('", "', self::$allowedParts))); + } + + return array( + 'year' => $widget, + 'month' => $widget, + 'day' => $widget, + ); + }; + $emptyValue = $emptyValueDefault = function (Options $options) { return $options['required'] ? null : ''; }; @@ -198,6 +263,9 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) 'years' => range(date('Y') - 5, date('Y') + 5), 'months' => range(1, 12), 'days' => range(1, 31), + 'year_options' => array(), + 'month_options' => array(), + 'day_options' => array(), 'widget' => 'choice', 'input' => 'datetime', 'format' => $format, @@ -220,6 +288,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) )); $resolver->setNormalizers(array( + 'widget' => $widgetNormalizer, 'empty_value' => $emptyValueNormalizer, )); @@ -229,12 +298,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) 'string', 'timestamp', 'array', - ), - 'widget' => array( - 'single_text', - 'text', - 'choice', - ), + ) )); $resolver->setAllowedTypes(array( diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php index 8973948a2f968..5ead67be67f3a 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php @@ -21,9 +21,27 @@ use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; class TimeType extends AbstractType { + private static $allowedSingleWidgets = array( + 'single_text', + 'text', + 'choice' + ); + + private static $allowedPartWidgets = array( + 'text', + 'choice', + ); + + private static $allowedParts = array( + 'hour', + 'minute', + 'second', + ); + /** * {@inheritdoc} */ @@ -43,49 +61,57 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'error_bubbling' => true, ); - if ('choice' === $options['widget']) { - $hours = $minutes = array(); + if ('choice' === $options['widget']['hour']) { + $hours = array(); foreach ($options['hours'] as $hour) { $hours[$hour] = str_pad($hour, 2, '0', STR_PAD_LEFT); } + + $hourOptions = array_merge($options['hour_options'], $hourOptions); + $hourOptions['choices'] = $hours; + $hourOptions['empty_value'] = $options['empty_value']['hour']; + } + + if ('choice' === $options['widget']['minute']) { + $minutes = array(); + foreach ($options['minutes'] as $minute) { $minutes[$minute] = str_pad($minute, 2, '0', STR_PAD_LEFT); } - // Only pass a subset of the options to children - $hourOptions['choices'] = $hours; - $hourOptions['empty_value'] = $options['empty_value']['hour']; + $minuteOptions = array_merge($options['minute_options'], $minuteOptions); $minuteOptions['choices'] = $minutes; $minuteOptions['empty_value'] = $options['empty_value']['minute']; + } - if ($options['with_seconds']) { - $seconds = array(); - - foreach ($options['seconds'] as $second) { - $seconds[$second] = str_pad($second, 2, '0', STR_PAD_LEFT); - } + if ('choice' === $options['widget']['second'] && $options['with_seconds']) { + $seconds = array(); - $secondOptions['choices'] = $seconds; - $secondOptions['empty_value'] = $options['empty_value']['second']; + foreach ($options['seconds'] as $second) { + $seconds[$second] = str_pad($second, 2, '0', STR_PAD_LEFT); } - // Append generic carry-along options - foreach (array('required', 'translation_domain') as $passOpt) { - $hourOptions[$passOpt] = $minuteOptions[$passOpt] = $options[$passOpt]; - if ($options['with_seconds']) { - $secondOptions[$passOpt] = $options[$passOpt]; - } + $secondOptions = array_merge($options['second_options'], $secondOptions); + $secondOptions['choices'] = $seconds; + $secondOptions['empty_value'] = $options['empty_value']['second']; + } + + // Append generic carry-along options + foreach (array('required', 'translation_domain') as $passOpt) { + $hourOptions[$passOpt] = $minuteOptions[$passOpt] = $options[$passOpt]; + if ($options['with_seconds']) { + $secondOptions[$passOpt] = $options[$passOpt]; } } $builder - ->add('hour', $options['widget'], $hourOptions) - ->add('minute', $options['widget'], $minuteOptions) + ->add('hour', $options['widget']['hour'], $hourOptions) + ->add('minute', $options['widget']['minute'], $minuteOptions) ; if ($options['with_seconds']) { - $builder->add('second', $options['widget'], $secondOptions); + $builder->add('second', $options['widget']['second'], $secondOptions); } $builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget'])); @@ -130,6 +156,53 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) return $options['widget'] !== 'single_text'; }; + $widgetNormalizer = function (Options $options, $widget) { + if ("single_text" === $widget) { + return $widget; + } + + if (is_array($widget)) { + if (0 < count(array_diff(array_keys($widget), self::$allowedParts))) { + throw new InvalidOptionsException(sprintf('The "widget" option can only be used to define the ' . + 'following time parts: "%s"', implode('", "', self::$allowedParts))); + + } + + if (0 < count(array_diff($widget, self::$allowedPartWidgets))) { + throw new InvalidOptionsException(sprintf( + 'The "widget" option time part widgets can only be one of "%s"', + implode('", "', self::$allowedPartWidgets) + )); + } + + if (isset($widget["second"]) && false === $options['with_seconds']) { + throw new InvalidOptionsException(sprintf( + 'The "widget" option for time part "second" cannot be set because the option "with_seconds" is ' + . 'not enabled', + implode('", "', self::$allowedPartWidgets) + )); + } + + return array_merge(array( + 'hour' => 'choice', + 'minute' => 'choice', + 'second' => 'choice', + ), $widget); + } + + if (!in_array($widget, self::$allowedSingleWidgets, true)) { + throw new InvalidOptionsException(sprintf('The "widget" option must be one of "%s" or individually' + . ' defined for each time part ("%s")', implode('", "', self::$allowedSingleWidgets), + implode('", "', self::$allowedParts))); + } + + return array( + 'hour' => $widget, + 'minute' => $widget, + 'second' => $widget, + ); + }; + $emptyValue = $emptyValueDefault = function (Options $options) { return $options['required'] ? null : ''; }; @@ -145,7 +218,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) } return array( - 'hour' => $emptyValue, + 'hour' => $emptyValue, 'minute' => $emptyValue, 'second' => $emptyValue ); @@ -165,6 +238,9 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) 'hours' => range(0, 23), 'minutes' => range(0, 59), 'seconds' => range(0, 59), + 'hour_options' => array(), + 'minute_options' => array(), + 'second_options' => array(), 'widget' => 'choice', 'input' => 'datetime', 'with_seconds' => false, @@ -187,6 +263,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) )); $resolver->setNormalizers(array( + 'widget' => $widgetNormalizer, 'empty_value' => $emptyValueNormalizer, )); @@ -196,12 +273,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) 'string', 'timestamp', 'array', - ), - 'widget' => array( - 'single_text', - 'text', - 'choice', - ), + ) )); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php index fdff501668098..d4bc75369d7c5 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php @@ -33,6 +33,36 @@ public function testInvalidWidgetOption() )); } + /** + * @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testInvalidWidgetDatePartOption() + { + $form = $this->factory->create('date', null, array( + 'widget' => array('fake_widget_part' => 'input'), + )); + } + + /** + * @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testInvalidWidgetDatePartWidgetOption() + { + $form = $this->factory->create('date', null, array( + 'widget' => array('year' => 'fake_widget'), + )); + } + + /** + * @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testInvalidWidgetDatePartSingleTextOption() + { + $form = $this->factory->create('date', null, array( + 'widget' => array('month' => 'single_text'), + )); + } + /** * @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ @@ -740,4 +770,74 @@ public function testDayErrorsBubbleUp($widget) $this->assertSame(array(), $form['day']->getErrors()); $this->assertSame(array($error), $form->getErrors()); } + + public function testSubmitFromYearChoiceAndMonthAndDayTextDateTime() + { + $form = $this->factory->create('date', null, array( + 'model_timezone' => 'UTC', + 'view_timezone' => 'UTC', + 'widget' => array('year' => 'choice', 'day' => 'text', 'month' => 'text'), + )); + + $text = array( + 'day' => '2', + 'month' => '6', + 'year' => '2010', + ); + + $form->bind($text); + + $dateTime = new \DateTime('2010-06-02 UTC'); + + $this->assertDateTimeEquals($dateTime, $form->getData()); + $this->assertEquals($text, $form->getViewData()); + } + + public function testDayWidgetOptions() + { + $form = $this->factory->create('date', null, array( + 'day_options' => array( + 'attr' => array( + 'data-mask' => 'd' + ) + ) + )); + + $view = $form->createView(); + $attr = $view['day']->vars['attr']; + + $this->assertEquals('d', $attr['data-mask']); + } + + public function testMonthWidgetOptions() + { + $form = $this->factory->create('date', null, array( + 'month_options' => array( + 'attr' => array( + 'data-mask' => 'm' + ) + ) + )); + + $view = $form->createView(); + $attr = $view['month']->vars['attr']; + + $this->assertEquals('m', $attr['data-mask']); + } + + public function testYearWidgetOptions() + { + $form = $this->factory->create('date', null, array( + 'year_options' => array( + 'attr' => array( + 'data-mask' => 'y' + ) + ) + )); + + $view = $form->createView(); + $attr = $view['year']->vars['attr']; + + $this->assertEquals('y', $attr['data-mask']); + } }