Skip to content

Preface

The solution of Claus Fassing served as a basis for this tutorial. I made some corrections and optimizations, then completed it to an entire TYPO3 extension.

EXT:ajaxselectlist on GitHub and in the TYPO3 Extension Repository

TYPO3 extensions built with Extbase use the Model-View-Controller (MVC) pattern for software development. Although I won't go into detail about that, even beginners should be able to follow this tutorial.

Creating a basic structure with the Extension Builder

We'll use the Extension Builder (EB) to generate a basic extension. After installing this extension in TYPO3, we can open the new backend module of the EB. When you open it for the first time, an introduction is shown which explains what the Extension Builder will create for us:

  • directory structure
  • basic classes for the Domain Model
  • Database Tables and TCA definitions which fit to the domain model
  • Locallang Files in the XML Localisation Interchange File Format
  • Plugin Configuration (TypoScript, Fluid-Templates)
  • standard database actions (list, show, create, update, delete)

You can now switch to Domain Modeling by using the select list on top. In the left column we have to enter the basic informations for our extension, the right column allows us to create our object.

Extension properties

The first three fields are mandatory. We will configure the frontend plugin directly, too.

  • Name: Is shown in the Extension Manager; should be short and concise.
  • Vendor name: Your identifier used for the PHP Namespaces. Ich chose my GitHub username.
  • Key: Unique name of this extension. If you intend to release the extension in the public repository, you'll want to check the availability of the desired name.
  • Descr.: Short description of your extension, will be shown in the Extension Manager on mouseover.
  • Persons: You can add your name and role, if you like.
  • Frontend plugins: This extension needs a plugin, so we'll create one now. The Extension Builder then generates some basic Fluid templates and TypoScript settings.
    • Name:Gets displayed in the select list of plugins.
    • Key: Unique name of the plugin within this extension.
    • We don't need the advanced options here.

Domain Model

To create a new Domain Model, we have to pull it from the box "New Model Object"! We get a box with several settings.

  • First of all we set the name of this Model in the boxes' header, here: OptionRecord
  • Domain object settings: We activate "Is aggregate root?". That causes the EB to create a repository for our object.
  • Default actions: For this extension, we need the list action as well as a custom action named 'ajaxCall'.
  • Properties: The desired fields. The Model and $TCA definitions are set up, too. erstellt werden. We create three properties:
    • title: Type 'String' (simple text field – its content is used as title of the select list options)
    • image: Type 'Image*' (FAL image)
    • text: "Type 'Rich text*' (a multiline text field with Rich Text Editor)

After this setup we can save the extension. The following image shows all files that were generated by the Extension Builder:

Among other things, the EB creates a complete template for a reStructuredText documentation as well as PHPUnit tests for the Controller and Domain Model. Both are not covered by this tutorial.

Be careful with subsequent changes through the Extension Builder! Any modifications you made manually will be overwritten except you exclude the affected files from it. This can be done in Configuration/ExtensionBuilder/settings.yaml. It never hurts to create backups!

Preparing records and inserting the plugin

The extension is already able to render records! So after installing it in the Extension Manager, we'll now create a few records inside a new folder and integrate the plugin into a page. For my example I used some entries about countries – you could just as well output a list of employees.

For the time being, we set the UID of our record folder with the storagePid inside our Constants.

Currently it won't work just to set the folder with the Record Storage Page field. More on this in the next step.

When opening the page including the plugin in the frontend, we see a tabular listing of all found records. We notice two things:

  • The list view includes the links Edit, Delete and New [Model name], although we didn't select these Default Actions. If you try to call any of these actions you'll end up with an error message that this action is not allowed by this plugin.
  • The image as well as the formatting from the Rich Text Editor aren't displayed like some may have expected. We'll have to adjust the template for that.

Cleaning up the extension

Before starting to make modifications, we'll remove some elements that aren't necessary for our extension, specifically:

  • Configuration/TypoScript/setup.txt:
    • Delete: The complete code block with _CSS_DEFAULT_STYLE
    • Change: plugin.tx_ajaxselectlist_selectlist to plugin.tx_ajaxselectlist
  • Configuration/TypoScript/constants.txt:
    • Change: plugin.tx_ajaxselectlist_selectlist to plugin.tx_ajaxselectlist

The Extension Builder creates the TypoScript specifically for the plugin. Although this basically works, we'll change the path to a general extension configuration. This approach has the advantage that entries in the field Record Storage Page no longer get overridden by storagePid.
TYPO3 or Extbase (the underlying Framework) determines the storage location in a given order. Plugin-specific configurations always overrides the values in the plugin form field Record Storage Page. This will occur even if there's no value set for storagePid inside tx_extensionname_pluginname – it is sufficient that this setting is declared.

Custom TypoScript

The Ajax response may not contain any header code. To achieve this, we use a new PAGE object where all header code is removed through config settings. Furthermore we assign a high random typeNum value and include our plugin as the sole content object. The latter will then render the detail view (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
}

Adjusting the Controller

The listAction is already completed by the Extension Builder. Our ajaxCallAction will read out the parameters of the requested action and make them available as arguments.

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);
    }
}

Building the form with select list

Now we'll modify the list view (List.html), which will be rendering the form including the select list. The current source code isn't required and can be removed completely. We'll replace it with a form that is built with Fluid Viewhelper.

Fluid Viewhelper are classes that can be used inside the templates as control structures (f:if), loops (f:for), to generate forms (f:form) and links (f:link), as well as to render and manipulate content (f:format, f:image). If you need a function that isn't supported by the TYPO3 core, you can just write a custom Viewhelper for that.

The following must be observed for the form:

  • In the Viewhelper f:formobject must match with name. The action method to be called is ajaxCall.
  • In f:form.select you have to use the plural of the Domain Model' name for options (the array with all records), but the name attribute has to be in singular form.
  • With f:form.hidden we call the Ajax function, which has to have the same name as the Controller action.
  • There is also another hidden field that is used to pass the typeNum value.
  • In the next step we'll add the language parameter L, so that the returned content is loaded in the current frontend language.

Below the form is a div element that is used as a container for the record's content. After that follows the JavaScript for the Ajax request, which requires jQuery.

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>

Supporting multilingualism

If we create translations in the backend, the option's titles will be rendered in the current website languages. However, the record's content is always shown in the initial language!

As said before, we'll have to pass the parameter L inside the form to get the correct language with the Ajax call. As the current language is not yet available as a value in the template, we'll now assign this to a new variable for the listAction in the Controller:

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

In the List.html template we'll now add a new hidden form field with this variable:

<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>

Sorting the list entries

If you want to get the option titles in alphabetical order, just add the following lines to the class OptionRecordRepository:

OptionRecordRepository.php

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

Removing fields from the plugin form

Every created TYPO3 plugin contains the field 'Plugin Mode'. As this isn't needed we'll now remove it, furthermore the checkbox to 'Append with Link to Top of Page' and the 'Layout' select list.

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';

Closing words

The finished extension is available on GitHub and in the TYPO3 Extension Repository. Newer versions of EXT:ajaxselectlist will differ in some points from the code blocks shown here – extensions are supposed to be continuously developed. I hope with this tutorial I was able to answer some of your questions.