[!!!][FEATURE] Add property to disable form elements/finishers 49/57049/14
authorRalf Zimmermann <ralf.zimmermann@tritum.de>
Fri, 25 May 2018 09:30:33 +0000 (11:30 +0200)
committerSusanne Moog <susanne.moog@typo3.org>
Wed, 27 Jun 2018 10:18:24 +0000 (12:18 +0200)
Add a new rendering option called 'enabled' to control
the visibility for form elements and finishers.

Releases: master
Resolves: #85080
Change-Id: I593df2cfa4ca15ed3ac39b8774e5cd8bde8d24de
Reviewed-on: https://review.typo3.org/57049
Reviewed-by: Björn Jacob <bjoern.jacob@tritum.de>
Tested-by: Björn Jacob <bjoern.jacob@tritum.de>
Reviewed-by: Mathias Brodala <mbrodala@pagemachine.de>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
typo3/sysext/core/Documentation/Changelog/master/Breaking-85080-MethodIsEnabledAddedToRenderableInterfaceAndFinisherInterface.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-85080-AddPropertyDisableFormElementsAndFinishers.rst [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Finishers/AbstractFinisher.php
typo3/sysext/form/Classes/Domain/Finishers/FinisherInterface.php
typo3/sysext/form/Classes/Domain/Model/Renderable/AbstractRenderable.php
typo3/sysext/form/Classes/Domain/Model/Renderable/RenderableInterface.php
typo3/sysext/form/Classes/Domain/Runtime/FormRuntime.php
typo3/sysext/form/Classes/ViewHelpers/RenderAllFormValuesViewHelper.php
typo3/sysext/form/Classes/ViewHelpers/RenderRenderableViewHelper.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-85080-MethodIsEnabledAddedToRenderableInterfaceAndFinisherInterface.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-85080-MethodIsEnabledAddedToRenderableInterfaceAndFinisherInterface.rst
new file mode 100644 (file)
index 0000000..7cb1cbe
--- /dev/null
@@ -0,0 +1,32 @@
+.. include:: ../../Includes.txt
+
+==========================================================================================
+Breaking: #85080 - Method "isEnabled()" added to RenderableInterface and FinisherInterface
+==========================================================================================
+
+See :issue:`85080`
+
+Description
+===========
+
+A new method :php:`isEnabled()` has been added to the :php:`RenderableInterface` as well as the :php:`FinisherInterface`.
+
+
+Impact
+======
+
+Third party code implementing these interfaces and not extending :php:`AbstractRenderable` or :php:`AbstractFinisher` will cause a fatal error if used in a form.
+
+
+Affected Installations
+======================
+
+Instances with 3rd party code implementing these interfaces and not extending :php:`AbstractRenderable` or :php:`AbstractFinisher`.
+
+
+Migration
+=========
+
+Third party code implementing these interfaces must be updated to implement the :php:`isEnabled()` method, preferably by extending :php:`AbstractRenderable` (or one of its subclasses) or :php:`AbstractFinisher`.
+
+.. index:: NotScanned, ext:form
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-85080-AddPropertyDisableFormElementsAndFinishers.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-85080-AddPropertyDisableFormElementsAndFinishers.rst
new file mode 100644 (file)
index 0000000..045ca31
--- /dev/null
@@ -0,0 +1,84 @@
+.. include:: ../../Includes.txt
+
+=====================================================================
+Feature: #85080 - Add property to disable form elements and finishers
+=====================================================================
+
+See :issue:`85080`
+
+Description
+===========
+
+A a new rendering option for form elements and finishers has been introduced named :yaml:`enabled`
+which takes a boolean value (:yaml:`true` or :yaml:`false`).
+
+Setting :yaml:`enabled: true` for a form element renders it in the frontend and enables processing
+of its value including property mapping and validation. Setting :yaml:`enabled: false` instead
+disables the form element in the frontend.
+
+Setting :yaml:`enabled: true` for a finisher executes it when submitting forms, setting :yaml:`enabled: false`
+skips the finisher instead.
+
+By default :yaml:`enabled` is set to :yaml:`true`.
+
+
+Usage:
+======
+
+All form elements and finishers except the root form element and the first form page can be enabled
+or disabled.
+
+An example:
+
+.. code-block:: yaml
+
+    type: Form
+    identifier: test
+    label: test
+    prototypeName: standard
+    renderables:
+      -
+        type: Page
+        identifier: page-1
+        label: Step
+        renderables:
+          -
+            type: Text
+            identifier: text-1
+            label: Text
+            defaultValue: ''
+          -
+            type: Checkbox
+            identifier: checkbox-1
+            label: Checkbox
+            renderingOptions:
+              enabled: true
+      -
+        type: SummaryPage
+        identifier: summarypage-1
+        label: 'Summary step'
+        renderingOptions:
+          enabled: false
+    finishers:
+      -
+        identifier: Confirmation
+        options:
+          message: thx
+      -
+        identifier: Confirmation
+        options:
+          message: 'thx again'
+          renderingOptions:
+            enabled: '{checkbox-1}'
+
+In this example the form element :yaml:`checkbox-1` has been enabled explicitly but it is fine to
+leave this out since this is the default state (which can be seen in the element :yaml:`text-1`).
+
+The :yaml:`summarypage-1` has been disabled completely, for example to temporarily remove it from
+the form.
+
+The second :yaml:`Confirmation` finisher takes the fact into account that finishers can refer to
+form values. It is only enabled if the form element :yaml:`checkbox-1` has been activated by the
+user. Otherwise the finisher is skipped.
+
+.. index:: Frontend, ext:form, NotScanned
index c001657..8077c8b 100644 (file)
@@ -125,6 +125,11 @@ abstract class AbstractFinisher implements FinisherInterface
     final public function execute(FinisherContext $finisherContext)
     {
         $this->finisherContext = $finisherContext;
+
+        if (!$this->isEnabled()) {
+            return null;
+        }
+
         return $this->executeInternal();
     }
 
@@ -350,6 +355,16 @@ abstract class AbstractFinisher implements FinisherInterface
     }
 
     /**
+     * Returns whether this finisher is enabled
+     *
+     * @return bool
+     */
+    public function isEnabled(): bool
+    {
+        return !isset($this->options['renderingOptions']['enabled']) || (bool)$this->parseOption('renderingOptions.enabled') === true;
+    }
+
+    /**
      * @return TypoScriptFrontendController
      */
     protected function getTypoScriptFrontendController()
index ba0bfcd..7471887 100644 (file)
@@ -25,7 +25,6 @@ namespace TYPO3\CMS\Form\Domain\Finishers;
  */
 interface FinisherInterface
 {
-
     /**
      * Executes the finisher
      *
@@ -49,4 +48,12 @@ interface FinisherInterface
      * @api
      */
     public function setOption(string $optionName, $optionValue);
+
+    /**
+     * Returns whether this finisher is enabled
+     *
+     * @return bool
+     * @api
+     */
+    public function isEnabled(): bool;
 }
index 7971811..49f2b81 100644 (file)
@@ -412,4 +412,14 @@ abstract class AbstractRenderable implements RenderableInterface
             ? $this->type
             : $this->renderingOptions['templateName'];
     }
+
+    /**
+     * Returns whether this renderable is enabled
+     *
+     * @return bool
+     */
+    public function isEnabled(): bool
+    {
+        return !isset($this->renderingOptions['enabled']) || (bool)$this->renderingOptions['enabled'] === true;
+    }
 }
index ed11bd9..b0c5a00 100644 (file)
@@ -86,4 +86,12 @@ interface RenderableInterface extends RootRenderableInterface
      * @api
      */
     public function getTemplateName(): string;
+
+    /**
+     * Returns whether this renderable is enabled
+     *
+     * @return bool
+     * @api
+     */
+    public function isEnabled(): bool;
 }
index 72f54b7..6b2f35a 100644 (file)
@@ -35,6 +35,7 @@ use TYPO3\CMS\Form\Domain\Model\FormElements\Page;
 use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
 use TYPO3\CMS\Form\Domain\Renderer\RendererInterface;
 use TYPO3\CMS\Form\Domain\Runtime\Exception\PropertyMappingException;
+use TYPO3\CMS\Form\Exception as FormException;
 use TYPO3\CMS\Form\Mvc\Validation\EmptyValidator;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 
@@ -205,6 +206,12 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
     {
         if (!$this->formState->isFormSubmitted()) {
             $this->currentPage = $this->formDefinition->getPageByIndex(0);
+            $renderingOptions = $this->currentPage->getRenderingOptions();
+
+            if (!$this->currentPage->isEnabled()) {
+                throw new FormException('Disabling the first page is not allowed', 1527186844);
+            }
+
             foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] ?? [] as $className) {
                 $hookObj = GeneralUtility::makeInstance($className);
                 if (method_exists($hookObj, 'afterInitializeCurrentPage')) {
@@ -218,21 +225,38 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
             }
             return;
         }
-        $this->lastDisplayedPage = $this->formDefinition->getPageByIndex($this->formState->getLastDisplayedPageIndex());
 
-        // We know now that lastDisplayedPage is filled
+        $this->lastDisplayedPage = $this->formDefinition->getPageByIndex($this->formState->getLastDisplayedPageIndex());
         $currentPageIndex = (int)$this->request->getInternalArgument('__currentPage');
-        if ($currentPageIndex > $this->lastDisplayedPage->getIndex() + 1) {
-            // We only allow jumps to following pages
-            $currentPageIndex = $this->lastDisplayedPage->getIndex() + 1;
+
+        if ($this->userWentBackToPreviousStep()) {
+            if ($currentPageIndex < $this->lastDisplayedPage->getIndex()) {
+                $currentPageIndex = $this->lastDisplayedPage->getIndex();
+            }
+        } else {
+            if ($currentPageIndex > $this->lastDisplayedPage->getIndex() + 1) {
+                $currentPageIndex = $this->lastDisplayedPage->getIndex() + 1;
+            }
         }
 
-        // We now know that the user did not try to skip a page
-        if ($currentPageIndex === count($this->formDefinition->getPages())) {
+        if ($currentPageIndex >= count($this->formDefinition->getPages())) {
             // Last Page
             $this->currentPage = null;
         } else {
             $this->currentPage = $this->formDefinition->getPageByIndex($currentPageIndex);
+            $renderingOptions = $this->currentPage->getRenderingOptions();
+
+            if (!$this->currentPage->isEnabled()) {
+                if ($currentPageIndex === 0) {
+                    throw new FormException('Disabling the first page is not allowed', 1527186845);
+                }
+
+                if ($this->userWentBackToPreviousStep()) {
+                    $this->currentPage = $this->getPreviousEnabledPage();
+                } else {
+                    $this->currentPage = $this->getNextEnabledPage();
+                }
+            }
         }
 
         foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] ?? [] as $className) {
@@ -440,6 +464,10 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
         }
 
         foreach ($page->getElementsRecursively() as $element) {
+            if (!$element->isEnabled()) {
+                continue;
+            }
+
             try {
                 $value = ArrayUtility::getValueByPath($requestArguments, $element->getIdentifier(), '.');
             } catch (MissingArrayPathException $exception) {
@@ -609,10 +637,10 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
     /**
      * Returns the currently selected page
      *
-     * @return Page
+     * @return Page|null
      * @api
      */
-    public function getCurrentPage(): Page
+    public function getCurrentPage(): ?Page
     {
         return $this->currentPage;
     }
@@ -623,7 +651,7 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
      * @return Page|null
      * @api
      */
-    public function getPreviousPage()
+    public function getPreviousPage(): ?Page
     {
         $previousPageIndex = $this->currentPage->getIndex() - 1;
         if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) {
@@ -638,7 +666,7 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
      * @return Page|null
      * @api
      */
-    public function getNextPage()
+    public function getNextPage(): ?Page
     {
         $nextPageIndex = $this->currentPage->getIndex() + 1;
         if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) {
@@ -648,6 +676,70 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
     }
 
     /**
+     * Returns the previous enabled page of the currently selected one
+     * or NULL if there is no previous page
+     *
+     * @return Page|null
+     * @api
+     */
+    public function getPreviousEnabledPage(): ?Page
+    {
+        $previousPage = null;
+        $previousPageIndex = $this->currentPage->getIndex() - 1;
+        while ($previousPageIndex >= 0) {
+            if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) {
+                $previousPage = $this->formDefinition->getPageByIndex($previousPageIndex);
+
+                if ($previousPage->isEnabled()) {
+                    break;
+                }
+
+                $previousPage = null;
+                $previousPageIndex--;
+            } else {
+                $previousPage = null;
+                break;
+            }
+        }
+
+        return $previousPage;
+    }
+
+    /**
+     * Returns the next enabled page of the currently selected one or
+     * NULL if there is no next page
+     *
+     * @return Page|null
+     * @api
+     */
+    public function getNextEnabledPage(): ?Page
+    {
+        $nextPage = null;
+        $pageCount = count($this->formDefinition->getPages());
+        $nextPageIndex = $this->currentPage->getIndex() + 1;
+
+        while ($nextPageIndex < $pageCount) {
+            if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) {
+                $nextPage = $this->formDefinition->getPageByIndex($nextPageIndex);
+                $renderingOptions = $nextPage->getRenderingOptions();
+                if (
+                    !isset($renderingOptions['enabled'])
+                    || (bool)$renderingOptions['enabled']
+                ) {
+                    break;
+                }
+                $nextPage = null;
+                $nextPageIndex++;
+            } else {
+                $nextPage = null;
+                break;
+            }
+        }
+
+        return $nextPage;
+    }
+
+    /**
      * @return ControllerContext
      */
     protected function getControllerContext(): ControllerContext
index 1d13478..c280a41 100644 (file)
@@ -21,6 +21,7 @@ use TYPO3\CMS\Core\Resource\File;
 use TYPO3\CMS\Extbase\Domain\Model\FileReference;
 use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
 use TYPO3\CMS\Form\Domain\Model\Renderable\CompositeRenderableInterface;
+use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
 use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
 use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
 use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
@@ -66,6 +67,10 @@ class RenderAllFormValuesViewHelper extends AbstractViewHelper
         $renderable = $arguments['renderable'];
         $as = $arguments['as'];
 
+        if (!$renderable->isEnabled()) {
+            return '';
+        }
+
         if ($renderable instanceof CompositeRenderableInterface) {
             $elements = $renderable->getRenderablesRecursively();
         } else {
@@ -79,24 +84,18 @@ class RenderAllFormValuesViewHelper extends AbstractViewHelper
         $output = '';
         foreach ($elements as $element) {
             $renderingOptions = $element->getRenderingOptions();
-
             if (
                 !$element instanceof FormElementInterface
-                || (
-                    isset($renderingOptions['_isCompositeFormElement'])
-                    && $renderingOptions['_isCompositeFormElement'] === true
-                )
-                || (
-                    isset($renderingOptions['_isHiddenFormElement'])
-                    && $renderingOptions['_isHiddenFormElement'] === true
-                )
-                || (
-                    isset($renderingOptions['_isReadOnlyFormElement'])
-                    && $renderingOptions['_isReadOnlyFormElement'] === true
-                )
+                || (isset($renderingOptions['_isCompositeFormElement']) && (bool)$renderingOptions['_isCompositeFormElement'] === true)
+                || !$element->isEnabled()
+                || self::hasDisabledParent($element)
+                // @todo: we can remove the next 2 conditions if variants are implemented
+                || (isset($renderingOptions['_isHiddenFormElement']) && (bool)$renderingOptions['_isHiddenFormElement'] === true)
+                || (isset($renderingOptions['_isReadOnlyFormElement']) && (bool)$renderingOptions['_isReadOnlyFormElement'] === true)
             ) {
                 continue;
             }
+
             $value = $formRuntime[$element->getIdentifier()];
 
             $formValue = [
@@ -219,4 +218,18 @@ class RenderAllFormValuesViewHelper extends AbstractViewHelper
         }
         return 'Object [' . get_class($object) . ']';
     }
+
+    /**
+     * @return bool
+     */
+    public static function hasDisabledParent(RenderableInterface $renderable): bool
+    {
+        while ($renderable = $renderable->getParentRenderable()) {
+            if ($renderable instanceof RenderableInterface && !$renderable->isEnabled()) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
index d23a43f..557d15c 100644 (file)
@@ -67,23 +67,28 @@ class RenderRenderableViewHelper extends AbstractViewHelper
             ->getViewHelperVariableContainer()
             ->get(self::class, 'formRuntime');
 
+        $renderable = $arguments['renderable'];
+
         foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRendering'] ?? [] as $className) {
             $hookObj = GeneralUtility::makeInstance($className);
             if (method_exists($hookObj, 'beforeRendering')) {
                 $hookObj->beforeRendering(
                     $formRuntime,
-                    $arguments['renderable']
+                    $renderable
                 );
             }
         }
 
-        $content = $renderChildrenClosure();
+        $content = '';
+
+        if ($renderable instanceof FormRuntime || $renderable instanceof RenderableInterface && $renderable->isEnabled()) {
+            $content = $renderChildrenClosure();
+        }
 
         // Wrap every renderable with a span with a identifier path data attribute if previewMode is active
         if (!empty($content)) {
             $renderingOptions = $formRuntime->getRenderingOptions();
             if (isset($renderingOptions['previewMode']) && $renderingOptions['previewMode'] === true) {
-                $renderable = $arguments['renderable'];
                 $path = $renderable->getIdentifier();
                 if ($renderable instanceof RenderableInterface) {
                     while ($renderable = $renderable->getParentRenderable()) {