Skip to content

For developers

The module's extension surface is a set of Symfony events plus a handful of services. There is no plugin type of its own: the action links are an extra field, and the per-field switches are field formatter third-party settings.

Extending Use
Who may add, move, delete or duplicate an item Access control events
Making links work in another rendering pipeline Rendering pipeline events and Rendering pipelines
Reading or changing field lineage FieldReferenceHelper
How requests are issued AJAX transports

Architecture

flowchart TB
    Module[frontend_editing.module<br/>thin dispatcher] --> Hooks[src/Hook/ services]
    Hooks --> ViewAlter[ViewAlter]
    ViewAlter --> Helper[FieldReferenceHelper]
    Helper --> Links[Action link render elements]
    ViewAlter -. dispatches .-> Events[Collect / Resolve / Render events]
    Events -. subscribed by .-> Subs[Display Builder and<br/>Layout Builder sub-modules]
    Links --> Controller[FrontendEditingController]
    Controller --> Helper

Hook implementations live in src/Hook/ as autowired services. The .module file is a dispatcher that calls them through #[LegacyHook]. Do not put logic in the .module file.

Where things are

src/
  Ajax/         Custom AJAX commands the controller returns
  Controller/   Form loading, toggle, item operations, content updates
  Event/        Access control events and rendering pipeline events
  Form/         Settings, the bundle selector, the add-reference form
  Hook/         Hook implementations as services
  Plugin/       The extra field display plugin and the top bar item
  Routing/      Route subscriber
modules/
  frontend_editing_paragraphs/
  frontend_editing_display_builder/
  frontend_editing_layout_builder/

Key routes

Route Path Purpose
frontend_editing.settings_form /admin/config/frontend-editing Settings
frontend_editing.entity_bundle_restrictions /admin/config/frontend-editing/entity-bundle-restrictions Entity types and bundles
frontend_editing.form /frontend-editing/form/{type}/{id}/{operation}/{display} The entity form the sidebar iframe loads
frontend_editing.item_add_page /frontend-editing/entity-reference/add/… The add panel
frontend_editing.entity_reference_item_add /frontend-editing/entity-reference/{bundle}/add/… Create and insert a new item
frontend_editing.item_move_up, …_move_down /frontend-editing/…/{up,down} Reorder
frontend_editing.entity_reference_item_delete /frontend-editing/…/delete Delete
frontend_editing.entity_reference_item_duplicate /frontend-editing/…/duplicate Duplicate
frontend_editing.update_content /frontend-editing/update-content/{entity_type_id}/{entity_id}/{field_name}/{view_mode} Re-render one field for an AJAX swap
frontend_editing.toggle /frontend-editing/toggle Flip the per-user on/off state

Every route that mutates a specific field item takes an optional ?delta=N query parameter identifying which occurrence is meant. A new route of that kind should follow the same convention rather than inventing another one. See Managing items.

Testing

# Kernel and Unit tests need a database connection.
ddev exec "cd /var/www/html && SIMPLETEST_DB='mysql://db:db@db/db' \
  vendor/bin/phpunit -c web/core/phpunit.xml.dist \
  web/modules/contrib/frontend_editing/tests"

Functional and FunctionalJavascript tests additionally need SIMPLETEST_BASE_URL and a Selenium or Chromedriver service the web container can reach.

New test classes need #[RunTestsInSeparateProcesses], which Drupal 11.3 deprecates the absence of and Drupal 12 requires.

Tests for content moderation live in two places. The main module's Kernel tests (RevisionHelper*Test, RevisionRedirectTest, FieldReferenceHelperRevisionTest) run without Paragraphs. Those that need paragraphs are in modules/frontend_editing_paragraphs/tests/src/Kernel/, plus the browser test FunctionalJavascriptTests/ModerationRedirectTest.php. They need workflows and content_moderation, and ContentModerationTestTrait to set up the Editorial workflow.

The save queue is private to the current user. A test that changes user after queueing finds an empty queue.

Three things catch people out in kernel tests:

  • $entity->get($field)->get($delta)->entity does not set _referringItem. Only a field formatter's own render pass does. Since most of this module reads _referringItem to find the parent, test helpers have to set it by hand.
  • Node access needs access content as well as the operation permission. Core forbids everything without it, before it looks at the operation.
  • A field with no matching EntityViewDisplay::setComponent() renders empty, because field_ui is not installed to add it for you. That looks identical to a real bug.