DEV Community

Cover image for Widgets/Blocks with Magento Page Builder
Gabriel Lima
Gabriel Lima

Posted on

Widgets/Blocks with Magento Page Builder

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, the widget.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>
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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) + '}}';
+       }
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Results

Image description

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.

Code Reference

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more