By Sebastiaan de Jonge, published on Tuesday, October 18, 2016 at 14:30

The standard content elements that are shipped with TYPO3 CMS offer basic functionality to get you started. Often, you will find yourself needing something just a little bit different. For an example an element with some kind of fixed styling or containers. Usually it will be possible to achieve this by altering the default elements, adding different layouts etc. However, in my opinion this affects the usability of these elements.

Creating custom elements isn't all that hard as I will demonstrate in this tutorial. You can have your custom element ready in just a matter of minutes. For the sake of an easy demonstration, I will use Twitter Bootstrap as a frontend framework.

Choosing Your Elements

Twitter Bootstrap has plenty of predefined elements already, of which most are not standard TYPO3 CMS elements. So let's convert some of these elements to TYPO3 content elements which you can even select from the content element wizard.

From the Bootstrap components I will take the following elements:

Getting started

Because the process is basically the same for each element, I will only use one (panels) inside this post. The other ones you can find in the attached example.

You will need an extension to work of from, I will use the name sdj_example for this example.

The elements will be based on the regular tt_content table, so I will try to make as much use of the default fields as possible. To make things a bit easier, I tend to create a domain model of the content element. This will be especially easier in more advanced elements, which might use file references etc.

Content Domain Model

In order to set up the domain model, let's have a look at the data I need and the fields I will use for that. All these three elements use a header (optionally) and a text field. Pretty much the same as the text with media element, just rendered in a different way. Additionally, Bootstrap offers different color schemes. I will implement these as well. That brings it down to just three fields.

  • Header (I will use the header field of tt_content)
  • Body (I will use the bodytext field of tt_content)
  • Background Color (I will use the layout field of tt_content)

This results in the following domain model (Classes/Domain/Model/ContentElement.php).

<?php
namespace SebastiaanDeJonge\SdjExample\Domain\Model;
/**
 * Domain model for content elements
 *
 * @package SebastiaanDeJonge\SdjExample\Domain\Model
 * @author Sebastiaan de Jonge <mail@sebastiaandejonge.com>, SebastiaanDeJonge.com
 */
class ContentElement extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
    /**
     * The header
     *
     * @var string
     */
    protected $header;
    /**
     * The body
     *
     * @var string
     */
    protected $body;
    /**
     * The background color
     *
     * @var string
     */
    protected $backgroundColor;
    /**
     * Gets the header
     *
     * @return string
     */
    public function getHeader()
    {
        return $this->header;
    }
    /**
     * Sets the header
     *
     * @param string $header
     * @return void
     */
    public function setHeader($header)
    {
        $this->header = $header;
    }
    /**
     * Gets the body
     *
     * @return string
     */
    public function getBody()
    {
        return $this->body;
    }
    /**
     * Sets the body
     *
     * @param string $body
     * @return void
     */
    public function setBody($body)
    {
        $this->body = $body;
    }
    /**
     * Gets the background color
     *
     * @return string
     */
    public function getBackgroundColor()
    {
        return $this->backgroundColor;
    }
    /**
     * Sets the background color
     *
     * @param string $backgroundColor
     * @return void
     */
    public function setBackgroundColor($backgroundColor)
    {
        $this->backgroundColor = $backgroundColor;
    }
}

Mapping Columns to Properties

An important step is to make TYPO3 understand what table should be mapped to this model, and additionally what columns should be mapped to what properties. You can do so by using the ext_typoscript_setup.ts file in the root of the extension, and adding the following configuration there:

config.tx_extbase {
    persistence {
        classes {
            SebastiaanDeJonge\SdjExample\Domain\Model\ContentElement {
                mapping {
                    tableName = tt_content
                    columns {
                        header.mapOnProperty = header
                        bodytext.mapOnProperty = text
                        layout.mapOnProperty = backgroundColor
                    }
                }
            }
        }
    }
}

The ext_typoscript_setup.txt file always gets loaded when your extension is installed, ensuring that the table mapping is always loaded.

The Layout Field

As specified before, the layout field will be used to change the _background_color of the new elements. I've used the code below to change the values and label of the selector to match my Bootstrap theme, by simply adding the following in my tt_content override file (Configuration/TCA/Overrides/ContentElement.php).

$_EXTKEY = "sdj_example";
$_MODEL_NAME = "ContentElement";
$_TABLE_NAME = "tt_content";
$_PLUGIN_PREFIX = "sdjexample";
// Modify layout column
$GLOBALS["TCA"][$_TABLE_NAME]["columns"]["layout"]["config"]["items"] = array(
    array(
        "",
        0,
        ""
    ),
    array(
        "LLL:EXT:{$_EXTKEY}/Resources/Private/Language/TCA/{$_MODEL_NAME}.xlf:color.black",
        1,
        "EXT:{$_EXTKEY}/Resources/Public/Icons/TCA/Color/Black.svg"
    ),
    array(
        "LLL:EXT:{$_EXTKEY}/Resources/Private/Language/TCA/{$_MODEL_NAME}.xlf:color.darkBlue",
        2,
        "EXT:{$_EXTKEY}/Resources/Public/Icons/TCA/Color/DarkBlue.svg"
    ),
    array(
        "LLL:EXT:{$_EXTKEY}/Resources/Private/Language/TCA/{$_MODEL_NAME}.xlf:color.green",
        3,
        "EXT:{$_EXTKEY}/Resources/Public/Icons/TCA/Color/Green.svg"
    ),
    array(
        "LLL:EXT:{$_EXTKEY}/Resources/Private/Language/TCA/{$_MODEL_NAME}.xlf:color.orange",
        4,
        "EXT:{$_EXTKEY}/Resources/Public/Icons/TCA/Color/Orange.svg"
    ),
    array(
        "LLL:EXT:{$_EXTKEY}/Resources/Private/Language/TCA/{$_MODEL_NAME}.xlf:color.red",
        5,
        "EXT:{$_EXTKEY}/Resources/Public/Icons/TCA/Color/Red.svg"
    ),
    array(
        "LLL:EXT:{$_EXTKEY}/Resources/Private/Language/TCA/{$_MODEL_NAME}.xlf:color.lightBlue",
        6,
        "EXT:{$_EXTKEY}/Resources/Public/Icons/TCA/Color/LightBlue.svg"
    ),
);

As you can see I'm using language files and SVG icons, you can find these in the downloadable example.

Using a Data Processor

In order to automatically add the content domain object to the templates of the new content elements, you can use a data processor. I'm using a data processor which is shared among all elements. It looks something like this:

<?php
namespace SebastiaanDeJonge\SdjExample\DataProcessing;
use SebastiaanDeJonge\SdjExample\Domain\Model\ContentElement;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
/**
 * The default data processor
 *
 * This adds the current content element (as a model) to the processed data
 *
 * @package SebastiaanDeJonge\SdjExample\DataProcessing
 * @author Sebastiaan de Jonge <mail@sebastiaandejonge.com>, SebastiaanDeJonge.com
 */
class DefaultDataProcessor implements DataProcessorInterface
{
    /**
     * Default data processor for content elements
     *
     * @param ContentObjectRenderer $cObj The content object renderer, which contains data of the content element
     * @param array $contentObjectConfiguration The configuration of Content Object
     * @param array $processorConfiguration The configuration of this processor
     * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
     * @return array the processed data as key/value store
     * @throws ContentRenderingException
     */
    public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData)
    {
        // Parse and map the content onto an object
        /* @var $contentElement ContentElement */
        $contentElement = $this->getDataMapper()->map(
            ContentElement::class,
            array(
                $cObj->data
            )
        )[0];
        // Add default data (current PID and content element)
        $processedData['contentElement'] = $contentElement;
        return $processedData;
    }
    /**
     * @return ObjectManager
     */
    protected function getObjectManager()
    {
        return GeneralUtility::makeInstance(ObjectManager::class);
    }
    /**
     * @return DataMapper
     */
    protected function getDataMapper()
    {
        return $this->getObjectManager()->get(DataMapper::class);
    }
}

Basically, this gets the content data (the tt_content record for the current content element) and uses the DataMapper to map it onto the previously created ContentElement domain model. And finally, adds this to the processed data which is later added to the view.

The following steps can be repeated for each element, the above is all simply required setup.

Adding a New Element

Finally you're at the point where you can start adding the new element, all preparations have been made! Let's start with the template. I've promised to show the panel, so here it is.

{namespace se=SebastiaanDeJonge\SdjExample\ViewHelpers}
<div class="panel {contentElement.backgroundColor->se:bootstrapColorClass(prefix:'panel-')}">
	<f:if condition="{contentElement.header}">
		<div class="panel-heading">
			<h3 class="panel-title">{contentElement.header}</h3>
		</div>
	</f:if>
	<div class="panel-body">
		{contentElement.body->f:format.html()}
	</div>
</div>

It contains the optional background color, optional header and finally the rich body content. The background color is processed by a view helper which transforms the number into a valid Bootstrap CSS class.

Now that the template is in place, it needs to be registered in order to show up. First, let's add it to the backend. For this, I've added the following to the TCA override for content (Configuration/TCA/Overrides/ContentElement.php).

// Custom palettes
$GLOBALS["TCA"][$_TABLE_NAME]["palettes"]["{$_PLUGIN_PREFIX}_head"] = array(
    "showitem" => "CType,colPos,layout,hidden,--linebreak--,sys_language_uid,l10n_parent",
    "canNotCollapse" => 1
);
// Add "Panel" element
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(
    "tt_content",
    "CType",
    [
        "LLL:EXT:{$_EXTKEY}/Resources/Private/Language/{$_MODEL_NAME}.xlf:{$_PLUGIN_PREFIX}_panel.title",
        "{$_PLUGIN_PREFIX}_panel",
        "content-text"
    ],
    "textmedia",
    "after"
);
$GLOBALS["TCA"]["tt_content"]["types"]["{$_PLUGIN_PREFIX}_panel"]["columnsOverrides"]["bodytext"]["defaultExtras"] = "richtext:rte_transform";
$GLOBALS["TCA"]["tt_content"]["types"]["{$_PLUGIN_PREFIX}_panel"]["showitem"] = <<<SHOWITEM
--palette--;LLL:EXT:{$_EXTKEY}/Resources/Private/Language/TCA/{$_MODEL_NAME}.xlf:palette.head;{$_PLUGIN_PREFIX}_head,
header,bodytext;;4;
SHOWITEM;

I've added a custom palette to make the element look a bit more friendly in the editor, this palette includes; content type, column, background color and the language settings. Additionally, I've added 2 language files in which the labels for the element and the palette are defined (these files are included in the downloadable example). Don't forget to add these, or you will end up having blank names.

Now if you take a peek in your backend, you can see that the new element is already available there!

Now all we need to do, is configure the frontend part and we're done! This will require some more TypoScript.

tt_content {
    sdjexample_panel < lib.fluidContent
    sdjexample_panel {
        # Set template paths
        templateRootPaths.100 = EXT:sdj_example/Resources/Private/Templates/
        partialRootPaths.100 = EXT:sdj_example/Resources/Private/Partials/
        layoutRootPaths.100 = EXT:sdj_example/Resources/Private/Layouts/
        # Set the template
        templateName = Panel
        # Remove the standard header
        10 >
        # Configure data processing
        dataProcessing {
            10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
            10 {
                references.fieldName = assets
            }
            20 = SebastiaanDeJonge\SdjExample\DataProcessing\DefaultDataProcessor
        }
    }
}

First of all, we inherit everything from lib.fluidContent, followed by setting the template paths. The templateName is the name of the actual template file. Fluid will look for the specified template path in a subfolder called Standard. So make sure your template is placed there (Resources/Private/Templates/Standard/Panel.html).

10 > will remove the default header in lib.fluidContent, as the header will be placed inside the panel and there is not header above the content element. And finally, data processing is added. Which will add our ContentElement object to the view.

Eat, sleep, rave, repeat..

Additional elements can be added in the same manner. Just add your template, element settings in the TCA override and small TypoScript configuration and you are all set. To make it easier, you can use a lot less TypoScript for the next elements. For example:

tt_content.sdjexample_alert < tt_content.sdjexample_panel
tt_content.sdjexample_alert.templateName = Alert

Will be enough for the next one.

Feel free to leave a comment, happy coding!

Comments