Add form integration (#6)

This commit is contained in:
Daniel Siepmann 2022-09-29 08:41:44 +02:00 committed by GitHub
parent bdc1572297
commit 46922636d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 483 additions and 10 deletions

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\Watchlist\Domain\Model;
/**
* Needs to be implemented by all Items which should be usable for form integration.
*/
interface FormElementAwareItem
{
/**
* Provides the actual value to use when submitting the element.
*/
public function getFormElementValue(): string;
/**
* Provides the actual title to use as label within the element.
*/
public function getFormElementTitle(): string;
}

View file

@ -43,6 +43,17 @@ class Watchlist
return array_values($this->items);
}
/**
* @return array<Item&FormElementAwareItem>
*/
public function getFormElementAwareItems(): array
{
return array_filter(
array_values($this->items),
fn (Item $item) => $item instanceof FormElementAwareItem
);
}
public function addItem(Item $item): void
{
$this->items[$item->getUniqueIdentifier()] = $item;

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\Watchlist\Form\FormElements;
use TYPO3\CMS\Form\Domain\Model\FormElements\AbstractFormElement;
use WerkraumMedia\Watchlist\Domain\Model\Watchlist;
use WerkraumMedia\Watchlist\Session\SessionServiceInterface;
final class WatchlistFormElement extends AbstractFormElement
{
private SessionServiceInterface $sessionService;
public function injectSessionService(SessionServiceInterface $sessionService): void
{
$this->sessionService = $sessionService;
}
public function getWatchlist(): ?Watchlist
{
// Prevent watchlist items from being cached for users.
// Only trigger once they are actually used = this method is used.
$GLOBALS['TSFE']->no_cache = 1;
return $this->sessionService->getWatchlist('default');
}
}

View file

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\Watchlist\Form\Hook;
use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
use WerkraumMedia\Watchlist\Form\FormElements\WatchlistFormElement;
use WerkraumMedia\Watchlist\Session\SessionServiceInterface;
/**
* Renderables are instantiated with makeInstance and constructor arguments.
* We therefore have no chance for DI.
*
* That's why we use the hook as this seems like the best workaround.
* He will use the inject* method, so we can remove the hook once DI should work.
*/
final class BeforeRenderingHook
{
private SessionServiceInterface $sessionService;
public function __construct(
SessionServiceInterface $sessionService
) {
$this->sessionService = $sessionService;
}
public function beforeRendering(
FormRuntime $formRuntime,
RootRenderableInterface $renderable
): void {
if (!$renderable instanceof WatchlistFormElement) {
return;
}
$renderable->injectSessionService($this->sessionService);
}
public static function register(): void
{
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRendering'][self::class] = self::class;
}
}

View file

@ -0,0 +1,8 @@
TYPO3:
CMS:
Form:
prototypes:
standard:
formElementsDefinition:
Watchlist:
implementationClassName: WerkraumMedia\Watchlist\Form\FormElements\WatchlistFormElement

View file

@ -12,3 +12,6 @@ services:
WerkraumMedia\Watchlist\Session\CookieSessionService:
# Has state and needs to be shared in order to provide this state to middleware
shared: true
WerkraumMedia\Watchlist\Form\Hook\BeforeRenderingHook:
public: true

View file

@ -25,15 +25,27 @@ Projects are way too different and should provide their own items, see "Custom I
The extension also provides a JavaScript which will parse data-Attributes of DOM and
attach listener to add elements to the watchlist.
Allows to render the items of watchlist within an EXT:form form.
Concept
=======
The extension only provides the functionality, no concrete project specific implementation.
It is up to the developer and integrator of the project to provide necessary pieces, see "Custom Items".
The extension always only uses identifier of each item and does not know anything else about items.
Custom Items
============
A developer needs to implement an ``ItemHandler`` and an class representing the ``Item``.
``FormElementAwareItem`` can optionally be implemented as well, in order to make use of items within an EXT:form form.
The used identifier has to be unique throughout the system.
The first part is the item type which is returned by the ItemHandler to be handled,
followed by a minus and the rest of the identifier, leading to: ``<type>-rest-of-identifier``.
The used identifier has to be unique throughout the system. The first part is the
item type which is returned by the ItemHandler to be handled, followed by a minus and
the rest of the identifier, leading to: ``<type>-rest-of-identifier``.
Commas should not be used within the identifier.
The identifiers will be stored in as csv within a cookie.
``ItemHandler``
---------------
@ -52,6 +64,14 @@ The Handler needs to be registered via Symfony Tags, e.g. via ``Services.yaml``:
The class needs to implement the ``WerkraumMedia\Watchlist\Domain\Model\Item``.
``FormElementAwareItem``
------------------------
The class can implement the ``WerkraumMedia\Watchlist\Domain\Model\FormElementAwareItem``.
The methods are used by the default EXT:form integration.
It is up to the developer + integrator to not use this interface and alter the rendering.
Example
-------
@ -60,12 +80,36 @@ The extension delivers an example implementation for testing purposes, check out
- ``Tests/Fixtures/WatchlistExample/Classes/Domain/Items/Page/ItemHandler.php``
- ``Tests/Fixtures/WatchlistExample/Classes/Domain/Items/Page/Page.php``
This implements both ``Item`` as well as ``FormElementAwareItem``.
- ``Tests/Fixtures/WatchlistExample/Configuration/Services.yaml``
The example demonstrates how to fetch information from database,
including file references.
EXT:form integration
====================
The provided Configuration needs to be loaded via TypoScript.
Use a free identifier:
.. code:: plain
plugin.tx_form.settings.yamlConfigurations {
80 = EXT:watchlist/Configuration/Form/Setup.yaml
}
This will register a new form element type ``Watchlist`` that can be used like this:
.. code:: yaml
-
type: Watchlist
identifier: watchlist-1
label: 'Watchlist'
A default template is provided which will render all items with checkboxes.
JavaScript
==========

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\Watchlist\Tests\Acceptance;
use WerkraumMedia\Watchlist\Tests\Acceptance\Support\AcceptanceTester;
class FormIntegrationCest
{
public function canSelectOneOfTwoSubmittedItemsFromWatchlist(AcceptanceTester $I): void
{
$I->amOnPage('/');
$I->click('button[data-watchlist-item="page-1"]');
$I->click('button[data-watchlist-item="page-2"]');
$I->amOnPage('/page-2');
$I->checkOption('#test-1-watchlist-1-1');
$I->click('Next step');
$I->see('Page: Page 2 Title');
$I->dontSee('Page: Page Title');
$I->click('Submit');
}
public function watchlistItemsInFormAreNotCached(AcceptanceTester $I): void
{
$this->canSelectOneOfTwoSubmittedItemsFromWatchlist($I);
$I->amOnPage('/');
$I->click('button[data-watchlist-item="page-1"]');
$I->amOnPage('/page-2');
$I->see('Form: Page 2 Title');
$I->dontSee('Form: Page Title');
}
}

View file

@ -44,6 +44,7 @@ class Environment extends BackendEnvironment
'frontend',
'fluid',
'fluid_styled_content',
'form',
],
'testExtensionsToLoad' => [
'typo3conf/ext/watchlist',
@ -51,6 +52,7 @@ class Environment extends BackendEnvironment
],
'csvDatabaseFixtures' => [
__DIR__ . '/../../Fixtures/BasicDatabase.csv',
__DIR__ . '/../../Fixtures/FormDatabase.csv',
],
'additionalFoldersToCreate' => [
'config',

View file

@ -0,0 +1,17 @@
"sys_template",,,,,
,"pid","config",,,
,2,"<INCLUDE_TYPOSCRIPT: source=""FILE:EXT:watchlist_example/Configuration/TypoScript/Form.typoscript"">
""",,,
"tt_content",,,,,
,"pid","header","CType","pi_flexform",
,2,"Form","form_formframework","<?xml version=""1.0"" encoding=""utf-8"" standalone=""yes"" ?><T3FlexForms>
<data>
<sheet index=""sDEF"">
<language index=""lDEF"">
<field index=""settings.persistenceIdentifier"">
<value index=""vDEF"">EXT:watchlist_example/Configuration/Forms/Example.form.yaml</value>
</field>
</language>
</sheet>
</data>
</T3FlexForms>",
1 sys_template
2 pid config
3 2 <INCLUDE_TYPOSCRIPT: source="FILE:EXT:watchlist_example/Configuration/TypoScript/Form.typoscript"> "
4 tt_content
5 pid header CType pi_flexform
6 2 Form form_formframework <?xml version="1.0" encoding="utf-8" standalone="yes" ?><T3FlexForms> <data> <sheet index="sDEF"> <language index="lDEF"> <field index="settings.persistenceIdentifier"> <value index="vDEF">EXT:watchlist_example/Configuration/Forms/Example.form.yaml</value> </field> </language> </sheet> </data> </T3FlexForms>

View file

@ -24,9 +24,10 @@ declare(strict_types=1);
namespace WerkraumMedia\WatchlistExample;
use TYPO3\CMS\Core\Resource\FileInterface;
use WerkraumMedia\Watchlist\Domain\Model\FormElementAwareItem;
use WerkraumMedia\Watchlist\Domain\Model\Item;
class Page implements Item
class Page implements Item, FormElementAwareItem
{
private int $pageUid;
@ -58,4 +59,14 @@ class Page implements Item
{
return $this->image;
}
public function getFormElementValue(): string
{
return 'Page: ' . $this->title;
}
public function getFormElementTitle(): string
{
return 'Form: ' . $this->title;
}
}

View file

@ -0,0 +1,31 @@
renderingOptions:
submitButtonLabel: Submit
type: Form
identifier: test
label: Test
prototypeName: standard
renderables:
-
renderingOptions:
previousButtonLabel: 'Previous step'
nextButtonLabel: 'Next step'
type: Page
identifier: page-1
label: First Step
renderables:
-
type: Watchlist
identifier: watchlist-1
label: 'Watchlist'
-
defaultValue: ''
type: Text
identifier: text-1
label: 'Example text field'
-
renderingOptions:
previousButtonLabel: 'Previous step'
nextButtonLabel: 'Next step'
type: SummaryPage
identifier: summarypage-1
label: 'Summary step'

View file

@ -0,0 +1,14 @@
TYPO3:
CMS:
Form:
persistenceManager:
allowedExtensionPaths:
10: EXT:watchlist_example/Configuration/Forms/
prototypes:
standard:
formElementsDefinition:
Form:
renderingOptions:
partialRootPaths:
99: 'EXT:watchlist_example/Resources/Private/Partials/Form/Frontend/'

View file

@ -0,0 +1,10 @@
<INCLUDE_TYPOSCRIPT: source="FILE:EXT:form/Configuration/TypoScript/setup.typoscript">
plugin.tx_form.settings.yamlConfigurations {
80 = EXT:watchlist/Configuration/Form/Setup.yaml
90 = EXT:watchlist_example/Configuration/Forms/Setup.yaml
}
page >
page = PAGE
page.10 =< styles.content.get

View file

@ -0,0 +1,26 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<formvh:renderRenderable renderable="{element}">
<f:if condition="{element.watchlist.formElementAwareItems}">
<f:render partial="Field/Field" arguments="{element: element, doNotShowLabel: 1}" contentAs="elementContent">
<f:for each="{element.watchlist.formElementAwareItems}" as="item" iteration="iterator">
<div class="form-check">
<label class="{element.properties.elementClassAttribute} form-check-label" for="{element.uniqueIdentifier}-{iterator.index}">
<f:form.checkbox
property="{element.identifier}"
id="{element.uniqueIdentifier}-{iterator.index}"
class="{element.properties.elementClassAttribute}"
value="{item.formElementValue}"
errorClass="{element.properties.elementErrorClassAttribute}"
additionalAttributes="{formvh:translateElementProperty(element: element, property: 'fluidAdditionalAttributes')}"
/>
<span>{item.formElementTitle}</span>
</label>
</div>
</f:for>
</f:render>
</f:if>
</formvh:renderRenderable>
</html>

View file

@ -4,6 +4,8 @@
"type": "typo3-cms-extension",
"license": "GPL-2.0-or-later",
"require": {
"typo3/cms-core": "*",
"typo3/cms-form": "*",
"werkraummedia/watchlist": "*"
},
"extra": {

View file

@ -28,9 +28,6 @@ use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalResponse;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
/**
* @covers \WerkraumMedia\Watchlist\Frontend\Basics
*/
class BasicsTest extends FunctionalTestCase
{
protected $coreExtensionsToLoad = [

View file

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\Watchlist\Tests\Functional;
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
class FormIntegrationTest extends FunctionalTestCase
{
protected $coreExtensionsToLoad = [
'fluid_styled_content',
'form',
'tstemplate',
];
protected $testExtensionsToLoad = [
'typo3conf/ext/watchlist',
'typo3conf/ext/watchlist/Tests/Fixtures/watchlist_example',
];
protected $pathsToLinkInTestInstance = [
'typo3conf/ext/watchlist/Tests/Fixtures/Sites' => 'typo3conf/sites',
'typo3conf/ext/watchlist/Tests/Fixtures/Fileadmin/Files' => 'fileadmin/Files',
];
protected function setUp(): void
{
parent::setUp();
$this->importCSVDataSet(__DIR__ . '/../Fixtures/BasicDatabase.csv');
$this->importCSVDataSet(__DIR__ . '/../Fixtures/FormDatabase.csv');
}
/**
* @test
*/
public function rendersWatchlistItemsIntoForm(): void
{
$request = new InternalRequest();
$request = $request->withPageId(2);
$request = $request->withHeader('Cookie', 'watchlist=page-1,page-2');
$result = $this->executeFrontendRequest($request);
self::assertSame(200, $result->getStatusCode());
$html = $result->getBody()->__toString();
self::assertSame(2, substr_count($html, 'form-group'));
self::assertStringContainsString('<span>Form: Page Title</span>', $html);
self::assertStringContainsString('value="Page: Page Title"', $html);
self::assertStringContainsString('<span>Form: Page 2 Title</span>', $html);
self::assertStringContainsString('value="Page: Page 2 Title"', $html);
}
/**
* @test
*/
public function doesntRenderFormElementForZeroItems(): void
{
$request = new InternalRequest();
$request = $request->withPageId(2);
$result = $this->executeFrontendRequest($request);
self::assertSame(200, $result->getStatusCode());
$html = $result->getBody()->__toString();
self::assertSame(1, substr_count($html, 'form-group'));
self::assertStringNotContainsString('checkbox', $html);
self::assertStringNotContainsString('Watchlist', $html);
}
}

View file

@ -47,6 +47,7 @@
"phpunit/phpunit": "^9.5",
"saschaegerer/phpstan-typo3": "^1.1",
"typo3/cms-fluid-styled-content": "^11.5",
"typo3/cms-form": "^11.5",
"typo3/cms-tstemplate": "^11.5",
"typo3/testing-framework": "^6.6"
},

View file

@ -5,6 +5,7 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
use WerkraumMedia\Watchlist\Controller\WatchlistController;
use WerkraumMedia\Watchlist\Extbase\TypeConverter\ItemTypeConverter;
use WerkraumMedia\Watchlist\Extbase\TypeConverter\WatchlistTypeConverter;
use WerkraumMedia\Watchlist\Form\Hook\BeforeRenderingHook;
defined('TYPO3') || die('Access denied.');
@ -20,10 +21,10 @@ defined('TYPO3') || die('Access denied.');
],
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
);
ExtensionUtility::registerTypeConverter(WatchlistTypeConverter::class);
ExtensionUtility::registerTypeConverter(ItemTypeConverter::class);
ExtensionManagementUtility::addPageTSConfig(
"@import 'EXT:watchlist/Configuration/TSconfig/Page/Default.tsconfig'"
);
ExtensionUtility::registerTypeConverter(WatchlistTypeConverter::class);
ExtensionUtility::registerTypeConverter(ItemTypeConverter::class);
BeforeRenderingHook::register();
})();