- Issue created by @pdureau
- ๐ซ๐ทFrance Grimreaper France ๐ซ๐ท
Proposed resolution:
In the YAML, a new "form" root level entry to contain those form "props" (ajax, description, etc.).
Or
Normal props declaration, and a mapping system to say "this 'message' prop/slot is sourced by 'description' form property".
- ๐ซ๐ทFrance pdureau Paris
Let's have a look on all 37 Render Elements from Core:
$ grep -r "\[FormElement(" . | sort | grep -v test ./lib/Drupal/Core/Datetime/Element/Datelist.php:#[FormElement('datelist')] ./lib/Drupal/Core/Datetime/Element/Datetime.php:#[FormElement('datetime')] ./lib/Drupal/Core/Entity/Element/EntityAutocomplete.php:#[FormElement('entity_autocomplete')] ./lib/Drupal/Core/Render/Element/Button.php:#[FormElement('button')] ./lib/Drupal/Core/Render/Element/Checkboxes.php:#[FormElement('checkboxes')] ./lib/Drupal/Core/Render/Element/Checkbox.php:#[FormElement('checkbox')] ./lib/Drupal/Core/Render/Element/Color.php:#[FormElement('color')] ./lib/Drupal/Core/Render/Element/Date.php:#[FormElement('date')] ./lib/Drupal/Core/Render/Element/Email.php:#[FormElement('email')] ./lib/Drupal/Core/Render/Element/File.php:#[FormElement('file')] ./lib/Drupal/Core/Render/Element/Hidden.php:#[FormElement('hidden')] ./lib/Drupal/Core/Render/Element/ImageButton.php:#[FormElement('image_button')] ./lib/Drupal/Core/Render/Element/Item.php:#[FormElement('item')] ./lib/Drupal/Core/Render/Element/LanguageSelect.php:#[FormElement('language_select')] ./lib/Drupal/Core/Render/Element/MachineName.php:#[FormElement('machine_name')] ./lib/Drupal/Core/Render/Element/Number.php:#[FormElement('number')] ./lib/Drupal/Core/Render/Element/PasswordConfirm.php:#[FormElement('password_confirm')] ./lib/Drupal/Core/Render/Element/Password.php:#[FormElement('password')] ./lib/Drupal/Core/Render/Element/PathElement.php:#[FormElement('path')] ./lib/Drupal/Core/Render/Element/Radio.php:#[FormElement('radio')] ./lib/Drupal/Core/Render/Element/Radios.php:#[FormElement('radios')] ./lib/Drupal/Core/Render/Element/Range.php:#[FormElement('range')] ./lib/Drupal/Core/Render/Element/Search.php:#[FormElement('search')] ./lib/Drupal/Core/Render/Element/Select.php:#[FormElement('select')] ./lib/Drupal/Core/Render/Element/Submit.php:#[FormElement('submit')] ./lib/Drupal/Core/Render/Element/Table.php:#[FormElement('table')] ./lib/Drupal/Core/Render/Element/Tableselect.php:#[FormElement('tableselect')] ./lib/Drupal/Core/Render/Element/Tel.php:#[FormElement('tel')] ./lib/Drupal/Core/Render/Element/Textarea.php:#[FormElement('textarea')] ./lib/Drupal/Core/Render/Element/Textfield.php:#[FormElement('textfield')] ./lib/Drupal/Core/Render/Element/Token.php:#[FormElement('token')] ./lib/Drupal/Core/Render/Element/Url.php:#[FormElement('url')] ./lib/Drupal/Core/Render/Element/Value.php:#[FormElement('value')] ./lib/Drupal/Core/Render/Element/VerticalTabs.php:#[FormElement('vertical_tabs')] ./lib/Drupal/Core/Render/Element/Weight.php:#[FormElement('weight')] ./modules/file/src/Element/ManagedFile.php:#[FormElement('managed_file')] ./modules/language/src/Element/LanguageConfiguration.php:#[FormElement('language_configuration')]
Form elements with #theme
23 of them are wrapper around theme hooks:
$ grep -r -A 1000 "\[FormElement(" . | grep '#theme..=' | sort | grep -v test ./lib/Drupal/Core/Datetime/Element/Datelist.php- '#theme' => 'datetime_form', ./lib/Drupal/Core/Datetime/Element/Datetime.php- '#theme' => 'datetime_form', ./lib/Drupal/Core/Render/Element/Checkbox.php- '#theme' => 'input__checkbox', ./lib/Drupal/Core/Render/Element/Color.php- '#theme' => 'input__color', ./lib/Drupal/Core/Render/Element/Date.php- '#theme' => 'input__date', ./lib/Drupal/Core/Render/Element/Email.php- '#theme' => 'input__email', ./lib/Drupal/Core/Render/Element/File.php- '#theme' => 'input__file', ./lib/Drupal/Core/Render/Element/Hidden.php- '#theme' => 'input__hidden', ./lib/Drupal/Core/Render/Element/MachineName.php- '#theme' => 'input__textfield', ./lib/Drupal/Core/Render/Element/Number.php- '#theme' => 'input__number', ./lib/Drupal/Core/Render/Element/Password.php- '#theme' => 'input__password', ./lib/Drupal/Core/Render/Element/Radio.php- '#theme' => 'input__radio', ./lib/Drupal/Core/Render/Element/Range.php- '#theme' => 'input__range', ./lib/Drupal/Core/Render/Element/Search.php- '#theme' => 'input__search', ./lib/Drupal/Core/Render/Element/Select.php- '#theme' => 'select', ./lib/Drupal/Core/Render/Element/Table.php- '#theme' => 'table', ./lib/Drupal/Core/Render/Element/Tableselect.php- '#theme' => 'table__tableselect', ./lib/Drupal/Core/Render/Element/Tel.php- '#theme' => 'input__tel', ./lib/Drupal/Core/Render/Element/Textarea.php- '#theme' => 'textarea', ./lib/Drupal/Core/Render/Element/Textfield.php- '#theme' => 'input__textfield', ./lib/Drupal/Core/Render/Element/Token.php- '#theme' => 'input__hidden', ./lib/Drupal/Core/Render/Element/Url.php- '#theme' => 'input__url', ./modules/file/src/Element/ManagedFile.php- '#theme' => 'file_link', ./modules/file/src/Element/ManagedFile.php- '#theme' => 'file_managed_file',
file_managed_file:
{{ element }}input.html.twig:
<input{{ attributes }} />{{ children }}
textarea.html.twig:
{{ value }}select.html.twig:
<select{{ attributes }}> {% for option in options %} ... {% endfor %} </select>
datetime-form.html.twig:
<div{{ attributes }}> {{ content }} </div>
Other form elements
The other 14 (15?) are:
./lib/Drupal/Core/Entity/Element/EntityAutocomplete.php:#[FormElement('entity_autocomplete')] ./lib/Drupal/Core/Render/Element/Button.php:#[FormElement('button')] ./lib/Drupal/Core/Render/Element/Checkboxes.php:#[FormElement('checkboxes')] ./lib/Drupal/Core/Render/Element/ImageButton.php:#[FormElement('image_button')] ./lib/Drupal/Core/Render/Element/Item.php:#[FormElement('item')] ./lib/Drupal/Core/Render/Element/LanguageSelect.php:#[FormElement('language_select')] ./lib/Drupal/Core/Render/Element/MachineName.php:#[FormElement('machine_name')] ./lib/Drupal/Core/Render/Element/PathElement.php:#[FormElement('path')] ./lib/Drupal/Core/Render/Element/PasswordConfirm.php:#[FormElement('password_confirm')] ./lib/Drupal/Core/Render/Element/Radios.php:#[FormElement('radios')] ./lib/Drupal/Core/Render/Element/Submit.php:#[FormElement('submit')] ./lib/Drupal/Core/Render/Element/Value.php:#[FormElement('value')] ./lib/Drupal/Core/Render/Element/VerticalTabs.php:#[FormElement('vertical_tabs')] ./lib/Drupal/Core/Render/Element/Weight.php:#[FormElement('weight')] ./modules/language/src/Element/LanguageConfiguration.php:#[FormElement('language_configuration')]
- ๐ซ๐ทFrance pdureau Paris
Current state of our investigation.
Slot or prop:
- #description: (string) Help or description text for the element. In an ideal user interface, the #title should be enough to describe the element, so most elements should not have a description; if you do need one, make sure it is translated. If it is not already wrapped in a safe markup object, it will be filtered for XSS safety.
- #description_display: (string) Where and how to display the #description.
- #prefix: (string) Prefix to display before the HTML input element. Should be translated, normally. If it is not already wrapped in a safe markup object, will be filtered for XSS safety.
- #suffix: (string) Suffix to display after the HTML input element. Should be translated, normally. If it is not already wrapped in a safe markup object, will be filtered for XSS safety.
- #title: (string) Title of the form element. Should be translated.
- #title_display: (string) Where and how to display the #title.
Already covered ion our POC:
- #name
- #default_value: Default value for the element. See also #value.
- #required: (bool) Whether or not input is required on the element.
- #value: Used to set values that cannot be edited by the user
To evaluate:
- #ajax: (array) Array of elements to specify Ajax behavior. See the Javascript API and AJAX Forms guides for more information.
- #disabled: (bool) If TRUE, the element is shown but does not accept user input.
- #required_error: (string) Override default error message "@field_title is required" will be used if this is undefined.
- Merge request !11876Draft: Issue #3508641 by pdureau, grimreaper: Define form elements from SDC โ (Open) created by Grimreaper
- ๐ซ๐ทFrance Grimreaper France ๐ซ๐ท
MR created from https://git.drupalcode.org/project/drupal/-/merge_requests/11866, so now in this other issue MR, I can remove what is purely to have form element as SDC component.
- ๐ซ๐ทFrance pdureau Paris
For information, Grimreaper is currently testing his proposal:
- With a "normal" form fully built in a PHP class
- As Field Widgets, using an UI Patterns โ mechanism (context-sensitive data sources, to retrieve and check field properties), but it will be relevant for other SDC usage
- ๐ซ๐ทFrance Grimreaper France ๐ซ๐ท
Before cleaning my workspace, here is the result of friday at the end of DDD 2025.
- Status changed to Needs work
about 2 months ago 10:39am 3 June 2025 - First commit to issue fork.
- ๐ฉ๐ชGermany D34dMan Hamburg
The latest changes adds some test coverage for usage of SDC Component as Form element.
- ๐ซ๐ทFrance Grimreaper France ๐ซ๐ท
Thanks for this first round of tests!
- ๐ซ๐ทFrance pdureau Paris
The other ones are internal, or not self-contained, or needs PHP. That means form elements created from SDC doesn't have:
- #after_build: (array) Array of callables or function names, which are called after the element is built. Arguments: $element, $form_state.
- #element_validate: (array) Array of callables or function names, which are called to validate the input. Arguments: $element, $form_state, $form.
- #process: (array) Array of callables or function names, which are called during form building. Arguments: $element, $form_state, $form.
- #value_callback: (callable) Callable or function name, which is called to transform the raw user input to the element's value. Arguments: $element, $input, $form_state.
- ...
After a talk with @d34dman we still believe those "processing" mechanism don't belong to the SDC, but an usage of the SDC by an applicative logic (so, most a time, as the return value of a plugin) must be able to add those form properties, which will be executed as they are normally are in a form element.
Example:
WidgetInterface::formElement(...) { // Some applicative logic return [ "#type" => 'component', '#component' => 'foo:date', '#slots' => [...], '#props' => [...], '#element_validate' => [...callback...] ]; }
- ๐ฉ๐ชGermany D34dMan Hamburg
@pdureau, thanks for summarizing it. I will add some test coverage for the support for "#element_validate". Keeping this as needs_work.
- ๐ฉ๐ชGermany D34dMan Hamburg
Maybe we can think of adopting #htmx instead of #ajax?
- ๐ซ๐ทFrance nod_ Lille
That could be a good idea to only have the "modern" stuff, that would definitely help with BC headaches later on
- ๐ซ๐ทFrance pdureau Paris
No update here but this issue is still working on ๐ We are still excited and we are still targeting Drupal 11.3
- ๐ฉ๐ชGermany D34dMan Hamburg
@pdureau,
Am not up-to-date regarding htmx in Drupal. Is there some plan which we can rely on for defining how ajax support can be brought about?
- ๐ซ๐ทFrance pdureau Paris
@d34dman A lot of exciting work here: ๐ฑ Gradually replace Drupal's AJAX system with HTMX Active
- ๐ฉ๐ชGermany D34dMan Hamburg
Please don't consider this as a criticism for the work being done by htmx team.
I find the way htmx is getting into Drupal quite counter productive for use case of SDC. If we look at HTMX spec, the idea is to be able to use it in a declarative way in html. So those dynamic instructions could be incorporated into Component itself.
Case in point, consider the example from https://htmx.org/docs/
<button hx-post="/clicked" hx-trigger="click" hx-target="#parent-div" hx-swap="outerHTML"> Click Me! </button>
This translates in my mind, the implementation basically becomes
// Declarative implementation in Component // Add supporting callback in a PHP controller
An equivalent in Drupal Ajax (status quo) would be (avoiding actual implementation for clarity),
// Inside Form Class use appropriate FormElement // Inside Form Class add some declaration via "#ajax" key // Supported in hook_form_alter // Template (*.html.twig) prints "attributes"
And hence my dilemma in figuring out how this would translate.
This is an architecture problem where, how do we maintain the separation of business logic away from SDC component and support htmx.
We don't know yet, if htmx is going to be drop in replacement of #ajax in Drupal. If that be the case, we can implement ajax support as it is now, and swap it with htmx based on a promise that htmx would be drop in replacement of ajax.
- ๐ซ๐ทFrance nod_ Lille
It is not going to be a drop in replacement see โจ Replace ajax frontend implementation with htmx Active . Trying to be backwards compatible is not reasonable. We tried that a few years ago with jQuery UI and autocomplete. Today autocomplete is still jQuery UI.
Going with htmx does mean we loose some separation of concerns for locality of behavior: https://htmx.org/essays/locality-of-behaviour/ Where we draw the line is necessarily going to change.
- ๐ฉ๐ชGermany D34dMan Hamburg
@nod_
It is not going to be a drop in replacement see
I stand corrected. Thats fresh news to me.
Does this mean, we can keep the ajax support for form elements when using "SDC" out of scope of this issue? We can create a follow up issue to track/guide users on how htmx can be implemented in SDC component in a general way. My reasoning being, it (implementation of htmx, aka dynamic behaviour) need not be limited to Form Element at all.