atwx / silverstripe-cms-popup
Generic CMS modal popup for SilverStripe 6 with React content components
Package info
github.com/atwx/silverstripe-cms-popup
Language:JavaScript
Type:silverstripe-vendormodule
pkg:composer/atwx/silverstripe-cms-popup
Requires
- php: ^8.2
- silverstripe/admin: ^3
- silverstripe/framework: ^6
This package is auto-updated.
Last update: 2026-03-25 16:20:49 UTC
README
Opens modal dialogs in the SilverStripe CMS from a button in the form action menu. Three built-in content types: Search (search with result selection), Batch (sequential processing of a queue), and Content (generic HTML loader).
Requirements: SilverStripe 6, PHP 8.2+
Installation
composer require atwx/silverstripe-cms-popup
The module registers itself automatically in LeftAndMain. No additional YAML configuration required.
Search modal
Opens a dialog with a server-rendered search form. The user enters search terms; results are loaded via AJAX. Clicking a result fires a cms-modal:select event on the triggering button.
use Atwx\CmsPopup\Forms\CmsModalSearchAction; $action = CmsModalSearchAction::create('findRecord', 'Find record') ->setModalTitle('Select a record') ->setModalSize('md') // sm | md (default) | lg ->setFormEndpoint($this->Link('searchForm')) ->setResultsEndpoint($this->Link('searchResults'));
Form endpoint (searchForm): Returns HTML containing a SilverStripe form. The form is rendered inside the modal. Text field changes trigger an automatic search (300 ms debounce); submitting the form triggers it immediately.
Results endpoint (searchResults): Receives form field values as query parameters (e.g. ?q=search+term). Returns HTML. Clickable elements must carry a data-cms-select attribute containing a JSON payload:
public function searchResults(): string { $q = $this->getRequest()->getVar('q'); $results = MyRecord::get()->filter('Title:PartialMatch', $q); $html = ''; foreach ($results as $record) { $payload = json_encode(['id' => $record->ID, 'title' => $record->Title]); $html .= "<button type='button' data-cms-select='{$payload}'>{$record->Title}</button>"; } return $html; }
Listening to the select event (optional, in your own JS):
document.querySelector('.my-trigger-button').addEventListener('cms-modal:select', (e) => { const { id, title } = e.detail; // Write the value into a form field, etc. });
Batch modal
Opens a dialog with a configuration form. After clicking "Start", queue items are sent one by one to an action endpoint; progress is displayed live.
use Atwx\CmsPopup\Forms\CmsModalBatchAction; $action = CmsModalBatchAction::create('runBatch', 'Run batch') ->setModalTitle('Translate all pages') ->setModalSize('lg') ->setFormEndpoint($this->Link('batchForm')) ->setActionEndpoint($this->Link('batchAction')) ->setQueueEndpoint($this->Link('batchQueue')) // optional ->setSubmitLabel('Start') // default: "Start" ->setBaseQueue([ // optional static queue ['id' => 1, 'title' => 'Home'], ['id' => 2, 'title' => 'About'], ]);
Form endpoint (batchForm): Returns HTML containing a SilverStripe form with options the user configures before starting. Can return an empty string if no configuration is needed.
Queue endpoint (batchQueue, optional): Called when a field named recursive is checked in the form. Returns JSON:
{
"items": [
{ "id": 10, "title": "Subpage A" },
{ "id": 11, "title": "Subpage B", "enabled": false }
]
}
Items with "enabled": false are skipped. The fetched items are merged with setBaseQueue().
Action endpoint (batchAction): Called via POST for each queue item. The request body contains the queue item merged with the form values as JSON:
{ "id": 1, "title": "Home", "recursive": true }
Return a response using the response helpers:
use Atwx\CmsPopup\Http\CmsPopupBatchResponse; use Atwx\CmsPopup\Http\CmsPopupBatchDetail; public function batchAction(): HTTPResponse { $body = json_decode($this->getRequest()->getBody(), true); $record = MyPage::get()->byID($body['id']); if (!$record) { return CmsPopupBatchResponse::error('Record not found'); } try { $record->doSomething(); return CmsPopupBatchResponse::success('Processed', [ CmsPopupBatchDetail::info('en_US', 'OK'), CmsPopupBatchDetail::info('de_DE', 'OK'), ]); } catch (\Exception $e) { return CmsPopupBatchResponse::error($e->getMessage(), [ CmsPopupBatchDetail::error('en_US', 'Failed'), ]); } }
Response helpers
| Method | HTTP status | Modal display |
|---|---|---|
CmsPopupBatchResponse::success($message, $details) |
200 | green check |
CmsPopupBatchResponse::warning($message, $details) |
200 | yellow warning |
CmsPopupBatchResponse::error($message, $details) |
422 | red cross |
Detail helpers
Used to populate the $details list shown beneath each item:
CmsPopupBatchDetail::info('en_US', 'Translated') // green CmsPopupBatchDetail::warning('de_DE', 'Skipped') // yellow CmsPopupBatchDetail::error('fr_FR', 'API error') // red
Content modal
Loads arbitrary HTML from a URL into the dialog. Useful for simple informational displays without user interaction.
use Atwx\CmsPopup\Forms\CmsModalAction; $action = CmsModalAction::create('showInfo', 'Show details') ->setModalComponent('CmsModalContent') ->setModalTitle('Information') ->setModalData(['url' => $this->Link('infoHtml')]);
Button icon
A SilverStripe admin font-icon class can be added to any action button:
$action->setButtonIcon('font-icon-search'); $action->setButtonIcon('font-icon-sync');
Modal sizes
| Value | Width |
|---|---|
sm |
480 px |
md |
640 px (default) |
lg |
860 px |
Writing the selected value into a form field
Typical pattern: open search modal → write selected value into a hidden field → save the form.
// In getCMSFields(): $hiddenId = HiddenField::create('MyRecordID'); $hiddenTitle = ReadonlyField::create('MyRecordTitle', 'Selected record'); $search = CmsModalSearchAction::create('pickRecord', 'Choose record') ->setFormEndpoint($this->Link('searchForm')) ->setResultsEndpoint($this->Link('searchResults'));
// Listen for selection in your own entwine or JS module: button.addEventListener('cms-modal:select', (e) => { document.querySelector('[name=MyRecordID]').value = e.detail.id; document.querySelector('[name=MyRecordTitle]').value = e.detail.title; });
Custom content components
The modal uses the SilverStripe Injector. Register a custom React component and reference it by name:
// In your own bundle.js import Injector from 'lib/Injector'; import MyCustomModal from './MyCustomModal'; Injector.component.register('MyCustomModal', MyCustomModal);
$action->setModalComponent('MyCustomModal'); $action->setModalData(['someParam' => 'value']);
The component receives the props data (from setModalData()), onClose (callback), and onSelect (callback — dispatches cms-modal:select on the trigger element).
Building assets
cd vendor/atwx/silverstripe-cms-popup npm install npm run build # production npm run dev # development npm run watch # watch mode
Output: client/dist/js/bundle.js and client/dist/atwx/silverstripe-cms-popup (CSS).