| description | Build, place, and retire CMS widgets, including those supplied by other modules. |
|---|
Widgets are the blocks of content an editor drops into a widget area, a page template, or any admin field that uses the widget editor. A widget is a class plus two views: one for admin, one for the front end.
App widgets live under application/modules/cms/widgets/{slug}/. Installed modules use the same layout under {module}/cms/widgets/{slug}/.
application/modules/cms/widgets/MyWidget/
widget.php
screenshot.png
views/
editor.php
render.php
js/
dropped.js
removed.js
widget.php is the definition. The directory name is the slug (MyWidget). The class name is that slug with the first letter uppercased (MyWidget), in the App\Cms\Widget namespace. A module widget uses the module namespace plus Cms\Widget, so the CMS module's accordion widget is Nails\Cms\Cms\Widget\Accordion in cms/widgets/accordion/.
screenshot.png (also .jpg or .gif) is optional. The editor shows it when the pointer is over the widget in the sidebar. Aim for about 500px wide, with as little surrounding page as possible.
views/editor.php is the admin form. views/render.php is the front-end markup. Either file can be omitted; a missing view renders as an empty string.
js/dropped.js runs when an instance is added to the editor. js/removed.js runs after the editor confirms removal. A .min.js file is preferred when both are present. Both are optional. Each file is turned into a function that receives the instance's DOM element as domElement. this is the editor.
You rarely need dropped.js to set up ordinary admin fields. Once the editor form is in the page, the widget editor calls refreshUi(). Admin plugins then bind themselves to the new markup.
nails make:cms:widget MyWidgetThe command writes the directory, widget.php, both views, and js/dropped.js under application/modules/cms/widgets/.
// application/modules/cms/widgets/MyWidget/widget.php
namespace App\Cms\Widget;
use Nails\Cms\Widget\WidgetBase;
class MyWidget extends WidgetBase
{
public function __construct()
{
parent::__construct();
$this->label = 'My Widget';
$this->icon = 'fa-cube';
$this->grouping = 'Generic';
$this->description = 'A short description about the widget';
$this->keywords = 'some,searchable,keywords';
// Keys defined here are available as variables in both views
$this->data = [
'sBody' => '<p>Default body text</p>',
];
}
}| Property | Role |
|---|---|
$label |
Name in the sidebar and on each placed instance |
$icon |
Font Awesome class. Empty falls back to DEFAULT_ICON (fa-cube) |
$grouping |
Sidebar group. An empty value, or the label Generic, joins the Generic group |
$description |
Shown on the placed instance |
$keywords |
Extra search terms for the sidebar filter |
$data |
Default values. Missing keys are filled before either view loads |
$assets_editor |
Styles and scripts for the editor |
$assets_render |
Styles and scripts for the front end |
Asset entries are a URL or path string, or a [path, location] pair passed to the asset service.
The editor lists groups with a numeric order first, then unordered groups. Ties sort by label. To give a group an order, overload the widget service with App\Cms\Service\Widget (see Overloading) and override getWidgetGroupOrder() so it returns an integer for that label.
The editor reads every named field in views/editor.php and stores it under that name. Checkboxes are booleans, name[] fields are arrays, and the payload is JSON so types survive the round trip. Prefer the form field helpers so the widget picks up the same admin chrome as the rest of admin.
// application/modules/cms/widgets/MyWidget/views/editor.php
echo form_field_textarea([
'key' => 'sBody',
'label' => 'Body',
'default' => $sBody,
'tip' => 'Shown on the front end as the widget body.',
]);views/render.php receives the saved fields as variables. Rendering fires WIDGET:RENDER:PRE and WIDGET:RENDER:POST (Nails\Cms\Events). Listeners receive the widget, its data, and the output so far; the output argument is a reference.
// application/modules/cms/widgets/MyWidget/views/render.php
if (!empty($sBody)) {
?>
<div class="cms-widget">
<?=$sBody?>
</div>
<?php
}A saved area is a list of instances:
[
{"slug": "MyWidget", "data": {"sBody": "<p>Hello</p>"}}
]dropped.js is for behaviour the admin plugins do not already provide, such as adding and removing repeatable panels.
When a widget's editor markup is inserted, the widget editor calls refreshUi(). That asks every admin plugin to look for new controls and bind them. A select.select2, a .wysiwyg textarea, a toggle, or a revealer in views/editor.php is picked up on its own. The same refresh runs again after a widget is dragged into a new position, because sorting tears down rich text editors first.
// application/modules/cms/widgets/MyWidget/js/dropped.js
domElement
.querySelector('.some-class')
.addEventListener('click', function () {
// ...
});Declare these on the widget class. Each one changes whether the widget can be chosen, edited, or rendered.
| Constant | Default | Effect |
|---|---|---|
DISABLED |
false |
The widget is never instantiated |
HIDDEN |
false |
Kept out of the editor sidebar; existing placements still render |
DEPRECATED |
false |
Kept out of the sidebar; existing placements stay editable and show a warning |
ALTERNATIVE |
'' |
Replacement named in that warning |
DEFAULT_ICON |
'fa-cube' |
Icon used when $this->icon is empty |
DISABLED = true drops the widget during discovery. The sidebar, getBySlug(), and cmsWidget() all miss it. An area that still references the slug skips it in production. Outside production, rendering that area throws Nails\Cms\Exception\Widget\NotFoundException.
Use this when a module ships a widget the app should not offer at all. The usual way is an app widget with the same slug that extends the module class and sets the constant.
Hidden
HIDDEN = true leaves the widget out of the editor sidebar (Widget::getAvailable()). getBySlug() loads hidden widgets, so a placement that is already saved still renders, and cmsWidget() can still target the slug.
The editor's catalogue is that same sidebar list. Opening an area which already contains a hidden widget shows the instance as missing, because the editor cannot look the slug up. Choose DEPRECATED when editors still need to open and adjust existing placements.
DEPRECATED = true retires a widget without removing it. The sidebar skips it, so it cannot be added again. A placement that is already saved still opens, and the editor shows "This widget is deprecated." Set ALTERNATIVE to the replacement's name or slug and the warning adds "Consider using {alternative} instead." The monitor flags the same state on the widget's detail screen.
namespace App\Cms\Widget;
use Nails\Cms\Widget\WidgetBase;
class MyWidget extends WidgetBase
{
const DEPRECATED = true;
const ALTERNATIVE = 'MyOtherWidget';
public function __construct()
{
parent::__construct();
$this->label = 'My Widget';
}
}The CMS module ships these widgets. Slugs match the directory names.
| Slug | Label | |
|---|---|---|
richtext |
Rich Text | CKEditor 4 field body, wrapped in .cms-widget-richtext. A block of type rich text uses form_field_wysiwyg() and is separate from this widget. |
html |
Plain Text | Unfiltered HTML in body, wrapped in .cms-widget-html. |
blockquote |
Blockquote | quote, cite_text, and cite_url. |
table |
Table | Handsontable editor. tblData is the cell JSON; tblAttr is extra markup on the <table>. |
tabs |
Tabs | Repeatable title and body fields, rendered as Bootstrap tabs. cmsWidget() can pass a tabs list of title / body pairs instead. |
accordion |
Accordion | Repeatable title and body fields, rendered as a Bootstrap collapse group. cmsWidget() can pass a panels list of title, body, and collapsed. |
Area |
CMS Area | Renders another area chosen by iAreaId. |
Other modules add widgets of their own. Those widgets are documented with the module:
- CDN supplies
image - Custom Forms supplies
customform
cmsWidget() renders a single widget by slug. The helper is autoloaded with the rest of the CMS helpers. Hidden widgets resolve; disabled widgets return an empty string.
<?=cmsWidget('MyWidget', ['sBody' => '<p>This is some body text.</p>'])?>Discovery walks installed modules, then the app. An app directory with the same slug replaces the module widget. Extend the module class when the views and behaviour should stay, and only the constants or copy need to change. getFilePath() walks the parent classes, so an override can keep the parent's views/ and js/ files.
// application/modules/cms/widgets/accordion/widget.php
namespace App\Cms\Widget;
class Accordion extends \Nails\Cms\Cms\Widget\Accordion
{
const DISABLED = true;
}The directory name has to match the module widget's slug exactly (accordion, including case). The class is Accordion because the loader uppercases the first letter.
The same pattern with HIDDEN or DEPRECATED applies when the widget should remain in existing content.