How to create custom content elements on TYPO3


TYPO3 ships fairly the basic content elements. Sometimes we need to create our own in order to use them regularly through the website. In this tutorial we are going deep when it comes creating custom content elements. The content element will have it's own table on the database so we are going to create relations between all the tables involved. The reason why, is that, nowdays a lot of custom content elements need to be created and in some point the tt_content table will become huge. Later i will create a tutorial which adds the fields on the tt_content table, but for now, we are going to do it this way.

This article addresses interators who have enough experience with TYPO3 and creating extensions. New integrators might find it a little bit harder, but i am going to do my best to make you understand how everything works. With no further ado let's begin.

CONCEPT
The content element will be a common header which is going to be either a single image or if more images selected, a carousel. This way we have two content elements in one. The content element will be based on the Foundation Zurb Framework.

 

Base extension

We first need a base extension to work with. If you haven't yet or you do not know how to create one, then follow the article below to understand what a base extension is, why you need one and how to create it.

Create a base extension

 

Step 1: Add the content element on the list

Under the: your_extension_key/Configuration/TSconfig/Page/CustomElements.typoscript you will have to add your content element in the list:

                mod.wizards.newContentElement.wizardItems.ownContentElements{
   header = LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:my_custom_elements
   after = common
   elements {
       header_with_title {
           iconIdentifier = HeaderWithTitle
           title = LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:header_with_title.title
           description = LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:header_with_title.description
           tt_content_defValues {
               CType = header_with_title
           }
        }
    }
    show := addToList(header_with_title)
}
            

Result:

Breaking step 1 down

                mod.wizards.newContentElement.wizardItems.ownContentElements{
      header = LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_elements
      after = common
            

This creates a new tab on your list, with the title that you want to give to it and you place it after the common tab.

                elements {
     header_with_title {
         iconIdentifier = HeaderWithTitle
         title = LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:header_with_title.title
         description = LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:header_with_title.description
         tt_content_defValues {
              CType = header_with_title
         }
     }
}
            

Unter the elements, you specify the identifier of your content element (it should be unique and not be repeated again by any other content element) which you are going to use throughout your code. 
The iconIdentifier = HeaderWithTitle would be defined on another file which i am going to cover as well. (This is the image of the content element)
Of course you can specify a title and a description as well.
With the tt_content_defValues you specify the name of your CType. Just name it the same with the identifier and you should be good to go.

                show := addToList(header_with_title )
            

At this point TYPO3 knows that there is a Custom Element registered but it doesn't know if it can use it. So on the show := addToList(header_with_title ) you specify which content elements should the user be able to use and in which sequence

Step 2: Add the content element on the dropdown list

Under the: your_extension_key/Configuration/TCA/Overrides/tt_content.php  you will have to add the content element into the dropdown list:

                \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin(
       array(
          'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:header_with_title.title',
          'header_with_title',
          'EXT:your_extension_key/Resources/Public/Icons/ContentElements/header_with_title.svg'
       ),
       'CType',
       'your_extension_key'
);
            

Result

Breaking step 2 down

The \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin  function is an array which contains the following:

  1. The content elements title
  2. The identifier of the content element
  3. The full path to the content element's image
  4. The Type content element (CType at this instance)
  5. The extension key where the content element comes from

 

Step 3: Create the tables

Under the: your_extension_key/ext_tables.sql  you will have to add the tables that are going to be used to store the information.
If you add an element on the website, any element, it will be saved in the tt_content table. That means that we need a column in the tt_content table which will have a relation to our content element.
Our element will have a title, a text, and an image.
Some settings for the slider will be added as well. Those settings will have their own table.

The code below is missing the standard columns that TYPO3 uses (uid, pid, tstamp, t3ver_oid) so go ahead and add them on your tables BUT NOT on the tt_content table. We just extend the tt_content table.

 

                CREATE TABLE tt_content (
	slider_settings_relation int(11) unsigned DEFAULT '0',
);

CREATE TABLE foundation_zurb_slidersettings (
	title varchar(255) DEFAULT 'Foundation Slider' NOT NULL,
	hide_arrows smallint(5) unsigned DEFAULT '0' NOT NULL,
	hide_bullets smallint(5) unsigned DEFAULT '0' NOT NULL,
        slider_content_relation int(11) unsigned DEFAULT '0' NOT NULL,
);

CREATE TABLE foundation_zurb_slidercontent (

	foundation_zurb_slidersettings int(11) unsigned DEFAULT '0' NOT NULL,

	title varchar(255) DEFAULT '' NOT NULL,
	text text,
	image int(11) unsigned NOT NULL default '0',
	slider_link varchar(255) DEFAULT '' NOT NULL,
	dark_mode smallint(5) unsigned DEFAULT '0' NOT NULL,
        sorting int(11) DEFAULT '0' NOT NULL,

);
            

Step 4: Create the TCA

foundation_zurb_slidersettings

Under the: your_extension_key/Configuration/TCA/  you will have to add the configuration for content element via the TCAs.

your_extension_key/Configuration/TCA/foundation_zurb_slidersettings.php

                <?php
return [
    'ctrl' => [
        'title' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_title',
        'label' => 'title',
        'tstamp' => 'tstamp',
        'crdate' => 'crdate',
        'cruser_id' => 'cruser_id',
        'versioningWS' => true,
        'languageField' => 'sys_language_uid',
        'transOrigPointerField' => 'l10n_parent',
        'transOrigDiffSourceField' => 'l10n_diffsource',
        'delete' => 'deleted',
        'enablecolumns' => [
            'disabled' => 'hidden',
            'starttime' => 'starttime',
            'endtime' => 'endtime',
        ],
        'searchFields' => 'title,hide_arrows,hide_bullets,slider_content_relation',
        'iconfile' => 'EXT:your_extension_key/Resources/Public/Icons/ContentElements/header_with_title.svg'
    ],
    'interface' => [
        'showRecordFieldList' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, title, hide_arrows, hide_bullets, slider_content_relation',
    ],
    'palettes' => [
        'slider_palette_0' => [
            'showitem' => '
                sys_language_uid, 
                l10n_parent, 
                l10n_diffsource, 
                hidden, 
            ',
        ],
        'slider_palette_1' => [
            'showitem' => '
                slider_content_relation,
            ',
        ],
        'slider_palette_2' => [
            'showitem' => '
                hide_arrows, hide_bullets,
            ',
        ],
    ],
    'types' => [
        '1' => [
            'showitem' => '
            --div--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_title.tab,
                --palette--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:language;slider_palette_0,
                --palette--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_create_slide;slider_palette_1,
            --div--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_settings_main.tab,
                --palette--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_settings_main;slider_palette_2,
            --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access, starttime, endtime'
        ],
    ],
    'columns' => [
        'sys_language_uid' => [
            'exclude' => true,
            'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.language',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'special' => 'languages',
                'items' => [
                    [
                        'LLL:EXT:lang/locallang_general.xlf:LGL.allLanguages',
                        -1,
                        'flags-multiple'
                    ]
                ],
                'default' => 0,
            ],
        ],
        'l10n_parent' => [
            'displayCond' => 'FIELD:sys_language_uid:>:0',
            'exclude' => true,
            'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.l18n_parent',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'default' => 0,
                'items' => [
                    ['', 0],
                ],
                'foreign_table' => 'foundation_zurb_slidersettings',
                'foreign_table_where' => 'AND foundation_zurb_slidersettings.pid=###CURRENT_PID### AND foundation_zurb_slidersettings.sys_language_uid IN (-1,0)',
            ],
        ],
        'l10n_diffsource' => [
            'config' => [
                'type' => 'passthrough',
            ],
        ],
        't3ver_label' => [
            'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.versionLabel',
            'config' => [
                'type' => 'input',
                'size' => 30,
                'max' => 255,
            ],
        ],
        'hidden' => [
            'exclude' => true,
            'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.hidden',
            'config' => [
                'type' => 'check',
                'items' => [
                    '1' => [
                        '0' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.enabled'
                    ]
                ],
            ],
        ],
        'starttime' => [
            'exclude' => true,
            'behaviour' => [
                'allowLanguageSynchronization' => true
            ],
            'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.starttime',
            'config' => [
                'type' => 'input',
                'renderType' => 'inputDateTime',
                'size' => 13,
                'eval' => 'datetime',
                'default' => 0,
            ],
        ],
        'endtime' => [
            'exclude' => true,
            'behaviour' => [
                'allowLanguageSynchronization' => true
            ],
            'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.endtime',
            'config' => [
                'type' => 'input',
                'renderType' => 'inputDateTime',
                'size' => 13,
                'eval' => 'datetime',
                'default' => 0,
                'range' => [
                    'upper' => mktime(0, 0, 0, 1, 1, 2038)
                ],
            ],
        ],
        'title' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_title',
            'config' => [
                'type' => 'input',
                'size' => 30,
                'eval' => 'trim'
            ],
        ],
        'hide_arrows' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_hide_arrows',
            'config' => [
                'type' => 'check',
                'items' => [
                    '1' => [
                        '0' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.enabled'
                    ]
                ],
                'default' => 0,
            ]
        ],
        'hide_bullets' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_hide_bullets',
            'config' => [
                'type' => 'check',
                'items' => [
                    '1' => [
                        '0' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.enabled'
                    ]
                ],
                'default' => 0,
            ]
        ],
        'slider_content_relation' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_create_item',
            'config' => [
                'type' => 'inline',
                'foreign_table' => 'foundation_zurb_slidercontent',
                'foreign_field' => 'foundation_zurb_slidersettings',
                'maxitems' => 9999,
                'appearance' => [
                    'useSortable' => 1,
                    'collapseAll' => 1,
                    'levelLinksPosition' => 'bottom',
                    'showSynchronizationLink' => 1,
                    'showPossibleLocalizationRecords' => 1,
                    'showAllLocalizationLink' => 1,
                    'enabledControls' => [
                        'info' => TRUE,
                        'new' => TRUE,
                        'dragdrop' => TRUE,
                        'sort' => TRUE,
                        'hide' => TRUE,
                        'delete' => TRUE,
                        'localize' => TRUE,
                    ],
                ],
            ],
        ],
    ]
];
            

Results: (These results won't be available to see until we create the relation between the tt_content table and the foundation_zurb_slidersettings table)

Breaking the foundation_zurb_slidersettings.php down

                'ctrl' => [
        'title' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_title',
        'label' => 'title',
        'tstamp' => 'tstamp',
        'crdate' => 'crdate',
        'cruser_id' => 'cruser_id',
        'versioningWS' => true,
        'languageField' => 'sys_language_uid',
        'transOrigPointerField' => 'l10n_parent',
        'transOrigDiffSourceField' => 'l10n_diffsource',
        'delete' => 'deleted',
        'enablecolumns' => [
            'disabled' => 'hidden',
            'starttime' => 'starttime',
            'endtime' => 'endtime',
        ],
        'searchFields' => 'title,hide_arrows,hide_bullets,slider_content_relation',
        'iconfile' => 'EXT:your_extension_key/Resources/Public/Icons/ContentElements/header_with_title.svg'
    ],
            

This is the main information that will be displayed once you choose to use the content element on a page in your website. The important thing here is the label which in this case will take the value from the column title once is set. That means after save. The searchFields will be included when searching for records in the TYPO3 backend. The iconfile is the full path to the image for this content element.

                'palettes' => [
        'slider_palette_0' => [
            'showitem' => '
                sys_language_uid, 
                l10n_parent, 
                l10n_diffsource, 
                hidden, 
            ',
        ],
        'slider_palette_1' => [
            'showitem' => '
                slider_content_relation,
            ',
        ],
        'slider_palette_2' => [
            'showitem' => '
                hide_arrows, hide_bullets,
            ',
        ],
    ],
            

The palettes allow you to align item horizontally (next to each other). This way, if you have a lot of fields, the backend user does not have to scroll without end until he finds the field he wants to edit. 

                'types' => [
        '1' => [
            'showitem' => '
            --div--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_title.tab,
                --palette--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:language;slider_palette_0,
                --palette--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_create_slide;slider_palette_1,
            --div--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_settings_main,
                --palette--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_settings_main;slider_palette_2,
            --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access, starttime, endtime'
        ],
    ],
            

The types allow you to specify the sequence on which the elements will be displayed. Addionally types allow you to create tabs as well. This way you can categorize your content. If for example you have apartments to create, a logical tab categorization would be: General, Price, Surface, Equipments, Contact etc. So if the user wants to edit something that has to do with the price, he/she can immediately click on the Price tab and edit the fields he/she wants

Structure of types

  1. --div--; --> Registers a new Tab 
  2.  LLL:EXT:....; --> The title of the Tab which in this case it is placed under the Language folder of your extension
  3. The third part defines which fields are going to be displayed and in which sequence.
    • --palettes--; --> Inserts the palette you have previously defined
    • LLL:EXT:....;  --> The title of the Palette which in this case it is placed under the Language folder of your extension
    • The name of your palette
                'columns' => [
        'title' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_title',
            'config' => [
                'type' => 'input',
                'size' => 30,
                'eval' => 'trim'
            ],
        ],
        'hide_arrows' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_hide_arrows',
            'config' => [
                'type' => 'check',
                'items' => [
                    '1' => [
                        '0' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.enabled'
                    ]
                ],
                'default' => 0,
            ]
        ],
        'hide_bullets' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_hide_bullets',
            'config' => [
                'type' => 'check',
                'items' => [
                    '1' => [
                        '0' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.enabled'
                    ]
                ],
                'default' => 0,
            ]
        ],
        'slider_content_relation' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_create_item',
            'config' => [
                'type' => 'inline',
                'foreign_table' => 'foundation_zurb_slidercontent',
                'foreign_field' => 'foundation_zurb_slidersettings',
                'maxitems' => 9999,
                'appearance' => [
                    'useSortable' => 1,
                    'collapseAll' => 1,
                    'levelLinksPosition' => 'bottom',
                    'showSynchronizationLink' => 1,
                    'showPossibleLocalizationRecords' => 1,
                    'showAllLocalizationLink' => 1,
                    'enabledControls' => [
                        'info' => TRUE,
                        'new' => TRUE,
                        'dragdrop' => TRUE,
                        'sort' => TRUE,
                        'hide' => TRUE,
                        'delete' => TRUE,
                        'localize' => TRUE,
                   ],
              ],
          ],
       ],
]
            

Under the columns you specify the configuration for each field. The title is just an input and the hide_arrows hide_bullets just a checkbox. The important part here is the slider_content_relation. 

It is a type of inline which means it is set to read/create/edit values from another table. The inline configuration comes into play when the relation is 1:n. Meaning that the current model, can have more children which will be defined on another table. The children theirselves can only have one parent. In this case, the slider has some settings (current model: foundation_zurb_slidersettings) and these settings have some children (Targeted model: foundation_zurb_slidercontent). This happens that way, because on a second slider the settings might be different. 

So under the foreign_table you specify which table contains the children, in this case the foundation_zurb_slidercontent. The foreign_field is a column which is defined on the child table and contains the uid of the parent. That means that if TYPO3 requests all the children of the slider with the uid 5, then all the children that have the uid 5 on the column foreign_fiel, will be displayed. 

foundation_zurb_slidercontent

Now we have to add the configuration for the content of the slider. Under the:

your_extension_key/Configuration/TCA/foundation_zurb_slidercontent.php

                <?php
return [
    'ctrl' => [
        'title' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_content',
        'label' => 'title',
        'sortby' => 'sorting',
        'tstamp' => 'tstamp',
        'crdate' => 'crdate',
        'cruser_id' => 'cruser_id',
        'versioningWS' => true,
        'languageField' => 'sys_language_uid',
        'transOrigPointerField' => 'l10n_parent',
        'translationSource' => 'l10n_source',
        'transOrigDiffSourceField' => 'l10n_diffsource',
        'delete' => 'deleted',
        'enablecolumns' => [
            'disabled' => 'hidden',
            'starttime' => 'starttime',
            'endtime' => 'endtime',
        ],
        'searchFields' => 'title,text,image',
        'iconfile' => 'EXT:your_extension_key/Resources/Public/Icons/ContentElements/header_with_title.svg'
    ],
    'interface' => [
        'showRecordFieldList' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, dark_mode, title, text, image, slider_link',
    ],
    'palettes' => [
        'slider_palette_0' => [
            'showitem' => '
                sys_language_uid, 
                l10n_parent, 
                l10n_diffsource, 
                hidden, 
            ',
        ],
    ],
    'types' => [
        '1' => ['showitem' => '
            --palette--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:language;slider_palette_0, 
            dark_mode, slider_link, title, text, image, 
            --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access, starttime, endtime'
        ],
    ],
    'columns' => [
        'sys_language_uid' => [
            'exclude' => true,
            'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.language',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'special' => 'languages',
                'items' => [
                    [
                        'LLL:EXT:lang/locallang_general.xlf:LGL.allLanguages',
                        -1,
                        'flags-multiple'
                    ]
                ],
                'default' => 0,
            ],
        ],
        'l10n_parent' => [
            'displayCond' => 'FIELD:sys_language_uid:>:0',
            'exclude' => true,
            'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.l18n_parent',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'default' => 0,
                'items' => [
                    ['', 0],
                ],
                'foreign_table' => 'foundation_zurb_slidercontent',
                'foreign_table_where' => 'AND foundation_zurb_slidercontent.pid=###CURRENT_PID### AND foundation_zurb_slidercontent.sys_language_uid IN (-1,0)',
            ],
        ],
        'l10n_diffsource' => [
            'config' => [
                'type' => 'passthrough',
            ],
        ],
        't3ver_label' => [
            'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.versionLabel',
            'config' => [
                'type' => 'input',
                'size' => 30,
                'max' => 255,
            ],
        ],
        'hidden' => [
            'exclude' => true,
            'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.hidden',
            'config' => [
                'type' => 'check',
                'items' => [
                    '1' => [
                        '0' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.enabled'
                    ]
                ],
            ],
        ],
        'starttime' => [
            'exclude' => true,
            'behaviour' => [
                'allowLanguageSynchronization' => true
            ],
            'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.starttime',
            'config' => [
                'type' => 'input',
                'renderType' => 'inputDateTime',
                'size' => 13,
                'eval' => 'datetime',
                'default' => 0,
            ],
        ],
        'endtime' => [
            'exclude' => true,
            'behaviour' => [
                'allowLanguageSynchronization' => true
            ],
            'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.endtime',
            'config' => [
                'type' => 'input',
                'renderType' => 'inputDateTime',
                'size' => 13,
                'eval' => 'datetime',
                'default' => 0,
                'range' => [
                    'upper' => mktime(0, 0, 0, 1, 1, 2038)
                ],
            ],
        ],
        'dark_mode' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_content.dark_mode',
            'config' => [
                'type' => 'check',
                'items' => [
                    '1' => [
                        '0' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.enabled'
                    ]
                ],
                'default' => 0,
            ]
        ],
        'slider_link' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_content.slide_link',
            'config' => [
                'type' => 'input',
                'renderType' => 'inputLink',
            ],
        ],
        'title' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_content.slide_title',
            'config' => [
                'type' => 'input',
                'size' => 30,
                'eval' => 'trim'
            ],
        ],
        'text' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_content.slide_text',
            'config' => [
                'type' => 'text',
                'enableRichtext' => true,
                'richtextConfiguration' => 'default',
                'fieldControl' => [
                    'fullScreenRichtext' => [
                        'disabled' => false,
                    ],
                ],
                'cols' => 40,
                'rows' => 15,
                'eval' => 'trim',
            ],
            
        ],
        'image' => [
            'exclude' => true,
            'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_content.slide_image',
            'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig(
                'image',
                [
                    'appearance' => [
                        'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference'
                    ],
                    'overrideChildTca' => [
                        'types' => [
                            '0' => [
                                'showitem' => '
                                --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
                                --palette--;;filePalette'
                            ],
                            \TYPO3\CMS\Core\Resource\File::FILETYPE_TEXT => [
                                'showitem' => '
                                --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
                                --palette--;;filePalette'
                            ],
                            \TYPO3\CMS\Core\Resource\File::FILETYPE_IMAGE => [
                                'showitem' => '
                                --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
                                --palette--;;filePalette'
                            ],
                            \TYPO3\CMS\Core\Resource\File::FILETYPE_AUDIO => [
                                'showitem' => '
                                --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
                                --palette--;;filePalette'
                            ],
                            \TYPO3\CMS\Core\Resource\File::FILETYPE_VIDEO => [
                                'showitem' => '
                                --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
                                --palette--;;filePalette'
                            ],
                            \TYPO3\CMS\Core\Resource\File::FILETYPE_APPLICATION => [
                                'showitem' => '
                                --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
                                --palette--;;filePalette'
                            ],
                        ],
                    ],
                    'maxitems' => 1
                ],
                $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
            ),
        ],
    
        'foundation_zurb_slidersettings' => [
            'config' => [
                'type' => 'passthrough',
            ],
        ],
    ],
];

            

Result

tt_content relation

At this point we have registered the content element, added it on the dropdown list, created the tables and the configuration for each field.

If you install the extension now and try to add your content element, then you will get a blank TCA that looks like that:

 

That's because we created all the relations between the content element's tables BUT we haven't resolve the relation between th tt_content to our content element. As mention before, if you add any content on a TYPO3 page, it doesn't matter if it is an extension or a simple header, then it will save the values or create a relation to the corresponding element on the tt_content table. So now what he have to do, is to set the relation between the tt_content table with the foundation_zurb_slidersettings.

Under the your_extension_key/Configuration/TCA/Overrides/Slider.php

                $GLOBALS['TCA']['tt_content']['types']['header_with_title'] = array(
  'showitem' => '
  --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
  --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.general;general,
  --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.headers;headers,
  --div--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_title.tab,
  --palette--;--linebreak--,slider_settings_relation,
  --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xml:tabs.appearance,
  --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xml:palette.frames;frames,
  --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xml:tabs.access,
  --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xml:palette.visibility;visibility,
  --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xml:palette.access;access,
  --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xml:tabs.extended',
);
$originalSliderContent = $GLOBALS['TCA']['tt_content'];
$overridesForSliderContent = [
  'ctrl' => [
    'typeicon_classes' => [
      'header_with_title' => 'HeaderWithTitle',
    ]
  ]
];
$GLOBALS['TCA']['tt_content'] = array_merge_recursive($originalSliderContent, $overridesForSliderContent);
$foundationSliderOptions = array(
  'slider_settings_relation' => [
    'exclude' => 1,
    'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_description',
    'config' => [
      'type' => 'inline',
      'foreign_table' => 'foundation_zurb_slidersettings',
      'maxitems' => 1,
      'appearance' => [
          'collapseAll' => 0,
          'levelLinksPosition' => 'top',
          'showSynchronizationLink' => 1,
          'showPossibleLocalizationRecords' => 1,
          'showAllLocalizationLink' => 1
      ],
    ],
  ],
);
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('tt_content',$foundationSliderOptions);
            

Result

Breaking the Slider.php down

                $GLOBALS['TCA']['tt_content']['types']['header_with_title'] = array(
  'showitem' => '
  --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
  --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.general;general,
  --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.headers;headers,
  --div--;LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_title,
  --palette--;--linebreak--,slider_settings_relation,
  --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xml:tabs.appearance,
  --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xml:palette.frames;frames,
  --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xml:tabs.access,
  --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xml:palette.visibility;visibility,
  --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xml:palette.access;access,
  --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xml:tabs.extended',
);
            

$GLOBALS['TCA']['tt_content']['types']['header_with_title'] it just accesses the tt_content table and it does some changes for the CType header_with_title. So everytime the CType header_with_title is requested on the backend, then the fields that are specified in the body of showitem, should be appear with the sequence that it are defined. In this case, the Tab General comes first, with the default palettes from TYPO3, then the Slider Tab with the slider_settings_relation column configuration which is defined below, etc.

                $originalSliderContent = $GLOBALS['TCA']['tt_content'];
$overridesForSliderContent = [
  'ctrl' => [
    'typeicon_classes' => [
      'header_with_title' => 'HeaderWithTitle',
    ]
  ]
];
$GLOBALS['TCA']['tt_content'] = array_merge_recursive($originalSliderContent, $overridesForSliderContent);
            

This snippet adds an icon on the backend once the content elements is saved. The HeaderWithTitle will be defined in another file later.

Result

                $foundationSliderOptions = array(
  'slider_settings_relation' => [
    'exclude' => 1,
    'label' => 'LLL:EXT:your_extension_key/Resources/Private/Language/locallang.xlf:foundation_slider_description',
    'config' => [
      'type' => 'inline',
      'foreign_table' => 'foundation_zurb_slidersettings',
      'maxitems' => 1,
      'appearance' => [
          'collapseAll' => 0,
          'levelLinksPosition' => 'top',
          'showSynchronizationLink' => 1,
          'showPossibleLocalizationRecords' => 1,
          'showAllLocalizationLink' => 1
      ],
    ],
  ],
);
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('tt_content',$foundationSliderOptions);
            

This is just the configuration for the relation between the tt_content and the foundation_zurb_slidersettings table. The important thing to watch here is the foreign_table which is the foundation_zurb_slidersettings as well as the maxitems. We need only one slider, so it doesn't make any sense to have more than one. The addTCAcolumns just adds the configuration on the tt_content table.

STEP 5: Register the icon

As you have already seen, we have used the HeaderWithTitle multiple times throughtout the code. This is an image identfier which is saved as global variable and you can use it wherever you want. The code looks like this:

Under your_extension_key/ext_tables.php

                $registerHeaderImage = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class);
$registerHeaderImage ->registerIcon('HeaderWithTitle',\TYPO3\CMS\Core\Imaging\IconProvider\BitmapIconProvider::class,['source' => 'EXT:your_extension_key/Resources/Public/Icons/ContentElements/header_with_title.svg']);
            

What this does, it makes an instance of the IconRegistry class and it saves tha given path to a global variable which can be called by the identifier HeaderWithTitle.

Now the icon on the backend should be changed to your current defined icon as well as the broken red icon when you are adding a new content element.

Step 6: Allow external tables

If you try to add your content element now, you will get an error. That's because TYPO3 allows only the default tables to be read (default: the tables that come by default with the TYPO3 installation). In order to allow your tables to be read you will have to add this code.

Under your_extension_key/ext_tables.php

                \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages('
			foundation_zurb_slidersettings, 
			foundation_zurb_slidercontent, 
');
            

Error

Step 7: Displaying the content in FrontEnd

Now that the configuration is ready, we can display the content in the FrontEnd. At this point TYPO3 knows that a content element is registered, configured etc. But it doesn't know how to bring the content into the FrontEnd. We need to define a way so TYPO3 will be able to achive that. DataProcessing does just that.

Under your_extension_key/Configuration/TypoScript/Setup/tt_content.typoscript

                tt_content   {     
        header_with_title < lib.contentElement
	header_with_title {
		templateRootPaths.10 = EXT:your_extension_key/Resources/Private/Templates/ContentElements/
		partialRootPaths.10 = your_extension_key/Resources/Private/Partials/ContentElements/
		templateName = Header.html
		dataProcessing {
			330 = TYPO3\CMS\Frontend\DataProcessing\DatabaseQueryProcessor
			330 {
				table = foundation_zurb_slidersettings
				pidInList = this
				where = uid=
				where.dataWrap = |{field:slider_settings_relation}
				
				as = sliderSettings
				dataProcessing {
					350 = TYPO3\CMS\Frontend\DataProcessing\DatabaseQueryProcessor
					350 {
						if.isTrue.field = slider_content_relation

						table = foundation_zurb_slidercontent

						pidInList = this
						where.field = uid
						where.intval = 1
						where.dataWrap = foundation_zurb_slidersettings = |
						orderBy = sorting

						as = sliderContents
						dataProcessing {
							370 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
							370 {
								references.fieldName = image
								as = images
							}
						}
					}
				}
			}
		}
	}
}
            

Breaking the tt_content.typoscript down

                header_with_title < lib.contentElement
header_with_title {
	templateRootPaths.10 = EXT:your_extension_key/Resources/Private/Templates/ContentElements/
	partialRootPaths.10 = your_extension_key/Resources/Private//Partials/ContentElements/
	templateName = Header.html
}
            

What lib.contentElement does is to add a basic default TYPO3 configuration on the content element header_with_title. After that, the code specifies the path to the template as well the name of the template. 

                dataProcessing {
        330 = TYPO3\CMS\Frontend\DataProcessing\DatabaseQueryProcessor
	330 {
		table = foundation_zurb_slidersettings
		pidInList = this
		where = uid=
		where.dataWrap = |{field:slider_settings_relation}
		as = sliderSettings
            

What dataProcessing does, is a database query where it request a content with a specific search. In this case, we call the class DatabaseQueryProcessor and we ask to go to the table foundation_zurb_slidersettings and firstly select everything that are in the current pid (pid = page uid) and then get the current uid of the content element and compare it with the slider_settings_relation column in the tt_content. If it finds something, then it assigns an identifier, in this case sliderSettings. At this point we got the settings for the slider/header.

                dataProcessing {
	350 = TYPO3\CMS\Frontend\DataProcessing\DatabaseQueryProcessor
	350 {
		if.isTrue.field = slider_content_relation

                table = foundation_zurb_slidercontent
                pidInList = this
		where.field = uid
		where.intval = 1
		where.dataWrap = foundation_zurb_slidersettings = |
		orderBy = sorting

                as = sliderContents
            

Here it evaluates if the column slider_content_relation in foundation_zurb_slidersettings table has a value and it is not empty. If it is not empty then it sends a request to the table foundation_zurb_slidercontent and asks for all the elements that have been created on this page and get all the contents that have in the foundation_zurb_slidersettings column (inside the foundation_zurb_content) the ​​​​​​parent's uid (in this case the uid of the foundation_zurb_slidersettings table). If it finds something, then it assigns an identifier, in this case sliderContents. Do not forget, on the TCA we specified the foreign_field which adds the uid of the parent in the foundation_zurb_slidersettings column inside the foundation_zurb_content table.

                dataProcessing {
	370 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
	370 {
		references.fieldName = image
		as = image
	}
}
            

Standard TYPO3 FileProcessor code to get the images. references.fieldName = image is the column specified for the slider image. If it finds something, then it assigns an identifier, in this case image.

Html

Go ahead and add some content on your content element. Once you save it, take the variable {sliderSettings} that we defined on the tt_content and see what info are saved on it. In order to see that, you will have to debug the variable like the following:

Unter your_extension_key/Resources/Private/Templates/ContentElements/Header.html

                <f:debug>{_all}</f:debug>
            

Result

Now we have to create the template for the content element. We first need to evaluate if there is a content element and then count the sum of the images in order to define which template should TYPO3 use.

                <html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
	data-namespace-typo3-fluid="true">

	<f:if condition="{sliderSettings}">
		<div class="grid-container">
			<div class="grid-x">
				<div class="cell">
					<f:if condition="{sliderSettings.0.sliderContents -> f:count()} > 1">
						<f:then>
							<f:render partial="Header/Slider" arguments="{_all}"/>
						</f:then>
						<f:else>
							<f:render partial="Header/Header" arguments="{_all}"/>
						</f:else>
					</f:if>
				</div>
			</div>
		</div>
	</f:if>
</html>
            

The Partials

If the count function finds that there is more than one item in the content element, then it will call  the Slider.html partial. As base i will use the Foundation Orbit and the HTML looks like this: Unter your_extension_key/Resources/Private/Partials/ContentElements/Slider.html

                <div class="orbit" role="region" aria-label="Favorite Space Pictures" data-orbit>
	<div class="orbit-wrapper">
		<f:if condition="{sliderSettings.0.data.hide_arrows} != 1">
			<div class="orbit-controls">
				<button class="orbit-previous"><span class="show-for-sr">Previous Slide</span>&#9664;&#xFE0E;</button>
				<button class="orbit-next"><span class="show-for-sr">Next Slide</span>&#9654;&#xFE0E;</button>
			</div>
		</f:if>
		<ul class="orbit-container">
			<f:for each="{sliderSettings.0.sliderContents}" as="content" iteration="iterator">
				<li class="{f:if(condition: '{iterator.isFirst}', then: 'is-active')} orbit-slide">
					<figure class="orbit-figure">
						<f:image image="{content.images.0}" class="orbit-image" />
					</figure>
					<div class="slide_info">
						<h2>{content.data.title}</h2>
						<f:format.raw>{content.data.text}</f:format.raw>
					</div>
				</li>
			</f:for>
		</ul>
	</div>
	<f:if condition="{sliderSettings.0.data.hide_bullets} != 1">
		<nav class="orbit-bullets">
			<f:for each="{sliderSettings.0.sliderContents}" as="content" iteration="iterator">
				<f:if condition="{iterator.isFirst}">
					<f:then>
						<button class="is-active" data-slide="{iterator.index}">
							<span class="show-for-sr">{content.data.title}</span>
							<span class="show-for-sr" data-slide-active-label>Current Slide</span>
						</button>
					</f:then>
					<f:else>
						<button data-slide="{iterator.index}"><span class="show-for-sr">{content.data.title}</span></button>
					</f:else>
				</f:if>
			</f:for>
		</nav>
	</f:if>
</div>
            

Breaking the partial Slider.html down

                <div class="orbit" role="region" aria-label="Favorite Space Pictures" data-orbit>
	<div class="orbit-wrapper">

        </div>
</div>
            

This is just the default HTML schema that Foundation uses to render the JavaScript and CSS for the slider.

                <f:if condition="{sliderSettings.0.data.hide_arrows} != 1">
	<div class="orbit-controls">
		<button class="orbit-previous"><span class="show-for-sr">Previous Slide</span>&#9664;&#xFE0E;</button>
		<button class="orbit-next"><span class="show-for-sr">Next Slide</span>&#9654;&#xFE0E;</button>
	</div>
</f:if>
            

Here we evaluate if the setting hide_arrows is not set to 1. If it is set to 1, then the arrows won't show. The rest is a default Foundation schema. You could now use the <f:translate/> ViewHelper to create a translation overview for the arrows. For example:  <f:translate key="slider.next" extensionName="yourExtensionName"/>.

                <ul class="orbit-container">
	<f:for each="{sliderSettings.0.sliderContents}" as="content" iteration="iterator">
		<li class="{f:if(condition: '{iterator.isFirst}', then: 'is-active')} orbit-slide">
			<figure class="orbit-figure">
				<f:image image="{content.images.0}" class="orbit-image" />
			</figure>
			<div class="slide_info">
				<h2>{content.data.title}</h2>
				<f:format.raw>{content.data.text}</f:format.raw>
			</div>
		</li>
	</f:for>
</ul>
            

Here we go through each child element (f:for each) and we render the information. Some key parts here would be the {f:if(condition: '{iterator.isFirst}', then: 'is-active')} which sets the active state of the slider to the first item.

One more key part is the <f:image image="{content.images.0}" class="orbit-image" />. Normally the FilesProcessor query which we used on the tt_content file, gives back an array meaning that we have to use another f:for ViewHelper to go through all the images. Since the current item can only use one image, we do not really need to that, so we can safely use the {content.images.0} which reads the first entry of the array.

The last key part is the f:format.raw ViewHelper. If you are using rich text editor with CKeditor, then you should use the f:format.raw ViewHelper. That is because the CKeditor saves the text in the database WITH the HTML tags. So if you render the text without the ViewHelper, then the HTML tags will be rendered as text as well.

                <f:if condition="{sliderSettings.0.data.hide_bullets} != 1">
	<nav class="orbit-bullets">
		<f:for each="{sliderSettings.0.sliderContents}" as="content" iteration="iterator">
			<f:if condition="{iterator.isFirst}">
				<f:then>
					<button class="is-active" data-slide="{iterator.index}">
						<span class="show-for-sr">{content.data.title}</span>
						<span class="show-for-sr" data-slide-active-label>Current Slide</span>
					</button>
				</f:then>
				<f:else>
					<button data-slide="{iterator.index}"><span class="show-for-sr">{content.data.title}</span></button>
				</f:else>
			</f:if>
		</f:for>
	</nav>
</f:if>
            

Here we evaluate if the setting hide_bullets is not set to 1. If it is set to 1, then the bullets won't show. The rest is a default Foundation schema.

An important key here is the {iterator.index}. This starts the iteration from 0. In programming the first item is always the 0. So now we have the data-slide="0"  and by clicking on the bullet we get the first image.  

You could now use the <f:translate/> ViewHelper to create a translation overview for the bullets. For example:  <f:translate key="slider.current" extensionName="yourExtensionName"/>.

Result for Slider:

Result for Header: (code not available because you can do whatever you want with the information)

Step 8: Backend View

The last step is to create the BackEnd view. The goal is to display the current information to the BackEnd user. Now the BackEnd view looks like this:

The PageLayoutView Hook

We first need to create a hook which implements the PageLayoutViewDrawItemHookInterface class. In order to do that we have to create a Class and a Hooks folder so the path looks like this:

your_extension_key/Classes/Hooks/PageLayoutView/

Then we need to create some folders to host the html templates. 

your_extension_key/Resources/Private/Backend/Templates/
your_extension_key/Resources/Private/Backend/Partials/
your_extension_key/Resources/Private/Backend/Layouts/

Inside the your_extension_key/Resources/Private/Backend/Templates/ create a template with some name. In my case i will name it PageLayoutView.html.
Inside the your_extension_key/Resources/Private/Backend/Partials/ create a template with some name. In my case i will name it Header.html.
Inside the your_extension_key/Resources/Private/Backend/Partials/Header/ create two templates with some name. In my case i will name them Header.html and Slider.html.

Now we have to create a class with some name. You can name everything as you please after the Hooks folder. I am going to name it HeaderPreviewRenderer. Inside this file, we have to add some mandatory structure. On the namespace the Karavas part can be your name or company. You can put whatever you like.

                <?php

namespace Karavas\YourExtensionKey\Hooks\PageLayoutView;

use \TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface;
use \TYPO3\CMS\Backend\View\PageLayoutView;
use \TYPO3\CMS\Core\Utility\GeneralUtility;
use \TYPO3\CMS\Core\Database\ConnectionPool;
use \TYPO3\CMS\Extbase\Object\ObjectManager;
use \TYPO3\CMS\Fluid\View\StandaloneView;

class HeaderPreviewRenderer implements PageLayoutViewDrawItemHookInterface
{
	/**
	 * Preprocesses the preview rendering of a content element of type "Foundation Slider/Header"
	 *
	 * @param \TYPO3\CMS\Backend\View\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
	 */
	public function preProcess(
		PageLayoutView &$parentObject,
		&$drawItem,
		&$headerContent,
		&$itemContent,
		array &$row
	)
	{

	}
}
            

The next piece of code, i prefer to seperate it into multiple classes, such as DatabaseQueries and Helper. This way i have a clean code and i know where to look if problems appear. For the sake of this tutorial, i will include everything in one file and i will explain each line of the code.

                <?php

namespace Karavas\YourExtensionKey\Hooks\PageLayoutView;

use TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface;
use TYPO3\CMS\Backend\View\PageLayoutView;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Fluid\View\StandaloneView;

class HeaderPreviewRenderer implements PageLayoutViewDrawItemHookInterface
{
	/**
	 * Preprocesses the preview rendering of a content element of type "Foundation Slider/Header"
	 *
	 * @param \TYPO3\CMS\Backend\View\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
	 */
	public function preProcess(
		PageLayoutView &$parentObject,
		&$drawItem,
		&$headerContent,
		&$itemContent,
		array &$row
	)
	{
		if ($row['CType'] === 'header_with_title')
		{
			$drawItem = false;
			
			$objectManager = GeneralUtility::makeInstance(ObjectManager::class);
			$standaloneView = $objectManager->get(StandaloneView::class);
			$standaloneView->setTemplateRootPaths([10, 'EXT:your_extension_key/Resources/Private/Backend/Templates/']);
			$standaloneView->setLayoutRootPaths([10,'EXT:your_extension_key/Resources/Private/Backend/Layouts/']);
			$standaloneView->setPartialRootPaths([10,'EXT:your_extension_key/Resources/Private/Backend/Partials/']);
			$standaloneView->setFormat('html');
			$standaloneView->setTemplate('PageLayoutView.html');

			$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('foundation_zurb_slidersettings');
			$settings = $queryBuilder
				->select('uid', 'hide_arrows', 'hide_bullets')
				->from('foundation_zurb_slidersettings')
				->where(
					$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($row['slider_settings_relation'],\PDO::PARAM_INT)),
					$queryBuilder->expr()->eq('hidden', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
					$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
				)
				->execute()
				->fetch(0);

			$queryBuilder1 = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('foundation_zurb_slidercontent');
			$content = $queryBuilder1
				->select('*')
				->from('foundation_zurb_slidercontent')
				->where(
					$queryBuilder1->expr()->eq('foundation_zurb_slidersettings', $queryBuilder1->createNamedParameter($settings['uid'],\PDO::PARAM_INT)),
					$queryBuilder1->expr()->eq('hidden', $queryBuilder1->createNamedParameter(0, \PDO::PARAM_INT)),
					$queryBuilder1->expr()->eq('deleted', $queryBuilder1->createNamedParameter(0, \PDO::PARAM_INT))
				)
				->execute()
				->fetchAll();


			$fileObjects = array();
			for ($i=0; $i < count($content); $i++) {
				$fileRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\FileRepository::class);
				$fileObjects[] = $fileRepository->findByRelation('foundation_zurb_slidercontent', 'image', $content[$i]['uid']);
			}

			$countItems = count($content);

			$standaloneView->assignMultiple([
				'items' => $countItems,
				'title' => $parentObject->CType_labels[$row['CType']],
				'type' => $row['CType'],
				'headerSettings' => $settings,
				'headerContent' => $content,
				'sliderImages' => $fileObjects
			]);

			$itemContent .= $standaloneView->render();

		}
	}
}

            

Breaking the partial HeaderPreviewRenderer down:

                if ($row['CType'] === 'header_with_title')
{
         $drawItem = false;
}
            

This reads the CType we have defined under your_extension_key/Configuration/TSconfig/Page/CustomElements.typoscript and executes the following code only if the current CType  belongs to your content element.

With the $drawItem = false; we replace the default TYPO3 BackEnd view with ours. It is important that you set that to false.

                $objectManager = GeneralUtility::makeInstance(ObjectManager::class);
$standaloneView = $objectManager->get(StandaloneView::class);
$standaloneView->setTemplateRootPaths([10, 'EXT:your_extension_key/Resources/Private/Backend/Templates/']);
$standaloneView->setLayoutRootPaths([10,'EXT:your_extension_key/Resources/Private/Backend/Layouts/']);
$standaloneView->setPartialRootPaths([10,'EXT:your_extension_key/Resources/Private/Backend/Partials/']);
$standaloneView->setFormat('html');
$standaloneView->setTemplate('PageLayoutView.html');
            

Here we call the StandAlone class which comes with TYPO3 and we set the templates, partials and layouts for the current content element. 

                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('foundation_zurb_slidersettings');
$settings = $queryBuilder
	->select('uid', 'hide_arrows', 'hide_bullets')
	->from('foundation_zurb_slidersettings')
	->where(
		$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($row['slider_settings_relation'],\PDO::PARAM_INT)),
		$queryBuilder->expr()->eq('hidden', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
		$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
				)
	->execute()
	->fetch(0);
            

Here we get all the settings for the current content element. Remember, at this point we are on the tt_content table and we want to get the settings from the foundation_zurb_slidersettings table. The relation between those tables is the column slider_settings_relation which is located in tt_content table. 

  1. We create a connection to the table foundation_zurb_slidersettings
  2. Select which fields we would like to have
  3. We get the setting entry where the uid of this entry is the same with the value from slider_settings_relation. The  $row['slider_settings_relation'] gets the value from the current tt_content entry and the field slider_settings_relation. Meaning that the variable $row contains all the information to the current tt_content entry.
                $queryBuilder1 = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('foundation_zurb_slidercontent');
$content = $queryBuilder1
	->select('*')
	->from('foundation_zurb_slidercontent')
	->where(
		$queryBuilder1->expr()->eq('foundation_zurb_slidersettings', $queryBuilder1->createNamedParameter($settings['uid'],\PDO::PARAM_INT)),
		$queryBuilder1->expr()->eq('hidden', $queryBuilder1->createNamedParameter(0, \PDO::PARAM_INT)),
		$queryBuilder1->expr()->eq('deleted', $queryBuilder1->createNamedParameter(0, \PDO::PARAM_INT))
				)
	->execute()
	->fetchAll();
            

With this code we get all the children of the current content element (foundation_zurb_slidercontent table). If you remember, the slides have an extra column in the database named foundation_zurb_slidersettings which contains the uid of their parent, in this case, the table foundation_zurb_slidersettings. What we now do, is to get all the entries where the foundation_zurb_slidersettings column is the same number with the uid from the entry of the foundation_zurb_slidersettings table. 

  1. We create a connection to the table foundation_zurb_slidercontent
  2. Select which fields we would like to have ('*' means all)
  3. We get all the slides where the column foundation_zurb_slidersettings is the same with the uid of the entry in foundation_zurb_slidersettings. 
                $fileObjects = array();
for ($i=0; $i < count($content); $i++) {
	$fileRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\FileRepository::class);
	$fileObjects[] = $fileRepository->findByRelation('foundation_zurb_slidercontent', 'image', $content[$i]['uid']);
            

With this piece of code, we get all the images as objects. We call the FileRepository and then the function findByRelation gets the relation between the foundation_zurb_slidercontent and the file by reading the uid of the entry and the column in which the relation is binded. At this case the column image.

                $countItems = count($content);
$standaloneView->assignMultiple([
	'items' => $countItems,
	'title' => $parentObject->CType_labels[$row['CType']],
	'type' => $row['CType'],
	'headerSettings' => $settings,
	'headerContent' => $content,
	'sliderImages' => $fileObjects
]);
            

Here the first variable reads how many slides we have so we can decide which template we should use. The rest are the general information plus the settings and the content we requested. Everything will be available to use on the templates.

                $itemContent .= $standaloneView->render();
            

With this line of code we tell TYPO3 to render our templates. If you forget to include this line, you will not get results on your BackEnd.

The templates

Now that the information is there, we need to render them. This structure will be the following. The PageLayoutView.html will read the type of the content element and then it will render the appropriate partial. The partial will count the slide items and if there are more than one, then it will render the Slider partial. If it is only one, then it will render the Header partial. So now under the your_extension_key/Resources/Private/Backend/Templates/PageLayoutView.html add the following:

                <html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
	data-namespace-typo3-fluid="true">
	<f:switch expression="{type}">
		<f:case value="header_with_title">
			<f:render partial="Header" arguments="{_all}"/>
		</f:case>
		<f:defaultCase></f:defaultCase>
	</f:switch>
</html>
            

Now we have read the CType and we called the Header.html in the Partial folder. If the same file we write the following code in order to decide which subpartial TYPO3 should call by counting the slides.  under the your_extension_key/Resources/Private/Backend/Partials/Header.html 

                <f:if condition="{items} > 1">
	<f:then>
		<f:render partial="Header/Slider" arguments="{_all}"/>
	</f:then>
	<f:else>
		<f:render partial="Header/Header" arguments="{_all}"/>
	</f:else>
</f:if>
            

I prefer a table element as a structure for the BackEnd view. So my HTML will be a table, or a series of tables. On the Header.html we evaluated if the items are more than one. If yes then the Slider.html will be called. So my HTML would look like this: under the your_extension_key/Resources/Private/Backend/Partials/Header/Slider.html 

                <p><strong class="element_title">{title}</strong></p>
<table class="element_table">
	<tbody>
		<tr>
			<th>Slider arrows</th>
			<f:if condition="{headerSettings.hide_arrows}">
				<f:then>
					<td>&#10008;</td>
				</f:then>
				<f:else>
					<td>&#10004;</td>
				</f:else>
			</f:if>
		</tr>
		<tr>
			<th>Slider bullets</th>
			<f:if condition="{headerSettings.hide_bullets}">
				<f:then>
					<td>&#10008;</td>
				</f:then>
				<f:else>
					<td>&#10004;</td>
				</f:else>
			</f:if>
		</tr>
	</tbody>
</table>
<p><strong class="element_subtitle">Content</strong></p>
<table class="element_table">
	<tbody>
		<tr>
			<th class="listing">Item Nr.</th>
			<th class="content">Title</th>
			<th class="content">Text</th>
		</tr>
		<f:for each="{headerContent}" as="item" iteration="iterator">
			<tr>
				<td class="fitwidth">{iterator.cycle}</td>
				<td>{item.title}</td>
				<td><f:format.stripTags>{item.text->f:format.crop(maxCharacters: 20)}</f:format.stripTags></td>
			</tr>
		</f:for>
	</tbody>
</table>
<p><strong class="element_subtitle">Images</strong></p>
<table class="element_table">
	<tbody>
		<tr>
			<f:for each="{sliderImages}" as="image">
				<td><f:image image="{image.0}"/></td>
			</f:for>
		</tr>
	</tbody>
</table>
            

Breaking the Slider.html down

                <tr>
	<th>Slider arrows</th>
	<f:if condition="{headerSettings.hide_arrows}">
		<f:then>
			<td>&#10008;</td>
		</f:then>
		<f:else>
			<td>&#10004;</td>
		</f:else>
	</f:if>
</tr>
<tr>
	<th>Slider bullets</th>
		<f:if condition="{headerSettings.hide_bullets}">
		<f:then>
			<td>&#10008;</td>
		</f:then>
		<f:else>
			<td>&#10004;</td>
		</f:else>
	</f:if>
</tr>
            

This reads the settings and if the slider bullets and the slider arrows are active then it sets the &#10008; icon. If not the it sets the &#10004; icon.

                <tr>
	<th class="listing">Item Nr.</th>
	<th class="content">Title</th>
	<th class="content">Text</th>
</tr>
<f:for each="{headerContent}" as="item" iteration="iterator">
	<tr>
		<td class="fitwidth">{iterator.cycle}</td>
		<td>{item.title}</td>
		<td><f:format.stripTags>{item.text->f:format.crop(maxCharacters: 20)}</f:format.stripTags></td>
	</tr>
</f:for>
            

The first table cell will have a listing in numbers. (1, 2, 3 etc). The foreach loop will go through all the children and will render the information. the f:format.stripTags will remove all the HTML tags and will display the text as plain text. The f:format.crop(maxCharacters: 20) will remove the string after the 20th letter. We just need an overview not the whole text.

                <tr>
	<f:for each="{sliderImages}" as="image">
		td><f:image image="{image.0}"/></td>
	</f:for>
</tr>
            

This will go through all the images and render them.

Now that we have filled the Slider.html is time to fill the Header.html as well. This is going to be easier, because we have no settings and we dont have to iterate through all the items. Under the your_extension_key/Resources/Private/Backend/Partials/Header/Header.html 

                <p><strong class="element_title">{title}</strong></p>
<table class="element_table">
	<tbody>
		<tr>
			<th class="content">Title</th>
			<th class="content">Text</th>
			<th class="content">Image</th>
		</tr>
		<tr>
			<td>{headerContent.0.title}</td>
			<td><f:format.stripTags>{headerContent.0.text->f:format.crop(maxCharacters: 20)}</f:format.stripTags></td>
			<td><f:image image="{sliderImages.0.0}" width="120"/></td>
		</tr>
	</tbody>
</table>
            

Step 9: The extras

Now we have everything configured right? Not really. You see, if you reload the BackEnd you will not see anything changed. That is because TYPO3 does not know that there is an HTML to render. As a matter in fact, it does not know that there is a Hook configured for this. So we need to register the PageLayoutView Hook so TYPO3 knows that it should take it into consideration. Under you your_extension_key/ext_localconf.php we include the class:

                $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']['header_with_title'] = \Karavas\YourExtensionKey\Hooks\PageLayoutView\HeaderPreviewRenderer::class;
            

What this does, it to call the default drawItem and include the configuration for the content element header_with_title which is located on \Karavas\YourExtensionKey\Hooks\PageLayoutView\HeaderPreviewRenderer::class; If you pay attension the path is the same with the namespace you had on the HeaderPreviewRenderer class plus the class itself.

We are not quite ready yet. Now that we have included the new class, TYPO3 has not loaded it yet. We need to run the autoload so TYPO3 will go through all the available classes again. So if you are NOT in composer mode, you need to got to Maintenance->Rebuild PHP Autoload Information and click it. Then you should get your template in BackEnd. If you are in composer mode then under your composer.json include the following.

ATTENTION: Replace the path to your current directory structure!

                "autoload"     : {
	"psr-4" : {
		"Karavas\\YourExtensionKey\\" : "htdocs/typo3conf/ext/your_extension_key/Classes/"
		}
}
            

Now run composer update, and you should be able to see the results in the BackEnd

Results:

Styling

Now that we have the templated rendered, it is time to style them a little bit, because it does not look nice. In order to include a css file for the BackEnd, we need to put the following code under the your_extension_key/ext_tables.php

                $GLOBALS['TBE_STYLES']['skins']['your_extension_key'] = array();
$GLOBALS['TBE_STYLES']['skins']['your_extension_key']['name'] = 'My Awesome Name';
$GLOBALS['TBE_STYLES']['skins']['your_extension_key']['stylesheetDirectories'] = array(
    'css' => 'EXT:your_extension_key/Resources/Public/Css/Backend/',
);
            

What this does is to add a css DIRECTORY and include all the files that are inside this directory. The  'css' => is a name which you can freely name it as you please. If you want to make sure that is included, after you cleared the system cache go to Configuration->$GLOBALS['TBE_STYLES'] (Skinning Styles) and see if it is there.

The CSS

Now that we have included the CSS direcory, we are going to create a css file inside the directory your_extension_key/Resources/Public/Css/Backend/ and you can name is as you please. I will name it Backend.css. This is my personal preference, but you can style it as you want. So under the your_extension_key/Resources/Public/Css/Backend/Backend.css include this:

                .element_title {
	background:#ededed;
	padding:10px;
	width:100%;
	display: block;
	border-bottom:1px solid #ccc;
}
.element_table {
	width:100%;
}
.element_table tr {
	border-bottom: 1px solid #ccc;
}
.element_table tr th {
	width:30rem;
}
.element_table tr th, .element_table tr td {
	padding: 5px 10px;
}
.element_subtitle {
	background:#ededed;
	padding:5px 10px;
	width:100%;
	display: block;
	border-bottom:1px solid #ccc;
}
.element_table.one_table tr th:nth-child(3) {
	padding-left:10px;
	border-left:none;
}
            

The results:

Conclusion

If you have reached this, well done, your patience deserves an award!

This tutorial is kinda huge, but, if you create 2-5 content elements, then it will not take you more than one hour to build a complex content element. At least the basics. The table structure, register the content element, create the TCAs etc. I tried to explain everything because when i begun to create content elements, in some points, i found it hard to find what should i do and what to do. I really hope that everything was understandable. If you have any question about content elements, or you did not understand something in this tutorial or you want me to improve parts of this tutorial, do not hesitate to contact me.

Happy developing!