Widgetkit — Developer Guide
Technical documentation for developers working with MageOS Widgetkit.
Installation
- Install the module into your Mage-OS / Magento 2 project with Composer:
composer require mage-os/module-widgetkit - Enable the module and run setup:
bin/magento module:enable MageOS_Widgetkit bin/magento setup:upgrade
Dependencies:
MageOS_PageBuilderWidgetandMageOS_AdvancedWidgetare installed automatically.
How to Develop a New Widget
The following walkthrough describes how to build a new Widgetkit-style widget from scratch. The Slideshow widget is used as the reference example throughout. The recommended approach is to copy an existing widget and adapt it.
Step 1 — Declare the widget in widget.xml
Every Magento widget starts with a declaration in etc/widget.xml. Widgetkit uses the extended XSD provided by MageOS_PageBuilderWidget, which adds preview-related nodes on top of the standard Magento schema.
Official widget.xml reference:
Minimal structure:
<widgets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:MageOS_PageBuilderWidget:etc/widget.xsd">
<widget id="mageos_slideshow" class="MageOS\Widgetkit\Block\Widgets\Slideshow">
<label translate="true">MageOS Slideshow</label>
<description translate="true">MageOS slideshow widget</description>
<parameters>
<!-- template selector — required for PageBuilderWidget preview mapping -->
<parameter name="template" sort_order="1" xsi:type="select" visible="true">
<label>Template</label>
<options>
<option name="default" value="widget/hyva/slideshow.phtml" selected="true">
<label translate="true">MageOS Hyvä Slideshow Template</label>
</option>
</options>
</parameter>
<!-- plain text / select parameters -->
<parameter name="mageos_slideshow_title" sort_order="10" visible="true" xsi:type="text">
<label>Title</label>
</parameter>
<!-- title separator (AdvancedWidget) -->
<parameter name="mageos_slideshow_content_section_title"
xsi:type="block" required="false" visible="true" sort_order="20">
<description><![CDATA[<h3 style="margin:40px 0;text-align:center;">Content</h3>]]></description>
<block class="MageOS\AdvancedWidget\Block\Adminhtml\Renderer\Title"/>
</parameter>
<!-- repeatable rows (AdvancedWidget) -->
<parameter name="repeatable_slideshow_items"
xsi:type="block" sort_order="21" required="false" visible="true">
<label translate="true">Slideshow items</label>
<block class="MageOS\Widgetkit\Block\Adminhtml\Slideshow\Item"/>
</parameter>
</parameters>
<!-- PageBuilderWidget preview declarations -->
<previewTemplates>
<previewTemplate name="widget/hyva/slideshow.phtml" xsi:type="string">
MageOS_Widgetkit::slideshow.phtml
</previewTemplate>
</previewTemplates>
<previewBlock>MageOS\Widgetkit\Block\Adminhtml\Slideshow\Preview</previewBlock>
<previewCss>MageOS_Widgetkit::css/widget/preview/hyva/slideshow.css</previewCss>
</widget>
</widgets>
Key points:
- The
classattribute on<widget>is the frontend Block class (Block/Widgets/Slideshow.php). - The
templateparameter value (widget/hyva/slideshow.phtml) must match thenameattribute of the corresponding<previewTemplate>node. repeatable_*parameter names are required byMageOS_AdvancedWidgetfor correct encode/decode of row data.
Step 2 — Define the repeatable rows with AdvancedWidget
The item configuration for each repeatable section is a PHP class extending MageOS\AdvancedWidget\Block\WidgetField\Rows. Declare the $rows property with the fields for each row.
// Block/Adminhtml/Slideshow/Item.php
namespace MageOS\Widgetkit\Block\Adminhtml\Slideshow;
use MageOS\AdvancedWidget\Block\WidgetField\Rows;
class Item extends Rows
{
protected $rows = [
'image' => [
'label' => 'Desktop Image',
'type' => 'image',
'description' => 'Image used on desktop.',
'required' => true,
'preview' => true,
],
'title' => [
'label' => 'Title',
'type' => 'text',
'required' => false,
'preview' => true,
],
'title_tag' => [
'label' => 'Title tag',
'type' => 'select',
'options' => ['h2' => 'H2', 'h3' => 'H3', 'p' => 'Paragraph'],
'required' => false,
'preview' => false,
],
// ... more fields
];
}
See the AdvancedWidget Developer Guide for the full field types reference.
Step 3 — Implement the Preview Block
The preview block is declared via <previewBlock> in widget.xml. In Widgetkit, each preview block extends the corresponding frontend widget block and overrides renderMainTemplate() to emulate the frontend area before rendering.
// Block/Adminhtml/Slideshow/Preview.php
namespace MageOS\Widgetkit\Block\Adminhtml\Slideshow;
use Magento\Store\Model\App\Emulation;
use MageOS\Widgetkit\Block\Widgets\Slideshow;
class Preview extends Slideshow
{
public function __construct(
protected Emulation $emulation,
protected Conditions $conditions,
protected Context $context
) {
return parent::__construct($conditions, $context);
}
public function renderMainTemplate(): string
{
$this->emulation->startEnvironmentEmulation(
1,
\Magento\Framework\App\Area::AREA_FRONTEND,
true
);
$mainTemplate = parent::renderMainTemplate();
$this->emulation->stopEnvironmentEmulation();
return $mainTemplate;
}
}
Why frontend emulation is necessary
Widget block classes and templates often depend on frontend-area services (store configuration, theme resolution, frontend-specific helpers). When the preview is rendered inside the admin panel, Magento is running in adminhtml area. Calling startEnvironmentEmulation temporarily switches the environment to frontend, ensuring that template paths, image URLs, and other area-sensitive data resolve exactly as they would for a customer. Emulation is stopped immediately after rendering to avoid side effects on the rest of the admin request.
Why templates live in view/base
The base templates (e.g. view/base/templates/widget/hyva/slideshow/templates/template.phtml) are placed in view/base rather than view/frontend or view/adminhtml because Magento resolves base area templates as a fallback for all areas. This means the same template file is used both on the frontend (when a customer views the page) and in the admin preview (after environment emulation switches the area to frontend). There is no need to duplicate templates across areas.
Step 4 — The preview phtml (view/adminhtml/templates)
The adminhtml preview template (e.g. view/adminhtml/templates/slideshow.phtml) is the entry point rendered by PageBuilderWidget on the canvas. It is placed in view/adminhtml because it is only ever rendered in the admin context — it is not a frontend template.
It must do three things:
- Call
previewAssets— loads the CSS declared in<previewCss>and the JS declared in<previewJs>:<?= $this->getChildHtml("previewAssets"); ?> - Call
addTwindManagement— initiates TWIND CSS generation for this widget instance:<?= $this->addTwindManagement($uniqId, 'slideshow-widget'); ?> - Wrap the output in a uniquely identified container and delegate actual rendering to the Block class:
<div class="slideshow-widget"> <?= $this->addTwindManagement($uniqId, 'slideshow-widget'); ?> <?= $this->getChildHtml("previewAssets"); ?> <div id="widget<?= $uniqId; ?>"> <?= $this->renderMainTemplate(); ?> </div> </div>
The $uniqId (generated with uniqid()) ensures that when multiple widget instances of the same type are present on the canvas, TWIND scopes the compiled CSS to the correct container and does not generate conflicts between instances.
Step 5 — TWIND: runtime Tailwind CSS in the admin
The Page Builder admin panel does not run the Tailwind CSS build pipeline, so utility classes in preview templates would be unstyled by default. Widgetkit solves this by bundling TWIND — a runtime Tailwind compiler that generates CSS from class names present in the DOM.
How TWIND is loaded
On cms_page_edit and cms_block_edit layout handles, Widgetkit injects Alpine.js and the two TWIND library files (twind.min.js and twind/sheet.min.js) into the page via a custom require_js.phtml template:
<!-- view/adminhtml/layout/cms_page_edit.xml -->
<referenceBlock name="require.js" template="MageOS_Widgetkit::page/js/require_js.phtml">
<arguments>
<argument name="alpinejs_path" xsi:type="string">Hyva_Theme::js/alpine3.min.js</argument>
<argument name="twindjs_path" xsi:type="string">MageOS_Widgetkit::js/twind.min.js</argument>
<argument name="twindjs-sheet_path" xsi:type="string">MageOS_Widgetkit::js/twind/sheet.min.js</argument>
</arguments>
</referenceBlock>
How TWIND generates scoped CSS per widget instance
addTwindManagement() in HyvaWidget renders view/base/templates/widget/hyva/twind.phtml, which executes the following steps at widget load time on the canvas:
// 1. create a virtual (in-memory) stylesheet
const { create } = window.twind;
const { virtualSheet } = window.TwindSheets;
const sheet = virtualSheet();
const { tw } = create({ sheet, rules: [...] });
// 2. walk every element inside the widget container (e.g. #widgetABC123)
// and collect all Tailwind classes
const target = document.querySelector('#widget' + uid);
collectTailwindClasses(target); // recurses into child nodes
tailwindClasses.forEach(cls => tw(cls)); // compile each class
// 3. prefix every generated rule with the widget CSS class
// (e.g. `.pagebuilder-stage-wrapper .slideshow-widget`)
// so styles are scoped and do not leak into the rest of the admin UI
const prefix = '.pagebuilder-stage-wrapper .' + widgetCssClass;
style.textContent = sheet.target.map(rule => {
// rewrite selectors to include the prefix
}).join('\n');
document.head.appendChild(style);
The generated <style> tag is given a stable id (widget{uid}-style) so it can be replaced cleanly when the widget is re-rendered (e.g. after a settings change).
This mechanism means every Tailwind class present in the rendered HTML is compiled and injected as scoped CSS — matching the styles the widget would have on the Hyvä frontend — with no build step required.
Step 6 — Base templates and resolveExpression
The template files under view/base/templates/widget/hyva/ contain the actual HTML markup shared between frontend and preview. The main template for the Slideshow (slideshow/templates/template.phtml) uses two utilities:
Child block rendering methods
Each Block/Widgets class exposes dedicated render* methods (renderMainTemplate, renderNavTemplate, renderItems, renderItemMedia, renderItemContent, etc.). Each method creates a new instance of the same block class, assigns a template and data, and returns the rendered HTML. This pattern:
- keeps individual template files small and focused on a single responsibility
- avoids passing large data structures through template variables
- makes it easy to override a single sub-template in a custom module by extending the Block class and swapping the template path in the constructor
Example from Block/Widgets/Slideshow.php:
public function renderItems(array $itemsSettings): string
{
return $this->getLayout()->createBlock(self::class)
->setTemplate($this->_itemsTemplate)
->setData([
'params' => $this->getData('params'),
'settings' => $itemsSettings,
'items' => $this->getData('items'),
])->toHtml();
}
The items array is populated from the repeatable field by calling getRepeatableField('repeatable_slideshow_items') — a helper defined in AbstractColumns that decodes the serialised row data stored by AdvancedWidget.
resolveExpression
resolveExpression is defined in MageOS\AdvancedWidget\Block\Widgets\Template and provides a concise way to build conditional class strings and inline styles from widget parameter values without long chains of if/else in templates.
It accepts an array of expressions and the widget $params array. Each entry is evaluated against the params and the results are joined with spaces.
Syntax:
- A plain string (no
{}) is always included. {paramName}— included only if$params['paramName']is truthy; replaced with the param value.{@paramName: value}— included only if$params['paramName']equalsvalue.{!paramName}— included only if$params['paramName']is falsy (negation).{0}— replaced with the$conditionvalue passed as the array value for that key.
Example from template.phtml:
// $itemsSettings['class'] resolves to a space-separated list of Tailwind classes
// based on the current widget parameter values
$itemsSettings = [
'class' => $this->resolveExpression([
'snap-track',
'h-auto {@mageos_slideshow_height: auto}',
'h-screen {@mageos_slideshow_height: viewport}',
'relative mx-auto {@mageos_slideshow_max_height}'
], $params),
'style' => $this->resolveExpression([
'min-height: {0}; {@mageos_slideshow_height: auto}' => $params['mageos_slideshow_min_height'] . 'px',
'max-height: {0}; {@mageos_slideshow_max_height}' => $params['mageos_slideshow_max_height'] . 'px',
], $params)
];
If mageos_slideshow_height is auto, the resolved class string will include snap-track h-auto. If it is viewport, it will include snap-track h-screen instead. This keeps template logic declarative and easy to follow.
Step 7 — Block/Widgets class hierarchy
MageOS\AdvancedWidget\Block\Widgets\Template (resolveExpression, stripTags)
└── MageOS\AdvancedWidget\Block\Widgets\AbstractColumns (getRepeatableField, getImageUrl, …)
└── MageOS\Widgetkit\Block\Widgets\HyvaWidget (addTwindManagement)
├── MageOS\Widgetkit\Block\Widgets\Slideshow (render* methods, template paths)
│ └── MageOS\Widgetkit\Block\Adminhtml\Slideshow\Preview (emulation)
├── MageOS\Widgetkit\Block\Widgets\Slider
├── MageOS\Widgetkit\Block\Widgets\Grid
└── MageOS\Widgetkit\Block\Widgets\ProductWidget (product collection, Hyva list item)
├── MageOS\Widgetkit\Block\Widgets\ProductSlider
└── MageOS\Widgetkit\Block\Widgets\ProductGrid
Each level adds a focused set of responsibilities:
Template— expression evaluation utilitiesAbstractColumns— repeatable field decoding, image/media URL helpersHyvaWidget— TWIND management- Concrete widget blocks — template paths and render methods
- Preview blocks — frontend area emulation
Hyva Config Registration
To ensure the Tailwind CSS build on the frontend includes classes from this module’s templates, the module registers itself with the Hyvä config system via an observer on hyva_config_generate_before:
// Observer/RegisterModuleForHyvaConfig.php
$extensions[] = ['src' => substr($modulePath, strlen(BP) + 1)];
$config->setData('extensions', $extensions);
This tells the Hyvä Tailwind build to scan the module’s source files for utility classes, preventing purge from stripping classes used only in widget templates.