Active
Project:
Read-only Field Widget
Version:
2.1.1
Component:
Code
Priority:
Major
Category:
Bug report
Assigned:
Unassigned
Reporter:
Created:
10 Oct 2026 at 10:57 UTC
Updated:
10 Oct 2026 at 10:57 UTC
Jump to comment: Most recent
ReadonlyFieldWidget fails in AJAX rebuilds due to missing default settings and unsafe formatter settings access
In Drupal 10/11, the ReadonlyFieldWidget triggers warnings during AJAX form rebuilds:
Warning: Undefined array key "text_default"
Warning: Undefined array key "<formatter_type>"This happens because:
The result is PHP warnings and broken AJAX responses when the widget is used inside forms with AJAX callbacks.
A patch is attached that:
nsures the widget behaves correctly in both full form builds and AJAX rebuilds.
This makes the widget fully compatible with Drupal 10/11 and prevents runtime warnings.
I have made changes in readonly_field_widget/src/Plugin/Field/FieldWidget/ReadonlyFieldWidget.php: My changes is commented with "Uv516" in the code.
<?php
namespace Drupal\readonly_field_widget\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\EntityDisplayBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* Plugin implementation of the 'readonly_field_widget' widget.
*
* @FieldWidget(
* id = "readonly_field_widget",
* label = @Translation("Readonly"),
* weight = 1,
* field_types = {
* }
* )
*/
class ReadonlyFieldWidget extends WidgetBase implements ContainerFactoryPluginInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The formatter plugin manager.
*
* @var \Drupal\Core\Field\FormatterPluginManager
*/
protected $fieldFormatterManager;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* {@inheritdoc}
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, $field_formatter_manager, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
$this->fieldFormatterManager = $field_formatter_manager;
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
/**
// Uv516
public static function defaultSettings() {
return [
'label' => 'above',
'formatter_type' => NULL,
'formatter_settings' => [],
'formatter_third_party_settings' => [],
'empty_text' => NULL,
'show_description' => FALSE,
'error_validation' => FALSE,
];
}*/
public static function defaultSettings() { // Uv516
return [
'label' => 'above',
'formatter_type' => NULL,
'formatter_settings' => [],
'formatter_third_party_settings' => [],
'empty_text' => NULL,
'show_description' => FALSE,
'error_validation' => FALSE,
// ← Din manglende default, som fjerner warningen
'text_default' => '',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['third_party_settings'],
$container->get('plugin.manager.field.formatter'),
$container->get('entity_type.manager'),
$container->get('module_handler'),
);
}
/**
* {@inheritdoc}
*/
protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
return $this->formSingleElement($items, 0, [], $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$initial_value = $items->getValue();
if ($this->isDefaultValueWidget($form_state)) {
$element['message'] = [
'#theme' => 'status_messages',
'#message_list' => [
'status' => [$this->t('Widget is set to Read-Only, switch the widget to something editable in order to set default values')],
],
];
}
$entity_type = $items->getEntity()->getEntityType()->id();
$view_builder = $this->entityTypeManager->getViewBuilder($entity_type);
$formatter_type = $this->getSetting('formatter_type');
$formatter_settings = $this->getSetting('formatter_settings');
$third_party_settings = $this->getSetting('formatter_third_party_settings');
// ⭐ Rettelse: Beskyt opslaget i formatter_settings
$settings = $formatter_type && isset($formatter_settings[$formatter_type])
? $formatter_settings[$formatter_type]
: [];
$options = [
'type' => $formatter_type,
'label' => $this->getSetting('label'),
// ⭐ Brug de beskyttede settings
'settings' => $settings,
// ⭐ Beskyt third-party settings
'third_party_settings' => $third_party_settings ?? [],
];
$element['readonly_field'] = $view_builder->viewField($items, $options);
// (Resten af metoden uændret)
if ($this->getSetting('empty_text') && $items->isEmpty()) {
$element['empty_text'] = [
'#type' => 'container',
['#markup' => $this->getSetting('empty_text')],
'#attributes' => [
'class' => ['empty-text'],
],
];
}
if ($this->getSetting('show_description') && !$items->isEmpty()) {
$element['description'] = [
'#type' => 'container',
['#markup' => $this->getFilteredDescription()],
'#attributes' => [
'class' => ['description', 'form-item__description'],
],
];
}
$element['#theme_wrappers'] = ['form_element'];
$items->setValue($initial_value);
return $element;
}
public function formElement_uv516_deprecated(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$initial_value = $items->getValue();
if ($this->isDefaultValueWidget($form_state)) {
$element['message'] = [
'#theme' => 'status_messages',
'#message_list' => [
'status' => [$this->t('Widget is set to Read-Only, switch the widget to something editable in order to set default values')],
],
];
}
$entity_type = $items->getEntity()->getEntityType()->id();
/**
* @var \Drupal\Core\Entity\EntityViewBuilderInterface $view_builder
*/
$view_builder = $this->entityTypeManager->getViewBuilder($entity_type);
$formatter_type = $this->getSetting('formatter_type');
$formatter_settings = $this->getSetting('formatter_settings');
$third_party_settings = $this->getSetting('formatter_third_party_settings');
$settings = $formatter_type && $formatter_settings[$formatter_type] ? $formatter_settings[$formatter_type] : []; // <- ny linje 132
$options = [
'type' => $formatter_type,
'label' => $this->getSetting('label'),
// 'settings' => $formatter_settings[$formatter_type] ?? [], // <- linje 132
'settings' => $settings // Uv516
? $formatter_settings[$formatter_type]
: [],
'third_party_settings' => $third_party_settings ?? [],
];
$element['readonly_field'] = $view_builder->viewField($items, $options);
// Show empty text if field has no value and the option is set.
if ($this->getSetting('empty_text') && $items->isEmpty()) {
$element['empty_text'] = [
'#type' => 'container',
['#markup' => $this->getSetting('empty_text')],
'#attributes' => [
'class' => ['empty-text'],
],
];
}
// Show description only if there are items to show too.
if ($this->getSetting('show_description') && !$items->isEmpty()) {
$element['description'] = [
'#type' => 'container',
['#markup' => $this->getFilteredDescription()],
'#attributes' => [
'class' => ['description', 'form-item__description'],
],
];
}
// Place our read-only widget inside the standard form-element template.
$element['#theme_wrappers'] = ['form_element'];
// Some formatters modify the field values when viewed
// (e.g. EntityReferenceFormatterBase) which can cause errors with some
// forms (e.g. the default value form), so set the item values back to
// their initial values here.
$items->setValue($initial_value);
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$field_type_formatters = $this->fieldFormatterManager->getOptions($this->fieldDefinition->getType());
$field_type_definitions = $this->fieldFormatterManager->getDefinitions();
$formatters = [];
foreach ($field_type_formatters as $formatter_type => $formatter_label) {
if (!empty($field_type_definitions[$formatter_type])
&& $field_type_definitions[$formatter_type]['class']::isApplicable($this->fieldDefinition)) {
$formatters[$formatter_type] = $formatter_label;
}
}
$field_name = $this->fieldDefinition->getName();
$element = [
'label' => [
'#title' => $this->t('Label'),
'#type' => 'select',
'#options' => $this->labelOptions(),
'#default_value' => $this->getSetting('label'),
],
'formatter_type' => [
'#prefix' => $this->getSetting('formatter_type'),
'#title' => $this->t('Format'),
'#type' => 'select',
'#options' => $formatters,
'#default_value' => $this->getFormatterInstance()->getPluginId(),
'#ajax' => [
'event' => 'change',
'callback' => [$this, 'ajaxUpdateFormatterSettings'],
],
],
'error_validation' => [
'#title' => $this->t('Error Validation'),
'#description' => $this->t('Maintain field error validation.'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('error_validation'),
],
'show_description' => [
'#title' => $this->t('Show Description'),
'#description' => $this->t('Show the configured description under widget.'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('show_description'),
],
'empty_text' => [
'#title' => $this->t('Empty text'),
'#description' => $this->t('Enter text to display if the field is empty.'),
'#type' => 'textarea',
'#default_value' => $this->getSetting('empty_text'),
],
'formatter_settings' => [
'#type' => 'container',
'#attributes' => ['class' => 'row-formatter-settings'],
],
];
$type_select_parents = [
'fields', $field_name,
'settings_edit_form',
'settings',
'formatter_type',
];
$formatter_plugin_id = $form_state->getValue($type_select_parents, $this->getFormatterInstance()->getPluginId());
$formatter_plugin = $this->getFormatterInstance($formatter_plugin_id);
$settings_form = $formatter_plugin->settingsForm($form, $form_state);
if (!empty($settings_form)) {
$label = $formatter_plugin->getPluginDefinition()['label'] ?? '';
$element['formatter_settings'][$formatter_plugin_id] = [
'#type' => 'fieldset',
'#title' => $label . ' ' . $this->t('Settings'),
] + $settings_form;
}
$field_definition = $this->fieldDefinition;
$this->moduleHandler->invokeAllWith(
'field_formatter_third_party_settings_form',
function (callable $hook, string $module) use (&$element, $formatter_plugin, $field_definition, $form, $form_state) {
$element['formatter_third_party_settings'][$module] = $hook(
$formatter_plugin,
$field_definition,
EntityDisplayBase::CUSTOM_MODE,
$form,
$form_state,
);
}
);
return $element;
}
/**
* Ajax update so the selected formatter form can re-render.
*/
public function ajaxUpdateFormatterSettings(array &$form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$parents = $triggering_element['#array_parents'];
array_pop($parents);
$parents[] = 'formatter_settings';
$settings_form_container = NestedArray::getValue($form, $parents);
$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand(".row-formatter-settings", $settings_form_container));
return $response;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$formatters = $this->fieldFormatterManager->getOptions($this->fieldDefinition->getType());
$label_options = $this->labelOptions();
$plugin = $this->getFormatterInstance();
if ($plugin) {
$summary = $plugin->settingsSummary();
$formatter_type = $this->getSetting('formatter_type');
/**
// Uv516
if (isset($formatters[$formatter_type])) {
$summary = [
$this->t('Format: @format', ['@format' => $formatters[$formatter_type]]),
] + $summary;
}*/
if ($formatter_type !== null && isset($formatters[$formatter_type])) { // Uv516
$summary = [
$this->t('Format: @format', ['@format' => $formatters[$formatter_type]]),
] + $summary;
}
}
$summary[] = $this->t('Label: @label', [
'@label' => $label_options[$this->getSetting('label')],
]);
$summary[] = $this->t('Show Description: @show_desc', [
'@show_desc' => $this->getSetting('show_description') ? $this->t('Yes') : $this->t('No'),
]);
return $summary;
}
/**
* Retrieves a formatter plugin instance.
*
* @param string|null $plugin_id
* The plugin_id for the formatter.
*
* @return \Drupal\Core\Field\FormatterInterface
* A formatter plugin instance.
*/
private function getFormatterInstance(?string $plugin_id = NULL) {
$settings = $this->getSetting('formatter_settings');
if (!is_array($settings)) {
$settings = []; // Uv516
}
$third_party_settings = $this->getSetting('formatter_third_party_settings') ?? [];
if (empty($plugin_id)) {
$plugin_id = $this->getSetting('formatter_type');
}
$options = [
'view_mode' => 'default',
'field_definition' => $this->fieldDefinition,
'configuration' => [
'type' => $plugin_id,
// Uv516 'settings' => $settings[$plugin_id] ?? [],
'settings' => $plugin_id !==null && $plugin_id !=='' && is_array($settings[$plugin_id] ?? null) ? $settings[$plugin_id] : [], // Uv516
'third_party_settings' => $third_party_settings,
],
];
return $this->fieldFormatterManager->getInstance($options);
}
/**
* Returns label options for field formatters.
*
* @return array
* The label options
*/
private function labelOptions() {
return [
'above' => $this->t('Above'),
'inline' => $this->t('Inline'),
'hidden' => '- ' . $this->t('Hidden') . ' -',
'visually_hidden' => '- ' . $this->t('Visually Hidden') . ' -',
];
}
/**
* {@inheritdoc}
*/
public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) {
if (!empty($this->getSetting('error_validation'))) {
return parent::errorElement($element, $error, $form, $form_state);
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
if (!empty($this->getSetting('error_validation'))) {
return parent::flagErrors($items, $violations, $form, $form_state);
}
}
/**
* {@inheritdoc}
*/
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
parent::extractFormValues($items, $form, $form_state);
if ($this->isDefaultValueWidget($form_state)) {
$items->filterEmptyItems();
}
}
}
Comments