0012. A read-only widget renders the document instead of disabling the editor¶
Status: Accepted
Date: 2026-09-15
Context¶
The widget has a Read-only setting, which a site builder turns on for a field whose stored document an author should see but not change. Until this decision it changed one thing: the widget built the same core text_format element an editable widget builds and set #disabled on it.
Everything an editable widget costs was still paid. Core's editor.js attached blocknote/editor because the element was a text_format element with a BlockNote format, so the browser downloaded and ran the whole editor library, React and BlockNote included, to draw a surface nobody was allowed to type in. Measured on 2026-09-15 with stat -f%z <file> and gzip -c <file> | wc -c on the committed files, an editable widget's library is 1,273,451 bytes of JavaScript and 168,442 bytes of CSS, 375,949 and 22,404 bytes gzipped. The page also carried a textarea holding the stored JSON, and the editor put itself into BlockNote's own read-only mode because the textarea it stands in front of was disabled.
The module already renders a stored document without any of that. The field formatter builds a processed_text element through the item's text format, the BlockNote filter turns the stored JSON into the module's own semantic HTML (0004), and docs/rendering.md is the contract that markup keeps. The text format is the policy either way (0002), so a block the format has stopped enabling, a link whose scheme it forbids and a file the account may not see are answered for by the same code on either side.
Form API has two element types whose submission it never reads. Drupal\Core\Form\FormBuilder::handleInputElement() sets $process_input false for an element of #type item or value, and for any element that is #disabled, so the submission is never read for them and #value stands as the form built it.
Decision¶
A read-only widget builds no editor. When the read_only setting is on, formElement() returns before it builds anything the editor attaches to. There is no text_format element, no textarea and no #blocknote marker on the page, so nothing attaches blocknote/editor and the field puts no format into drupalSettings.editor.
The element is the widget's own, and the document is rendered inside it. The element is #type item, which is the form element Form API never reads a submission for and which still carries the #title, #description, required marker, error anchor and form item wrapper that WidgetBase hands every widget. Under it, renderedDocument() builds the element the field formatter builds: #type processed_text with the item's value, its format and its langcode, and the blocknote/content library attached. The markup is therefore the rendering contract's markup, byte for byte, and docs/rendering.md says so. An item whose format is not a BlockNote format renders nothing but its cacheability, as the formatter does, because a format with no BlockNote filter would print the stored JSON on the form.
The item is submitted back as it was loaded. The value, the format and the document identity are #type value elements, filled from the form and never from the submission. The element keeps #disabled as well, which is what stops a multiple-value field's weight selects and remove buttons, children of the widget element, from reordering or dropping rows of a field nobody may edit.
The stylesheet is not a setting here. The content stylesheet is always attached to a read-only widget. The formatter's Attach the default styles setting exists so that a theme can own the palette on public pages; an edit form is the site builder's own screen.
Alternatives considered¶
Keep disabling the text_format element. This is what the decision replaces. It loads the entire editor library on a form that cannot be edited, leaves a textarea carrying the stored JSON on the page, and makes the setting's meaning a property of the client: the document is protected because the editor decided not to offer editing, rather than because the form has no input for it.
Attach the editor and mount it read-only. BlockNote takes an editable flag, and the module already keeps it in step with the textarea. It lost for the same reason: the flag is worth nothing to a form that should not have shipped the library at all, and an editor that refuses to type is a worse way to read a document than the document.
Reuse the field formatter's render array as it stands. Rejected on four counts. The formatter answers for a whole item list while a widget builds one element per delta, so a multiple-value field would have to put the entire list in delta 0 and lose its rows. Instantiating the formatter plugin brings a second settings surface, attach_styles, that the form display does not configure, so one page would be controlled from two unrelated places. The formatter's refusal path logs a message naming an entity display, which would be wrong on an edit form. And a formatter's render array carries no label, no description, no required marker and nothing for flagErrors() to anchor a violation on, so the field would look like a display dropped into a form. What is reused is the policy, not the array: renderedDocument() builds the same element with the same properties, which is why the markup comes out identical.
Drop the setting and tell site builders to hide the field on the form display. Rejected. A field hidden from the form display is not on the form at all, and the reason the setting exists is that the document has to be readable beside the fields that are being edited.
Consequences¶
A read-only field is cheap. It brings the rendered document and the content stylesheet to the page in place of the editor library measured above, and the page carries no input for it at all, so a crafted request has nothing to write through.
The setting no longer means BlockNote's read-only mode. That mode is still in the bundle and is still reached, by a textarea that is disabled or made read-only after the editor attached, which is what #states does. The tests that cover it drive it that way, because the widget setting no longer produces a textarea for them to disable.
An edit form now carries the classes and the elements of the HTML contract. A theme that styles blocknote- classes will meet them on forms as well as on pages, and a change to the contract is visible in both places.
What the format refuses looks the same on the form as it does to a visitor, which is the point, and it means the editor's unsupported-block carrier is not there to explain it. A block the format has stopped enabling renders nothing, logged at debug level, instead of being carried in a notice. An author who needs to see those blocks needs an editable widget.
The widget's own required check never fires. It exists to recognize the single empty paragraph a blank editor submits, and a widget nobody may edit submits no such thing, so a required field with no stored document is refused by the entity validator with NotNull's own message, "This value should not be null.", rather than with core's "@name field is required." This was true of the disabled widget too, and is now pinned by a test.
References¶
src/Plugin/Field/FieldWidget/BlockNoteEditorWidget.php:formElement(),readOnlyElement(),renderedDocument().src/Plugin/Field/FieldFormatter/BlockNoteDefaultFormatter.php:viewElements(), andCONTENT_LIBRARY.- Drupal core 11.4.6:
Drupal\Core\Form\FormBuilder::handleInputElement(),Drupal\Core\Render\Element\Item,Drupal\Core\Render\Element\Value,core/modules/editor/js/editor.js. - Tests:
tests/src/Kernel/ReadOnlyWidgetTest.phpandtests/src/Functional/WidgetSettingsTest.php. - 0002, 0003, 0004.