By Sebastiaan de Jonge, published on Thursday, October 20, 2016 at 14:00

Another way to improve backend experience for editors is by creating preview inside the Page module for custom or even for already existing content elements.

Getting started

Like before, I will use the content elements that I've created a few posts before to demonstrate. Currently, they look like this inside the page module:

Not very clear or appealing, right?

The drawItem hook

In order to modify what is displayed here, we can use a hook within the TYPO3 CMS. In this case, the tt_content_drawItem hook. The hook can be registered like so:

$GLOBALS["TYPO3_CONF_VARS"]["SC_OPTIONS"]["cms/layout/class.tx_cms_layout.php"]["tt_content_drawItem"][] = \SebastiaanDeJonge\SdjExample\Hook\DrawItemHook::class;

The class itself must implement the \TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface together with the preProcess() method. That's where all the magic is going to happen.

My implementation looks somewhat like this:

<?php
namespace SebastiaanDeJonge\SdjExample\Hook;
use SebastiaanDeJonge\SdjExample\Domain\Model\ContentElement;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
 * Draw item hook
 *
 * @package SebastiaanDeJonge\SdjExample\Hook
 * @author Sebastiaan de Jonge <mail@sebastiaandejonge.com>, SebastiaanDeJonge.com
 */
class DrawItemHook implements \TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface, \TYPO3\CMS\Core\SingletonInterface
{
    /**
     * The object manager
     *
     * @var ObjectManager
     */
    protected $objectManager;
    /**
     * The data mapper
     *
     * @var DataMapper
     */
    protected $dataMapper;
    /**
     * The page renderer
     *
     * @var PageRenderer
     */
    protected $pageRenderer;
    /**
     * The template base path
     *
     * @var string
     */
    protected $templateBasePath;
    /**
     * Initialize the object
     *
     * @return void
     */
    public function initialize()
    {
        if (!($this->objectManager instanceof ObjectManager)) {
            // Initialize objects, DI is not available at this level
            $this->objectManager = GeneralUtility::makeInstance(ObjectManager::class);
            $this->dataMapper = $this->objectManager->get(DataMapper::class);
            $this->pageRenderer = $this->objectManager->get(PageRenderer::class);
            // Add stylesheets to the page renderer
            $this->pageRenderer->addCssFile(
                ExtensionManagementUtility::extRelPath("sdj_example") . "Resources/Public/Styles/Bootstrap.min.css"
            );
            $this->pageRenderer->addCssFile(
                ExtensionManagementUtility::extRelPath("sdj_example") . "Resources/Public/Styles/BootstrapTheme.min.css"
            );
            // Define the base path (used for template loading)
            $this->templateBasePath = ExtensionManagementUtility::extPath(
                "sdj_example",
                "Resources/Private/Templates/Standard/"
            );
        }
    }
    /**
     * Pre-processes the item
     *
     * @param \TYPO3\CMS\Backend\View\PageLayoutView $parentObject
     * @param boolean $drawItem
     * @param string $headerContent
     * @param string $itemContent
     * @param array $row
     * @return void
     */
    public function preProcess(\TYPO3\CMS\Backend\View\PageLayoutView &$parentObject, &$drawItem, &$headerContent, &$itemContent, array &$row)
    {
        // Check if the CType matches that of any of the custom elements
        switch ($row['CType']) {
            // The default option is to return without doing anything. This way we ensure that only specific content is
            // processed.
            default:
                return;
            // Panel
            case "sdjexample_panel":
                $templateName = "Panel";
                break;
            // Alert
            case "sdjexample_alert":
                $templateName = "Alert";
                break;
            // Well
            case "sdjexample_well":
                $templateName = "Well";
                break;
        }
        // Initialize (only if it's actually necessary)
        $this->initialize();
        // Create a new view
        /* @var $view StandaloneView */
        $view = $this->objectManager->get(StandaloneView::class);
        $view->setTemplatePathAndFilename("{$this->templateBasePath}{$templateName}.html");
        // Filter content
        $row["bodytext"] = $this->filterContent($row["bodytext"]);
        // Map the element onto a domain model, in the same way the DataProcessor does
        /* @var $contentElement ContentElement */
        $contentElement = $this->dataMapper->map(
            ContentElement::class,
            array(
                $row
            )
        )[0];
        $view->assign('contentElement', $contentElement);
        // Finally, render the content and set the output
        $itemContent = $view->render();
        $headerContent = "";
        $drawItem = false;
    }
    /**
     * Strips the content of any unwanted items, such as links.
     *
     * @param string $content
     * @return string
     */
    protected function filterContent($content)
    {
        // Make image paths for in-body RTE images relative
        $content = preg_replace("#<img\\ssrc=\"(fileadmin|uploads)#i", "<img src=\"../\$1", $content);
        // Apply strip tags to remove any unwanted tags (such as links)
        return strip_tags($content, "<br><div><em><img><li><ol><p><span><strong><ul>");
    }
}

The examples are quite simple and plain, so the implementation is like that as well. I'm using a the SingletonInterface to ensure everything just needs to be initialized once (because it's all the same for each content element). Also, templates are not really configurable, the default templates (which are also used for the frontend) are loaded for the backend previews. There is no need to return anything in preProcess() method, as all variables are referenced.

Content filter

The content is being filtered with strip_tags() to prevent any unwanted tags from showing up within the output. In particular, links. For two reasons:

  • Internal links won't work, since there is no proper frontend context to build them in. This will result missing pieces of text (the link and text won't show up at all).
  • It would be annoying to be redirected outside of the backend due to accidental clicking of the link.

Additional settings

At the bottom of the preProcess() method there are some final but important things being set.

  • First of all $itemContent is being set with the rendered view, this is what will be displayed inside the Page module.
  • Secondly, the $headerContent is made empty. This prevents the header value from the tt_content element from showing up above the $itemContent.
  • Finally, $drawItem is set to false. This prevents the default content and type of the element from being placed below $itemContent. If you do want to show the element type to be displayed below the preview, set this to true (or remove the line), and instead set $row["bodytext"] to "".

Looking better already

The result is already much better than before.

Finishing touch

Now that we have rendered the templates, it's time to mimic the frontend view a bit more. You can do so by loading a custom stylesheet(s) inside the TYPO3 backend, so that the same styling is available. There are several ways to do this.

  1. The files could be added as a skin for TYPO3 CMS, this would load them at all times though. Not really what you're looking for I guess.
  2. Use the render-preProcess hook inside the PageRenderer ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_pagerenderer.php']['render-preProcess']), but this gives a similar result to the first option.
  3. It can be done inside the initialization of the DrawItemHook. Since the PageRenderer implements the SingletonInterface, there is only a single instance of it. Thus you can simply get it by calling $objectManager->get(PageRenderer::class). Next thing to do is to simply add any CSS file you want with the addCssFile() method inside the PageRenderer.
$this->pageRenderer = $this->objectManager->get(PageRenderer::class);
// Add stylesheets to the page renderer
$this->pageRenderer->addCssFile(
    ExtensionManagementUtility::extRelPath("sdj_example") . "Resources/Public/Styles/Bootstrap.min.css"
);
$this->pageRenderer->addCssFile(
    ExtensionManagementUtility::extRelPath("sdj_example") . "Resources/Public/Styles/BootstrapTheme.min.css"
);

Loading the same view in the backend now results in the following:

Be careful when adding a stylesheet to the backend that there are no conflicting classes. It might be a good idea to add a class wrapper around your own classes, to ensure you are not interfering with the default TYPO3 stylesheets. In the example you can already see that the appearance of the title (Example) has changed a bit.

Pretty cool right?

This setup is quite basic and offers previews for simplistic elements. Once you are dealing with more complex elements, problems might arise with rendering things like images and links. In most cases it will probably be easier (but less flexible) to alter the templates for backend previews specifically.

The example code is available in a working extension, included at the side of this post. It's worth mentioning that the extension has been built on and tested on TYPO3 CMS 8.4.0. However I know for a fact that this method is actually applicable since at least TYPO3 CMS 6.2.x

Happy coding!

Comments

Vadym Gyrkalo
Vadym Gyrkalo - Thursday, May 4, 2017 at 14:22

Thanks for cool article, Sebastian! but I cant download sdj_example_1.2.0.zip, downloaded file has 0 bytes.