Ein Backend Modul in TYPO3 zu erstellen ist dank dem Extension Builder nicht schwer. Erweitert man jedoch etwas, wird die native Unterstützung etwas dünn.
Dieser Artikel setzt eine leere Extension „easy_teasers“, erstellt mit dem „Extension Builder“, voraus.
Der Usercase
Für Seiten-Teaser nutze ich die Seiteneigenschaften „title,abstract,media“.
Es wird also die Tabelle „pages“ bearbeitet um die Informationen zu pflegen.
Die Teaser lassen sich dann leicht mit dem Menü-Inhaltselement aufbereiten und sie werden in der Suche unterstützt.
Eine Übersicht über die gepflegten Teaser im Backend zu erhalten ist jedoch quasi unmöglich.
Das Backend-Modul soll eine Übersicht der Seitenteaser-Information herstellen und deren Bearbeitung unterstützen.
Zur Anordnung der Teaser wird Masonry verwendet.
Das Problem mit dem Cache
Auch wenn das Backend weitestgehen ungecached ausgespielt wird, so werden Extbase Klassen und Fluid Templates trotzdem gecached.
Während dem Ausbauen der Klassen und der Ansicht musste häufig nicht nur der komplette Cache gelöscht sondern auch das typo3temp-Verzeichnis geleert werden.
Das Problem mit masonry und requireJS
Masonry unterstützt requireJS. Das bedeutet im Umkehrschluss, dass es sich nicht auf „normalem“ Wege einbinden lässt, wenn requireJS aktiv ist … was im TYPO3 Backend der Fall ist.
Das Einbinden über
<f:be.container includeJsFiles="{0: '{f:uri.resource(path:\'JavaScript/masonry.pkg.min.js\')}'}">
...
</f:be.container>
als auch über ein normales Script-Tag,
<script src="../typo3conf/ext/easy_teasers/Resources/Public/JavaScript/masonry.pkgd.min.js"></script>
als auch über einen ViewHelper
$this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$baseUrl = '../' . \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::siteRelPath('easy_teasers');
$this->pageRenderer->disableCompressJavascript();
$this->pageRenderer->addJsFile($baseUrl . 'Resources/Public/JavaScript/masonry.pkg.min.js');
Alles schlug fehl. Der Fehler war stets derselbe:
Uncaught Error: Mismatched anonymous define() module
Der Weg über requireJS funktionierte dann.
Die Ordnerstruktur
├── ext_emconf.php ├── ext_icon.gif ├── ext_localconf.php ├── ext_tables.php ├── Classes │ ├── Controller │ │ └── TeaserPageController.php │ ├── Domain │ │ ├── Model │ │ │ └── TeaserPage.php │ │ └── Repository │ │ └── TeaserPageRepository.php │ └── ViewHelpers │ └── BackendLinkViewHelper.php ├── Configuration │ └── TypoScript │ ├── constants.txt │ └── setup.txt └── Resources ├── Private │ ├── Backend │ │ ├── Layouts │ │ │ └── Default.html │ │ ├── Partials │ │ │ └── TeaserPage │ │ │ └── Teaser.html │ │ └── Templates │ │ └── TeaserPage │ │ └── List.html │ └── Language │ ├── locallang_db.xlf └── Public ├── Icons │ └── easy_teasers.svg ├── Images │ └── noimage.svg ├── JavaScript │ ├── easy_teasers.js │ └── masonry.pkgd.min.js └── Stylesheet └── easy_teasers.css
Das Modul im Backend unter „Web“ verfügbar machen
ext_tables.php
<?php
if (TYPO3_MODE === 'BE') {
/**
* Registers a Backend Module
*/
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule(
'Marwein.' . $_EXTKEY,
'web', // Make module a submodule of 'web'
'teasers', // Submodule key
'', // Position
array(
'TeaserPage' => 'list, edit',
),
array(
'access' => 'user,group',
'icon' => 'EXT:' . $_EXTKEY . '/ext_icon.gif',
'labels' => 'LLL:EXT:' . $_EXTKEY . '/Resources/Private/Language/locallang_teasers.xlf',
)
);
}
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addStaticFile($_EXTKEY, 'Configuration/TypoScript', 'Easy Teasers');
Der Controller
Der Controller wird die Listenansicht im Backend steuern.
Classes/Controller/TeaserPageController.php
<?php
namespace Marwein\EasyTeasers\Controller;
class TeaserPageController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController
{
/**
* @var \Marwein\EasyTeasers\Domain\Repository\TeaserPageRepository
* @inject
*/
protected $teaserPageRepository = NULL;
public function listAction()
{
$id = $_GET['id'] ? intval($_GET['id']) : 0;
$teaserPages = $this->teaserPageRepository->findByUidRecursive($id);
$this->view->assign('teaserPages', $teaserPages);
}
}
Das Model
Das Model besteht aus den 3 Seiteneigenschafts-Feldern. Das Model wird nur für die Anzeige der Teaser im Backend verwendet. Daher sind Setter optional und ich habe sie weggelassen.
Classes/Domain/Model/TeaserPage.php
<?php
namespace Marwein\EasyTeasers\Domain\Model;
class TeaserPage extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
/**
* @var string
*/
protected $title = '';
/**
* @var string
*/
protected $abstract = '';
/**
* @var \TYPO3\CMS\Extbase\Domain\Model\FileReference
*/
protected $media = null;
public function getTitle()
{
return $this->title;
}
public function getAbstract()
{
return $this->abstract;
}
public function getMedia()
{
return $this->media;
}
}
Das Repository
Mit dem Repository werden die Teaser-Daten rekursiv aus dem Seitenbaum ausgelesen.
Anmerkung: Eine besondere Sortierung oder hierarchische Ansicht ist mit diesem Ansatz nicht möglich.
Classes/Domain/Repository/TeaserPageRepository.php
<?php
namespace Marwein\EasyTeasers\Domain\Repository;
class TeaserPageRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
public function findByUidRecursive($pid) {
$queryGenerator = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance( 'TYPO3\\CMS\\Core\\Database\\QueryGenerator' );
// get 10 levels of the tree recursively starting at $pid
$pidList = $queryGenerator->getTreeList($pid, 10, 0, 1);
$pids = explode(',',$pidList);
$query = $this->createQuery();
$query->matching(
$query->logicalAnd(
$query->in('uid', $pids),
// Exclude the non-page-type pages
$query->logicalNot($query->in('doktype', array('254','255','199')))
)
);
// load from no matter where
$query->getQuerySettings()->setRespectStoragePage(FALSE);
return $query->execute();
}
}
Die Anzeige konfigurieren
Das nötige TypoScript
Die Extension hat selbst keine Tabellen. Das Model muss auf die „pages“ Tabelle verweisen bzw. „ge-mapped“ werden.
Configuration/TypoScript/constants.txt
module.tx_easyteasers_teasers {
view {
# cat=module.tx_easyteasers_teasers/file; type=string; label=Path to template root (BE)
templateRootPath = EXT:easy_teasers/Resources/Private/Backend/Templates/
# cat=module.tx_easyteasers_teasers/file; type=string; label=Path to template partials (BE)
partialRootPath = EXT:easy_teasers/Resources/Private/Backend/Partials/
# cat=module.tx_easyteasers_teasers/file; type=string; label=Path to template layouts (BE)
layoutRootPath = EXT:easy_teasers/Resources/Private/Backend/Layouts/
}
persistence {
# cat=module.tx_easyteasers_teasers//a; type=string; label=Default storage PID
storagePid =
}
}
Configuration/TypoScript/setup.txt
module.tx_easyteasers_web_easyteasersteasers {
persistence {
storagePid = 0
classes {
Marwein\EasyTeasers\Domain\Model\TeaserPage {
mapping {
tableName = pages
}
}
}
}
view {
templateRootPaths.0 = {$module.tx_easyteasers_teasers.view.templateRootPath}
partialRootPaths.0 = {$module.tx_easyteasers_teasers.view.partialRootPath}
layoutRootPaths.0 = {$module.tx_easyteasers_teasers.view.layoutRootPath}
}
}
Ein ViewHelper für Links
Für die Erzeugung von Links im Backend gibt es in Fluid keine Unterstützung … daher der eigene Viewhelper.
Classes/Domain/ViewHelper/BackendLinkViewHelper.php
<?php
namespace Marwein\EasyTeasers\ViewHelpers;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
class BackendLinkViewHelper extends \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper {
protected $tagName = 'a';
/**
* @param string $parameters
* @param string $returnUrl
*/
public function render($parameters, $returnUrl = '') {
$uri = \TYPO3\CMS\Backend\Utility\BackendUtility::getModuleUrl('record_edit').'&'.$parameters;
if (!empty($returnUrl)) {
$uri .= '&returnUrl='.rawurlencode($returnUrl);
} else {
$uri .= '&returnUrl='.rawurlencode(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('REQUEST_URI'));
}
$this->tag->addAttribute('href', $uri);
$this->tag->setContent($this->renderChildren());
$this->tag->forceClosingTag(TRUE);
return $this->tag->render();
}
}
Die Anzeige beschreiben
Das Stylesheet
Für das Masonry-Grid werden die nötigsten Einstellungen gemacht.
Resources/Public/Stylesheet/easy_teasers.css
.typo3-docbody {
background-color: #ddd;
}
.grid-item {
width: 300px; height: auto;
margin-bottom: 10px;
box-shadow: 0 0 20px #666;
background-color: #fff;
}
.grid-item b {
font-size: 1.2em;
display:block;
padding:5px;
background: #f80;
color:#fff;
}
.grid-item p {
padding:5px;
}
.grid-item img {
positon:relative;
bottom:0px;
width:100%;
height:auto;
}
Das JavaScript
Masonry lässt sich im Backend nur via requireJS einbinden.
Resources/Public/JavaScript/easy_teasers.js
requirejs( [
'../typo3conf/ext/easy_teasers/Resources/Public/JavaScript/masonry.pkgd.min.js',
], function( Masonry ) {
new Masonry( '.tx_easyteasers', {columnWidth: 300, gutter: 10});
});
Layout, Template und Partial
Stylesheet und JavaScript werden über den Be.Container ViewHelper geladen.
Resources/Private/Backend/Layouts/Default.html
{namespace m=Marwein\EasyTeasers\ViewHelpers}
<f:be.container
includeCssFiles="{0: '{f:uri.resource(path:\'Stylesheet/easy_teasers.css\')}'}"
includeJsFiles="{0: '{f:uri.resource(path:\'JavaScript/easy_teasers.js\')}'}">
<div class="typo3-fullDoc">
<div id="typo3-docbody">
<div id="typo3-inner-docbody">
<f:flashMessages />
<f:render section="content" />
</div>
</div>
</div>
</f:be.container>
Das Template für die Listenansicht arbeiten mit einem Partial für den Teaser selbst.
Resources/Private/Backend/Templates/TeaserPage/List.html
{namespace m=Marwein\EasyTeasers\ViewHelpers}
<f:layout name="Default" />
<f:section name="content">
<h1>Page teasers</h1>
<div class="tx_easyteasers" >
<f:for each="{teaserPages}" as="teaserPage">
<f:render partial="TeaserPage/Teaser" arguments="{teaserPage:teaserPage}" />
</f:for>
</div>
</f:section>
Das Partial wiederum sorgt für die Bild-Anzeige und den Bearbeitungs-Link über den ViewHelper.
Der Link sorgt dafür, dass nur 3 Felder im Formular auftauchen.
Der Teaser Text („abstract“) wird beschnitten.
Resources/Private/Backend/Partials/TeaserPage/Teaser.html
{namespace m=Marwein\EasyTeasers\ViewHelpers}
<div class="grid-item">
<m:backendLink parameters="edit[pages][{teaserPage.uid}]=edit&columnsOnly=title,_CONTROL_,_CLIPBOARD_,abstract,media&route=%2Frecord%2Fedit">
<b>{teaserPage.title}</b>
<f:if condition="{teaserPage.media.originalResource.publicUrl}">
<f:then>
<f:image src="{teaserPage.media.originalResource.publicUrl}" width="300c" height="150" />
</f:then>
<f:else>
<f:image src="typo3conf/ext/easy_teasers/Resources/Public/Images/noimage.svg" width="300" height="150" />
</f:else>
</f:if>
<p><f:format.crop maxCharacters="100" append=" [...]">{teaserPage.abstract}</f:format.crop>
</p>
</m:backendLink>
</div>
Damit ist die Extension einsatzfähig und hilft bei der übersichtlichen Bearbeitung von Seiten-Teasern, die über die Seiteneigenschaften hergestellt werden.
Durch eine Erweiterung des „Menü“-Elements lassen sich diese Teaser dann einfach aufbereiten und gewohnt flexibel konfigurieren.
In einer Ausbaustufe könnten „pages“ um weitere Teaser-Felder erweitert werden, wie auch unter ‚Die Extbase Extension „news“ um ein Feld erweitern‚ für „News“ beschrieben. Felder aus der Praxis sind bspw „teaser link“, „teaser link text“, „teaser title“ oder „teaser_layout“
Danke für das gelungene Tutorial.
Speziell die Info zu requireJS hat mir bei der Einbindung einer Javascript Library im Backend sehr geholfen.
Gruß,
Andreas