Define form elements from SDC

Created on 24 February 2025, 2 months ago

Problem/Motivation

In ๐Ÿ“Œ Compatibility between SDC and the Form API Active , 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 applictaive/business need. ยตour 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:

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.

๐Ÿ“Œ Task
Status

Active

Version

11.0 ๐Ÿ”ฅ

Component

single-directory components

Created by

๐Ÿ‡ซ๐Ÿ‡ทFrance pdureau Paris

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @pdureau
  • ๐Ÿ‡ซ๐Ÿ‡ทFrance Grimreaper France ๐Ÿ‡ซ๐Ÿ‡ท
  • ๐Ÿ‡ซ๐Ÿ‡ทFrance Grimreaper France ๐Ÿ‡ซ๐Ÿ‡ท
  • ๐Ÿ‡ซ๐Ÿ‡ทFrance Grimreaper France ๐Ÿ‡ซ๐Ÿ‡ท
  • ๐Ÿ‡ซ๐Ÿ‡ท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.
  • ๐Ÿ‡ซ๐Ÿ‡ทFrance Grimreaper France ๐Ÿ‡ซ๐Ÿ‡ท
  • Pipeline finished with Canceled
    8 days ago
    Total: 33s
    #476440
  • ๐Ÿ‡ซ๐Ÿ‡ท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.

Production build 0.71.5 2024