Add a dedicated form field for plain select fields
authorMarcel Werk <burntime@woltlab.com>
Wed, 3 May 2023 16:06:10 +0000 (18:06 +0200)
committerMarcel Werk <burntime@woltlab.com>
Wed, 3 May 2023 16:06:10 +0000 (18:06 +0200)
The existing `SingleSelectionFormField` has multiple design flaws that are difficult to solve in a backward compatible way. The main issue with the existing implementation is that it tries to solve too many problems at once, creating an API that is inconsistent and difficult to use / easy to misuse.

This is the first implementation with more to follow that will eventually allow us to phase out the `SingleSelectionFormField`.

Closes #5265
Closes #4789

com.woltlab.wcf/templates/__selectFormField.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/__selectFormField.tpl [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/field/SelectFormField.class.php [new file with mode: 0644]

diff --git a/com.woltlab.wcf/templates/__selectFormField.tpl b/com.woltlab.wcf/templates/__selectFormField.tpl
new file mode 100644 (file)
index 0000000..7c96627
--- /dev/null
@@ -0,0 +1,15 @@
+<select
+       id="{$field->getPrefixedId()}"
+       name="{$field->getPrefixedId()}"
+       {if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}
+       {if $field->isRequired()} required{/if}
+>
+       <option value="">{lang}wcf.global.noSelection{/lang}</option>
+       {foreach from=$field->getNestedOptions() item=__fieldNestedOption}
+               <option
+                       value="{$__fieldNestedOption[value]}"
+                       {if $field->getValue() == $__fieldNestedOption[value] && $__fieldNestedOption[isSelectable]} selected{/if}
+                       {if $field->isImmutable() || !$__fieldNestedOption[isSelectable]} disabled{/if}
+               >{@'&nbsp;'|str_repeat:$__fieldNestedOption[depth] * 4}{@$__fieldNestedOption[label]}</option>
+       {/foreach}
+</select>
diff --git a/wcfsetup/install/files/acp/templates/__selectFormField.tpl b/wcfsetup/install/files/acp/templates/__selectFormField.tpl
new file mode 100644 (file)
index 0000000..7c96627
--- /dev/null
@@ -0,0 +1,15 @@
+<select
+       id="{$field->getPrefixedId()}"
+       name="{$field->getPrefixedId()}"
+       {if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}
+       {if $field->isRequired()} required{/if}
+>
+       <option value="">{lang}wcf.global.noSelection{/lang}</option>
+       {foreach from=$field->getNestedOptions() item=__fieldNestedOption}
+               <option
+                       value="{$__fieldNestedOption[value]}"
+                       {if $field->getValue() == $__fieldNestedOption[value] && $__fieldNestedOption[isSelectable]} selected{/if}
+                       {if $field->isImmutable() || !$__fieldNestedOption[isSelectable]} disabled{/if}
+               >{@'&nbsp;'|str_repeat:$__fieldNestedOption[depth] * 4}{@$__fieldNestedOption[label]}</option>
+       {/foreach}
+</select>
diff --git a/wcfsetup/install/files/lib/system/form/builder/field/SelectFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/SelectFormField.class.php
new file mode 100644 (file)
index 0000000..f35a2d7
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+
+namespace wcf\system\form\builder\field;
+
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+
+/**
+ * Implementation of a form field for selecting a single value.
+ *
+ * @author  Matthias Schmidt, Marcel Werk
+ * @copyright   2001-2023 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since   6.0
+ */
+final class SelectFormField extends AbstractFormField implements
+    ICssClassFormField,
+    IImmutableFormField
+{
+    use TCssClassFormField;
+    use TImmutableFormField;
+    use TSelectionFormField;
+
+    /**
+     * @inheritDoc
+     */
+    protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/Value';
+
+    /**
+     * @inheritDoc
+     */
+    protected $templateName = '__selectFormField';
+
+    /**
+     * @inheritDoc
+     */
+    public function getSaveValue()
+    {
+        if ($this->getValue() === '') {
+            return;
+        }
+
+        return parent::getSaveValue();
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function readValue()
+    {
+        if ($this->getDocument()->hasRequestData($this->getPrefixedId())) {
+            $value = $this->getDocument()->getRequestData($this->getPrefixedId());
+
+            if (\is_string($value)) {
+                $this->value = $value;
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function validate()
+    {
+        if ($this->getValue() === '') {
+            if ($this->isRequired()) {
+                $this->addValidationError(new FormFieldValidationError('empty'));
+            }
+        } else if (!isset($this->getOptions()[$this->getValue()])) {
+            $this->addValidationError(new FormFieldValidationError(
+                'invalidValue',
+                'wcf.global.form.error.noValidSelection'
+            ));
+        }
+
+        parent::validate();
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function value($value)
+    {
+        // ignore `null` as value which can be passed either for nullable
+        // fields or as value if no options are available
+        if ($value === null) {
+            return $this;
+        }
+
+        if (!isset($this->getOptions()[$value])) {
+            throw new \InvalidArgumentException("Unknown value '{$value}' for field '{$this->getId()}'.");
+        }
+
+        return parent::value($value);
+    }
+}