Problem/Motivation

In #3494634: Compatibility between SDC and the Form API, we discussed 2 incompatibilities between SDC and the form API:

  • we can put a full form into a component slot, but we can't put a form element of a form defined outside the component
  • we can't define form element, directly usable with the Form API, with SDC. So we can't easily implement some design systems components like https://getbootstrap.com/docs/5.3/forms/checks-radios/#switches

Let's deal with the second point in this dedicated ticket.

Proposed resolution

https://www.drupal.org/docs/drupal-apis/form-api/form-render-elements

Form properties to evaluate:

  • #ajax: (array) Array of elements to specify Ajax behavior. See the Javascript API and AJAX Forms guides for more information.
  • #default_value: Default value for the element. See also #value.
  • #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.
  • #disabled: (bool) If TRUE, the element is shown but does not accept user input.
  • #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.
  • #required: (bool) Whether or not input is required on the element.
  • #required_error: (string) Override default error message "@field_title is required" will be used if this is undefined.
  • #title: (string) Title of the form element. Should be translated.
  • #title_display: (string) Where and how to display the #title.
  • #value: Used to set values that cannot be edited by the user

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

This will not be a missing feature, but a welcomed feature, however:

  • If really needed by the front developer, to implement UI logic, we can imagine so declarative ways:
    • inline twig transformation for #process
    • json schema for #element_validate
    • ...
  • If a back developer want to add some callbacks, it will be a applicative/business need. The form elements that will be created will be alterable like any other form elements.

So, it will be the opportunity to split UI logic from business logic, which is not currently the case in the Form API.

How do we create form elements (which are plugins too) from those SDC plugins? Plugin derivatives?

Remaining tasks

Let's start by trying to re-implement a few Core form element with SDC, some simple, some complex:

  • Textfield
  • Checkboxes
  • Checkbox
  • Actions
  • Submit button
  • ...

Out of scope

The scope of this ticket is not to override or replace existing form element but to create new form elements, aside the existing ones, from SDC. We hope one day all Core form elements will be defined as SDC components, but it will be other issues.

Also, we don't address the need of derivative configurable plugins like Field Widgets or WebformElement. It may be the purpose of some contrib modules.

Issue fork drupal-3508641

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

pdureau created an issue. See original summary.

grimreaper’s picture

Assigned: Unassigned » grimreaper
grimreaper’s picture

Issue summary: View changes
grimreaper’s picture

Issue summary: View changes
grimreaper’s picture

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

pdureau’s picture

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')]

The "inside" of form elements

23 of them are direct calls to 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:

<div{{ attributes.addClass(classes) }}>
  {{ element }}
</div>

input.html.twig:

<input{{ attributes }} />{{ children }}

For many render elements with a template suggestion system, but suggested templates are not used.

With template_preprocess_input in form.inc

textarea.html.twig:

<div{{ wrapper_attributes }}>
  <textarea{{ attributes }}>{{ value }}</textarea>
</div>

With template_preprocess_textarea in form.inc

select.html.twig:

<select{{ attributes }}>
  {% for option in options %}
    ...
  {% endfor %}
</select>

With template_preprocess_select in form.inc.

datetime-form.html.twig:

<div{{ attributes }}>
  {{ content }}
</div>

5 are using #theme_wrappers instead

./lib/Drupal/Core/Render/Element/Button.php-      '#theme_wrappers' => ['input__submit'],
./lib/Drupal/Core/Render/Element/Checkboxes.php-      '#theme_wrappers' => ['checkboxes'],
./lib/Drupal/Core/Render/Element/ImageButton.php-      '#theme_wrappers' => ['input__image_button'],
./lib/Drupal/Core/Render/Element/Radios.php-      '#theme_wrappers' => ['radios'],
./lib/Drupal/Core/Render/Element/VerticalTabs.php-      '#theme_wrappers' => ['vertical_tabs', 'form_element'],

checkboxes.html.twig:

<div{{ attributes.addClass('form-checkboxes') }}>{{ children }}</div>

With template_preprocess_checkboxes in form.inc

radios.html.twig:

<div{{ attributes }}>{{ children }}</div>

With template_preprocess_radios in form.inc

vertical-tabs.html.twig:

<div{{ attributes.setAttribute('data-vertical-tabs-panes', TRUE) }}>{{ children }}</div>

With template_preprocess_vertical_tabs in form.inc

Other form elements

The other 10 (9?) are:

./lib/Drupal/Core/Entity/Element/EntityAutocomplete.php:#[FormElement('entity_autocomplete')]
./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/Submit.php:#[FormElement('submit')]
./lib/Drupal/Core/Render/Element/Value.php:#[FormElement('value')]
./lib/Drupal/Core/Render/Element/Weight.php:#[FormElement('weight')]
./modules/language/src/Element/LanguageConfiguration.php:#[FormElement('language_configuration')]

How are they built? Do they have templates?

Wrappers

2 have custom wrapper

./lib/Drupal/Core/Datetime/Element/Datelist.php-      '#theme_wrappers' => ['datetime_wrapper'],
./lib/Drupal/Core/Datetime/Element/Datetime.php-      '#theme_wrappers' => ['datetime_wrapper'],

datetime-wrapper.html.twig:

{%
  set title_classes = [
    required ? 'js-form-required',
    required ? 'form-required',
  ]
%}
{% if title %}
  <h4{{ title_attributes.addClass(title_classes) }}>{{ title }}</h4>
{% endif %}
{{ content }}
{% if errors %}
  <div>
    {{ errors }}
  </div>
{% endif %}
{% if description %}
  <div{{ description_attributes }}>
    {{ description }}
  </div>
{% endif %}

19 are using standard form_element wrapper

./lib/Drupal/Core/Render/Element/Checkbox.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Color.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Date.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Email.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/File.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Item.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/MachineName.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Number.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/PasswordConfirm.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Password.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Radio.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Search.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Select.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Tel.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Textarea.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Textfield.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/Url.php-      '#theme_wrappers' => ['form_element'],
./modules/file/src/Element/ManagedFile.php-      '#theme_wrappers' => ['form_element'],
./lib/Drupal/Core/Render/Element/VerticalTabs.php-      '#theme_wrappers' => ['vertical_tabs', 'form_element'],

form-element.html.twig:

{%
  set classes = [
    'js-form-item',
    'form-item',
    'js-form-type-' ~ type|clean_class,
    'form-item-' ~ name|clean_class,
    'js-form-item-' ~ name|clean_class,
    title_display not in ['after', 'before'] ? 'form-no-label',
    disabled == 'disabled' ? 'form-disabled',
    errors ? 'form-item--error',
  ]
%}
{%
  set description_classes = [
    'description',
    description_display == 'invisible' ? 'visually-hidden',
  ]
%}
<div{{ attributes.addClass(classes) }}>
  {% if label_display in ['before', 'invisible'] %}
    {{ label }}
  {% endif %}
  {% if prefix is not empty %}
    <span class="field-prefix">{{ prefix }}</span>
  {% endif %}
  {% if description_display == 'before' and description.content %}
    <div{{ description.attributes }}>
      {{ description.content }}
    </div>
  {% endif %}
  {{ children }}
  {% if suffix is not empty %}
    <span class="field-suffix">{{ suffix }}</span>
  {% endif %}
  {% if label_display == 'after' %}
    {{ label }}
  {% endif %}
  {% if errors %}
    <div class="form-item--error-message">
      {{ errors }}
    </div>
  {% endif %}
  {% if description_display in ['after', 'invisible'] and description.content %}
    <div{{ description.attributes.addClass(description_classes) }}>
      {{ description.content }}
    </div>
  {% endif %}
</div>

What about the 16 (17?) others?

./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/Hidden.php:#[FormElement('hidden')]
./lib/Drupal/Core/Render/Element/ImageButton.php:#[FormElement('image_button')]
./lib/Drupal/Core/Render/Element/LanguageSelect.php:#[FormElement('language_select')]
./lib/Drupal/Core/Render/Element/PathElement.php:#[FormElement('path')]
./lib/Drupal/Core/Render/Element/Radios.php:#[FormElement('radios')]
./lib/Drupal/Core/Render/Element/Range.php:#[FormElement('range')]
./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/Token.php:#[FormElement('token')]
./lib/Drupal/Core/Render/Element/Value.php:#[FormElement('value')]
./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')]

Processing

20 have a custom valueCallback()

$ grep -r -A 1000  "\[FormElement(" . | grep ' valueCallback' | sort | grep -v test | awk '{print $1}'
./lib/Drupal/Core/Datetime/Element/Datelist.php-
./lib/Drupal/Core/Datetime/Element/Datetime.php-
./lib/Drupal/Core/Entity/Element/EntityAutocomplete.php-
./lib/Drupal/Core/Render/Element/Checkboxes.php-
./lib/Drupal/Core/Render/Element/Checkbox.php-
./lib/Drupal/Core/Render/Element/File.php-
./lib/Drupal/Core/Render/Element/ImageButton.php-
./lib/Drupal/Core/Render/Element/MachineName.php-
./lib/Drupal/Core/Render/Element/PasswordConfirm.php-
./lib/Drupal/Core/Render/Element/Password.php-
./lib/Drupal/Core/Render/Element/PathElement.php-
./lib/Drupal/Core/Render/Element/Radios.php-
./lib/Drupal/Core/Render/Element/Range.php-
./lib/Drupal/Core/Render/Element/Select.php-
./lib/Drupal/Core/Render/Element/Table.php-
./lib/Drupal/Core/Render/Element/Tableselect.php-
./lib/Drupal/Core/Render/Element/Textarea.php-
./lib/Drupal/Core/Render/Element/Textfield.php-
./lib/Drupal/Core/Render/Element/Token.php-
./modules/file/src/Element/ManagedFile.php-

29 have custom #process callback:

$ grep -r -A 1000  "\[FormElement(" . | grep "'#process'" | sort | grep -v test | awk '{print $1}'
./lib/Drupal/Core/Datetime/Element/Datelist.php-
./lib/Drupal/Core/Datetime/Element/Datetime.php-
./lib/Drupal/Core/Entity/Element/EntityAutocomplete.php-
./lib/Drupal/Core/Render/Element/Button.php-
./lib/Drupal/Core/Render/Element/Checkboxes.php-
./lib/Drupal/Core/Render/Element/Checkbox.php-
./lib/Drupal/Core/Render/Element/Color.php-
./lib/Drupal/Core/Render/Element/Date.php-
./lib/Drupal/Core/Render/Element/Email.php-
./lib/Drupal/Core/Render/Element/File.php-
./lib/Drupal/Core/Render/Element/Hidden.php-
./lib/Drupal/Core/Render/Element/MachineName.php-
./lib/Drupal/Core/Render/Element/Number.php-
./lib/Drupal/Core/Render/Element/PasswordConfirm.php-
./lib/Drupal/Core/Render/Element/Password.php-
./lib/Drupal/Core/Render/Element/Radio.php-
./lib/Drupal/Core/Render/Element/Radios.php-
./lib/Drupal/Core/Render/Element/Search.php-
./lib/Drupal/Core/Render/Element/Select.php-
./lib/Drupal/Core/Render/Element/Table.php-
./lib/Drupal/Core/Render/Element/Tableselect.php-
./lib/Drupal/Core/Render/Element/Tel.php-
./lib/Drupal/Core/Render/Element/Textarea.php-
./lib/Drupal/Core/Render/Element/Textfield.php-
./lib/Drupal/Core/Render/Element/Url.php-
./lib/Drupal/Core/Render/Element/VerticalTabs.php-
./lib/Drupal/Core/Render/Element/Weight.php-
./modules/file/src/Element/ManagedFile.php-
./modules/language/src/Element/LanguageConfiguration.php-

29 have custom #pre_render callback:

 $ grep -r -A 1000  "\[FormElement(" . | grep "'#pre_render" | sort | grep -v test | awk '{print $1}'
./lib/Drupal/Core/Datetime/Element/Datetime.php-
./lib/Drupal/Core/Render/Element/Button.php-
./lib/Drupal/Core/Render/Element/Checkboxes.php-
./lib/Drupal/Core/Render/Element/Checkbox.php-
./lib/Drupal/Core/Render/Element/Color.php-
./lib/Drupal/Core/Render/Element/Date.php-
./lib/Drupal/Core/Render/Element/Email.php-
./lib/Drupal/Core/Render/Element/File.php-
./lib/Drupal/Core/Render/Element/Hidden.php-
./lib/Drupal/Core/Render/Element/MachineName.php-
./lib/Drupal/Core/Render/Element/Number.php-
./lib/Drupal/Core/Render/Element/Password.php-
./lib/Drupal/Core/Render/Element/Radio.php-
./lib/Drupal/Core/Render/Element/Radios.php-
./lib/Drupal/Core/Render/Element/Range.php-
./lib/Drupal/Core/Render/Element/Search.php-
./lib/Drupal/Core/Render/Element/Select.php-
./lib/Drupal/Core/Render/Element/Table.php-
./lib/Drupal/Core/Render/Element/Tableselect.php-
./lib/Drupal/Core/Render/Element/Tel.php-
./lib/Drupal/Core/Render/Element/Textarea.php-
./lib/Drupal/Core/Render/Element/Textfield.php-
./lib/Drupal/Core/Render/Element/Token.php-
./lib/Drupal/Core/Render/Element/Url.php-
./lib/Drupal/Core/Render/Element/VerticalTabs.php-
./modules/file/src/Element/ManagedFile.php-

9 have #element_validate callbacks:

$ grep -r -A 1000  "\[FormElement(" . | grep "'#element_validate' => " | sort | grep -v test | awk '{print $1}' 
./lib/Drupal/Core/Datetime/Element/Datelist.php-
./lib/Drupal/Core/Datetime/Element/Datetime.php-
./lib/Drupal/Core/Render/Element/Color.php-
./lib/Drupal/Core/Render/Element/Email.php-
./lib/Drupal/Core/Render/Element/MachineName.php-
./lib/Drupal/Core/Render/Element/Number.php-
./lib/Drupal/Core/Render/Element/Table.php-
./lib/Drupal/Core/Render/Element/Url.php-
./modules/file/src/Element/ManagedFile.php-
pdureau’s picture

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.
grimreaper’s picture

Issue tags: +ddd2025

grimreaper’s picture

Status: Active » Needs work

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.

pdureau’s picture

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
grimreaper’s picture

StatusFileSize
new4.03 MB

Before cleaning my workspace, here is the result of friday at the end of DDD 2025.

demo

d34dman made their first commit to this issue’s fork.

d34dman’s picture

Status: Needs work » Needs review

The latest changes adds some test coverage for usage of SDC Component as Form element.

grimreaper’s picture

Status: Needs review » Needs work

Thanks for this first round of tests!

d34dman’s picture

pdureau’s picture

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...]
  ];  
}
pdureau’s picture

d34dman’s picture

@pdureau, thanks for summarizing it. I will add some test coverage for the support for "#element_validate". Keeping this as needs_work.

d34dman’s picture

Status: Needs work » Needs review
d34dman’s picture

Maybe we can think of adopting #htmx instead of #ajax?

nod_’s picture

That could be a good idea to only have the "modern" stuff, that would definitely help with BC headaches later on

pdureau’s picture

No update here but this issue is still working on 👍 We are still excited and we are still targeting Drupal 11.3

d34dman’s picture

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

pdureau’s picture

d34dman’s picture

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.

nod_’s picture

It is not going to be a drop in replacement see #3528440: Proof of concept, Replace ajax frontend implementation with htmx. 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.

d34dman’s picture

@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 ajaxhtmx 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.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

d34dman’s picture

@nod_, since this MR is in #3535173: Support dynamic forms using HTMX is merged, couldn't we use that for ajax and state support in this issue?

nod_’s picture

+1, yes

d34dman’s picture

@pdureau and @_node,

Thanks for the patience with me. Would it be possible to reduce the scope of this issue with explicit list of Elements and properties? Basically am trying to define the definition of done for this Issue so that I can complete the test and implementation in the upcoming MR

Form Elements in scope for target 11.3

I propose we remove "Actions" from scope as it somehow belong to category "container" like "fieldset" does. I feel we might want to approach this as a generic subject of use of containers/wrappers.

  • Textfield
  • Checkboxes
  • Checkbox
  • Submit button

Form Properties in Scope for target 11.3

we start with a list that we know for sure we want.

  • #title
  • #default_value
  • #disabled
  • #required
  • #description

Please let me know if you want to add more to this. Idea of keeping it simple means, we don't need to worry about backwards compatiblity. Example should we continue using "#value" or should we take the opportunity to refactor and bring "#read_only" or "#hidden" or something that make sense in SDC world, where it doesn't know about Drupal's internal.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

mglaman’s picture

Issue tags: +Chicago2026

I gave this a review and I think there's just a little tidying up that can be done

d34dman’s picture

Status: Needs work » Needs review
d34dman’s picture

The recent changes were basically stripping down the MR.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new2.39 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

grimreaper’s picture

In comment 7:

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.

So #ajax is out of scope.

As #disabled and #required_error been tested? Does it need a special treatment?

d34dman’s picture

Update:

In the tests, I have removed assertions around processed state. Since we don't do ajax, and we don't prohibit processing of element, i guess thats should be ok.

grimreaper’s picture

Assigned: grimreaper » Unassigned
Status: Needs work » Needs review

Hello,

As pipeline is green, back to Needs review.

Discussed with @pdureau about #disabled, #required_error, let's include that later in follow up issue if needed.

mglaman’s picture

Status: Needs review » Reviewed & tested by the community

I can't approve the MR, but all of my concerns were addressed and we have a great start. core/lib/Drupal/Core/Render/Element/ComponentElement.php contains minimal changes to form element support.

RTBC will probably be rejected due to missing CR

d34dman’s picture

Am creating the CR

d34dman’s picture

grimreaper’s picture

Thanks @d34dman for the CR :)

catch’s picture

This looks good to me, about 1:3 code to test ratio etc.

Because some scope has been cut here, do we need follow-ups to implement those things opened?

pdureau’s picture

Assigned: Unassigned » pdureau

I will commit it.

Because some scope has been cut here, do we need follow-ups to implement those things opened?

The #ajax/#htmx debate? I am meeting Florent (@grimreaper) and Shibin (d34dman) at DDD Athens, we will plan the eventual follow-ups.

pdureau’s picture

Category: Task » Feature request

  • pdureau committed 671d81e9 on main
    feat: #3508641 Define form elements from SDC
    
    By: pdureau
    By: grimreaper...
pdureau’s picture

Assigned: pdureau » Unassigned
Status: Reviewed & tested by the community » Fixed

Committed 671d81e and pushed to main. Thanks!

Do we backport to 11.x?

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

smustgrave’s picture

Could we please backport to 11.4 :)

godotislate’s picture

Version: main » 11.x-dev
Status: Fixed » Patch (to be ported)

Not seeing anything 12.x specific, so we should try for 11.x backport.

grimreaper’s picture

Issue tags: +DevDaysAthens2026
mherchel’s picture

Congrats everyone! Should the change record be published?

pdureau’s picture

Assigned: Unassigned » pdureau

I will port to 11.x

kentr’s picture

Because some scope has been cut here, do we need follow-ups to implement those things opened?

I didn't see #title in the MR or the CR. Did it get implemented?

Asking because my understanding is without #title on the element, validation error messages will be empty.

I played around with some examples on a real form, and that is what I observed.

Might only apply on 11.4+ with HTML5 form validation disabled, or with the Disable HTML5 validation module.

Here's a screenshot. Both fields are required, and the form was submitted with both fields empty. There should be error messages for both fields, but there's only one error message.

Also noticed that the input doesn't have the error class or the aria-invalid attribute.

Here's the gist of the element definition, just an element from FormTestValidateRequiredForm with '#required' => TRUE added.

    $form['sdc_input_with_label_no_title'] = [
      '#type' => 'component',
      '#component' => 'sdc_theme_test:input',
      '#required' => TRUE,
      '#slots' => [
        'label' => [
          '#type' => 'html_tag',
          '#tag' => 'span',
          '#attributes' => [
            'id' => 'test_data_label_container_no_title',
          ],
          'content' => [
            '#markup' => 'Label slot for SDC Input no #title',
          ],
        ],
      ],
    ];

I'm happy to be told that I'm doing something wrong. If I'm not, this shouldn't go into a release until those are worked out. They're big UX and accessibility issues.

Adding static::setAttributes($element); to ComponentElement::mergeElementAttributesToPropAttributes() gets part of the way to having the error class or the aria-invalid attribute.

pdureau’s picture

Thanks Kent for your interest about this contribution. Me and Florent (@grimreaper) have studied your feedback.

Form properties

I didn't see #title in the MR or the CR. Did it get implemented?

There are many form properties we didn't automatically manage yet, because the merged commit only support:

  • #name (string)
  • #required (bool)
  • #value

#title may be a good candidate for a future addition in form_state "magic" prop, it seems our model is extensible enough to not block this. Can you create a follow-up issue? We will be happy to discuss there.

Attributes

Also noticed that the input doesn't have the error class or the aria-invalid attribute.

We are already injecting #attributes to the components and we expect component authors to use it as "wrapper attributes" or "item attributes". We know it is a but light and we can go further in follow-up issues.

However, let's be careful about not being too much normative here, the UI components own their markup, which is most of the time defined upstream, sometimes in a design system. So, instead of sending Drupal markup, it would be better to send some additional information about the form state.

For example, the Drupal .error class may not be the expected class in the component, which may be using something like .is-invalid (in Bootstrap) or .input-error (in Daisy UI), so it may be better to inject form_state.value.invalid boolean value.

In our opinion, we must be normative for properties filling those requirements:

  • keep the Form API working
  • keep the accessibility level Drupal is targeting

And be loose for everything else.