diff --git a/Build/Packages/hbs-forms-example/Configuration/Sets/Example/TypoScript/form.typoscript b/Build/Packages/hbs-forms-example/Configuration/Sets/Example/TypoScript/form.typoscript
index 5e14dc5..07420f3 100644
--- a/Build/Packages/hbs-forms-example/Configuration/Sets/Example/TypoScript/form.typoscript
+++ b/Build/Packages/hbs-forms-example/Configuration/Sets/Example/TypoScript/form.typoscript
@@ -13,6 +13,9 @@ plugin.tx_form {
action = HBS_TAG
action.attribute = action
+
+ enctype = HBS_TAG
+ enctype.attribute = enctype
}
fields = HBS_RENDERABLES
@@ -64,6 +67,35 @@ plugin.tx_form {
type = hidden
}
+ FileUpload < .Text
+ FileUpload {
+ template = @form-field-file
+
+ accept = HBS_TAG
+ accept.attribute = accept
+
+ multiple = HBS_TAG
+ multiple.attribute = multiple
+
+ resource = HBS_PROPERTY
+ resource {
+ subject = viewModel
+ path = children?[resource?]
+ }
+
+ resourcePointerFields = HBS_PROPERTY
+ resourcePointerFields {
+ subject = viewModel
+ path = children?[resourcePointerFields?]
+ }
+
+ deleteCheckboxes = HBS_PROPERTY
+ deleteCheckboxes {
+ subject = viewModel
+ path = children?[deleteCheckboxes?]
+ }
+ }
+
Honeypot {
content = HBS_PASSTHROUGH
}
@@ -87,6 +119,7 @@ plugin.tx_form {
label.path = label
value = HBS_FORM_VALUE
+ value.output = PROCESSED_VALUE
}
# Disable fieldsets
diff --git a/Build/Packages/hbs-forms-example/Resources/Private/Partials/form-field-checkbox.hbs b/Build/Packages/hbs-forms-example/Resources/Private/Partials/form-field-checkbox.hbs
new file mode 100644
index 0000000..22fd56e
--- /dev/null
+++ b/Build/Packages/hbs-forms-example/Resources/Private/Partials/form-field-checkbox.hbs
@@ -0,0 +1,10 @@
+{{#unless disableContainer}}
{{/unless}}
+
+
+ {{> '@form-label' id=id label=label}}
+{{#unless disableContainer}}
{{/unless}}
diff --git a/Build/Packages/hbs-forms-example/Resources/Private/Partials/form-field-file-preview.hbs b/Build/Packages/hbs-forms-example/Resources/Private/Partials/form-field-file-preview.hbs
new file mode 100644
index 0000000..565893c
--- /dev/null
+++ b/Build/Packages/hbs-forms-example/Resources/Private/Partials/form-field-file-preview.hbs
@@ -0,0 +1,14 @@
+
+ {{resource.originalResource.originalFile.name}}
+
+
+ {{#if deleteCheckbox}}
+ {{> '@form-field-checkbox'
+ disableContainer=true
+ id=(join ../id "-remove-" index)
+ label="Delete file"
+ name=deleteCheckbox.element.name
+ value=deleteCheckbox.element.value
+ }}
+ {{/if}}
+
diff --git a/Build/Packages/hbs-forms-example/Resources/Private/Partials/form-field-file.hbs b/Build/Packages/hbs-forms-example/Resources/Private/Partials/form-field-file.hbs
new file mode 100644
index 0000000..5cba1ad
--- /dev/null
+++ b/Build/Packages/hbs-forms-example/Resources/Private/Partials/form-field-file.hbs
@@ -0,0 +1,35 @@
+{{#unless disableContainer}}{{/unless}}
+ {{> '@form-label' id=id label=label}}
+
+
+
+ {{#if resourcePointerFields}}
+ {{#each resourcePointerFields}}
+
+ {{/each}}
+ {{/if}}
+{{#unless disableContainer}}
{{/unless}}
+
+{{#if resource.resource}}
+ {{#each resource.resource}}
+ {{! Multiple file uploads (TYPO3 >= v14)}}
+ {{> '@form-field-file-preview'
+ resource=this
+ deleteCheckbox=(lookup ../deleteCheckboxes.children @index)
+ index=@index
+ }}
+ {{else}}
+ {{! Single file upload}}
+ {{> '@form-field-file-preview'
+ resource=resource.resource
+ deleteCheckbox=(lookup ../deleteCheckboxes.children 0)
+ index=0
+ }}
+ {{/each}}
+{{/if}}
diff --git a/Classes/Domain/ViewModel/Builder/FileUploadViewModelBuilder.php b/Classes/Domain/ViewModel/Builder/FileUploadViewModelBuilder.php
index 6e94414..6c192e5 100644
--- a/Classes/Domain/ViewModel/Builder/FileUploadViewModelBuilder.php
+++ b/Classes/Domain/ViewModel/Builder/FileUploadViewModelBuilder.php
@@ -18,9 +18,12 @@
namespace CPSIT\Typo3HandlebarsForms\Domain\ViewModel\Builder;
use CPSIT\Typo3HandlebarsForms\Domain;
+use CPSIT\Typo3HandlebarsForms\Fluid\ViewHelperInvoker;
+use TYPO3\CMS\Core;
use TYPO3\CMS\Extbase;
use TYPO3\CMS\Fluid;
use TYPO3\CMS\Form;
+use TYPO3Fluid\Fluid as FluidStandalone;
/**
* FileUploadViewModelBuilder
@@ -37,31 +40,47 @@ final class FileUploadViewModelBuilder extends AbstractViewModelBuilder
'ImageUpload',
];
+ private readonly Core\Information\Typo3Version $typo3Version;
+
+ public function __construct(ViewHelperInvoker $viewHelperInvoker)
+ {
+ parent::__construct($viewHelperInvoker);
+
+ $this->typo3Version = new Core\Information\Typo3Version();
+ }
+
public function renderRenderable(
Form\Domain\Model\Renderable\RootRenderableInterface $renderable,
Fluid\Core\Rendering\RenderingContext $renderingContext,
): Domain\ViewModel\ViewModelCollection|Domain\ViewModel\ViewHelperContainedViewModel {
$resource = null;
$resourceVariableName = 'resource';
+ $arguments = [
+ 'property' => $renderable->getIdentifier(),
+ 'as' => $resourceVariableName,
+ 'id' => $renderable->getUniqueIdentifier(),
+ 'class' => $renderable->getProperties()['elementClassAttribute'] ?? null,
+ 'errorClass' => $renderable->getProperties()['elementErrorClassAttribute'] ?? null,
+ 'additionalAttributes' => $this->renderAdditionalAttributes($renderable, $renderingContext),
+ 'accept' => $renderable->getProperties()['allowedMimeTypes'] ?? null,
+ ];
+
+ // @todo Remove condition once support for TYPO3 v13 is dropped
+ if ($this->typo3Version->getMajorVersion() >= 14) {
+ $arguments['multiple'] = $renderable->getProperties()['multiple'] ?? false;
+ }
+
$result = $this->viewHelperInvoker->invoke(
$renderingContext,
Form\ViewHelpers\Form\UploadedResourceViewHelper::class,
- [
- 'property' => $renderable->getIdentifier(),
- 'as' => $resourceVariableName,
- 'id' => $renderable->getUniqueIdentifier(),
- 'class' => $renderable->getProperties()['elementClassAttribute'] ?? null,
- 'errorClass' => $renderable->getProperties()['elementErrorClassAttribute'] ?? null,
- 'additionalAttributes' => $this->renderAdditionalAttributes($renderable, $renderingContext),
- 'accept' => $renderable->getProperties()['allowedMimeTypes'] ?? null,
- ],
+ $arguments,
static function () use ($renderingContext, &$resource, $resourceVariableName) {
$resource = $renderingContext->getVariableProvider()->get($resourceVariableName);
},
);
$inputViewModel = new Domain\ViewModel\ViewHelperContainedViewModel($renderable, $result);
- if (!($resource instanceof Extbase\Domain\Model\FileReference)) {
+ if (!$this->isValidResource($resource)) {
return $inputViewModel;
}
@@ -72,9 +91,86 @@ static function () use ($renderingContext, &$resource, $resourceVariableName) {
];
if ($hiddenFields !== []) {
- $viewModels['resourcePointerField'] = new Domain\ViewModel\StandaloneTagViewModel($renderable, $hiddenFields[0]);
+ $viewModels['resourcePointerFields'] = $this->buildResourcePointerFields($renderable, $hiddenFields);
+ }
+
+ // @todo Remove first condition once support for TYPO3 v13 is dropped
+ if ($this->typo3Version->getMajorVersion() >= 14 && (bool)($renderable->getProperties()['allowRemoval'] ?? false)) {
+ $viewModels['deleteCheckboxes'] = $this->buildDeleteCheckboxes($renderable, $renderingContext, $resource);
}
return new Domain\ViewModel\ViewModelCollection($renderable, $viewModels);
}
+
+ /**
+ * @param list $hiddenFields
+ */
+ private function buildResourcePointerFields(
+ Form\Domain\Model\FormElements\FileUpload $renderable,
+ array $hiddenFields,
+ ): Domain\ViewModel\ViewModelCollection {
+ $resourcePointerFields = [];
+
+ foreach ($hiddenFields as $hiddenField) {
+ $resourcePointerFields[] = new Domain\ViewModel\StandaloneTagViewModel($renderable, $hiddenField);
+ }
+
+ return new Domain\ViewModel\ViewModelCollection($renderable, $resourcePointerFields);
+ }
+
+ /**
+ * @param Extbase\Domain\Model\FileReference|Extbase\Persistence\ObjectStorage $resource
+ */
+ private function buildDeleteCheckboxes(
+ Form\Domain\Model\FormElements\FileUpload $renderable,
+ Fluid\Core\Rendering\RenderingContext $renderingContext,
+ Extbase\Domain\Model\FileReference|Extbase\Persistence\ObjectStorage $resource,
+ ): Domain\ViewModel\ViewModelCollection {
+ if (!is_iterable($resource)) {
+ $resource = [$resource];
+ }
+
+ $viewModels = [];
+ $index = 0;
+
+ // Map file references to view models
+ foreach ($resource as $fileReference) {
+ $result = $this->viewHelperInvoker->invoke(
+ $renderingContext,
+ Form\ViewHelpers\Form\UploadDeleteCheckboxViewHelper::class,
+ [
+ 'property' => $renderable->getIdentifier(),
+ 'fileReference' => $fileReference,
+ 'fileIndex' => $index++,
+ ],
+ );
+
+ $viewModels[] = Domain\ViewModel\FormFieldViewModel::forLabelAndElement(
+ $fileReference->getOriginalResource()->getOriginalFile()->getName(),
+ new Domain\ViewModel\ViewHelperContainedViewModel($renderable, $result),
+ );
+ }
+
+ return new Domain\ViewModel\ViewModelCollection($renderable, $viewModels);
+ }
+
+ /**
+ * @phpstan-assert-if-true Extbase\Domain\Model\FileReference|Extbase\Persistence\ObjectStorage $resource
+ */
+ private function isValidResource(mixed $resource): bool
+ {
+ if ($resource instanceof Extbase\Domain\Model\FileReference) {
+ return true;
+ }
+
+ // @todo Combine with previous condition once support for TYPO3 v13 is dropped
+ if ($this->typo3Version->getMajorVersion() >= 14
+ && $resource instanceof Extbase\Persistence\ObjectStorage
+ && count($resource) > 0
+ ) {
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/Classes/Domain/ViewModel/FileResourceViewModel.php b/Classes/Domain/ViewModel/FileResourceViewModel.php
index ee18851..01fd94d 100644
--- a/Classes/Domain/ViewModel/FileResourceViewModel.php
+++ b/Classes/Domain/ViewModel/FileResourceViewModel.php
@@ -28,12 +28,17 @@
* @license GPL-2.0-or-later
*
* @extends \ArrayObject
+ *
+ * @phpstan-type FileResource Core\Resource\File|Core\Resource\FileReference|Extbase\Domain\Model\File|Extbase\Domain\Model\FileReference
*/
final class FileResourceViewModel extends \ArrayObject implements ViewModel
{
+ /**
+ * @param FileResource|Extbase\Persistence\ObjectStorage $resource
+ */
public function __construct(
public readonly Form\Domain\Model\Renderable\RootRenderableInterface $renderable,
- public readonly Core\Resource\File|Core\Resource\FileReference|Extbase\Domain\Model\File|Extbase\Domain\Model\FileReference $resource,
+ public readonly Core\Resource\File|Core\Resource\FileReference|Extbase\Domain\Model\File|Extbase\Domain\Model\FileReference|Extbase\Persistence\ObjectStorage $resource,
) {
parent::__construct(['resource' => $this->resource]);
}
diff --git a/Documentation/DeveloperCorner/ViewModels.rst b/Documentation/DeveloperCorner/ViewModels.rst
index 263d359..c9156b0 100644
--- a/Documentation/DeveloperCorner/ViewModels.rst
+++ b/Documentation/DeveloperCorner/ViewModels.rst
@@ -86,15 +86,17 @@ Supported renderables
\(a) :php:`ViewModelCollection`
If uploaded resource can be resolved. Contains two or three view models:
- +------------------------+--------------------------------+--------------------------------------------------------------------------------------------------+
- | **Name** | **Type** | **Description** |
- +------------------------+--------------------------------+--------------------------------------------------------------------------------------------------+
- | `uploadField` | `ViewHelperContainedViewModel` | Contains result from `` view helper invocation for password field. |
- +------------------------+--------------------------------+--------------------------------------------------------------------------------------------------+
- | `resource` | `FileResourceViewModel` | References file upload, which is an instance of `FileReference` or `PseudoFileReference`. |
- +------------------------+--------------------------------+--------------------------------------------------------------------------------------------------+
- | `resourcePointerField` | `StandaloneTagViewModel` | Optional. References hidden `` field with resource pointer, if available. |
- +------------------------+--------------------------------+--------------------------------------------------------------------------------------------------+
+ +-------------------------+---------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+ | **Name** | **Type** | **Description** |
+ +-------------------------+---------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+ | `uploadField` | `ViewHelperContainedViewModel` | Contains result from `` view helper invocation for password field. |
+ +-------------------------+---------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+ | `resource` | `FileResourceViewModel` | References file upload, which is an instance of `FileReference`, `PseudoFileReference` or `ObjectStorage` (TYPO3 >= v14). |
+ +-------------------------+---------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+ | `resourcePointerFields` | `ViewModelCollection` of `StandaloneTagViewModel` | Optional. References hidden `` fields with resource pointers, if available. |
+ +-------------------------+---------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+ | `deleteCheckboxes` | `ViewModelCollection` of `ViewHelperContainedViewModel` | Optional and TYPO3 >= v14 only. Contains results from `` view helper invocations which allow to delete existing file uploads on submit. |
+ +-------------------------+---------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
\(b) :php:`ViewHelperContainedViewModel`
If uploaded resource cannot be resolved. Contains result from ``
diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php
index 7842810..357a9f4 100644
--- a/composer-dependency-analyser.php
+++ b/composer-dependency-analyser.php
@@ -27,6 +27,7 @@
->ignoreUnknownClasses([
// @todo Remove once support for TYPO3 v13 is dropped
Form\Event\BeforeRenderableIsRenderedEvent::class,
+ Form\ViewHelpers\Form\UploadDeleteCheckboxViewHelper::class,
Frontend\ContentObject\RegisterStack::class,
])
;
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 8c98635..706f165 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -105,3 +105,21 @@ parameters:
identifier: class.notFound
count: 1
path: Classes/Domain/Renderer/HandlebarsFormRenderer.php
+
+ -
+ rawMessage: Class TYPO3\CMS\Form\ViewHelpers\Form\UploadDeleteCheckboxViewHelper not found.
+ identifier: class.notFound
+ count: 1
+ path: Classes/Domain/ViewModel/Builder/FileUploadViewModelBuilder.php
+
+ -
+ rawMessage: 'Parameter #2 $resource of class CPSIT\Typo3HandlebarsForms\Domain\ViewModel\FileResourceViewModel constructor expects TYPO3\CMS\Core\Resource\File|TYPO3\CMS\Core\Resource\FileReference|TYPO3\CMS\Extbase\Domain\Model\File|TYPO3\CMS\Extbase\Domain\Model\FileReference|TYPO3\CMS\Extbase\Persistence\ObjectStorage, TYPO3\CMS\Extbase\Domain\Model\FileReference|TYPO3\CMS\Extbase\Persistence\ObjectStorage given.'
+ identifier: argument.type
+ count: 1
+ path: Classes/Domain/ViewModel/Builder/FileUploadViewModelBuilder.php
+
+ -
+ rawMessage: 'Parameter #2 $viewHelperClassName of method CPSIT\Typo3HandlebarsForms\Fluid\ViewHelperInvoker::invoke() expects class-string, string given.'
+ identifier: argument.type
+ count: 1
+ path: Classes/Domain/ViewModel/Builder/FileUploadViewModelBuilder.php