Widgets/Blocks with Magento Page Builder
Gabriel Lima

Gabriel Lima @devgfnl

Location:
Brazil
Joined:
Sep 8, 2024

Widgets/Blocks with Magento Page Builder

Publish Date: Feb 24
0 0

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

Comments 0 total

    Add comment