This guide explains how to render dynamic content in Magento Page Builder using Blocks or Widgets without
relying on the native "HTML Code" content type.
This guide is based on a custom content type generated
using commerce-docs/pbmodules.
Requirements
- A pre-configured Custom Page Builder or a new installation.
- Block Class and .phtml template file
- (Optional) A
widget.xml
file if you choose to use the widget setup. If using a block-based implementation, thewidget.xml
file is not required.
this tutorial as created based on the custom content type that was generated by
the https://github.com/commerce-docs/pbmodules
1. Update Dependency Injection (DI) Configuration
Create or update your module's etc/di.xml
, add the following code.
<type name="Magento\PageBuilder\Model\Stage\RendererPool">
<arguments>
<argument name="renderers" xsi:type="array">
<item name="CONTENT_TYPE_NAME" xsi:type="object">
Magento\PageBuilder\Model\Stage\Renderer\WidgetDirective
</item>
</argument>
</arguments>
</type>
Note: Replace
CONTENT_TYPE_NAME
with your content type name.
2. Content Type XML Configuration
Create or update the file: view/adminhtml/pagebuilder/content_type/CONTENT_TYPE_NAME.xml
-
Within the section:
Add an HTML attribute (inside or any other element as needed):
<html name="html" preview_converter="Magento_PageBuilder/js/converter/attribute/preview/store-id"/>
-
After the
</elements>
:
Add the converters configuration.
<converters> <converter component="Vendor_Module/js/content-type/CONTENT_TYPE_FOLDER/mass-converter/widget-directive" name="widget_directive"> <config> <item name="html_variable" value="html"/> </config> </converter> </converters>
Note: Replace
CONTENT_TYPE_FOLDER
with your content type folder name.
3. Create the Mass Converter JavaScript
Create the file: view/adminhtml/web/js/content-type/CONTENT_TYPE_FOLDER/mass-converter/widget-directive.js
Add the following code:
Note: Replace
CONTENT_TYPE_FOLDER
with your content type folder name.
define([
'Magento_PageBuilder/js/mass-converter/widget-directive-abstract',
'Magento_PageBuilder/js/utils/object'
], function (widgetDirective, dataObject) {
'use strict';
class WidgetDirective extends widgetDirective {
/**
* Convert value to internal format
*
* @param {object} data
* @param {object} config
* @returns {object}
*/
fromDom(data, config) {
var attributes = super.fromDom(data, config);
return data;
}
toDom(data, config) {
const attributes = {
type: 'Devgfnl\\WidgetBlockPageBuilder\\Block\\Info',
template: 'Devgfnl_WidgetBlockPageBuilder::info.phtml',
type_name: 'Widget/Block with PageBuilder',
my_field: data.my_field
// ... other attributes to be passed to the block/widget
};
dataObject.set(data, config.html_variable, this.buildDirective(attributes));
return data;
}
}
return WidgetDirective;
});
Using a Block Instead of a Widget
If you are not using the widget setup, modify the toDom function as follows:
toDom(data, config) {
const attributes = {
- type: 'Devgfnl\\WidgetBlockPageBuilder\\Block\\Info',
+ class: 'Devgfnl\\WidgetBlockPageBuilder\\Block\\Info',
template: 'Devgfnl_WidgetBlockPageBuilder::info.phtml',
type_name: 'Widget/Block with PageBuilder',
my_field: data.my_field
};
- dataObject.set(data, config.html_variable, this.buildDirective(attributes));
+ dataObject.set(data, config.html_variable, this.buildBlockDirective(attributes));
return data;
}
+ buildBlockDirective(attributes) {
+ return '{{block ' + this.createAttributesString(attributes) + '}}';
+ }
Note: When using a widget, ensure that you have the proper widget.xml setup; otherwise, the Page Builder will not
render the PHTML content. The block-based approach is recommended if you want to avoid creating an extra widget.xml
file.
4. Update the Preview JavaScript
Create or update the file view/adminhtml/web/js/content-type/CONTENT_TYPE_FOLDER/preview.js
file, update it with the
following code.
Note: Replace
CONTENT_TYPE_FOLDER
with your content type folder name.
define([
'jquery',
'mage/translate',
'knockout',
'underscore',
'Magento_PageBuilder/js/config',
'Magento_PageBuilder/js/content-type/preview'
], function (
$,
$t,
ko,
_,
Config,
PreviewBase
) {
'use strict';
var $super;
/**
* Quote content type preview class
*
* @param parent
* @param config
* @param stageId
* @constructor
*/
function Preview(parent, config, stageId) {
PreviewBase.call(this, parent, config, stageId);
this.displayPreview = ko.observable(false);
this.previewElement = $.Deferred();
this.loading = ko.observable(false);
this.widgetUnsanitizedHtml = ko.observable();
this.element = null;
this.messages = {
EMPTY: $t('Empty...'),
NO_RESULTS: $t('No result were found.'),
LOADING: $t('Loading...'),
UNKNOWN_ERROR: $t('An unknown error occurred. Please try again.')
};
this.placeholderText = ko.observable(this.messages.EMPTY);
}
Preview.prototype = Object.create(PreviewBase.prototype);
$super = PreviewBase.prototype;
/**
* Modify the options returned by the content type
*
* @returns {*}
*/
Preview.prototype.retrieveOptions = function () {
var options = $super.retrieveOptions.call(this, arguments);
// Customize options here
return options;
};
/**
* On afterRender callback.
*
* @param {Element} element
*/
Preview.prototype.onAfterRender = function (element) {
this.element = element;
this.previewElement.resolve(element);
};
/**
* @inheritdoc
*/
Preview.prototype.afterObservablesUpdated = function () {
$super.afterObservablesUpdated.call(this);
const data = this.contentType.dataStore.getState();
if (this.hasDataChanged(this.previousData, data)) {
this.displayPreview(false);
if (!this.shouldDisplay(data)) {
this.placeholderText(this.messages.EMPTY);
return;
}
const url = Config.getConfig('preview_url'),
requestConfig = {
// Prevent caching
method: 'POST',
data: {
role: this.config.name,
directive: this.data.main.html()
}
};
this.placeholderText(this.messages.LOADING);
$.ajax(url, requestConfig)
.done((response) => {
if (typeof response.data !== 'object' || !response.data.content) {
this.placeholderText(this.messages.NO_RESULTS);
return;
}
if (response.data.error) {
this.widgetUnsanitizedHtml(response.data.error);
} else {
this.widgetUnsanitizedHtml(response.data.content);
this.displayPreview(true);
}
this.previewElement.done(() => {
$(this.element).trigger('contentUpdated');
});
})
.fail(() => {
this.placeholderText(this.messages.UNKNOWN_ERROR);
});
}
this.previousData = Object.assign({}, data);
};
/**
* Determine if the preview should be displayed
*
* @param data
* @returns {boolean}
*/
Preview.prototype.shouldDisplay = function (data) {
const myField = data.my_field;
return !!myField;
};
/**
* Determine if the data has changed, whilst ignoring certain keys which don't require a rebuild
*
* @param {object} previousData
* @param {object} newData
* @returns {boolean}
*/
Preview.prototype.hasDataChanged = function (previousData, newData) {
previousData = _.omit(previousData, this.ignoredKeysForBuild);
newData = _.omit(newData, this.ignoredKeysForBuild);
return !_.isEqual(previousData, newData);
};
return Preview;
});
5. Create the Preview Template
Create or update the file preview template file:
view/adminhtml/web/template/content-type/CONTENT_TYPE_FOLDER/default/preview.html
, add
the following code:
Note: Replace
CONTENT_TYPE_FOLDER
with your content type folder name.
<div class="pagebuilder-content-type" attr="data.main.attributes" ko-style="data.main.style" css="data.main.css"
event="{ mouseover: onMouseOver, mouseout: onMouseOut }, mouseoverBubble: false">
<div class="my-class"
data-bind="liveEdit: { field: 'my_field', placeholder: $t('Your custom content type!') }"></div>
<div if="displayPreview" class="rendered-content" html="widgetUnsanitizedHtml" afterRender="onAfterRender"></div>
<div ifnot="displayPreview" class="pagebuilder-products-placeholder">
<span class="placeholder-text" text="placeholderText"></span>
</div>
<render args="getOptions().template"></render>
</div>
6. Create the Master Template
Create or update the file master template file:
view/adminhtml/web/template/content-type/CONTENT_TYPE_FOLDER/default/master.html
, add the
following code:
Note: Replace
CONTENT_TYPE_FOLDER
with your content type folder name.
<div html="data.main.html" attr="data.main.attributes" css="data.main.css" ko-style="data.main.style"></div>
Results
Observations
- KnockoutJS Rendering: In tests, a PHTML file incorporating KnockoutJS rendered correctly on the frontend. However, KnockoutJS may not render as expected in the admin area.
-
Widget vs. Block: If you use the widget setup, ensure that the corresponding
widget.xml
is configured properly; otherwise, the PHTML content may not be rendered. The block-based approach is recommended for simplicity, as it does not require an extra XML configuration file. -
BlockInterface: If you use the block and widget approach, ensure that the Block class implements the
BlockInterface
to avoid any issues with the Page Builder rendering the content.
Known Issues & Solutions
-
Issue: The PHTML content is not rendered in the admin area.
-
Solution:
- If you are using the widget setup, ensure that the
widget.xml
file is configured properly. - Ensure that the Block class implements the
BlockInterface
.
- If you are using the widget setup, ensure that the
-
Solution:
Top comments (0)