For coders TYPO3 Tech Corner

[TYPO3] PreviewRenderer: Vorschau im Backend für eigene Content Elemente

[TYPO3] PreviewRenderer: Vorschau im Backend für eigene Content Elemente

Mit Hilfe eines PreviewRenderers könnt ihr im Backend für etwas Abwechslung sorgen. Gerade wenn ihr eigene Content Elemente im Einsatz habt, wollt ihr vielleicht auch, dass sich eure Redakteure im Seitenmodul schnell zurechtfinden. Gleichzeitig könnt ihr wichtige Informationen präsentieren, bevor das Content Element geöffnet wurde. Der Einsatz eines PreviewRenderers in TYPO3 ist nichts neues, wird aber leider viel zu selten genutzt.

Bei der Umsetzung von Vorschauen gibt es zwei generell unterschiedliche Ansätze, mit verschieden Vor- und Nachteilen. Man kann sich entscheiden, ob man eine ähnliche Ansicht wie im Frontend auch gleich im Backend rendert (siehe Screenshot oben) oder ob man das Content Element stark abstrahiert darstellen möchte (wie z.B. im News- oder Powermail-Plugin). Während erstere Methode ein einfaches Zurechtfinden der Redakteure erlaubt, legt letztere Methode den Fokus auf die vom Redakteur gemachten Einstellungen im Plugin. Was und wie ihr eine passende Vorschau darstellen wollt, liegt somit also ganz bei euch.

Alle Änderungen und nachfolgenden Code-Beispiele befinden sich bei uns in der Sitepackage-Extension - bei uns mit dem Namen in2template.

1. Einfaches Beispiel

Wir nutzen in der Regel eine Abstrakte-Klasse die bereits ein HTML-Template an einer bestimmten Stelle erwartet und diesem wichtige Informationen über das Content Element (auch mit FlexForm-Konfiguration) übergibt.

EXT:in2template/Classes/Hooks/PageLayoutView/AbstractPreviewRenderer.php:

<?php declare(strict_types=1); namespace In2code\In2template\Hooks\PageLayoutView; use In2code\In2template\Exception\ConfigurationMissingException; use In2code\In2template\Exception\TemplateFileMissingException; use In2code\In2template\Utility\DatabaseUtility; use In2code\In2template\Utility\ObjectUtility; use TYPO3\CMS\Backend\View\PageLayoutView; use TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface; use TYPO3\CMS\Core\Resource\FileReference; use TYPO3\CMS\Core\Resource\FileRepository; use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Object\Exception; use TYPO3\CMS\Fluid\View\StandaloneView; /** * Class AbstractPreviewRenderer */ abstract class AbstractPreviewRenderer implements PageLayoutViewDrawItemHookInterface { /** * @var array tt_content.* */ protected $data = []; /** * Define a CType * * @var string */ protected $cType = ''; /** * @var string */ protected $templatePath = 'EXT:in2template/Resources/Private/Templates/PreviewRenderer/'; /** * AbstractPreviewRenderer constructor. * @throws ConfigurationMissingException */ public function __construct() { if (empty($this->cType)) { throw new ConfigurationMissingException('Property cType must not be empty', 1586703436); } } /** * Preprocesses the preview rendering of a content element of type "My new content element" * * @param PageLayoutView $parentObject Calling parent object * @param bool $drawItem Whether to draw the item using the default functionality * @param string $headerContent Header content * @param string $itemContent Item content * @param array $row Record row of tt_content * * @return void * @throws Exception * @throws TemplateFileMissingException */ public function preProcess( PageLayoutView &$parentObject, &$drawItem, &$headerContent, &$itemContent, array &$row ) { $this->data = &$row; if ($this->isCtypeMatching() && $this->checkTemplateFile()) { $drawItem = false; $headerContent = $this->getHeaderContent(); $itemContent .= $this->getBodytext(); } } /** * @return string */ protected function getHeaderContent(): string { return '<div id="element-tt_content-' . (int)$this->data['uid'] . '" class="t3-ctype-identifier " data-ctype="' . $this->cType . '"></div>'; } /** * @return string * @throws Exception */ protected function getBodytext(): string { $standaloneView = ObjectUtility::getObjectManager()->get(StandaloneView::class); $standaloneView->setTemplatePathAndFilename($this->getTemplateFile()); $standaloneView->assignMultiple($this->getAssignmentsForTemplate() + [ 'data' => $this->data, 'flexForm' => $this->getFlexForm(), 'firstImage' => count($this->getImages()) > 0 ? $this->getImages()[0] : null, 'images' => $this->getImages() ]); return $standaloneView->render(); } /** * Can be extended from children classes * * @return array */ protected function getAssignmentsForTemplate(): array { return []; } /** * @return array * @throws Exception */ protected function getFlexForm(): array { $flexFormService = ObjectUtility::getObjectManager()->get(FlexFormService::class); return $flexFormService->convertFlexFormContentToArray($this->data['pi_flexform']); } /** * @return FileReference[] * @throws Exception */ protected function getImages(): array { $references = []; $fileRepository = ObjectUtility::getObjectManager()->get(FileRepository::class); $queryBuilder = DatabaseUtility::getQueryBuilderForTable('sys_file_reference'); $identifiers = (array)$queryBuilder ->select('uid') ->from('sys_file_reference') ->where('uid_foreign=' . (int)$this->data['uid'] . ' and tablenames="tt_content" and fieldname="image"') ->execute() ->fetch(\PDO::FETCH_COLUMN); foreach ($identifiers as $identifier) { if ($identifier > 0) { $reference = $fileRepository->findFileReferenceByUid($identifier); if ($reference !== null) { $references[] = $reference; } } } return $references; } /** * @return bool */ protected function isCtypeMatching(): bool { return $this->data['CType'] === $this->cType; } /** * @return bool * @throws TemplateFileMissingException */ protected function checkTemplateFile(): bool { if (is_file($this->getTemplateFile()) === false) { throw new TemplateFileMissingException( 'Expected template file for preview rendering for CType ' . $this->cType . ' is missing', 1586703260 ); } return true; } /** * Get absolute path to template file * * @return string */ protected function getTemplateFile(): string { return GeneralUtility::getFileAbsFileName( $this->templatePath . GeneralUtility::underscoredToUpperCamelCase($this->data['CType']) . '.html' ); } }

Hinweis: In oben stehenden Beispiel werden die Klassen DatabaseUtility, ObjectUtility, ConfigurationMissingException und TemplateFileMissingException verwendet. Diese zeigen wir nicht explizit, weil es sich hierbei um einfache Dinge handelt, die eventuell sowieso von euch anders umgesetzt werden.

Danach gibt es eine Klasse für jedes Element, das eine eigene Vorschau erhält. In diesem Beispiel ein Youtube-Content-Element mit Hilfe dessen Redakteure Youtube-Videos darstellen können.

EXT:in2template/Classes/Hooks/PageLayoutView/YouTubePreviewRenderer.php:

<?php declare(strict_types=1); namespace In2code\In2template\Hooks\PageLayoutView; /** * Class YouTubePreviewRenderer */ class YouTubePreviewRenderer extends AbstractPreviewRenderer { /** * @var string */ protected $cType = 'youtube'; }

Diese Klasse muss auch noch in der EXT:in2template/ext_tables.php registriert werden:

<?php defined('TYPO3_MODE') || die(); call_user_func( function () { /** * Register own preview renderer for content elements */ $layout = 'cms/layout/class.tx_cms_layout.php'; $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][$layout]['tt_content_drawItem']['youtube'] = \In2code\In2template\Hooks\PageLayoutView\YouTubePreviewRenderer::class; } );

Ab jetzt können wir ein HTML-Template für die Vorschau im Backend anlegen. Dem HTML-Template werden automatisch wichtige Variablen übergeben, die man nutzen kann ({data}, {flexForm}, {firstImage}, {images}).

EXT:in2template/Resources/Private/Templates/PreviewRenderer/Youtube.html für CType "youtube":

<style> .in2template-preview-youtube { overflow: hidden; position: relative; } .in2template-preview-youtube img { float: left; } .in2template-preview-youtube-icon { position: absolute; width: 100px; top: 130px; left: 192px; } </style> <div class="in2template-preview-youtube"> <h3>{data.header}</h3> <img src="https://img.youtube.com/vi/{flexForm.id}/0.jpg" width="480" height="360" /> <img src="/_assets/a92153751098915699a1afa17e77f864/Images/Backend/Icons/Contentelements/ce-youtube.svg" class="in2template-preview-youtube-icon" /> </div>

2. Erweitertes Beispiel

2.a Weitere Variablen im Template

Wenn ihr in eurem HTML-Template weitere Variablen braucht, könnt ihr in eurem PreviewRenderer die Methode getAssignmentsForTemplate() nutzen.

<?php declare(strict_types=1); namespace In2code\In2template\Hooks\PageLayoutView; /** * Class ContactPreviewRenderer */ class ContactPreviewRenderer extends AbstractPreviewRenderer { /** * @var string */ protected $cType = 'contact'; /** * @return array * @throws Exception */ protected function getAssignmentsForTemplate(): array { return [ 'variableNew' => 'foo', 'variableNew2' => ['foo' => 'bar'] ]; } }

2.b Bootstrap

Wenn ihr Bootstrap mögt, könnt ihr in euren Templates im Backend auch direkt Bootstrap Klassen verwenden, da TYPO3 im Backend bereits darauf setzt:

<div class="container"> <div class="row"> <div class="col-md-2">{data.header}</div> <div class="col-md-2"><f:image image="{firstImage}" height="150c" width="150c"/></div> <div class="col-md-6"> <h1>{flexForm.title}</h1> <f:format.raw>{flexForm.text}</f:format.raw> </div> <div class="col-md-2">{flexForm.email}</div> </div> </div>

2.c Nützliche ViewHelper in Fluid

Oftmals setzen wir 3 verschiedene ViewHelper im Fluid ein

  • exception.catch Es kann passieren, dass Ihr beispielsweise ein Bild rendern wollt, dieses aber vom Redakteur nicht gepflegt wurde. Hierbei würde es über den Image-ViewHelper zu einer Exception kommen. Das ist insofern schlecht, weil der Redakteur dann die ganze Seite im Backend nicht mehr bearbeiten kann. Daher fangen wir diese Fehler ab und stellen diese in der Vorschau dar.
  • backend.editLink Wenn ihr gerne irgendwo auf die Vorschau klicken wollt und es soll die Bearbeitungsansicht aufgehen, könnt ihr dieses ViewHelper nutzen.
  • backend.removeLinks Wenn ihr Text aus einem RTE darstellen wollt, kann es passieren, dass A-Tags gerendert werden. Gerade wenn ihr mit backend.editLink aber einen äußeren Link gesetzt habt, wollt ihr nicht noch weitere Links erzeugen.
{namespace in2template=In2code\In2template\ViewHelpers} <in2template:exception.catch> <in2template:backend.editLink identifier="{data.uid}"> <div class="in2template-preview-stage" style="background-image: url({f:uri.image(image:firstImage,width:'2000m')});"> <div class="in2template-preview-stage-box"> <h1>{data.header}</h1> <in2template:backend.removeLinks>{data.bodytext}</in2template:backend.removeLinks> <p> <span class="btn">{data.button_label}</span> </p> </div> </div> </in2template:backend.editLink> </in2template:exception.catch>

EXT:in2template/Classes/ViewHelpers/Exception/CatchViewHelper.php:

<?php declare(strict_types=1); namespace In2code\In2template\ViewHelpers\Exception; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; /** * Class CatchViewHelper * @noinspection PhpUnused */ class CatchViewHelper extends AbstractViewHelper { /** * @var bool */ protected $escapeOutput = false; /** * @return string */ public function render(): string { try { return $this->renderChildren(); } catch (\Exception $exception) { $string = '<div class="alert alert-danger" role="alert">'; $string .= $exception->getMessage(); $string .= ' (' . $exception->getCode() . ')'; $string .= '</div>'; return $string; } } }

EXT:in2template/Classes/ViewHelpers/Backend/EditLinkViewHelper.php:

<?php declare(strict_types=1); namespace In2code\In2template\ViewHelpers\Backend; use In2code\In2template\Utility\BackendUtility; use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; /** * Class EditLinkViewHelper * @noinspection PhpUnused */ class EditLinkViewHelper extends AbstractViewHelper { /** * @var bool */ protected $escapeOutput = false; /** * @return void */ public function initializeArguments() { parent::initializeArguments(); $this->registerArgument('identifier', 'int', 'Identifier', true); $this->registerArgument('table', 'string', 'Tablename', false, 'tt_content'); } /** * @return string * @throws RouteNotFoundException */ public function render(): string { $string = '<a href="'; $string .= BackendUtility::createEditUri($this->arguments['table'], (int)$this->arguments['identifier']); $string .= '" class="in2template_editlink">'; $string .= $this->renderChildren(); $string .= '</a>'; return $string; } }

Hierzu braucht es noch eine EXT:in2template/Classes/Utility/BackendUtility.php:

<?php declare(strict_types=1); namespace In2code\In2template\Utility; use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException; use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException; use TYPO3\CMS\Backend\Routing\Router; use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Class BackendUtility */ class BackendUtility { /** * @param string $tableName * @param int $identifier * @param bool $addReturnUrl * @return string * @throws RouteNotFoundException */ public static function createEditUri(string $tableName, int $identifier, bool $addReturnUrl = true): string { $uriParameters = [ 'edit' => [ $tableName => [ $identifier => 'edit' ] ] ]; if ($addReturnUrl) { $uriParameters['returnUrl'] = self::getReturnUrl(); } return self::getRoute('record_edit', $uriParameters); } /** * Get return URL from current request * * @return string * @throws RouteNotFoundException */ protected static function getReturnUrl(): string { return self::getRoute(self::getModuleName(), self::getCurrentParameters()); } /** * @param string $route * @param array $parameters * @return string * @throws RouteNotFoundException */ public static function getRoute(string $route, array $parameters = []): string { $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); return (string)$uriBuilder->buildUriFromRoute($route, $parameters); } /** * Get module name or route as fallback * * @return string */ protected static function getModuleName(): string { $moduleName = 'record_edit'; if (GeneralUtility::_GET('route') !== null) { $routePath = (string)GeneralUtility::_GET('route'); $router = GeneralUtility::makeInstance(Router::class); try { $route = $router->match($routePath); $moduleName = $route->getOption('_identifier'); } catch (ResourceNotFoundException $exception) { unset($exception); } } return $moduleName; } /** * Get all GET/POST params without module name and token * * @param array $getParameters * @return array */ public static function getCurrentParameters(array $getParameters = []): array { if (empty($getParameters)) { $getParameters = GeneralUtility::_GET(); } $parameters = []; $ignoreKeys = [ 'M', 'moduleToken', 'route', 'token' ]; foreach ($getParameters as $key => $value) { if (in_array($key, $ignoreKeys)) { continue; } $parameters[$key] = $value; } return $parameters; } }

EXT:in2template/Classes/ViewHelpers/Backend/RemoveLinksViewHelper.php:

<?php declare(strict_types=1); namespace In2code\In2template\ViewHelpers\Backend; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; /** * Class RemoveLinksViewHelper * to replace a-tags with span-tags and some styles * @noinspection PhpUnused */ class RemoveLinksViewHelper extends AbstractViewHelper { /** * @var bool */ protected $escapeOutput = false; /** * set a style for the new span-tags * * @var string */ protected $style = 'color: #A5C85A; font-weight: bold; text-decoration: underline;'; /** * @return string */ public function render(): string { $string = html_entity_decode($this->renderChildren()); $dom = new \DOMDocument(); @$dom->loadHTML( $this->wrapHtmlWithMainTags($string), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); $aTags = $dom->getElementsByTagName('a'); while ($aTags->length) { /** @var \DOMElement $aTag */ $aTag = $aTags[0]; $linkText = $aTag->nodeValue; $class = $aTag->getAttribute('class'); $span = $dom->createElement('span'); $span->setAttribute('class', $class); $span->setAttribute('style', $this->style); $span->nodeValue = $linkText; $aTag->parentNode->replaceChild($span, $aTag); } return $this->stripMainTagsFromHtml($dom->saveHTML()); } /** * Wrap html with "<?xml encoding="utf-8" ?><html><body>|</body></html>" * * This is a workarround for HTML parsing and wrting with \DOMDocument() * - The html and body tag are preventing strange p-tags while using LIBXML_HTML_NOIMPLIED * - The doctype declaration allows us the usage of umlauts and special characters * * @param string $html * @return string */ protected function wrapHtmlWithMainTags(string $html): string { return '<?xml encoding="utf-8" ?><html><body>' . $html . '</body></html>'; } /** * Remove tags <?xml encoding="utf-8" ?><html><body></body></html> * This function is normally used after wrapHtmlWithMainTags * * @param string $html * @return string */ protected function stripMainTagsFromHtml(string $html): string { return str_replace(['<html>', '</html>', '<body>', '</body>', '<?xml encoding="utf-8" ?>'], '', $html); } }

Zurück

Kennst du das: Immer nur schnell schnell?

Wie wäre es einmal mit Zeit und Respekt für Codequalität? Arbeiten im Team? Automatisierte Tests?

Komm zu uns