For coders TYPO3 Tech Corner

[TYPO3] PreviewRenderer: Preview in the backend for your own content elements

[TYPO3] PreviewRenderer: Preview in the backend for your own content elements

With the help of a PreviewRenderer you can provide a little variety in the backend. Especially if you are using your own content elements, you might also want your editors to find their way around the page module quickly. At the same time, you can present important information before the content element has been opened. The use of a preview renderer in TYPO3 is nothing new, but unfortunately it is used far too rarely.

There are two generally different approaches to implementing previews, with different advantages and disadvantages. You can decide whether you want to render a similar view as in the frontend in the backend (see screenshot above) or whether you want to display the content element in a highly abstracted way (e.g. in the news or powermail plugin). While the former method allows the editors to find their way easily, the latter method focuses on the settings made by the editor in the plugin. So what and how you want to present a suitable preview is entirely up to you.

All changes and the following code examples can be found in the sitepackage extension - with us under the name in2template.

1. Easy Example

We usually use an abstract class which already expects an HTML template at a certain point and which transfers important information about the content element (also with FlexForm configuration).

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' ); } }

Note: In the above example the classes DatabaseUtility, ObjectUtility, ConfigurationMissingException and TemplateFileMissingException are used. We do not show these explicitly, because these are simple things that you may implement differently anyway.

After that there is a class for each element that gets its own preview. In this example, a YouTube content element that editors can use to display YouTube videos.

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'; }

This class must also be registered in the EXT:in2template/ext_tables.php:

<?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; } );

From now on we can create an HTML template for the preview in the backend. Important variables that can be used are automatically transferred to the HTML template ({data}, {flexForm}, {firstImage}, {images}).

EXT:in2template/Resources/Private/Templates/PreviewRenderer/Youtube.html for 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. Advanced example

2.a Further variables in template file

If you need further variables in your HTML template, you can use the getAssignmentsForTemplate () method in your PreviewRenderer.

<?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

If you like Bootstrap, you can also use Bootstrap classes directly in your templates in the backend:

<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 Useful ViewHelpers in Fluid

We often use 3 different ViewHelpers in the fluid

  • exception.catch It can happen that you want to render an image, for example, but the editor has not maintained it. This would result in an exception via the Image-ViewHelper. This is bad because the editor can then no longer edit the entire page in the backend. Therefore we catch these errors and show them in the preview.
  • backend.editLink If you want to click somewhere on the preview and the editing view should open, you can use this ViewHelper.
  • backend.removeLinks If you want to display text from an RTE, it can happen that A tags are rendered. But especially if you have set an external link with backend.editLink, you do not want to create further links.
{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); } }

Back

"Code faster, look at the time" - does this sound familiar to you?

How about time and respect for code quality? Working in a team? Automated tests?

Join us