#6114·october

Change monitor (unsaved changes warning) is only available in the `document` form design, with no way to opt in from other designs

Author: mplodowskiCreated Sep 11, 2026Updated Sep 11, 2026

Body

Summary

The backend change monitor — the browser warning when you navigate away from a form with unsaved changes — is hardcoded into a single design partial. Only the document design enables it; basic, sidebar, survey and popup do not, and there is no supported way for a plugin to turn it on for those designs.

Tested on October CMS v4.3.8 (october/backend v4.3.8).

Current state

data-change-monitor appears in exactly two places in the codebase:

  • modules/backend/formdesigns/documentdesign/partials/_mode.php:10
    php
    <?= Form::open(['class' => 'position-relative h-100', 'data-change-monitor' => true]) ?>
  • modules/tailor/controllers/entries/_form_mode_document.php:1 (same design, controller-level override)

The other designs open the form without it:

  • modules/backend/formdesigns/basicdesign/partials/_mode.php:4Form::open(['class' => 'd-flex flex-column h-100 design-basic'])
  • modules/backend/formdesigns/sidebardesign/partials/_mode.php:31Form::open(['class' => 'position-relative h-100'])
  • modules/backend/formdesigns/popupdesign/partials/_mode.php:1 — id + data-popup-size only
  • SurveyDesign extends BasicDesign and shares its partials, so it inherits the omission

The control itself is generic and design-agnostic (modules/system/assets/toolbox/controls/change-monitor/) — nothing about it is specific to the document layout.

Why this is a problem

Any backend form rendered through formRenderDesign() with the default basic design silently discards unsaved edits when the user hits Cancel, clicks a menu link, or closes the tab. That is the majority of plugin backend forms. Before the form designs refactor this was less visible; now that basic is the default path, the inconsistency is easy to hit and there is no configuration for it.

The only workaround today is to register a custom design class extending BasicDesign and copy basicdesign/partials/_mode.php verbatim into the plugin just to add one attribute to Form::open(). That copy then has to be diffed against core on every October update, for a one-attribute difference.

Proposals

In order of how small the change is:

1. A design config option (preferred). FormDesignBase::getDesignConfig() already exists and reads design[...] from the FormController YAML with a context-specific fallback, so the plumbing is in place:

yaml
design:
    displayMode: basic
    changeMonitor: true

and in each _mode.php:

php
<?= Form::open(['class' => '...', 'data-change-monitor' => $this->getDesignConfig('changeMonitor', false)]) ?>

No API change, defaults to the current behaviour, one line per partial. (getDesignConfig() is protected, so it would need to be reachable from the partial — either made public or exposed through a small helper.)

2. A form attributes hook. Add FormDesignBase::getDesignFormAttributes(): array returning [] by default, merged into Form::open() in every _mode.php. A plugin could then override attributes in a design subclass without copying any partial — useful beyond the change monitor.

3. Enable it by default in basic, matching document. Most consistent, but it changes behaviour for every existing backend form, so option 1 with a false default seems safer.