Show-Notes zu „Eigene Elemente in Contao“
von Dennis Erdmann (Kommentare: 0)
In Episode #21 der Contao Show sprachen Christian und ich darüber, wie wir vorhandene Contao Elemente erweitern und eigene erstellen können.
Wie angekündigt, haben wir unsere Beispiele wieder öffentlich gestellt, damit du sie möglichst leicht nachvollziehen und damit rumspielen kannst. Christians Beispiele findest du bei Github, meine Beispiele findest du unten:
1. Contao Slider erweitern
1.1. DCA-Erweiterung
// contao/dca/tl_content.php
use Contao\CoreBundle\DataContainer\PaletteManipulator;
PaletteManipulator::create()
->addField('swiperSettings', 'sliderContinuous', PaletteManipulator::POSITION_AFTER)
->applyToPalette('swiper', 'tl_content');
$GLOBALS['TL_DCA']['tl_content']['fields']['swiperSettings'] = [
'search' => true,
'inputType' => 'textarea',
'eval' => [
'decodeEntities' => true,
'class' => 'monospace',
'rte' => 'ace|yml',
'tl_class' => 'clr'
],
'sql' => "text NULL"
];
1.2. Twig Template anpassen
{% extends "@Contao/content_element/swiper.html.twig" %}
{# ergänze data-settings am Slider #}
{% set slider_attributes = attrs(slider_attributes|default).set('data-settings', data.swiperSettings|replace({"\n":"", "\t":"", " ": ""}) ) %}
{# -- #}
{% block script %}
{% add "swiper_js" to body %}
{% set script_attributes = attrs()
.setIfExists('nonce', csp_nonce('script-src'))
.mergeWith(script_attributes|default)
%}
<script src="{{ asset('js/swiper-bundle.min.js', 'contao-components/swiper') }}"></script>
<script{{ script_attributes }}>
(function() {
const swiper = document.querySelectorAll('.swiper');
swiper.forEach (el => {
let opts = {
speed: el.getAttribute('data-speed'),
initialSlide: el.getAttribute('data-offset'),
loop: el.hasAttribute('data-loop'),
};
let delay = el.getAttribute('data-delay');
if (delay > 0) {
opts['autoplay'] = {
delay: delay,
pauseOnMouseEnter: true,
};
}
{# auslesen und übergeben der data-settings #}
let swiperSettings = el.getAttribute('data-settings');
if (swiperSettings) {
swiperSettings = JSON.parse(swiperSettings);
opts = Object.assign({}, swiperSettings, opts);
}
{# -- #}
new Swiper(el, Object.assign({
{% block init_options %}
pagination: {
el: '.swiper-pagination',
clickable: true,
},
navigation: {
prevEl: '.swiper-button-prev',
nextEl: '.swiper-button-next',
},
{# Put custom options here #}
{% endblock %}
}, opts));
});
})();
</script>
{% endadd %}
{% endblock %}
1.3. Fade-Effekt
{
"effect": "fade",
"fadeEffect": {
"crossFade": true
}
}
1.4. Mehrere Slides pro View
{
"breakpoints": {
"768": {
"spaceBetween": "16",
"slidesPerView": "2"
},
"1100": {
"spaceBetween": "2",
"slidesPerView": "3"
}
}
}
2. Contao Tab-Element erstellen
2.1. DCA erweitern
// contao/dca/tl_content.php
$GLOBALS['TL_DCA']['tl_content']['palettes']['tab'] =
'{type_legend},type, headline;'.
'{template_legend:hide},customTpl;'.
'{protected_legend:hide},protected;'.
'{expert_legend:hide},guests,cssID,space;'.
'{invisible_legend:hide},invisible,start,stop';
2.2. Controller anlegen
// src Controller/ContentElement/TabController.php
namespace App\Controller\ContentElement;
use Contao\ContentModel;
use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController;
use Contao\CoreBundle\DependencyInjection\Attribute\AsContentElement;
use Contao\CoreBundle\Fragment\Reference\ContentElementReference;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Twig\FragmentTemplate;
use Contao\StringUtil;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
#[AsContentElement(category: 'miscellaneous', type: 'tab', nestedFragments: [true])]
class TabController extends AbstractContentElementController
{
public function __construct(private readonly ContaoFramework $framework)
{
}
protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response
{
$tabs = [];
$contents = [];
/** @var ContentElementReference $reference */
foreach ($template->get('nested_fragments') as $reference) {
$nestedModel = $reference->getContentModel();
if (!$nestedModel instanceof ContentModel) {
$nestedModel = $this->framework->getAdapter(ContentModel::class)->findById($nestedModel);
}
$header = StringUtil::deserialize($nestedModel->sectionHeadline, true);
$tabs[] = [
'label' => $header['value'] ?? '',
];
$contents[] = [
'header' => $header['value'] ?? '',
'header_tag' => $header['unit'] ?? 'h2',
'reference' => $reference
];
}
$template->set('tabs', $tabs);
$template->set('contents', $contents);
return $template->getResponse();
}
}
2.3. EventListener anlegen
// src/EventListener/TabListener.php
declare(strict_types=1);
namespace App\EventListener\DataContainer;
use Contao\CoreBundle\DataContainer\PaletteManipulator;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
#[AsCallback(table: 'tl_content', target: 'config.onpalette')]
class TabListener
{
public function __invoke(string $palette, DataContainer $dc): string
{
$currentRecord = $dc->getCurrentRecord();
if (!$currentRecord || 'tl_content' !== $currentRecord['ptable']) {
return $palette;
}
$parentRecord = $dc->getCurrentRecord($currentRecord['pid'], 'tl_content');
if (!$parentRecord || 'tab' !== $parentRecord['type']) {
return $palette;
}
return PaletteManipulator::create()
->addLegend('section_legend', 'type_legend', PaletteManipulator::POSITION_BEFORE)
->addField('sectionHeadline', 'section_legend', PaletteManipulator::POSITION_APPEND)
->applyToString($palette)
;
}
}
2.4. Twig-Template anlegen
{% extends "@Contao/content_element/_base.html.twig" %}
{#
contao/templates/content_element/tab.html.twig,
.twig-root in templates nicht vergessen!
#}
{% block content %}
<div class="tabs">
<div class="tablist">
{% for tab in tabs %}
<button class="tab__button" data-tab="tab-{{ loop.index0 }}">
{{- tab.label -}}
</button>
{% endfor %}
</div>
</div>
<div class="contents">
{% for content in contents %}
<div class="tab__content" data-content="tab-{{ loop.index0 }}">
<{{ content.header_tag }}> {# optionale Ausgabe des Tab-Titels, z.B. für Smartphone-Ansicht #}
{{- content.header -}}
</{{ content.header_tag }}>
{{ content_element(content.reference) }}
</div>
{% endfor %}
</div>
{% endblock %}
{% block script %}
{% add "tab" to body %}
<script src="/files/theme/js/tabs.js"></script>
{% endadd %}
{% endblock %}
2.5. SCSS und JS
// _tabs.scss für OPTIMIST Theme
.content-tab {
.tabs {
position: relative;
}
.tab__button {
background: none;
border: var(--base-border);
border-radius: 4px 4px 0 0;
border-bottom: none;
padding: var(--base-spacing-unit-sm) var(--base-spacing-unit);
&:not(.active) {
color: var(--color-gray-light);
}
}
.contents {
display: grid;
}
.tab__content {
grid-column: 1 / -1;
grid-row: 1;
background: var(--color-page-background);
border: var(--base-border);
padding: 1rem;
opacity: 0;
&.active {
z-index: 1001;
opacity: 1;
}
> div {
height: 100%;
}
}
}
// tabs.js
document.addEventListener('DOMContentLoaded', function () {
const tabs = document.querySelectorAll('.content-tab');
tabs.forEach((tab) => {
const tabButtons = tab.querySelectorAll('.tablist .tab__button');
const tabContents = tab.querySelectorAll('.contents .tab__content');
tabButtons.forEach((button) => {
button.addEventListener('click', function () {
const tabId = this.getAttribute('data-tab');
// Alle Inhalte und Buttons zurücksetzen
tabContents.forEach((content) => content.classList.remove('active'));
tabButtons.forEach((btn) => btn.classList.remove('active'));
// Den entsprechenden Inhalt anzeigen und den Button aktivieren
tab
.querySelector(`.contents .tab__content[data-content="${tabId}"]`)
.classList.add('active');
this.classList.add('active');
});
});
// Den ersten Tab und Inhalt standardmäßig anzeigen
if (tabButtons.length > 0) {
tabButtons[0].click();
}
// Alternativ kann die active Klasse auch per Twig-Template gesetzt werden
});
});
Kommentare
Einen Kommentar schreiben