Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ plugin.tx_form {

action = HBS_TAG
action.attribute = action

enctype = HBS_TAG
enctype.attribute = enctype
}

fields = HBS_RENDERABLES
Expand Down Expand Up @@ -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
}
Expand All @@ -87,6 +119,7 @@ plugin.tx_form {
label.path = label

value = HBS_FORM_VALUE
value.output = PROCESSED_VALUE
}

# Disable fieldsets
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{{#unless disableContainer}}<p>{{/unless}}
<input type="checkbox"
id="{{id}}"
name="{{name}}"
value="{{{value}}}"
{{#if required}}required{{/if}}
/>

{{> '@form-label' id=id label=label}}
{{#unless disableContainer}}</p>{{/unless}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<p>
<strong>{{resource.originalResource.originalFile.name}}</strong>
<br>

{{#if deleteCheckbox}}
{{> '@form-field-checkbox'
disableContainer=true
id=(join ../id "-remove-" index)
label="Delete file"
name=deleteCheckbox.element.name
value=deleteCheckbox.element.value
}}
{{/if}}
</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{{#unless disableContainer}}<p>{{/unless}}
{{> '@form-label' id=id label=label}}

<input type="file"
id="{{id}}"
name="{{name}}"
{{#if required}}required{{/if}}
{{#if multiple}}multiple{{/if}}
{{#if accept}}accept="{{accept}}"{{/if}}
/>

{{#if resourcePointerFields}}
{{#each resourcePointerFields}}
<input type="hidden" name="{{name}}" value="{{value}}" />
{{/each}}
{{/if}}
{{#unless disableContainer}}</p>{{/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}}
118 changes: 107 additions & 11 deletions Classes/Domain/ViewModel/Builder/FileUploadViewModelBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

Expand All @@ -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<FluidStandalone\Core\ViewHelper\TagBuilder> $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<Extbase\Domain\Model\FileReference> $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<Extbase\Domain\Model\FileReference> $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;
}
}
7 changes: 6 additions & 1 deletion Classes/Domain/ViewModel/FileResourceViewModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,17 @@
* @license GPL-2.0-or-later
*
* @extends \ArrayObject<string|int, mixed>
*
* @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<FileResource> $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]);
}
Expand Down
20 changes: 11 additions & 9 deletions Documentation/DeveloperCorner/ViewModels.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<formvh:form.uploadedResource>` view helper invocation for password field. |
+------------------------+--------------------------------+--------------------------------------------------------------------------------------------------+
| `resource` | `FileResourceViewModel` | References file upload, which is an instance of `FileReference` or `PseudoFileReference`. |
+------------------------+--------------------------------+--------------------------------------------------------------------------------------------------+
| `resourcePointerField` | `StandaloneTagViewModel` | Optional. References hidden `<input>` field with resource pointer, if available. |
+------------------------+--------------------------------+--------------------------------------------------------------------------------------------------+
+-------------------------+---------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| **Name** | **Type** | **Description** |
+-------------------------+---------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| `uploadField` | `ViewHelperContainedViewModel` | Contains result from `<formvh:form.uploadedResource>` 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 `<input>` fields with resource pointers, if available. |
+-------------------------+---------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| `deleteCheckboxes` | `ViewModelCollection` of `ViewHelperContainedViewModel` | Optional and TYPO3 >= v14 only. Contains results from `<formvh:form.uploadDeleteCheckbox>` view helper invocations which allow to delete existing file uploads on submit. |
+-------------------------+---------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

\(b) :php:`ViewHelperContainedViewModel`
If uploaded resource cannot be resolved. Contains result from `<formvh:form.uploadedResource>`
Expand Down
1 change: 1 addition & 0 deletions composer-dependency-analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
])
;
Expand Down
18 changes: 18 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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\Core\Resource\File|TYPO3\CMS\Core\Resource\FileReference|TYPO3\CMS\Extbase\Domain\Model\File|TYPO3\CMS\Extbase\Domain\Model\FileReference>, TYPO3\CMS\Extbase\Domain\Model\FileReference|TYPO3\CMS\Extbase\Persistence\ObjectStorage<TYPO3\CMS\Extbase\Domain\Model\FileReference> 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<TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface>, string given.'
identifier: argument.type
count: 1
path: Classes/Domain/ViewModel/Builder/FileUploadViewModelBuilder.php
Loading