Ajax-Aufruf in TYPO3-Extension

Erstellung einer TYPO3-Extension mit Ajax-Aufruf

Heute erstelle ich eine komplette Extension mit Plugin, das ein Auswahlmenü (select) ausgibt und dessen Ergebnisse mit jQuery und Ajax anzeigt, ohne die gesamte Webseite neu zu laden.

Grundlage für dieses Tutorial war die Lösung von Claus Fassing. Diese habe ich etwas korrigiert, optimiert und als komplette TYPO3-Extension umgesetzt.

EXT:ajaxselectlist auf GitHub und im TYPO3 Extension Repository

TYPO3-Extensions basierend auf Extbase verwenden das Model-View-Controller (MVC) Muster zur Software-Entwicklung. Obwohl ich an dieser Stelle nicht näher darauf eingehe, sollten auch Einsteiger diesem Tutorial folgen können.

Bauen des Grundgerüsts mit dem Extension Builder

Für die grundlegende Extension greifen wir auf den Extension Builder (EB) zurück. Nach der Installation in TYPO3 öffnen wir das Backend Modul des Extension Builder. Beim ersten Aufruf wird eine Einführung geladen, die auch erklärt was der EB alles praktischerweise für uns erstellt:

  • Verzeichnisstruktur
  • Basis-Klassen für das Domain Model
  • Datenbanktabellen und $TCA-Definitionen passend zum Domain Model
  • Lokalisierungsdateien (locallang) im XML Localisation Interchange File Format
  • Plugin-Konfiguration (TypoScript, Fluid-Templates)
  • Grundlegende Datenbank-Aktionen (list, show, create, update, delete)

Über das obere Select-Menü wechseln wir von Introduction auf Domain Modeling. In der linken Spalte geben wir die Informationen zur Extension ein, im rechten Bereich können wir unser Objekt erstellen.

Extension properties

Die ersten drei Felder sind Pflichtfelder. Auch das Frontend-Plugin richten wir sofort ein.

  • Name: Wird im Extension Manager angezeigt, sollte kurz und prägnant sein.
  • Vendor name: Für die PHP Namespaces verwendetes, eindeutiges Kürzel. Ich habe meine GitHub-Kennung gewählt.
  • Schlüssel: Der eindeutige Name der Extension. Falls diese im öffentlichen Repository veröffentlicht werden soll, solltest du diesen Namen vorher auf Verfügbarkeit prüfen.
  • Beschr.: Kurze Beschreibung der Extension, wird beim Mouseover im Extension Manager angezeigt.
  • Personen: Du kannst deinen Namen und Informationen hinzufügen, wenn du möchtest.
  • Frontend Plug-Ins: Unsere Extension benötigt ein Plugin, daher legen wir das direkt mit an. Der Extension Builder generiert uns dann schon erste Templates und das TypoScript.
    • Name: Erscheint im Auswahlmenü für Plugins.
    • Schlüssel: Eindeutige Kennung des Plugins innerhalb der Extension.
    • Die fortgeschrittenen Optionen benötigen wir hier nicht.

Domain Model

Um ein neues Domain Model zu erstellen, muss dieses vom Kasten "New Model Object" auf die Arbeitsfläche gezogen werden! Man erhält einen Kasten, in dem man verschiedene Einstellungen vornehmen kann. 

  • Im Kopfbereich des Kastens vergeben wir zuerst den Namen unseres Models, hier: OptionRecord
  • Einstellungen des Domainobjekts: Wir aktivieren "Is aggregate root?", damit der EB uns ein Repository für unser Objekt erstellt.
  • Standardaktionen: Für diese Extension lassen wir uns eine Liste sowie eine Custom action namens "ajaxCall" einrichten.
  • Eigenschaften: Unsere gewünschten Felder, für die auch das Model und die $TCA-Definitionen erstellt werden. Wir legen drei Eigenschaften an:
    • title: Typ "String" (einfaches Textfeld – dessen Inhalt verwenden wir als Bezeichnung im Auswahlmenü)
    • image: Typ "Bild*" (FAL-Bild)
    • text: "Typ "Rich text*" (ein mehrzeiliges Textfeld mit Rich Text Editor)

Alle Einstellungen des Model Objects 'OptionRecord'

Nach dieser Einrichtung kann die Extension gespeichert werden. Im folgenden Bild ist zu sehen, was der Extension Builder anschließend generiert:

Default-Extension-Struktur

Unter anderem erstellt er eine komplette Vorlage für eine reStructuredText-Dokumentation sowie PHPUnit-Tests für den Controller und das Domain Model. Beides wird nicht Bestandteil der weiteren Erläuterungen sein.

Vorsicht bei nachträglichen Änderungen über den Extension Manager! Manuelle Anpassungen im Quelltext werden überschrieben, wenn die geänderte Datei nicht explizit von Änderungen ausgeschlossen wird. Dies erledigt man in der Datei Configuration/ExtensionBuilder/settings.yaml. Es schadet sowieso nie, Backups zu erstellen.

Erstellen von Einträgen und Einfügen des Plugins

Schon jetzt liefert uns die Extension eine Ausgabe! Nach der Installation im Extension Manager legen wir daher einen neuen Ordner mit einigen Datensätzen an und binden das Plugin auf einer Seite ein. Ich habe als Beispiel ein paar Einträge zu Ländern erstellt; genauso gut könnte man die Extension für eine Mitarbeiter-Liste verwenden.

In den Constants belegen wir vorerst die storagePid mit der UID des Datensatz-Ordners.

Zu diesem Zeitpunkt genügt es nicht, den Ordner im Plugin im Feld Datensatzsammlung zu hinterlegen. Mehr dazu im nächsten Schritt.

Öffnen wir nun die Seite mit Plugin, erhalten wir eine tabellarische Auflistung aller gefundenen Datensätze. Zwei Dinge fallen auf:

  • Die Listenansicht enthält die Links EditDelete und New [Model name], obwohl wir diese Default Actions nicht ausgewählt hatten. Folgt man einem dieser Links, erhalten wir daher eine Fehlermeldung, dass die dazugehörige Action nicht erlaubt ist.
  • Das Bild und Formatierungen des Rich Text Editors werden nicht so ausgeben, wie vielleicht erwartet. Die gewünschte Ausgabe müssen wir selbst im Template einrichten.

Bereinigen der Extension

Bevor wir unsere Ergänzungen durchführen, entfernen wir einige Elemente, die wir für unsere Extension nicht benötigen. Das sind im Einzelnen:

  • Configuration/TypoScript/setup.txt:
    • Löschen: Der komplette Codeblock mit dem _CSS_DEFAULT_STYLE
    • Ändern: plugin.tx_ajaxselectlist_selectlist zu plugin.tx_ajaxselectlist
  • Configuration/TypoScript/constants.txt:
    • Ändern: plugin.tx_ajaxselectlist_selectlist zu plugin.tx_ajaxselectlist

Der Extension Builder generiert das TypoScript direkt für das einzelne Plugin. Obwohl das im Grunde funktioniert, ändern wir den Pfad auf eine allgemeine Extension-Konfiguration ab. Ein Vorteil ist, dass die Einträge im Feld Datensatzsammlung nicht länger von storagePid überschrieben werden.
TYPO3 bzw. Extbase (das dieser Extension zugrundeliegende Framework) sucht nach Speicherorten in einer festgelegten Reihenfolge. Plugin-spezifische Konfigurationen überschreiben dabei stets die Werte im Feld Datensatzsammlung. Dazu muss für storagePid in tx_extensionname_pluginname nicht einmal ein Wert hinterlegt sein – es genügt schon die grundsätzliche Deklaration.

Eigenes TypoScript

Der vom Ajax-Aufruf zurückgegebene Inhalt soll keinen Header-Code enthalten. Dafür legen wir ein neues PAGE-Objekt an, dass über config-Einstellungen von allem Ballast befreit wird. Es erhält zudem eine hohe zufällige typeNum und – als einzigen Inhalt – unser Plugin, welches die Detailansicht ausgeben wird (AjaxCall.html).

constants.txt

plugin.tx_ajaxselectlist {
    view {
        # cat=plugin.tx_ajaxselectlist/file; type=string; label=Path to template root (FE): Specify a path to your custom templates. There is a fallback for any template that cannot be found in this path.
        templateRootPath =
        # cat=plugin.tx_ajaxselectlist/file; type=string; label=Path to template partials (FE): Specify a path to your custom partials. There is a fallback for any partial that cannot be found in this path.
        partialRootPath =
        # cat=plugin.tx_ajaxselectlist/file; type=string; label=Path to template layouts (FE): Specify a path to your custom layouts. There is a fallback for any layout that cannot be found in this path.
        layoutRootPath =
    }

    persistence {
        # cat=plugin.tx_ajaxselectlist//1; type=string; label=Storage folder(s): Comma-separated list of pages (UIDs) which contain records for this extension.
        storagePid =
    }

    settings {
        # cat = plugin.tx_ajaxselectlist//3; type=int+; label=typeNum for AJAX call

        typeNum = 427590

    }
}

setup.txt

plugin.tx_ajaxselectlist {
    view {
        templateRootPaths {
            0 = EXT:ajaxselectlist/Resources/Private/Templates/
            1 = {$plugin.tx_ajaxselectlist.view.templateRootPath}
        }
        partialRootPaths {
            0 = EXT:ajaxselectlist/Resources/Private/Partials/
            1 = {$plugin.tx_ajaxselectlist.view.partialRootPath}
        }
        layoutRootPaths {
            0 = EXT:ajaxselectlist/Resources/Private/Layouts/
            1 = {$plugin.tx_ajaxselectlist.view.layoutRootPath}
        }
    }

    persistence {
        storagePid = {$plugin.tx_ajaxselectlist.persistence.storagePid}
    }

    settings {
        typeNum = {$plugin.tx_ajaxselectlist.settings.typeNum}
    }
}


// PAGE object for Ajax call:
ajaxselectlist_page = PAGE
ajaxselectlist_page {
    typeNum = 427590

    config {
        disableAllHeaderCode = 1
        additionalHeaders = Content-type:application/html
        xhtml_cleaning = 0
        debug = 0
        no_cache = 1
        admPanel = 0
    }

    10 < tt_content.list.20.ajaxselectlist_selectlist
}

Anpassung der Controller

Die listAction wurde vom Extension Builder schon fertig erstellt. Die ajaxCallAction erhält die Parameter der aufzurufenden Action als Argumente.

OptionRecordController.php

/**
 * OptionRecordController
 */
class OptionRecordController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController
{
 
    /**
     * optionRecordRepository
     *
     * @var \Sebkln\Ajaxselectlist\Domain\Repository\OptionRecordRepository
     * @inject
     */
    protected $optionRecordRepository = NULL;
 
    /**
     * action list
     *
     * @return void
     */
    public function listAction()
    {
        $optionRecords = $this->optionRecordRepository->findAll();
        $this->view->assign('optionRecords', $optionRecords);
    }
 
    /**
     * action ajaxCall
     *
     * @param \Sebkln\Ajaxselectlist\Domain\Model\OptionRecord $optionRecord
     * @return void
     */
    public function ajaxCallAction(\Sebkln\Ajaxselectlist\Domain\Model\OptionRecord $optionRecord)
    {
        $this->view->assign('optionRecord', $optionRecord);
    }
}

Erstellung des Formulars mit Auswahlmenü

Nun passen wir das Template der Listenansicht (List.html) an, welches das Formular mit dem Auswahlmenü ausgeben soll. Die Ausgabe in seiner jetzigen Form benötigen wir nicht – wir ersetzen daher den kompletten Part mit unserem Formular, das wir mit Fluid Viewhelpern aufbauen.

Fluid Viewhelper sind Klassen, die im Template unter anderem für Kontrollstrukturen (f:if) und Schleifen (f:for), zur Generierung von Formularen (f:form) und Links (f:link) sowie der Ausgabe von Inhalten (f:format, f:image) verwendet werden können. Wenn eine Funktion benötigt wird die der TYPO3-Kern nicht bietet, kann man sich auch eigene Viewhelper programmieren.

Beim Formular sind folgende Dinge zu beachten:

  • Im Viewhelper f:form muss object mit name übereinstimmen. Als action wird ajaxCall aufgerufen.
  • Bei f:form.select wird für options zwar die Pluralform des Domain Models (also das Array mit allen Datensätzen) verwendet, das name-Attribut muss aber im Singular stehen.
  • Mit f:form.hidden rufen wir die Ajax-Funktion auf, die denselben Namen wie die Controller action tragen muss.
  • Dazu kommt noch ein weiteres verstecktes Feld, in dem wir den typeNum übergeben
  • Im nächsten Schritt fügen wir noch den Sprach-Parameter L hinzu, damit der Inhalt in der gewählten Sprache geladen wird.

Unterhalb des Formulars folgt ein div-Element, in das wir den Inhalt der Datensätze laden. Zu guter Letzt folgt das JavaScript für den Ajax-Aufruf, dass in diesem Fall auf jQuery setzt.

List.html

<f:layout name="Default" />
 
<f:section name="main">
  <f:if condition="{optionRecords}">
    <f:then>
      <f:form
          action="ajaxCall"
          object="{optionRecord}"
          name="optionRecord"
          id="ajaxselectlist-form">
            
        <f:form.select
            options="{optionRecords}"
            optionLabelField="title"
            class="ajaxFormOption"
            name="optionRecord" />

        <f:form.hidden name="action" value="ajaxCall"/>
        <input type="hidden" name="type" value="{settings.typeNum}">
      </f:form>
    </f:then>
    <f:else>
      <f:translate key="tx_ajaxselectlist_domain_model_optionrecord.noEntriesFound"/>
    </f:else>
  </f:if>
  
  <f:comment>The record entry is loaded inside this element.</f:comment>
  <div id="ajaxCallResult"></div>
  
  <script>
    jQuery(document).ready(function ($) {
      var form = $('#ajaxselectlist-form');
      var selectForm = $('.ajaxFormOption');
      var resultContainer = $('#ajaxCallResult');
      var service = {
        ajaxCall: function (data) {
          $.ajax({
            url: 'index.php',
            cache: false,
            data: data.serialize(),
            success: function (result) {
              resultContainer.html(result).fadeIn('fast');
            },
            error: function (jqXHR, textStatus, errorThrow) {
              resultContainer.html('Ajax request - ' + textStatus + ': ' + errorThrow).fadeIn('fast');
            }
          });
        }
      };
      form.submit(function (ev) {
        ev.preventDefault();
        service.ajaxCall($(this));
      });
      selectForm.on('change', function () {
        resultContainer.fadeOut('fast');
        form.submit();
      });
      selectForm.trigger('change');
    });
  </script>
  
</f:section>

Einrichten der Mehrsprachigkeit

Wenn wir Übersetzungen im Backend anlegen, werden die Titel im Auswahlmenü schon in den jeweiligen Sprachen ausgegeben. Die Inhalte werden aber immer in der Grundsprache angezeigt!

Wir müssen im Formular den Parameter L mit der aktuellen Website-Sprache übergeben, damit der Ajax-Aufruf die korrekte Sprache zurückgibt. Die aktuell gewählte Sprache ist aber noch nicht im Template abrufbar. Dies ändern wir, indem wir im Controller für die listAction die aktuelle Sprache auslesen und sie einer Variable zuweisen:

public function listAction()
{
    $optionRecords = $this->optionRecordRepository->findAll();
    $this->view->assign('optionRecords', $optionRecords);
    $this->view->assign("sysLanguageUid", $GLOBALS['TSFE']->sys_language_uid);
}

Im Template List.html fügen wir ein neues, verstecktes Formularfeld mit dieser Variable ein: 

<f:form
    action="ajaxCall"
    object="{optionRecord}"
    name="optionRecord"
    id="ajaxselectlist-form">
  
    <f:form.select
        options="{optionRecords}"
        optionLabelField="title"
        class="ajaxFormOption"
        name="optionRecord" />
  
    <f:form.hidden name="action" value="ajaxCall"/>
    <input type="hidden" name="type" value="{settings.typeNum}">
    <input type="hidden" name="L" value="{sysLanguageUid}">
</f:form>

Sortierung der Listeneinträge

Wer eine alphabetische Sortierung der Titel wünscht, fügt zur Klasse OptionRecordRepository folgende Zeilen hinzu:

OptionRecordRepository.php

class OptionRecordRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
    protected $defaultOrderings = array(
            'title' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING
    );
}

Bereinigung des Plugins

Jedes erstellte TYPO3-Plugin erhält das Feld "Plug-In-Modus". Da wir es nicht benötigen, wird es von uns entfernt. Ebenso die Checkbox für den "Link zum Seitenanfang" und das "Layout"-Auswahlmenü.

Configuration/TCA/Overrides/tt_content.php

<?php
if (!defined('TYPO3_MODE')) {
    die ('Access denied.');
}

$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist']['ajaxselectlist_selectlist'] = 'layout,select_key,linkToTop';

Schlusswort

Die fertige Extension ist auf GitHub sowie im TYPO3 Extension Repository verfügbar. Neuere Versionen von EXT:ajaxselectlist unterscheiden sich an einigen Stellen von den hier gezeigten Codestücken – Extensions sind schließlich dazu da, um sie weiterzuentwickeln. Ich hoffe, diese Anleitung konnte einige Fragen für euch beantworten.

Zurück