TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:24 +02:00
commit aad9daaefd
1506 changed files with 94005 additions and 0 deletions
@@ -0,0 +1,182 @@
.. include:: /Includes.rst.txt
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel:
.. _apireference-formeditor-formelementmodel:
==================
FormElement model
==================
Every form element in the editor is represented by a **FormElement model**
object. This model is the single source of truth for all element properties
during an editing session; it is separate from the YAML form definition on
disk (which is only written on save).
.. contents::
:depth: 1
:local:
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-property-identifierpath:
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-property-parentrenderable:
.. _apireference-formeditor-formelementmodel-structure:
Model structure
===============
A FormElement model carries all YAML properties of the element plus two
internal bookkeeping properties:
.. list-table::
:header-rows: 1
:widths: 30 70
* - Property
- Description
* - :js:`__identifierPath`
- Slash-separated path from the root element to this element
(e.g. :js:`'example-form/page-1/name'`). Used as a unique key
for API lookups.
* - :js:`__parentRenderable`
- Reference to the parent FormElement model (filtered for display).
Example model in memory:
.. literalinclude:: _codesnippets/_model-structure.js
:language: javascript
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-get:
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-get-propertycollectionproperties:
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-get-renderables:
.. _apireference-formeditor-formelementmodel-api-get:
get()
-----
Reads a property by its dot-separated path. All intermediate levels must
be objects.
.. literalinclude:: _codesnippets/_get-simple.js
:language: javascript
For **property collections** (validators / finishers), whose position in
the array is unknown, use :js:`buildPropertyPath()` first:
.. literalinclude:: _codesnippets/_get-property-collection.js
:language: javascript
For **renderables** (child elements), :js:`get('renderables')` returns a
plain array of FormElement models. To access a specific child, use
:js:`formEditorApp.getFormElementByIdentifierPath()` with the full path.
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-set:
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-set-propertycollectionproperties:
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-set-renderables:
.. _apireference-formeditor-formelementmodel-api-set:
set()
-----
Writes a property by its dot-separated path. Every :js:`set()` call
automatically publishes all events registered for that path via
:ref:`on() <apireference-formeditor-formelementmodel-api-on>`, including
the built-in
:ref:`core/formElement/somePropertyChanged <apireference-formeditor-jsevents-core-formelement-somepropertychanged>`.
.. literalinclude:: _codesnippets/_set.js
:language: javascript
To modify property collection properties or add child renderables, use
the dedicated API methods on :js:`formEditorApp` / :js:`getViewModel()`
instead of setting array positions directly:
- :js:`createAndAddFormElement()`
- :js:`addFormElement()`
- :js:`moveFormElement()`
- :js:`removeFormElement()`
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-unset:
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-unset-propertycollectionproperties:
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-unset-renderables:
.. _apireference-formeditor-formelementmodel-api-unset:
unset()
-------
Removes a property at the given dot-separated path.
.. literalinclude:: _codesnippets/_unset.js
:language: javascript
For property collection properties, use :js:`buildPropertyPath()` in the
same way as for :ref:`get() <apireference-formeditor-formelementmodel-api-get>`.
To remove a child renderable, call
:js:`formEditorApp.removeFormElement()`.
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-on:
.. _apireference-formeditor-formelementmodel-api-on:
on()
----
Registers an additional publish/subscribe event name that is fired
whenever :js:`set()` is called for a given property path.
.. literalinclude:: _codesnippets/_on.js
:language: javascript
By default EXT:form registers
:ref:`core/formElement/somePropertyChanged <apireference-formeditor-jsevents-core-formelement-somepropertychanged>`
for every known property path of every form element.
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-off:
.. _apireference-formeditor-formelementmodel-api-off:
off()
-----
Removes an event registration created with :js:`on()`.
.. literalinclude:: _codesnippets/_off.js
:language: javascript
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-getobjectdata:
.. _apireference-formeditor-formelementmodel-api-getobjectdata:
getObjectData()
---------------
Returns a deep-cloned plain object of all properties. Used internally for
Ajax serialisation. Provides read access to data set via :js:`set()` from
outside the model without breaking encapsulation.
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-clone:
.. _apireference-formeditor-formelementmodel-api-clone:
clone()
-------
Returns a fully dereferenced clone of the FormElement model.
.. literalinclude:: _codesnippets/_clone.js
:language: javascript
.. _apireference-formeditor-basicjavascriptconcepts-formelementmodel-method-tostring:
.. _apireference-formeditor-formelementmodel-api-tostring:
toString()
----------
Returns the model data as a JSON string. Intended for debugging.
.. literalinclude:: _codesnippets/_to-string.js
:language: javascript
@@ -0,0 +1,5 @@
export function bootstrap(formEditorApp) {
const formElement = formEditorApp
.getFormElementByIdentifierPath('example-form/page-1/name');
const copy = formElement.clone();
}
@@ -0,0 +1,10 @@
export function bootstrap(formEditorApp) {
const formElement = formEditorApp
.getFormElementByIdentifierPath('example-form/page-1/name');
const propertyPath = formEditorApp
.buildPropertyPath('options.minimum', 'StringLength', 'validators', formElement);
// propertyPath = e.g. 'validators.0.options.minimum'
const value = formElement.get(propertyPath); // '1'
}
@@ -0,0 +1,6 @@
export function bootstrap(formEditorApp) {
// Returns 'Name'
const placeholder = formEditorApp
.getFormElementByIdentifierPath('example-form/page-1/name')
.get('properties.fluidAdditionalAttributes.placeholder');
}
@@ -0,0 +1,19 @@
// Illustrative snapshot of a FormElement model as it exists in memory at runtime.
// The actual object is managed by the FormElement class access it via
// formEditorApp.getFormElementByIdentifierPath() and the get()/set() API.
export const formElementSnapshot = {
identifier: 'name',
defaultValue: '',
label: 'Name',
type: 'Text',
properties: {
fluidAdditionalAttributes: {
placeholder: 'Name',
},
},
__parentRenderable: 'example-form/page-1 (filtered)',
__identifierPath: 'example-form/page-1/name',
validators: [
{ identifier: 'NotEmpty' },
],
};
@@ -0,0 +1,5 @@
export function bootstrap(formEditorApp) {
formEditorApp
.getFormElementByIdentifierPath('example-form/page-1/name')
.off('properties.fluidAdditionalAttributes.placeholder', 'my/custom/event');
}
@@ -0,0 +1,9 @@
export function bootstrap(formEditorApp) {
const element = formEditorApp
.getFormElementByIdentifierPath('example-form/page-1/name');
element.on('properties.fluidAdditionalAttributes.placeholder', 'my/custom/event');
// The next set() on that path will also publish 'my/custom/event'.
element.set('properties.fluidAdditionalAttributes.placeholder', 'New Placeholder');
}
@@ -0,0 +1,5 @@
export function bootstrap(formEditorApp) {
formEditorApp
.getFormElementByIdentifierPath('example-form/page-1/name')
.set('properties.fluidAdditionalAttributes.placeholder', 'New Placeholder');
}
@@ -0,0 +1,5 @@
export function bootstrap(formEditorApp) {
const formElement = formEditorApp
.getFormElementByIdentifierPath('example-form/page-1/name');
console.log(formElement.toString());
}
@@ -0,0 +1,5 @@
export function bootstrap(formEditorApp) {
formEditorApp
.getFormElementByIdentifierPath('example-form/page-1/name')
.unset('properties.fluidAdditionalAttributes.placeholder');
}
+97
View File
@@ -0,0 +1,97 @@
.. include:: /Includes.rst.txt
.. _apireference-formeditor:
.. _apireference-formeditor-basicjavascriptconcepts:
.. _apireference-formeditor-basicjavascriptconcepts-events:
.. _apireference-formeditor-stage:
===========
Form Editor
===========
This chapter is the developer reference for the TYPO3 backend form editor.
It covers the JavaScript extension points and the data model used by the
editor's TypeScript modules.
.. contents::
:depth: 1
:local:
.. _apireference-formeditor-architecture:
Architecture overview
=====================
The form editor consists of four cooperating TypeScript modules, each
responsible for one UI component:
.. list-table::
:header-rows: 1
:widths: 30 35 35
* - Module (import path)
- Component
- Responsibility
* - :js:`@typo3/form/backend/form-editor/view-model`
- —
- Central view model; wires DOM events and publishes/subscribes
to all cross-component events.
* - :js:`@typo3/form/backend/form-editor/stage-component`
- **Stage**
- Renders the abstract and preview views of the current form page.
* - :js:`@typo3/form/backend/form-editor/inspector-component`
- **Inspector**
- Renders the property editors for the selected form element.
* - :js:`@typo3/form/backend/form-editor/tree-component-adapter`
- **Structure tree**
- Wraps the TYPO3 backend tree web component and bridges its
events to the publish/subscribe bus.
* - :js:`@typo3/form/backend/form-editor/mediator`
- —
- Wires all publish/subscribe events to view-model actions.
Loaded automatically; replace via
:yaml:`dynamicJavaScriptModules.mediator` only when you need to
completely swap the event-wiring logic.
All modules communicate exclusively via a **publish/subscribe bus**
(:js:`PublisherSubscriber`). Direct module-to-module calls are avoided
so that extension code can hook into any point without modifying core
files.
.. _apireference-formeditor-custom-modules:
Registering a custom JavaScript module
=======================================
Custom modules must export a :js:`bootstrap` function. The form editor
calls this function once all built-in modules have loaded, passing the
central :js:`FormEditor` application object as the sole argument.
.. rst-class:: bignums-xxl
1. Create the JavaScript module
.. literalinclude:: _codesnippets/_bootstrap.js
:language: javascript
:caption: EXT:my_extension/Resources/Public/JavaScript/backend/form-editor/view-model.js
2. Register the module in the importmap
.. literalinclude:: _codesnippets/_JavaScriptModules.php
:language: php
:caption: EXT:my_extension/Configuration/JavaScriptModules.php
3. Tell the form editor to load the module
.. literalinclude:: _codesnippets/_prototype-setup.yaml
:language: yaml
:caption: EXT:my_extension/Configuration/Form/MyFormSet/config.yaml
.. toctree::
:maxdepth: 1
JavaScriptEvents/Index
StageTemplates/Index
FormElementModel/Index
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
export function bootstrap(formEditorApp) {
formEditorApp.getPublisherSubscriber().subscribe(
'view/inspector/editor/insert/perform',
(topic, args) => {
const [editorConfiguration, editorHtml] = args;
if (editorConfiguration.templateName !== 'Inspector-MyCustomEditor') {
return;
}
// Wire up your custom editor UI inside editorHtml
const input = editorHtml.querySelector('.my-custom-input');
if (input) {
input.addEventListener('change', (e) => {
formEditorApp
.getCurrentlySelectedFormElement()
.set(editorConfiguration.propertyPath, e.target.value);
});
}
},
);
}
@@ -0,0 +1,18 @@
prototypes:
standard:
formEditor:
dynamicJavaScriptModules:
additionalViewModelModules:
10: '@vendor/my-extension/backend/form-editor/view-model.js'
formEditorPartials:
Inspector-MyCustomEditor: 'Inspector/MyCustomEditor'
formEditorFluidConfiguration:
partialRootPaths:
100: 'EXT:my_extension/Resources/Private/Backend/Partials/FormEditor/'
formElementsDefinition:
Text:
formEditor:
editors:
600:
templateName: 'Inspector-MyCustomEditor'
myOption: 'example'
@@ -0,0 +1,3 @@
export function bootstrap(formEditorApp) {
formEditorApp.getPublisherSubscriber().publish('my/custom/event', ['arg1', 'arg2']);
}
@@ -0,0 +1,11 @@
export function bootstrap(formEditorApp) {
formEditorApp.getPublisherSubscriber().subscribe(
'core/formElement/somePropertyChanged',
(topic, args) => {
const [propertyPath, newValue, oldValue, identifierPath] = args;
if (propertyPath === 'label' && identifierPath?.startsWith('my-form/page-1/')) {
console.log('Label changed from', oldValue, 'to', newValue);
}
},
);
}
@@ -0,0 +1,4 @@
<div class="formeditor-element-body">
<div data-identifier="elementLabel"></div>
<div data-identifier="elementSummary"></div>
</div>
@@ -0,0 +1,24 @@
export function bootstrap(formEditorApp) {
formEditorApp.getPublisherSubscriber().subscribe(
'view/stage/abstract/render/template/perform',
(topic, args) => {
const [formElement, template] = args;
if (formElement.get('type') !== 'MyCustomElement') {
return;
}
const labelEl = template.querySelector('[data-identifier="elementLabel"]');
if (labelEl) {
labelEl.textContent =
formElement.get('label') || formElement.get('identifier');
}
const summaryEl = template.querySelector('[data-identifier="elementSummary"]');
if (summaryEl) {
summaryEl.textContent =
formElement.get('properties.myCustomProperty') ?? '';
}
},
);
}
@@ -0,0 +1,11 @@
prototypes:
standard:
formEditor:
dynamicJavaScriptModules:
additionalViewModelModules:
10: '@vendor/my-extension/backend/form-editor/view-model.js'
formEditorPartials:
FormElement-MyCustomElement: 'Stage/MyCustomElement'
formEditorFluidConfiguration:
partialRootPaths:
100: 'EXT:my_extension/Resources/Private/Backend/Partials/FormEditor/'
@@ -0,0 +1,11 @@
export function bootstrap(formEditorApp) {
const ps = formEditorApp.getPublisherSubscriber();
// Subscribe returns a token for later unsubscription
const token = ps.subscribe('view/ready', (topic, args) => {
// args is a typed tuple matching the event signature
});
// Unsubscribe
ps.unsubscribe(token);
}
@@ -0,0 +1,5 @@
export function bootstrap(formEditorApp) {
formEditorApp.getPublisherSubscriber().subscribe('view/ready', () => {
// Safe to call any formEditorApp API here.
});
}
@@ -0,0 +1,137 @@
.. include:: /Includes.rst.txt
.. _apireference-formeditor-stage-commonabstractformelementtemplates:
.. _apireference-formeditor-stagetemplates:
===============
Stage templates
===============
The **Stage** component renders each form element as an HTML item in the
abstract view. This section explains the two rendering strategies: the
modern web-component approach (recommended) and the legacy Fluid-partial
approach (deprecated).
.. contents::
:depth: 1
:local:
.. _apireference-formeditor-stagetemplates-webcomponent:
Built-in web component (recommended)
=====================================
When no :yaml:`formEditorPartials` entry exists for a form element type,
the Stage component automatically renders it using the built-in
:html:`<typo3-form-form-element-stage-item>` web component. The component
displays the element's label, type icon, validators, select options and
allowed MIME types without requiring any custom JavaScript.
.. tip::
For most custom form elements this is the recommended approach. Simply
omit :yaml:`formEditorPartials` from the prototype configuration and the
editor handles the rest.
Properties set on the web component from the :js:`FormElement` model:
.. list-table::
:header-rows: 1
:widths: 30 70
* - Property
- Source in FormElement model
* - :js:`elementType`
- Form element definition :yaml:`label`
* - :js:`elementLabel`
- :yaml:`label` (falls back to :yaml:`identifier`)
* - :js:`elementIconIdentifier`
- Form element definition :yaml:`iconIdentifier`
* - :js:`validators`
- :yaml:`validators` array (excludes ``NotEmpty``, shown via :js:`isRequired`)
* - :js:`isRequired`
- ``true`` when a ``NotEmpty`` validator is present
* - :js:`options`
- :yaml:`properties.options` (for select-like elements)
* - :js:`allowedMimeTypes`
- :yaml:`properties.allowedMimeTypes`
* - :js:`content`
- :yaml:`properties.text` or :yaml:`properties.contentElementUid`
* - :js:`isHidden`
- ``true`` when :yaml:`renderingOptions.enabled` is ``false``
.. _apireference-formeditor-stagetemplates-fluid:
Custom Fluid partial (advanced)
================================
If you need fully custom stage rendering for example to display a
proprietary summary of complex properties you can still provide a Fluid
partial and subscribe to the
:ref:`view/stage/abstract/render/template/perform <apireference-formeditor-jsevents-view-stage-abstract-render-template-perform>`
event to populate it with DOM manipulation.
The core Fluid partials are located in
:file:`EXT:form/Resources/Private/Backend/Partials/FormEditor/Stage/`.
.. warning::
The legacy stage rendering helpers
:js:`renderSimpleTemplateWithValidators()` and
:js:`renderSelectTemplates()` from
:js:`@typo3/form/backend/form-editor/stage-component` are deprecated
since TYPO3 v14.2 and will be removed in TYPO3 v15. Migrate to the
web component approach (omit :yaml:`formEditorPartials`) or implement
custom DOM manipulation in the event subscriber.
.. _apireference-formeditor-stage-commonabstractformelementtemplates-simpletemplate:
.. _apireference-formeditor-stagetemplates-fluid-simpletemplate:
Stage/SimpleTemplate (deprecated)
----------------------------------
Displays the element :yaml:`label`. When the element has validators, a
validator icon and their labels appear on hover/selection. Rendered via
the deprecated :js:`renderSimpleTemplateWithValidators()`.
.. deprecated:: 14.2
Use the :html:`<typo3-form-form-element-stage-item>` web component
by omitting :yaml:`formEditorPartials`, or implement custom DOM
manipulation in the
:ref:`view/stage/abstract/render/template/perform <apireference-formeditor-jsevents-view-stage-abstract-render-template-perform>`
subscriber. See Deprecation :issue:`109306`.
.. _apireference-formeditor-stage-commonabstractformelementtemplates-selecttemplate:
.. _apireference-formeditor-stagetemplates-fluid-selecttemplate:
Stage/SelectTemplate (deprecated)
----------------------------------
Extends ``Stage/SimpleTemplate`` by additionally listing the chosen option
labels from :yaml:`properties.options.*`. Rendered via the deprecated
:js:`renderSelectTemplates()`.
Example form element using select options:
.. literalinclude:: _codesnippets/_select-template.yaml
:language: yaml
The template partial contains a container with the path to read:
.. literalinclude:: _codesnippets/_select-template-partial.html
:language: html
For elements using a different array property (e.g. ``FileUpload`` with
:yaml:`properties.allowedMimeTypes`), adjust the :html:`data-template-property`
attribute accordingly:
.. literalinclude:: _codesnippets/_file-upload-partial.html
:language: html
The web component handles both cases automatically.
.. deprecated:: 14.2
Use the :html:`<typo3-form-form-element-stage-item>` web component
by omitting :yaml:`formEditorPartials`.
See `Deprecation: #109306 - Deprecate form editor stage template rendering functions <https://docs.typo3.org/permalink/changelog:deprecation-109306-1774010043>`_.
@@ -0,0 +1,2 @@
<div data-identifier="multiValueContainer"
data-template-property="properties.allowedMimeTypes"></div>
@@ -0,0 +1,2 @@
<div data-identifier="multiValueContainer"
data-template-property="properties.options"></div>
@@ -0,0 +1,7 @@
type: MultiCheckbox
identifier: multicheckbox-1
label: 'Multi checkbox'
properties:
options:
value1: label1
value2: label2
@@ -0,0 +1,9 @@
<?php
return [
'dependencies' => ['form'],
'imports' => [
'@vendor/my-extension/'
=> 'EXT:my_extension/Resources/Public/JavaScript/',
],
];
@@ -0,0 +1,10 @@
/**
* Custom form editor module for EXT:my_extension.
*/
export function bootstrap(formEditorApp) {
const ps = formEditorApp.getPublisherSubscriber();
ps.subscribe('view/ready', () => {
// Editor is fully initialised set up your custom logic here.
});
}
@@ -0,0 +1,6 @@
prototypes:
standard:
formEditor:
dynamicJavaScriptModules:
additionalViewModelModules:
10: '@vendor/my-extension/backend/form-editor/view-model.js'