From 0a55aa965fe2dc37a18438af294c862d09c0390d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 27 Jan 2023 16:37:03 +0100 Subject: [PATCH 01/20] AttributesTest: Test deep cloning --- tests/AttributesTest.php | 71 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/AttributesTest.php b/tests/AttributesTest.php index 8ce23e8b..61556e20 100644 --- a/tests/AttributesTest.php +++ b/tests/AttributesTest.php @@ -158,4 +158,75 @@ public function testClone(): void $cloneCone->render() ); } + + public function testAttributesAreDeepCloned() + { + $attributes = Attributes::create(['class' => 'one']); + + $clone = clone $attributes; + $clone->add('class', 'two'); + + $this->assertNotSame( + $attributes->get('class'), + $clone->get('class'), + 'Attribute instances are not cloned' + ); + $this->assertSame( + 'one', + $attributes->get('class')->getValue(), + 'Attribute instances are not cloned correctly' + ); + $this->assertSame( + ['one', 'two'], + $clone->get('class')->getValue(), + 'Attribute instances are not cloned correctly' + ); + } + + public function testCallbacksOfClonedAttributesPointToTheirClone() + { + $element = new class extends BaseHtmlElement { + protected $value; + + protected $noGetterOrSetter; + + public function setValue($value) + { + $this->value = $value; + } + + public function getValue() + { + return $this->value; + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + $attributes->registerAttributeCallback('value', [$this, 'getValue'], [$this, 'setValue']); + $attributes->registerAttributeCallback('data-ngos', function () { + return $this->noGetterOrSetter; + }, function ($value) { + $this->noGetterOrSetter = $value; + }); + } + }; + + $element->setAttribute('value', 'foo'); + + $clone = clone $element; + + $clone->setAttribute('value', 'bar') + ->setAttribute('data-ngos', true); + + $this->assertSame( + ' value="foo"', + $element->getAttributes()->render(), + 'Attribute callbacks are not rebound to their new owner' + ); + $this->assertSame( + ' value="bar" data-ngos', + $clone->getAttributes()->render(), + 'Attribute callbacks are not rebound to their new owner' + ); + } } From ab92a140401f912caa213c3a96e017d0c910003b Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Thu, 2 Feb 2023 14:58:10 +0100 Subject: [PATCH 02/20] Add `CloneTest` with `CloningDummyElement` --- tests/CloneTest.php | 155 ++++++++++++++++++++++++++++++ tests/Lib/CloningDummyElement.php | 34 +++++++ 2 files changed, 189 insertions(+) create mode 100644 tests/CloneTest.php create mode 100644 tests/Lib/CloningDummyElement.php diff --git a/tests/CloneTest.php b/tests/CloneTest.php new file mode 100644 index 00000000..a4e08c80 --- /dev/null +++ b/tests/CloneTest.php @@ -0,0 +1,155 @@ +getAttributes()->set('class', 'original_class'); + + $firstClone = clone $original; + $firstClone->getAttributes()->set('class', 'first_clone_class'); + + $secondClone = clone $firstClone; + $secondClone->getAttributes()->set('class', 'second_clone_class'); + + $originalHtml = <<<'HTML' +

+

+HTML; + + $firstCloneHtml = <<<'HTML' +

+

+HTML; + + + $secondCloneHtml = <<<'HTML' +

+

+HTML; + + $this->assertHtml($originalHtml, $original); + $this->assertHtml($firstCloneHtml, $firstClone); + $this->assertHtml($secondCloneHtml, $secondClone); + } + + public function testElementCallbacksCloning(): void + { + $element = new CloningDummyElement(); + $element->getAttributes(); + + $clone = clone $element; + + $this->assertCallbacksFor($element); + $this->assertCallbacksFor($clone); + } + + public function testCloningAttributes(): void + { + $original = Attributes::create([Attribute::create('class', 'class01')]); + + $clone = clone $original; + foreach ($clone->getAttributes() as $attribute) { + if ($attribute->getName() === 'class') { + $attribute->setValue('class02'); + } + } + + $this->assertSame($original->get('class')->getValue(), 'class01'); + $this->assertSame($clone->get('class')->getValue(), 'class02'); + } + + protected function getCallbackThis(callable $callback): ?object + { + if (! $callback instanceof Closure) { + if (is_array($callback) && ! is_string($callback[0])) { + return $callback[0]; + } else { + return null; + } + } + + return (new ReflectionFunction($callback)) + ->getClosureThis(); + } + + protected function isCallbackGlobalOrStatic(callable $callback): bool + { + if (! $callback instanceof Closure) { + if (is_array($callback) && ! is_string($callback[0])) { + return false; + } + } else { + $closureThis = (new ReflectionFunction($callback)) + ->getClosureThis(); + + if ($closureThis) { + return false; + } + } + + return true; + } + + protected function getAttributeCallback(Attributes $attributes, string $name): callable + { + $callbacksProperty = new ReflectionProperty(get_class($attributes), 'callbacks'); + $callbacksProperty->setAccessible(true); + $callbacks = $callbacksProperty->getValue($attributes); + + return $callbacks[$name]; + } + + protected function assertCallbacksFor(CloningDummyElement $element) + { + $this->assertCallbackBelongsTo($element->getAttributes(), 'test-instance-scope-noop-inline', $element); + $this->assertCallbackBelongsTo( + $element->getAttributes(), + 'test-instance-noop-attribute', + $element + ); + $this->assertGlobalOrStaticCallback( + $element->getAttributes(), + 'test-closure-static-scope-noop' + ); + $this->assertGlobalOrStaticCallback( + $element->getAttributes(), + 'test-closure-instance-scope-noop' + ); + } + + protected function assertGlobalOrStaticCallback(Attributes $attributes, string $callbackName) + { + $callback = $this->getAttributeCallback($attributes, $callbackName); + $this->assertTrue($this->isCallbackGlobalOrStatic($callback)); + } + + protected function assertCallbackBelongsTo(Attributes $attributes, string $callbackName, object $owner) + { + $callback = $this->getAttributeCallback($attributes, $callbackName); + $callbackThis = $this->getCallbackThis($callback); + $this->assertSame($callbackThis, $owner); + } +} diff --git a/tests/Lib/CloningDummyElement.php b/tests/Lib/CloningDummyElement.php new file mode 100644 index 00000000..98d648ee --- /dev/null +++ b/tests/Lib/CloningDummyElement.php @@ -0,0 +1,34 @@ +registerAttributeCallback('test-instance-scope-noop-inline', function () { + return 'inline'; + }); + $attributes->registerAttributeCallback('test-instance-noop-attribute', [$this, 'staticMethod']); + $attributes->registerAttributeCallback( + 'test-closure-static-scope-noop', + Closure::fromCallable(self::class . '::staticMethod') + ); + + $attributes->registerAttributeCallback( + 'test-closure-instance-scope-noop', + Closure::fromCallable([$this, 'staticMethod']) + ); + } +} From 890e34f9ab39a9b02c033d4897b0c6da050ff88b Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Thu, 2 Feb 2023 14:59:38 +0100 Subject: [PATCH 03/20] Rebind Attribute callbacks when cloning also add `rebind()` and `rebindCallbacks()` to `Attributes` --- src/Attributes.php | 49 +++++++++++++++++++++++++++++++++++++++++ src/BaseHtmlElement.php | 12 ++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/Attributes.php b/src/Attributes.php index ffe22eea..67fcbd17 100644 --- a/src/Attributes.php +++ b/src/Attributes.php @@ -4,8 +4,10 @@ use ArrayAccess; use ArrayIterator; +use Closure; use InvalidArgumentException; use IteratorAggregate; +use ReflectionFunction; use Traversable; use function ipl\Stdlib\get_php_type; @@ -509,6 +511,53 @@ public function getIterator(): Traversable return new ArrayIterator($this->attributes); } + /** + * Rebind all callbacks that point to `$oldThisId` to `$newThis` + * + * @param int $oldThisId + * @param object $newThis + */ + public function rebind(int $oldThisId, object $newThis): void + { + $this->rebindCallbacks($this->callbacks, $oldThisId, $newThis); + $this->rebindCallbacks($this->setterCallbacks, $oldThisId, $newThis); + } + + /** + * Loops over all `$callbacks`, binds them to `$newThis` only where `$oldThisId` matches. The callbacks are + * modified directly on the `$callbacks` reference. + * + * @param callable[] $callbacks + * @param int $oldThisId + * @param object $newThis + */ + private function rebindCallbacks(array &$callbacks, int $oldThisId, object $newThis): void + { + foreach ($callbacks as &$callback) { + if (! $callback instanceof Closure) { + if (is_array($callback) && ! is_string($callback[0])) { + if (spl_object_id($callback[0]) === $oldThisId) { + $callback[0] = $newThis; + } + } + + continue; + } + + $closureThis = (new ReflectionFunction($callback)) + ->getClosureThis(); + + // Closure is most likely static + if ($closureThis === null) { + continue; + } + + if (spl_object_id($closureThis) === $oldThisId) { + $callback = $callback->bindTo($newThis); + } + } + } + public function __clone() { foreach ($this->attributes as &$attribute) { diff --git a/src/BaseHtmlElement.php b/src/BaseHtmlElement.php index 41b6e4b5..39e2541d 100644 --- a/src/BaseHtmlElement.php +++ b/src/BaseHtmlElement.php @@ -75,6 +75,9 @@ abstract class BaseHtmlElement extends HtmlDocument /** @var string Tag of element. Set this property in order to provide the element's tag when extending this class */ protected $tag; + /** @var int Holds an ID to identify itself, used to get the ID of the Object for comparison when cloning */ + private $thisRefId; + /** * Get the attributes of the element * @@ -83,6 +86,8 @@ abstract class BaseHtmlElement extends HtmlDocument public function getAttributes() { if ($this->attributes === null) { + $this->thisRefId = spl_object_id($this); + $default = $this->getDefaultAttributes(); if (empty($default)) { $this->attributes = new Attributes(); @@ -105,6 +110,8 @@ public function getAttributes() */ public function setAttributes($attributes) { + $this->thisRefId = spl_object_id($this); + $this->attributes = Attributes::wantAttributes($attributes); $this->attributeCallbacksRegistered = false; @@ -359,6 +366,11 @@ public function __clone() if ($this->attributes !== null) { $this->attributes = clone $this->attributes; + + // `$this->thisRefId` is the ID to this Object prior of cloning, `$this` is the newly cloned Object + $this->attributes->rebind($this->thisRefId, $this); + + $this->thisRefId = spl_object_id($this); } } } From 3a9be595888f72e524322c981c86b2790f1d261a Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Sun, 16 Oct 2022 22:14:55 +0200 Subject: [PATCH 04/20] FieldsetElement: Propagate decorator --- src/FormElement/FieldsetElement.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/FormElement/FieldsetElement.php b/src/FormElement/FieldsetElement.php index 90281804..ee0337c0 100644 --- a/src/FormElement/FieldsetElement.php +++ b/src/FormElement/FieldsetElement.php @@ -56,6 +56,19 @@ public function setWrapper(Wrappable $wrapper) return parent::setWrapper($wrapper); } + public function setWrapper(Wrappable $wrapper) + { + // TODO(lippserd): Revise decorator implementation to properly implement decorator propagation + if ( + ! $this->hasDefaultElementDecorator() + && $wrapper instanceof FormElementDecorator + ) { + $this->setDefaultElementDecorator($wrapper); + } + + return parent::setWrapper($wrapper); + } + protected function onElementRegistered(FormElement $element) { $element->getAttributes()->registerAttributeCallback('name', function () use ($element) { From a8cb818114471ba63f4163301b8d93aa26e64c3b Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Thu, 9 Feb 2023 16:20:49 +0100 Subject: [PATCH 05/20] Add Collection with test --- src/FormElement/Collection.php | 117 ++++++++++ tests/CollectionTest.php | 397 +++++++++++++++++++++++++++++++++ 2 files changed, 514 insertions(+) create mode 100644 src/FormElement/Collection.php create mode 100644 tests/CollectionTest.php diff --git a/src/FormElement/Collection.php b/src/FormElement/Collection.php new file mode 100644 index 00000000..09dce627 --- /dev/null +++ b/src/FormElement/Collection.php @@ -0,0 +1,117 @@ + 'collection' + ]; + + /** + * @param callable $callback + * + * @return void + */ + public function onAssembleGroup(callable $callback): void + { + $this->onAssembleGroup = $callback; + } + + /** + * @param FormElement $element + * + * @return $this + */ + public function setAddElement(FormELement $element): self + { + $this->addElement = clone $element; + + return $this; + } + + /** + * @param FormElement $element + * + * @return $this + */ + public function setRemoveElement(FormElement $element): self + { + $this->removeElement = clone $element; + + return $this; + } + + /** + * @param $group + * @param $addElement + * @param $removeElement + * + * @return $this + */ + protected function assembleGroup($group, $addElement, $removeElement): self + { + if (is_callable($this->onAssembleGroup)) { + call_user_func($this->onAssembleGroup, $group, $addElement, $removeElement); + } + + return $this; + } + + protected function assemble() + { + $values = $this->getPopulatedValues(); + + $valid = true; + foreach ($values as $key => $items) { + if ($this->removeElement !== null && isset($items[0][$this->removeElement->getName()])) { + continue; + } + + $group = $this->addGroup($key); + + if (empty($group->getValue($this->addElement->getName()))) { + $valid = false; + } + } + + if ($valid) { + $lastKey = $values ? key(array_slice($values, -1, 1, true)) + 1 : 0; + $this->addGroup($lastKey); + } + } + + protected function addGroup($key): FieldsetElement + { + $group = new FieldsetElement( + $key, + Attributes::create(['class' => static::GROUP_CSS_CLASS]) + ); + + $this + ->registerElement($group) + ->assembleGroup( + $group, + $this->addElement ? clone $this->addElement : null, + $this->removeElement ? clone $this->removeElement : null + ) + ->addHtml($group); + + return $group; + } +} diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php new file mode 100644 index 00000000..92047bea --- /dev/null +++ b/tests/CollectionTest.php @@ -0,0 +1,397 @@ +label = "Test Collection Label"; + $this->form = (new Form())->setDefaultElementDecorator(new SimpleFormElementDecorator()); + } + + public function testCanBeConstructed() + { + $collection = new Collection('testCollection'); + $this->assertInstanceOf(Collection::class, $collection); + } + + public function testSetLabel() + { + $collection = new Collection('testCollection'); + + $this->assertSame($collection, $collection->setLabel($this->label)); + $this->assertSame($this->label, $collection->getLabel()); + + $this->form->addHtml($collection); + + $expected = <<<'HTML' +
+
+
+
+ +HTML; + $this->assertHtml($expected, $this->form); + } + + public function testNoAddTriggerProvided() + { + $this->expectException(InvalidArgumentException::class); + + $collection = new Collection('testCollection'); + $collection->onAssembleGroup(function ($group, $addElement, $removeElement) { + // Throws Exception, because $addElement is null. + $group->addElement($addElement); + }); + + $collection->render(); + } + + public function testNoRemoveTriggerProvided() + { + $this->expectException(InvalidArgumentException::class); + + $collection = new Collection('testCollection'); + $collection->onAssembleGroup(function ($group, $addElement, $removeElement) { + // Throws Exception, because $removeElement is null. + $group->addElement($removeElement); + }); + + $collection->render(); + } + + public function testAddTrigger() + { + $collection = new Collection('testCollection'); + $collection + ->setAddElement(new SelectElement('add_element', [ + 'required' => false, + 'label' => 'Add Trigger', + 'options' => [null => 'Please choose', 'first' => 'First Option'], + 'class' => 'autosubmit' + ])) + ->onAssembleGroup(function ($group, $addElement, $removeElement) { + $group + ->addElement($addElement) + ->addElement('input', 'test_input', [ + 'label' => 'Test Input' + ]); + }); + + $this->form->addHtml($collection); + + $expected = <<<'HTML' +
+
+
+ + +
+
+
+HTML; + + $this->assertHtml($expected, $this->form); + } + + public function testRemoveTrigger() + { + $collection = new Collection('testCollection'); + $collection + ->setRemoveElement(new SubmitButtonElement('remove_trigger', [ + 'label' => 'Remove Trigger', + ])) + ->onAssembleGroup(function ($group, $addElement, $removeElement) { + $group->addElement('input', 'test_input', [ + 'label' => 'Test Input' + ]); + + $group->addElement($removeElement); + }); + + $this->form->addHtml($collection); + + $expected = <<<'HTML' +
+
+
+ + +
+
+
+HTML; + + $this->assertHtml($expected, $this->form); + } + + public function testFullCollection() + { + $collection = (new Collection('testCollection')) + ->setAddElement(new SelectElement('add_element', [ + 'required' => false, + 'label' => 'Add Trigger', + 'options' => [null => 'Please choose', 'first' => 'First Option'], + 'class' => 'autosubmit' + ])) + ->setRemoveElement(new SubmitButtonElement('remove_trigger', [ + 'label' => 'Remove Trigger', + 'value' => 'Remove Trigger' + ])); + + $collection->onAssembleGroup(function ($group, $addElement, $removeElement) { + $group->addElement($addElement); + + $group->addElement('input', 'test_input', [ + 'label' => 'Test Input' + ]); + $group->addElement('input', 'test_select', [ + 'label' => 'Test Select' + ]); + + $group->addElement($removeElement); + }); + + $this->form->addHtml($collection); + + $expected = <<<'HTML' +
+
+
+ + + + +
+
+
+HTML; + + $this->assertHtml($expected, $this->form); + } + + public function testMultipleCollections() + { + $collection = (new Collection('testCollection')) + ->setAddElement(new SelectElement('add_element', [ + 'required' => false, + 'label' => 'Add Trigger', + 'options' => [null => 'Please choose', 'first' => 'First Option'] + ])); + + $collection->onAssembleGroup(function ($group, $addElement, $removeElement) { + $group->addElement($addElement); + + $inner = (new Collection('innerCollection')) + ->setLabel('Inner Collection') + ->setAddElement(new SubmitButtonElement('inner_add_trigger', [ + 'label' => 'Inner Add Trigger' + ])); + + $inner->onAssembleGroup(function ($innerGroup, $innerAddElement, $innerRemoveElement) { + $innerGroup->addElement($innerAddElement); + $innerGroup->addElement('input', 'test_input'); + }); + + $group->addElement($inner); + $group->addElement('input', 'test_input'); + }); + + $this->form->addHtml($collection); + + $expected = <<<'HTML' +
+
+
+ +
+
+ + +
+
+ +
+
+
+HTML; + + $this->assertHtml($expected, $this->form); + } + + public function testPopulatingCollection(): void + { + $addElement = new SelectElement('test_select1', [ + 'options' => [ + 'key1' => 'value1', + 'key2' => 'value2' + ] + ]); + $testElement3 = clone $addElement; + $testElement3->setName('test_select3'); + $testElement3->getAttributes()->set('options', ['key5' => 'value5', 'key6' => 'value6']); + + $collection = (new Collection('testCollection')) + ->setAddElement($addElement); + + $collection->onAssembleGroup(function ($group, $addElement) use ($testElement3) { + $group->addElement(new InputElement('test_input')); + $group->addElement($addElement); + $group->addElement(new SelectElement('test_select2', [ + 'options' => [ + 'key3' => 'value3', + 'key4' => 'value4' + ] + ])); + $group->addElement($testElement3); + }); + + $this->form + ->registerElement($collection) + ->addHtml($collection) + ->populate([ + 'testCollection' => [ + [ + 'test_input' => 'test_value', + 'test_select1' => '', + 'test_select2' => 'key4', + 'test_select3' => 'key6' + ] + ] + ]); + + $expected = <<<'HTML' +
+
+
+ + + + +
+
+
+HTML; + + $this->assertHtml($expected, $this->form); + } + + public function testCollidingElementNames(): void + { + $addElement = new SelectElement('add_element', ['options' => ['key1' => 'value1', 'key2' => 'value2']]); + + $firstCollection = (new Collection('first_collection'))->setAddElement(clone $addElement); + $secondCollection = (new Collection('second_collection')) ->setAddElement(clone $addElement); + + $firstCollection->onAssembleGroup(function ($group, $addElement) { + $group->addElement($addElement); + }); + + $secondCollection->onAssembleGroup(function ($group, $addElement) { + $group->addElement($addElement); + }); + + $this->form + ->registerElement($firstCollection) + ->addHtml($firstCollection) + ->registerElement($secondCollection) + ->addHtml($secondCollection) + ->addElement(new SubmitButtonElement('add_element')) + ->populate([ + 'first_collection' => [ + [ + 'add_element' => 'key1' + ] + ], + 'second_collection' => [ + [ + 'add_element' => 'key2' + ], + [ + 'add_element' => 'key1' + ] + ] + ]); + + $expected = <<<'HTML' +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+HTML; + + $this->assertHtml($expected, $this->form); + } +} From 803604054c9581035bb82da5827981e5171177cb Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Thu, 9 Feb 2023 16:21:44 +0100 Subject: [PATCH 06/20] SelectElement: Support options cloning --- src/FormElement/SelectElement.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/FormElement/SelectElement.php b/src/FormElement/SelectElement.php index e6b4f217..150e0586 100644 --- a/src/FormElement/SelectElement.php +++ b/src/FormElement/SelectElement.php @@ -2,7 +2,6 @@ namespace ipl\Html\FormElement; -use InvalidArgumentException; use ipl\Html\Attributes; use ipl\Html\Common\MultipleAttribute; use ipl\Html\Html; @@ -235,4 +234,21 @@ protected function registerAttributeCallbacks(Attributes $attributes) $this->registerMultipleAttributeCallback($attributes); } + + public function __clone() + { + foreach ($this->options as &$option) { + $option = clone $option; + $option->attributes = clone $option->attributes; + $option->getAttributes()->rebind($this->thisRefId, $this); + } + + foreach ($this->optionContent as &$option) { + $option = clone $option; + $option->attributes = clone $option->attributes; + $option->getAttributes()->rebind($this->thisRefId, $this); + } + + parent::__clone(); + } } From 503d4daa30c66afc777bd9b70b31ae1b58297105 Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Thu, 9 Feb 2023 16:22:31 +0100 Subject: [PATCH 07/20] FieldsetElement: Add legend decoration test --- tests/FormElement/FieldsetElementTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/FormElement/FieldsetElementTest.php b/tests/FormElement/FieldsetElementTest.php index 005e577d..01248869 100644 --- a/tests/FormElement/FieldsetElementTest.php +++ b/tests/FormElement/FieldsetElementTest.php @@ -187,6 +187,27 @@ public function testDecoratorPropagationWithDivDecorator(): void +HTML; + + $this->assertHtml($expected, $form); + } + + public function testLegendDecoration(): void + { + $fieldset = (new FieldsetElement('test_fieldset'))->setLabel('fieldset_label'); + + $form = (new Form()) + ->setDefaultElementDecorator(new DivDecorator()) + ->addElement($fieldset); + + $expected = <<< 'HTML' +
+
+
+ fieldset_label +
+
+
HTML; $this->assertHtml($expected, $form); From f4f6d73d4d16d3546d94f0edaef647101d39d332 Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Thu, 9 Feb 2023 16:23:22 +0100 Subject: [PATCH 08/20] BaseHtmlElement: Set `$thisRefId` to protected --- src/BaseHtmlElement.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BaseHtmlElement.php b/src/BaseHtmlElement.php index 39e2541d..3d57a797 100644 --- a/src/BaseHtmlElement.php +++ b/src/BaseHtmlElement.php @@ -76,7 +76,7 @@ abstract class BaseHtmlElement extends HtmlDocument protected $tag; /** @var int Holds an ID to identify itself, used to get the ID of the Object for comparison when cloning */ - private $thisRefId; + protected $thisRefId; /** * Get the attributes of the element From 4346e93db3380e26534c9d292622c2ef5e143df0 Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Thu, 9 Feb 2023 16:23:57 +0100 Subject: [PATCH 09/20] FormElements: Add `getPopulatedValues()` --- src/FormElement/FormElements.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/FormElement/FormElements.php b/src/FormElement/FormElements.php index 2f8e4e5a..f1c14c52 100644 --- a/src/FormElement/FormElements.php +++ b/src/FormElement/FormElements.php @@ -342,6 +342,16 @@ public function getPopulatedValue($name, $default = null) : $default; } + /** + * Get all populated values of the element + * + * @return array + */ + public function getPopulatedValues(): array + { + return $this->populatedValues; + } + /** * Clear populated value of the given element * From 58713cd6063e1842f63a3c8ee234ffcd85ea4007 Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Thu, 9 Feb 2023 16:24:34 +0100 Subject: [PATCH 10/20] FieldsetElement: Remove duplicate `setWrapper()` --- src/FormElement/FieldsetElement.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/FormElement/FieldsetElement.php b/src/FormElement/FieldsetElement.php index ee0337c0..90281804 100644 --- a/src/FormElement/FieldsetElement.php +++ b/src/FormElement/FieldsetElement.php @@ -56,19 +56,6 @@ public function setWrapper(Wrappable $wrapper) return parent::setWrapper($wrapper); } - public function setWrapper(Wrappable $wrapper) - { - // TODO(lippserd): Revise decorator implementation to properly implement decorator propagation - if ( - ! $this->hasDefaultElementDecorator() - && $wrapper instanceof FormElementDecorator - ) { - $this->setDefaultElementDecorator($wrapper); - } - - return parent::setWrapper($wrapper); - } - protected function onElementRegistered(FormElement $element) { $element->getAttributes()->registerAttributeCallback('name', function () use ($element) { From 7c5da2bfe4d55953ea106c90dcc9dcfe0a2e2582 Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Fri, 10 Feb 2023 11:53:32 +0100 Subject: [PATCH 11/20] SelectElementTest: Add clone test --- tests/FormElement/SelectElementTest.php | 116 ++++++++++++++++-------- 1 file changed, 79 insertions(+), 37 deletions(-) diff --git a/tests/FormElement/SelectElementTest.php b/tests/FormElement/SelectElementTest.php index 3f8689c2..e9b399de 100644 --- a/tests/FormElement/SelectElementTest.php +++ b/tests/FormElement/SelectElementTest.php @@ -43,10 +43,10 @@ public function testOptionValidity() 'label' => 'Customer', 'value' => '3', 'options' => [ - null => 'Please choose', - '1' => 'The one', - '4' => 'Four', - '5' => 'Hi five', + null => 'Please choose', + '1' => 'The one', + '4' => 'Four', + '5' => 'Hi five', 'sub' => [ 'Down' => 'Here' ] @@ -72,10 +72,10 @@ public function testSelectingDisabledOptionIsNotPossible() 'label' => 'Customer', 'value' => '4', 'options' => [ - null => 'Please choose', - '1' => 'The one', - '4' => 'Four', - '5' => 'Hi five', + null => 'Please choose', + '1' => 'The one', + '4' => 'Four', + '5' => 'Hi five', 'sub' => [ 'Down' => 'Here' ] @@ -92,13 +92,13 @@ public function testNestedOptions() $select = new SelectElement('elname', [ 'label' => 'Customer', 'options' => [ - null => 'Please choose', + null => 'Please choose', 'Some Options' => [ - '1' => 'The one', - '4' => 'Four', + '1' => 'The one', + '4' => 'Four', ], 'More options' => [ - '5' => 'Hi five', + '5' => 'Hi five', ] ], ]); @@ -124,13 +124,13 @@ public function testDisabledNestedOptions() $select = new SelectElement('elname', [ 'label' => 'Customer', 'options' => [ - null => 'Please choose', + null => 'Please choose', 'Some options' => [ - '1' => 'The one', - '4' => 'Four', + '1' => 'The one', + '4' => 'Four', ], 'More options' => [ - '5' => 'Hi five', + '5' => 'Hi five', ] ], ]); @@ -159,17 +159,17 @@ public function testDeeplyDisabledNestedOptions() $select = new SelectElement('elname', [ 'label' => 'Customer', 'options' => [ - null => 'Please choose', + null => 'Please choose', 'Some options' => [ - '1' => 'The one', - '4' => [ + '1' => 'The one', + '4' => [ 'Deeper' => [ '4x4' => 'Fourfour', ], ], ], 'More options' => [ - '5' => 'Hi five', + '5' => 'Hi five', ] ], ]); @@ -277,8 +277,8 @@ public function testSetValueSelectsAnOption() public function testSetArrayAsValueWithoutMultipleAttributeThrowsException() { $select = new SelectElement('elname', [ - 'label' => 'Customer', - 'options' => [ + 'label' => 'Customer', + 'options' => [ null => 'Please choose', '1' => 'The one', '4' => 'Four', @@ -297,9 +297,9 @@ public function testSetArrayAsValueWithoutMultipleAttributeThrowsException() public function testSetNonArrayAsValueWithMultipleAttributeThrowsException() { $select = new SelectElement('elname', [ - 'label' => 'Customer', - 'multiple' => true, - 'options' => [ + 'label' => 'Customer', + 'multiple' => true, + 'options' => [ null => 'Please choose', '1' => 'The one', '4' => 'Four', @@ -445,11 +445,11 @@ public function testNullAndTheEmptyStringAreEquallyHandled() $form = new Form(); $form->addElement('select', 'select', [ 'options' => ['' => 'Please choose'], - 'value' => '' + 'value' => '' ]); $form->addElement('select', 'select2', [ 'options' => [null => 'Please choose'], - 'value' => null + 'value' => null ]); /** @var SelectElement $select */ @@ -512,10 +512,10 @@ public function testDisablingOptionsIsWorking() { $form = new Form(); $form->addElement('select', 'select', [ - 'options' => ['' => 'Please choose', 'foo' => 'FOO', 'bar' => 'BAR'], - 'disabledOptions' => [''], - 'required' => true, - 'value' => '' + 'options' => ['' => 'Please choose', 'foo' => 'FOO', 'bar' => 'BAR'], + 'disabledOptions' => [''], + 'required' => true, + 'value' => '' ]); $html = <<<'HTML' @@ -558,12 +558,12 @@ public function testNullAndTheEmptyStringAreAlsoEquallyHandledWhileDisablingOpti public function testOrderOfOptionsAndDisabledOptionsDoesNotMatter() { $select = new SelectElement('test', [ - 'label' => 'Test', - 'options' => [ + 'label' => 'Test', + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar' ], - 'disabledOptions' => ['foo', 'bar'] + 'disabledOptions' => ['foo', 'bar'] ]); $html = <<<'HTML' @@ -575,9 +575,9 @@ public function testOrderOfOptionsAndDisabledOptionsDoesNotMatter() $this->assertHtml($html, $select); $select = new SelectElement('test', [ - 'disabledOptions' => ['foo', 'bar'], - 'label' => 'Test', - 'options' => [ + 'disabledOptions' => ['foo', 'bar'], + 'label' => 'Test', + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar' ] @@ -615,4 +615,46 @@ public function testGetOptionReturnsPreviouslySetOption() $this->assertNull($select->getOption('')->getValue()); $this->assertSame('car', $select->getOption('car')->getValue()); } + + public function testCloning(): void + { + $form = new Form(); + + $select = new SelectElement('select', [ + 'options' => [ + 'key1' => 'value1', + 'key2' => 'value2' + ] + ]); + + $clone = clone $select; + $clone->setName('clone'); + $clone->setOptions([ + 'key3' => 'value3', + 'key4' => 'value4' + ]); + + $form + ->addElement($select) + ->addElement($clone) + ->populate([ + 'select' => 'key1', + 'clone' => 'key3' + ]); + + $expected = <<<'HTML' +
+ + +
+HTML; + + $this->assertHtml($expected, $form); + } } From ebb4791971aca654eebbced8b86437f2835d6798 Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Fri, 10 Feb 2023 14:50:18 +0100 Subject: [PATCH 12/20] SelectElementTest: Expand cloning test --- tests/FormElement/SelectElementTest.php | 63 +++++++++++++++++-------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/tests/FormElement/SelectElementTest.php b/tests/FormElement/SelectElementTest.php index e9b399de..d83d4faf 100644 --- a/tests/FormElement/SelectElementTest.php +++ b/tests/FormElement/SelectElementTest.php @@ -621,38 +621,61 @@ public function testCloning(): void $form = new Form(); $select = new SelectElement('select', [ - 'options' => [ - 'key1' => 'value1', - 'key2' => 'value2' + 'multiple' => true, + 'options' => [ + 'first' => [ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3' + ], + 'second' => [ + 'key4' => 'value4', + 'key5' => 'value5' + ] ] ]); + $select->setDisabledOptions(['key2']); - $clone = clone $select; - $clone->setName('clone'); - $clone->setOptions([ - 'key3' => 'value3', - 'key4' => 'value4' - ]); + $clone = (clone $select) + ->setName('clone') + ->setDisabledOptions([]); + + $clone->getAttributes()->set('multiple', false); + $clone->getOption('key4')->setLabel('label4'); $form ->addElement($select) ->addElement($clone) ->populate([ - 'select' => 'key1', + 'select' => ['key2', 'key4'], 'clone' => 'key3' ]); $expected = <<<'HTML' -
- - -
+
+ + +
HTML; $this->assertHtml($expected, $form); From 51112e6bdb3fa516b32930d76005eb21ba39f15a Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Fri, 10 Feb 2023 14:51:01 +0100 Subject: [PATCH 13/20] SelectElement: Clone `disabledOptions` --- src/FormElement/SelectElement.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/FormElement/SelectElement.php b/src/FormElement/SelectElement.php index 150e0586..863e6b6d 100644 --- a/src/FormElement/SelectElement.php +++ b/src/FormElement/SelectElement.php @@ -249,6 +249,12 @@ public function __clone() $option->getAttributes()->rebind($this->thisRefId, $this); } + foreach ($this->disabledOptions as &$option) { + $option = clone $option; + $option->attributes = clone $option->attributes; + $option->getAttributes()->rebind($this->thisRefId, $this); + } + parent::__clone(); } } From 455f497c68c7581b2cf2fd0c5164750ef5a46270 Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Fri, 10 Feb 2023 15:17:28 +0100 Subject: [PATCH 14/20] SelectElement: Remove redundant cloning --- src/FormElement/SelectElement.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/FormElement/SelectElement.php b/src/FormElement/SelectElement.php index 863e6b6d..13bba0c4 100644 --- a/src/FormElement/SelectElement.php +++ b/src/FormElement/SelectElement.php @@ -239,19 +239,11 @@ public function __clone() { foreach ($this->options as &$option) { $option = clone $option; - $option->attributes = clone $option->attributes; $option->getAttributes()->rebind($this->thisRefId, $this); } foreach ($this->optionContent as &$option) { $option = clone $option; - $option->attributes = clone $option->attributes; - $option->getAttributes()->rebind($this->thisRefId, $this); - } - - foreach ($this->disabledOptions as &$option) { - $option = clone $option; - $option->attributes = clone $option->attributes; $option->getAttributes()->rebind($this->thisRefId, $this); } From 01eb129d0e79fb8c40502a74e6c48775f70d26f1 Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Mon, 13 Feb 2023 17:50:17 +0100 Subject: [PATCH 15/20] SelectElement: Rebuild `optionContent` when cloning --- src/FormElement/SelectElement.php | 35 +++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/FormElement/SelectElement.php b/src/FormElement/SelectElement.php index 13bba0c4..1751b7b2 100644 --- a/src/FormElement/SelectElement.php +++ b/src/FormElement/SelectElement.php @@ -235,18 +235,45 @@ protected function registerAttributeCallbacks(Attributes $attributes) $this->registerMultipleAttributeCallback($attributes); } + private function parseOptionFromContent($content): array + { + $result = []; + + if ($content->getTag() === 'optgroup') { + $label = $content->getAttributes()->get('label')->getValue(); + + foreach ($content->getContent() as $item) { + $result[$label][$item->getValue()] = $item->getLabel(); + } + + return $result; + } + + /** @var SelectOption $content */ + $result[$content->getValue()] = $content->getLabel(); + + return $result; + } + public function __clone() { foreach ($this->options as &$option) { $option = clone $option; - $option->getAttributes()->rebind($this->thisRefId, $this); } - foreach ($this->optionContent as &$option) { - $option = clone $option; - $option->getAttributes()->rebind($this->thisRefId, $this); + $rawOptions = []; + foreach ($this->optionContent as $content) { + if (is_array($content)) { + foreach ($content as $contentEntry) { + $rawOptions += $this->parseOptionFromContent($contentEntry); + } + } + + $rawOptions += $this->parseOptionFromContent($content); } + $this->setOptions($rawOptions); + parent::__clone(); } } From 947a247c4b191823618d2a3d0b2d86f585b7041f Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Mon, 13 Feb 2023 17:51:35 +0100 Subject: [PATCH 16/20] SelectElementTest: Set `class` attribute in clone test --- tests/FormElement/SelectElementTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/FormElement/SelectElementTest.php b/tests/FormElement/SelectElementTest.php index d83d4faf..5199ffd4 100644 --- a/tests/FormElement/SelectElementTest.php +++ b/tests/FormElement/SelectElementTest.php @@ -641,7 +641,7 @@ public function testCloning(): void ->setDisabledOptions([]); $clone->getAttributes()->set('multiple', false); - $clone->getOption('key4')->setLabel('label4'); + $clone->getOption('key4')->getAttributes()->set('class', 'test_class'); $form ->addElement($select) @@ -671,7 +671,7 @@ public function testCloning(): void - + From 83d8603d4b141240fff009111f49b621acf50570 Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Mon, 13 Feb 2023 17:51:52 +0100 Subject: [PATCH 17/20] RadioElementTest: Add cloning test --- tests/FormElement/RadioElementTest.php | 108 +++++++++++++++++-------- 1 file changed, 73 insertions(+), 35 deletions(-) diff --git a/tests/FormElement/RadioElementTest.php b/tests/FormElement/RadioElementTest.php index 61570f3c..fc4e3f3d 100644 --- a/tests/FormElement/RadioElementTest.php +++ b/tests/FormElement/RadioElementTest.php @@ -16,8 +16,8 @@ class RadioElementTest extends TestCase public function testRendersElementCorrectly() { $radio = new RadioElement('test', [ - 'label' => 'Test', - 'options' => [ + 'label' => 'Test', + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar', 'yes' => 'Yes' @@ -35,8 +35,8 @@ public function testRendersElementCorrectly() public function testNumbersOfTypeIntOrStringAsOptionKeysAreHandledEqually() { $radio = new RadioElement('test', [ - 'label' => 'Test', - 'options' => [ + 'label' => 'Test', + 'options' => [ '1' => 'Foo', 2 => 'Bar', 3 => 'Yes' @@ -77,13 +77,13 @@ public function testNumbersOfTypeIntOrStringAsOptionKeysAreHandledEqually() public function testSetValueAddsTheCheckedAttribute() { $radio = new RadioElement('test', [ - 'label' => 'Test', - 'options' => [ + 'label' => 'Test', + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar', 'yes' => 'Yes' ], - 'value' => 'bar' + 'value' => 'bar' ]); $html = <<<'HTML' @@ -115,9 +115,9 @@ public function testSetValueAddsTheCheckedAttribute() public function testDisabledRadioOptions() { $radio = new RadioElement('test', [ - 'label' => 'Test', - 'disabledOptions' => ['foo', 'bar', 'yes'], - 'options' => [ + 'label' => 'Test', + 'disabledOptions' => ['foo', 'bar', 'yes'], + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar', 'yes' => 'Yes', @@ -134,14 +134,14 @@ public function testDisabledRadioOptions() $this->assertHtml($html, $radio); $radio = new RadioElement('test', [ - 'label' => 'Test', - 'options' => [ + 'label' => 'Test', + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar', 'yes' => 'Yes', 'no' => 'No' ], - 'value' => 'bar' + 'value' => 'bar' ]); $radio->getOption('yes')->setDisabled(); @@ -168,9 +168,9 @@ public function testDisabledRadioOptions() public function testNonCallbackAttributesOfTheElementAreAppliedToEachOption() { $radio = new RadioElement('test', [ - 'label' => 'Test', - 'class' => 'blue', - 'options' => [ + 'label' => 'Test', + 'class' => 'blue', + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar', 'yes' => 'Yes', @@ -192,8 +192,8 @@ public function testNonCallbackAttributesOfTheElementAreAppliedToEachOption() public function testAddCssClassToTheLabelOfASpecificOption() { $radio = new RadioElement('test', [ - 'label' => 'Test', - 'options' => [ + 'label' => 'Test', + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar', 'yes' => 'Yes', @@ -219,9 +219,9 @@ public function testAddCssClassToTheLabelOfASpecificOption() public function testAddAttributesToASpecificOption() { $radio = new RadioElement('test', [ - 'label' => 'Test', - 'class' => 'blue', - 'options' => [ + 'label' => 'Test', + 'class' => 'blue', + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar', 'yes' => 'Yes', @@ -245,13 +245,13 @@ public function testRadioNotValidIfCheckedValueIsInvalid() { StaticTranslator::$instance = new NoopTranslator(); $radio = new RadioElement('test', [ - 'label' => 'Test', - 'options' => [ + 'label' => 'Test', + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar', 'yes' => 'Yes' ], - 'value' => 'bar' + 'value' => 'bar' ]); $this->assertTrue($radio->isValid()); @@ -267,13 +267,13 @@ public function testRadioNotValidIfCheckedValueIsDisabled() { StaticTranslator::$instance = new NoopTranslator(); $radio = new RadioElement('test', [ - 'label' => 'Test', - 'options' => [ + 'label' => 'Test', + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar', 'yes' => 'Yes' ], - 'value' => 'bar' + 'value' => 'bar' ]); $radio->setValue('yes'); @@ -287,11 +287,11 @@ public function testNullAndTheEmptyStringAreEquallyHandled() $form = new Form(); $form->addElement('radio', 'radio', [ 'options' => ['' => 'Please choose'], - 'value' => '' + 'value' => '' ]); $form->addElement('radio', 'radio2', [ 'options' => [null => 'Please choose'], - 'value' => null + 'value' => null ]); /** @var RadioElement $radio */ @@ -364,12 +364,12 @@ public function testSetOptionsResetsOptions() public function testOrderOfOptionsAndDisabledOptionsDoesNotMatter() { $radio = new RadioElement('test', [ - 'label' => 'Test', - 'options' => [ + 'label' => 'Test', + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar' ], - 'disabledOptions' => ['foo', 'bar'] + 'disabledOptions' => ['foo', 'bar'] ]); $html = <<<'HTML' @@ -379,9 +379,9 @@ public function testOrderOfOptionsAndDisabledOptionsDoesNotMatter() $this->assertHtml($html, $radio); $radio = new RadioElement('test', [ - 'disabledOptions' => ['foo', 'bar'], - 'label' => 'Test', - 'options' => [ + 'disabledOptions' => ['foo', 'bar'], + 'label' => 'Test', + 'options' => [ 'foo' => 'Foo', 'bar' => 'Bar' ] @@ -451,4 +451,42 @@ public function testGetOptionGetValueAndElementGetValueHandleNullAndTheEmptyStri $this->assertNull($radio->getValue()); $this->assertNull($radio->getOption(null)->getValue()); } + + public function testCloning(): void + { + $form = new Form(); + + $radio = new RadioElement('radio', [ + 'options' => [ + 'key1' => 'value1', + 'key2' => 'value2' + ] + ]); + + $clone = clone $radio; + $clone->setName('clone'); + $clone->setOptions([ + 'key3' => 'value3', + 'key4' => 'value4' + ]); + + $form + ->addElement($radio) + ->addElement($clone) + ->populate([ + 'radio' => 'key1', + 'clone' => 'key4' + ]); + + $expected = <<<'HTML' +
+ + + + +
+HTML; + + $this->assertHtml($expected, $form); + } } From 7cb788785bc3a606eb386206d283bb7e6033f5c8 Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Tue, 14 Feb 2023 11:41:44 +0100 Subject: [PATCH 18/20] Don't use cloning in Collection --- src/FormElement/Collection.php | 49 ++++++++++++++++++++--------- tests/CollectionTest.php | 56 +++++++++++++++++----------------- 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/src/FormElement/Collection.php b/src/FormElement/Collection.php index 09dce627..ea1d2067 100644 --- a/src/FormElement/Collection.php +++ b/src/FormElement/Collection.php @@ -3,7 +3,6 @@ namespace ipl\Html\FormElement; use ipl\Html\Attributes; -use ipl\Html\Contract\FormElement; class Collection extends FieldsetElement { @@ -12,11 +11,19 @@ class Collection extends FieldsetElement /** @var callable */ protected $onAssembleGroup; - /** @var FormElement */ - protected $addElement; + /** @var array */ + protected $addElement = [ + 'type' => null, + 'name' => null, + 'options' => null + ]; - /** @var FormElement */ - protected $removeElement; + /** @var array */ + protected $removeElement = [ + 'type' => null, + 'name' => null, + 'options' => null + ]; /** @var string[] */ protected $defaultAttributes = [ @@ -34,25 +41,29 @@ public function onAssembleGroup(callable $callback): void } /** - * @param FormElement $element + * @param string $typeOrElement + * @param string|null $name + * @param null $options * * @return $this */ - public function setAddElement(FormELement $element): self + public function setAddElement(string $typeOrElement, string $name = null, $options = null): self { - $this->addElement = clone $element; + $this->addElement = ['type' => $typeOrElement, 'name' => $name, 'options' => $options]; return $this; } /** - * @param FormElement $element + * @param string $typeOrElement + * @param string|null $name + * @param null $options * * @return $this */ - public function setRemoveElement(FormElement $element): self + public function setRemoveElement(string $typeOrElement, string $name = null, $options = null): self { - $this->removeElement = clone $element; + $this->removeElement = ['type' => $typeOrElement, 'name' => $name, 'options' => $options]; return $this; } @@ -79,13 +90,13 @@ protected function assemble() $valid = true; foreach ($values as $key => $items) { - if ($this->removeElement !== null && isset($items[0][$this->removeElement->getName()])) { + if ($this->removeElement !== null && isset($items[0][$this->removeElement['name']])) { continue; } $group = $this->addGroup($key); - if (empty($group->getValue($this->addElement->getName()))) { + if (empty($group->getValue($this->addElement['name']))) { $valid = false; } } @@ -107,8 +118,16 @@ protected function addGroup($key): FieldsetElement ->registerElement($group) ->assembleGroup( $group, - $this->addElement ? clone $this->addElement : null, - $this->removeElement ? clone $this->removeElement : null + $this->addElement['type'] ? $this->createElement( + $this->addElement['type'], + $this->addElement['name'], + $this->addElement['options'] + ) : null, + $this->removeElement['type'] ? $this->createElement( + $this->removeElement['type'], + $this->removeElement['name'], + $this->removeElement['options'] + ) : null ) ->addHtml($group); diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php index 92047bea..f011e829 100644 --- a/tests/CollectionTest.php +++ b/tests/CollectionTest.php @@ -81,12 +81,12 @@ public function testAddTrigger() { $collection = new Collection('testCollection'); $collection - ->setAddElement(new SelectElement('add_element', [ + ->setAddElement('select', 'add_element', [ 'required' => false, 'label' => 'Add Trigger', 'options' => [null => 'Please choose', 'first' => 'First Option'], 'class' => 'autosubmit' - ])) + ]) ->onAssembleGroup(function ($group, $addElement, $removeElement) { $group ->addElement($addElement) @@ -118,9 +118,9 @@ public function testRemoveTrigger() { $collection = new Collection('testCollection'); $collection - ->setRemoveElement(new SubmitButtonElement('remove_trigger', [ + ->setRemoveElement('submitButton', 'remove_trigger', [ 'label' => 'Remove Trigger', - ])) + ]) ->onAssembleGroup(function ($group, $addElement, $removeElement) { $group->addElement('input', 'test_input', [ 'label' => 'Test Input' @@ -148,16 +148,16 @@ public function testRemoveTrigger() public function testFullCollection() { $collection = (new Collection('testCollection')) - ->setAddElement(new SelectElement('add_element', [ + ->setAddElement('select', 'add_element', [ 'required' => false, 'label' => 'Add Trigger', 'options' => [null => 'Please choose', 'first' => 'First Option'], 'class' => 'autosubmit' - ])) - ->setRemoveElement(new SubmitButtonElement('remove_trigger', [ + ]) + ->setRemoveElement('submitButton', 'remove_trigger', [ 'label' => 'Remove Trigger', 'value' => 'Remove Trigger' - ])); + ]); $collection->onAssembleGroup(function ($group, $addElement, $removeElement) { $group->addElement($addElement); @@ -196,11 +196,11 @@ public function testFullCollection() public function testMultipleCollections() { $collection = (new Collection('testCollection')) - ->setAddElement(new SelectElement('add_element', [ + ->setAddElement('select', 'add_element', [ 'required' => false, 'label' => 'Add Trigger', 'options' => [null => 'Please choose', 'first' => 'First Option'] - ])); + ]); $collection->onAssembleGroup(function ($group, $addElement, $removeElement) { $group->addElement($addElement); @@ -249,20 +249,15 @@ public function testMultipleCollections() public function testPopulatingCollection(): void { - $addElement = new SelectElement('test_select1', [ - 'options' => [ - 'key1' => 'value1', - 'key2' => 'value2' - ] - ]); - $testElement3 = clone $addElement; - $testElement3->setName('test_select3'); - $testElement3->getAttributes()->set('options', ['key5' => 'value5', 'key6' => 'value6']); - $collection = (new Collection('testCollection')) - ->setAddElement($addElement); + ->setAddElement('select', 'test_select1', [ + 'options' => [ + 'key1' => 'value1', + 'key2' => 'value2' + ] + ]); - $collection->onAssembleGroup(function ($group, $addElement) use ($testElement3) { + $collection->onAssembleGroup(function ($group, $addElement) { $group->addElement(new InputElement('test_input')); $group->addElement($addElement); $group->addElement(new SelectElement('test_select2', [ @@ -271,7 +266,12 @@ public function testPopulatingCollection(): void 'key4' => 'value4' ] ])); - $group->addElement($testElement3); + $group->addElement('select', 'test_select3', [ + 'options' => [ + 'key5' => 'value5', + 'key6' => 'value6' + ] + ]); }); $this->form @@ -315,10 +315,10 @@ public function testPopulatingCollection(): void public function testCollidingElementNames(): void { - $addElement = new SelectElement('add_element', ['options' => ['key1' => 'value1', 'key2' => 'value2']]); - - $firstCollection = (new Collection('first_collection'))->setAddElement(clone $addElement); - $secondCollection = (new Collection('second_collection')) ->setAddElement(clone $addElement); + $firstCollection = (new Collection('first_collection')) + ->setAddElement('select', 'add_element', ['options' => ['key1' => 'value1', 'key2' => 'value2']]); + $secondCollection = (new Collection('second_collection')) + ->setAddElement('select', 'add_element', ['options' => ['key1' => 'value1', 'key2' => 'value2']]); $firstCollection->onAssembleGroup(function ($group, $addElement) { $group->addElement($addElement); @@ -335,7 +335,7 @@ public function testCollidingElementNames(): void ->addHtml($secondCollection) ->addElement(new SubmitButtonElement('add_element')) ->populate([ - 'first_collection' => [ + 'first_collection' => [ [ 'add_element' => 'key1' ] From bc1e46fb58e8c5a4142784abf5da2ecdd8f0e119 Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Tue, 14 Feb 2023 11:48:10 +0100 Subject: [PATCH 19/20] CollectionTest: Fix `setAddElement()` call --- tests/CollectionTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php index f011e829..23821615 100644 --- a/tests/CollectionTest.php +++ b/tests/CollectionTest.php @@ -207,9 +207,9 @@ public function testMultipleCollections() $inner = (new Collection('innerCollection')) ->setLabel('Inner Collection') - ->setAddElement(new SubmitButtonElement('inner_add_trigger', [ + ->setAddElement('submitButton', 'inner_add_trigger', [ 'label' => 'Inner Add Trigger' - ])); + ]); $inner->onAssembleGroup(function ($innerGroup, $innerAddElement, $innerRemoveElement) { $innerGroup->addElement($innerAddElement); From 527e0c7c7a2622e67f81d68f19987c2c97f142c0 Mon Sep 17 00:00:00 2001 From: Timm Ortloff Date: Mon, 27 Feb 2023 11:44:14 +0100 Subject: [PATCH 20/20] Collection: Add Docs --- src/FormElement/Collection.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/FormElement/Collection.php b/src/FormElement/Collection.php index ea1d2067..52f63d2d 100644 --- a/src/FormElement/Collection.php +++ b/src/FormElement/Collection.php @@ -4,6 +4,31 @@ use ipl\Html\Attributes; +/** + * Collection can be used for creating dynamic forms or elements by describing a template which + * will create as many iteration as provided in `populate()`. + * + * Example: + * ```php + * $collection = new Collection('testCollection'); + * + * $collection->setAddElement('add_element', [ + * 'required' => false, + * 'label' => 'Add Trigger', + * 'options' => [null => 'Please choose', 'first' => 'First Option'], + * 'class' => 'autosubmit' + * ]); + * + * $collection->onAssembleGroup(function ($group, $addElement, $removeElement) { + * $group->addElement($addElement); + * $group->addElement('input', 'test_input'); + * }); + * + * $form + * ->registerElement($collection) + * ->addHtml($collection) + * ``` + */ class Collection extends FieldsetElement { protected const GROUP_CSS_CLASS = 'form-element-collection';