From 0d0f388ef6c863431f2d6ab91e2b031e31ae97cb Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Fri, 29 Jul 2022 16:01:27 +0200 Subject: [PATCH] FormElement: Add `RadioElement` --- src/FormElement/RadioElement.php | 177 ++++++++++ src/FormElement/RadioOption.php | 148 ++++++++ tests/FormElement/RadioElementTest.php | 454 +++++++++++++++++++++++++ 3 files changed, 779 insertions(+) create mode 100644 src/FormElement/RadioElement.php create mode 100644 src/FormElement/RadioOption.php create mode 100644 tests/FormElement/RadioElementTest.php diff --git a/src/FormElement/RadioElement.php b/src/FormElement/RadioElement.php new file mode 100644 index 00000000..2c719f23 --- /dev/null +++ b/src/FormElement/RadioElement.php @@ -0,0 +1,177 @@ +options = []; + foreach ($options as $value => $label) { + $option = (new RadioOption($value, $label)) + ->setDisabled( + in_array($value, $this->disabledOptions, ! is_int($value)) + || ($value === '' && in_array(null, $this->disabledOptions, true)) + ); + + $this->options[$value] = $option; + } + + $this->disabledOptions = []; + + return $this; + } + + /** + * Get the option with specified value + * + * @param string|int $value + * + * @return RadioOption + * + * @throws InvalidArgumentException If no option with the specified value exists + */ + public function getOption($value): RadioOption + { + if (! isset($this->options[$value])) { + throw new InvalidArgumentException(sprintf('There is no such option "%s"', $value)); + } + + return $this->options[$value]; + } + + /** + * Set the specified options as disable + * + * @param array $disabledOptions + * + * @return $this + */ + public function setDisabledOptions(array $disabledOptions): self + { + if (! empty($this->options)) { + foreach ($this->options as $value => $option) { + $option->setDisabled( + in_array($value, $disabledOptions, ! is_int($value)) + || ($value === '' && in_array(null, $disabledOptions, true)) + ); + } + + $this->disabledOptions = []; + } else { + $this->disabledOptions = $disabledOptions; + } + + return $this; + } + + public function renderUnwrapped() + { + // Parent::renderUnwrapped() requires $tag and the content should be empty. However, since we are wrapping + // each button in a label, the call to parent cannot work here and must be overridden. + return HtmlDocument::renderUnwrapped(); + } + + protected function assemble() + { + foreach ($this->options as $option) { + $radio = (new InputElement($this->getName())) + ->setType($this->type) + ->setValue($option->getValue()); + + // Only add the non-callback attributes to all options + foreach ($this->getAttributes() as $attribute) { + $radio->getAttributes()->addAttribute(clone $attribute); + } + + $radio->getAttributes() + ->merge($option->getAttributes()) + ->registerAttributeCallback( + 'checked', + function () use ($option) { + $optionValue = $option->getValue(); + + return ! is_int($optionValue) + ? $this->getValue() === $optionValue + : $this->getValue() == $optionValue; + } + ) + ->registerAttributeCallback( + 'disabled', + function () use ($option) { + return $this->getAttributes()->get('disabled')->getValue() || $option->isDisabled(); + } + ); + + $label = new HtmlElement( + 'label', + new Attributes(['class' => $option->getLabelCssClass()]), + $radio, + Text::create($option->getLabel()) + ); + + $this->addHtml($label); + } + } + + protected function addDefaultValidators(ValidatorChain $chain): void + { + $chain->add(new DeferredInArrayValidator(function (): array { + $possibleValues = []; + + foreach ($this->options as $option) { + if ($option->isDisabled()) { + continue; + } + + $possibleValues[] = $option->getValue(); + } + + return $possibleValues; + })); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $this->getAttributes()->registerAttributeCallback( + 'options', + null, + [$this, 'setOptions'] + ); + + $this->getAttributes()->registerAttributeCallback( + 'disabledOptions', + null, + [$this, 'setDisabledOptions'] + ); + } +} diff --git a/src/FormElement/RadioOption.php b/src/FormElement/RadioOption.php new file mode 100644 index 00000000..4968c35a --- /dev/null +++ b/src/FormElement/RadioOption.php @@ -0,0 +1,148 @@ +value = $value === '' ? null : $value; + $this->label = $label; + } + + /** + * Set the label of the option + * + * @param string $label + * + * @return $this + */ + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + /** + * Get the label of the option + * + * @return string + */ + public function getLabel(): string + { + return $this->label; + } + + /** + * Get the value of the option + * + * @return string|int|null + */ + public function getValue() + { + return $this->value; + } + + /** + * Set css class to the option label + * + * @param string|string[] $labelCssClass + * + * @return $this + */ + public function setLabelCssClass($labelCssClass): self + { + $this->labelCssClass = $labelCssClass; + + return $this; + } + + /** + * Get css class of the option label + * + * @return string|string[] + */ + public function getLabelCssClass() + { + return $this->labelCssClass; + } + + /** + * Set whether to disable the option + * + * @param bool $disabled + * + * @return $this + */ + public function setDisabled(bool $disabled = true): self + { + $this->disabled = $disabled; + + return $this; + } + + /** + * Get whether the option is disabled + * + * @return bool + */ + public function isDisabled(): bool + { + return $this->disabled; + } + + /** + * Add the attributes + * + * @param Attributes $attributes + * + * @return $this + */ + public function addAttributes(Attributes $attributes): self + { + $this->attributes = $attributes; + + return $this; + } + + /** + * Get the attributes + * + * @return Attributes + */ + public function getAttributes(): Attributes + { + if ($this->attributes === null) { + $this->attributes = new Attributes(); + } + + return $this->attributes; + } +} diff --git a/tests/FormElement/RadioElementTest.php b/tests/FormElement/RadioElementTest.php new file mode 100644 index 00000000..61570f3c --- /dev/null +++ b/tests/FormElement/RadioElementTest.php @@ -0,0 +1,454 @@ + 'Test', + 'options' => [ + 'foo' => 'Foo', + 'bar' => 'Bar', + 'yes' => 'Yes' + ] + ]); + + $html = <<<'HTML' + + + +HTML; + $this->assertHtml($html, $radio); + } + + public function testNumbersOfTypeIntOrStringAsOptionKeysAreHandledEqually() + { + $radio = new RadioElement('test', [ + 'label' => 'Test', + 'options' => [ + '1' => 'Foo', + 2 => 'Bar', + 3 => 'Yes' + ] + ]); + + $radio->setValue(2); + + $html = <<<'HTML' + + + +HTML; + $this->assertHtml($html, $radio); + + $radio->setValue('2'); + $this->assertHtml($html, $radio); + + $radio->setDisabledOptions([2]); + $this->assertTrue($radio->getOption(2)->isDisabled()); + + $radio->setDisabledOptions(['2']); + $this->assertTrue($radio->getOption(2)->isDisabled()); + + $radio->setOptions([]); + + $radio->setDisabledOptions(['5']); + + $radio->setOptions(['5' => 'Five', 8 => 'Eight']); + $this->assertTrue($radio->getOption(5)->isDisabled()); + + $radio->getOption(5)->setDisabled(false); + + $radio->setDisabledOptions([5]); + $this->assertTrue($radio->getOption(5)->isDisabled()); + } + + public function testSetValueAddsTheCheckedAttribute() + { + $radio = new RadioElement('test', [ + 'label' => 'Test', + 'options' => [ + 'foo' => 'Foo', + 'bar' => 'Bar', + 'yes' => 'Yes' + ], + 'value' => 'bar' + ]); + + $html = <<<'HTML' + + + +HTML; + $this->assertHtml($html, $radio); + + $radio->setValue('yes'); + + $html = <<<'HTML' + + + +HTML; + $this->assertHtml($html, $radio); + + $radio->setValue('no'); + + $html = <<<'HTML' + + + +HTML; + $this->assertHtml($html, $radio); + } + + public function testDisabledRadioOptions() + { + $radio = new RadioElement('test', [ + 'label' => 'Test', + 'disabledOptions' => ['foo', 'bar', 'yes'], + 'options' => [ + 'foo' => 'Foo', + 'bar' => 'Bar', + 'yes' => 'Yes', + 'no' => 'No' + ] + ]); + + $html = <<<'HTML' + + + + +HTML; + $this->assertHtml($html, $radio); + + $radio = new RadioElement('test', [ + 'label' => 'Test', + 'options' => [ + 'foo' => 'Foo', + 'bar' => 'Bar', + 'yes' => 'Yes', + 'no' => 'No' + ], + 'value' => 'bar' + ]); + + $radio->getOption('yes')->setDisabled(); + + $html = <<<'HTML' + + + + +HTML; + $this->assertHtml($html, $radio); + + $radio->setDisabledOptions(['no', 'foo']); + + $html = <<<'HTML' + + + + +HTML; + $this->assertHtml($html, $radio); + } + + public function testNonCallbackAttributesOfTheElementAreAppliedToEachOption() + { + $radio = new RadioElement('test', [ + 'label' => 'Test', + 'class' => 'blue', + 'options' => [ + 'foo' => 'Foo', + 'bar' => 'Bar', + 'yes' => 'Yes', + 'no' => 'No' + ], + ]); + + $radio->getAttributes()->add('data-js', 'radio'); + + $html = <<<'HTML' + + + + +HTML; + $this->assertHtml($html, $radio); + } + + public function testAddCssClassToTheLabelOfASpecificOption() + { + $radio = new RadioElement('test', [ + 'label' => 'Test', + 'options' => [ + 'foo' => 'Foo', + 'bar' => 'Bar', + 'yes' => 'Yes', + 'no' => 'No' + ], + ]); + + $radio->getOption('bar') + ->setLabelCssClass('new'); + + $radio->getOption('yes') + ->setLabelCssClass([RadioOption::LABEL_CLASS, 'new']); + + $html = <<<'HTML' + + + + +HTML; + $this->assertHtml($html, $radio); + } + + public function testAddAttributesToASpecificOption() + { + $radio = new RadioElement('test', [ + 'label' => 'Test', + 'class' => 'blue', + 'options' => [ + 'foo' => 'Foo', + 'bar' => 'Bar', + 'yes' => 'Yes', + 'no' => 'No' + ], + ]); + + $radio->getOption('bar') + ->addAttributes(Attributes::create(['class' => 'dark', 'id' => 'fav'])); + + $html = <<<'HTML' + + + + +HTML; + $this->assertHtml($html, $radio); + } + + public function testRadioNotValidIfCheckedValueIsInvalid() + { + StaticTranslator::$instance = new NoopTranslator(); + $radio = new RadioElement('test', [ + 'label' => 'Test', + 'options' => [ + 'foo' => 'Foo', + 'bar' => 'Bar', + 'yes' => 'Yes' + ], + 'value' => 'bar' + ]); + + $this->assertTrue($radio->isValid()); + + $radio->setValue('no'); + $this->assertFalse($radio->isValid()); + + $radio->setValue('tom'); + $this->assertFalse($radio->isValid()); + } + + public function testRadioNotValidIfCheckedValueIsDisabled() + { + StaticTranslator::$instance = new NoopTranslator(); + $radio = new RadioElement('test', [ + 'label' => 'Test', + 'options' => [ + 'foo' => 'Foo', + 'bar' => 'Bar', + 'yes' => 'Yes' + ], + 'value' => 'bar' + ]); + + $radio->setValue('yes'); + + $radio->getOption('yes')->setDisabled(); + $this->assertFalse($radio->isValid()); + } + + public function testNullAndTheEmptyStringAreEquallyHandled() + { + $form = new Form(); + $form->addElement('radio', 'radio', [ + 'options' => ['' => 'Please choose'], + 'value' => '' + ]); + $form->addElement('radio', 'radio2', [ + 'options' => [null => 'Please choose'], + 'value' => null + ]); + + /** @var RadioElement $radio */ + $radio = $form->getElement('radio'); + /** @var RadioElement $radio2 */ + $radio2 = $form->getElement('radio2'); + + $this->assertNull($radio->getValue()); + $this->assertNull($radio2->getValue()); + + $this->assertInstanceOf(RadioOption::class, $radio->getOption('')); + $this->assertInstanceOf(RadioOption::class, $radio2->getOption(null)); + $this->assertInstanceOf(RadioOption::class, $radio->getOption(null)); + $this->assertInstanceOf(RadioOption::class, $radio2->getOption('')); + + $this->assertTrue($radio->isValid()); + $this->assertTrue($radio2->isValid()); + + $radio->setValue(null); + $this->assertTrue($radio->isValid()); + $radio2->setValue(''); + $this->assertTrue($radio2->isValid()); + + $html = <<<'HTML' + +HTML; + + $this->assertHtml($html, $radio); + + $html = <<<'HTML' + +HTML; + + $this->assertHtml($html, $radio2); + + $radio->setDisabledOptions([null, 'foo']); + $radio2->setDisabledOptions(['', 'foo']); + + $html = <<<'HTML' + +HTML; + + $this->assertHtml($html, $radio); + + $html = <<<'HTML' + +HTML; + + $this->assertHtml($html, $radio2); + } + + public function testSetOptionsResetsOptions() + { + $radio = new RadioElement('radio'); + $radio->setOptions(['foo' => 'Foo', 'bar' => 'Bar']); + + $this->assertInstanceOf(RadioOption::class, $radio->getOption('foo')); + $this->assertInstanceOf(RadioOption::class, $radio->getOption('bar')); + + $radio->setOptions(['car' => 'Car', 'train' => 'Train']); + + $this->assertInstanceOf(RadioOption::class, $radio->getOption('car')); + $this->assertInstanceOf(RadioOption::class, $radio->getOption('train')); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('There is no such option "foo"'); + $radio->getOption('foo'); + } + + public function testOrderOfOptionsAndDisabledOptionsDoesNotMatter() + { + $radio = new RadioElement('test', [ + 'label' => 'Test', + 'options' => [ + 'foo' => 'Foo', + 'bar' => 'Bar' + ], + 'disabledOptions' => ['foo', 'bar'] + ]); + + $html = <<<'HTML' + + +HTML; + $this->assertHtml($html, $radio); + + $radio = new RadioElement('test', [ + 'disabledOptions' => ['foo', 'bar'], + 'label' => 'Test', + 'options' => [ + 'foo' => 'Foo', + 'bar' => 'Bar' + ] + ]); + + $this->assertHtml($html, $radio); + } + + public function testGetOptionReturnsPreviouslySetOption() + { + $radio = new RadioElement('radio'); + $radio->setOptions(['' => 'Empty String', 'foo' => 'Foo', 'bar' => 'Bar']); + + $this->assertNull($radio->getOption('')->getValue()); + $this->assertSame('Empty String', $radio->getOption('')->getLabel()); + + $this->assertSame('foo', $radio->getOption('foo')->getValue()); + $this->assertSame('Foo', $radio->getOption('foo')->getLabel()); + + $radio->setOptions(['' => 'Please Choose', 'car' => 'Car', 'train' => 'Train']); + + $this->assertNull($radio->getOption('')->getValue()); + $this->assertSame('Please Choose', $radio->getOption('')->getLabel()); + + $this->assertSame('car', $radio->getOption('car')->getValue()); + $this->assertSame('Car', $radio->getOption('car')->getLabel()); + } + + public function testNullAndTheEmptyStringAreAlsoEquallyHandledWhileDisablingOptions() + { + $radio = new RadioElement('radio'); + $radio->setOptions([null => 'Foo', 'bar' => 'Bar']); + $radio->setDisabledOptions([null]); + + $this->assertTrue($radio->getOption(null)->isDisabled()); + + $radio = new RadioElement('radio'); + $radio->setOptions(['' => 'Foo', 'bar' => 'Bar']); + $radio->setDisabledOptions(['']); + + $this->assertTrue($radio->getOption('')->isDisabled()); + + $radio = new RadioElement('radio'); + $radio->setOptions([null => 'Foo', 'bar' => 'Bar']); + $radio->setDisabledOptions(['']); + + $this->assertTrue($radio->getOption(null)->isDisabled()); + $radio = new RadioElement('radio'); + $radio->setOptions(['' => 'Foo', 'bar' => 'Bar']); + $radio->setDisabledOptions([null]); + + $this->assertTrue($radio->getOption('')->isDisabled()); + } + + public function testGetOptionGetValueAndElementGetValueHandleNullAndTheEmptyStringEqually() + { + $radio = new RadioElement('radio'); + $radio->setOptions(['' => 'Foo']); + $radio->setValue(''); + + $this->assertNull($radio->getValue()); + $this->assertNull($radio->getOption('')->getValue()); + + $radio = new RadioElement('radio'); + $radio->setOptions([null => 'Foo']); + + $this->assertNull($radio->getValue()); + $this->assertNull($radio->getOption(null)->getValue()); + } +}