La puissance du PageBuilder

La puissance du PageBuilder

Introduit en mars 2019 dans la version 2.3.1 de Magento, PageBuilder est un game-changer dans le monde de l'e-commerce !
Cette fonctionnalité est similaire à Gutenberg dans Wordpress. Elle permet de faire des mises en page complexes sans aucune connaissance en HTML ou CSS.
En plus d'être une aide dans la réalisation de page et de bloc CMS, il peut être utilisé directement dans les descriptions des catégories et des articles ou pour n'importe quel attribut spécifique.

De quoi permettre des mises en page complexes de l'ensemble du catalogue !

À l'heure actuelle, et hormis WooCommerce qui bénéficie de la puissance de Wordpress, le PageBuilder de Magento est la fonctionnalité de mise en page la plus avancée disponible nativement dans une solution e-commerce gratuite.

PageBuilder bénéficie d'un suivi et de mises à jour très fréquentes comme l'atteste la release note du module consultable ici 

Pour couronner le tout, le PageBuilder peut être facilement enrichi avec de nouveaux composants. Ainsi, il est possible de mettre en place des éléments spécifiques pour un client.

Une simplicité à toutes épreuves

Le PageBuilder est étudié pour être très visuel et facile d'usage. Dans l'éditeur de page, il faut glisser et déposer des « composants » pour formater le contenu. Ainsi, on a un affichage dans l'éditeur qui est proche du rendu final.

Le composant est la clé de voûte du PageBuilder. Il existe toutes sortes de composants aussi bien pour positionner les autres composants (grille, colonne, …) que pour afficher du textes, des images, des vidéos, …

Prenons l'exemple suivant : je souhaite créer un bloc dans ma page avec à gauche une image, et à droite un texte.
Je souhaite que ce bloc soit d'un fond gris foncé et le texte doit être blanc.
Mais en mobile, je souhaite que l'image soit en pleine largeur et le texte en dessous.
L'image doit faire 1/4 de la largeur totale.

C'est parti !

On commence par ajouter l'élément "colonne" :


On définit la largeur des colonnes :


On rajoute l'image :


On écrit le texte :


On définit la couleur de fond :


Après enregistrement, voici le résultat :

Des patrons de page pour produire du contenu rapidement

L'une des fonctionnalités intéressantes est le patron de page. Cela permet de prédéfinir un ensemble de type de page.

Imaginons que vous avez des pages de type galeries d'images dédiées à la présentation de collections et d'autres dédiées à décrire en détail la confection de certain produit.

Vous pouvez définir deux patrons de page. Le premier avec un chapeau et une grille d'images. Le second avec un chapeau et un ensemble de bloc avec une photo et un texte descriptif.

Aussi, l'éditorialiste pourra très simplement créer des nouvelles pages en utilisant le modèle prédéfini qui convient au type de contenu.

Pour créer une nouveau patron, il fait créer une page et depuis le menu en haut cliquer sur « Sauvegarder en tant que template »


Pour utiliser un patron, il faut de la même manière choisir depuis le menu cliquer sur « Appliquer un template » :


Ensuite choisir le patron à appliquer :


Et voilà, le patron est appliqué :

Un rendu rapide comme l'éclair

D'un point de vue technique, le PageBuilder génère le contenu HTML en JavaScript depuis le backoffice lors de l'enregistrement de la page.

Ainsi, le rendu des pages est une simple lecture du contenu de la base de données sans réinterprétation nécessaire.

Ce fonctionnement permet une grande souplesse sans impacter les performances d'affichage lors de la navigation.

En revanche, il ne permet pas d'effectuer des rendus dynamiques. Nous verrons plus bas des solutions à ce problème.

Des composants sur mesures : dites adieu aux Widgets

Les composants natifs permettent de faire beaucoup de choses. Et les possibilités sont presque infinies.
Mais, pour des besoins spécifiques, il est possible de créer des composants adaptés à vos besoins.

Anatomie d'un composant

La composant génère le code html en JavaScript. Un composant c'est donc avant tout du JavaScript, des templates et des XML définissant les attributs et le formulaire.

Voici la structure de notre exemple de composant :

app/code/Tws/DemoPageBuilder
├── etc
│   ├── adminhtml
│   │   └── di.xml
│   └── module.xml
├── registration.php
└── view
    ├── adminhtml
    │   ├── layout
    │   │   └── pagebuilder_tws_hello_form.xml <-- layout permettant le chargement du formulaire
    │   ├── pagebuilder
    │   │   └── content_type
    │   │       └── tws_hello.xml <-- définition du composant
    │   ├── ui_component
    │   │   └── pagebuilder_tws_hello_form.xml <-- définition du formulaire
    │   └── web
    │       ├── css
    │       │   └── source
    │       │       ├── content-type
    │       │       │   └── tws-hello
    │       │       │       └── _import.less <-- CSS pour le rendu du composant dans le back office
    │       │       └── _module.less
    │       ├── js
    │       │   └── content-type
    │       │       └── tws-hello
    │       │           ├── master.js <-- Classe JavaScript générant le rendu pour le frontoffice et stocké en base de données
    │       │           └── preview.js <-- Classe JavaScript générant le rendu en backoffice
    │       └── template
    │           └── content-type
    │               └── tws-hello
    │                   └── default
    │                       ├── master.html <-- Template HTML pour le front
    │                       └── preview.html <-- Template HTML pour le backoffice
    └── frontend
        └── web
            └── css
                └── source
                    ├── content-type
                    │   └── tws-hello
                    │       └── _import.less <-- CSS pour le rendu front
                    └── _module.less

Vous pouvez télécharger les sources du module ici . Nous ne détaillerons pas ici l'écriture du composant. La documentation Magento / Adobe Commerce vous donnera plus de détails .

Utiliser des données dynamiques dans vos composants

Comme nous avons pu le voir, les composants du PageBuilder sont entièrement basés sur du JavaScript. Ils ne peuvent dont pas récupérer des données issues de la base.

Il existe des astuces pour récupérer des informations depuis la base de données. Adobe publie un exemple ici .

La solution est de faire un appel Ajax. L'exemple d'Adobe fonctionne mais ne permet pas l'enregistrement dans la vue finale.
C'est donc un bon exemple si on souhaite afficher une information dynamiquement dans la preview et utiliser un appel Ajax depuis le front pour mettre à jour les informations affichées.

Si vous avez des informations qui ne varient pas dans le temps alors, il est possible de mettre directement les données dans le rendu final.

Dans notre exemple,  nous allons modifier notre widget pour faire un « coucou » au dernier éditeur de la page. Le code du module peut être téléchargé ici

Pour cela, nous rajoutons un argument à Magento\PageBuilder\Model\Stage\RendererPool dans le di.xml du module :

etc/di.xml
    <type name="Magento\PageBuilder\Model\Stage\RendererPool">
        <arguments>
            <argument name="renderers" xsi:type="array">
                <item name="tws-hello" xsi:type="object">Tws\DemoPageBuilder\Model\Stage\Renderer\TwsHello</item>
            </argument>
        </arguments>
    </type>


Puis, nous créons la classe TwsHello :

Model/Stage/Renderer/TwsHello.php
<?php
namespace Tws\DemoPageBuilder\Model\Stage\Renderer;

class TwsHello implements \Magento\PageBuilder\Model\Stage\RendererInterface
\{
    protected $authSession;

    public function __construct(
        \Magento\Backend\Model\Auth\Session $authSession
    ) \{
        $this->authSession = $authSession;
    }

    public function render(array $params): array
    \{
        return ['message' => __('Hello %1', $this->authSession->getUser()->getUserName())];
    }
}


Enfin, nous modifions le preview.js pour y rajouter la méthode « afterObservablesUpdated » :

view/adminhtml/web/js/content-type/tws-hello/preview.js
    Preview.prototype.afterObservablesUpdated = function() \{
        $super.afterObservablesUpdated.call(this);

        if (this.flagAuthorUpdated) \{
            // observable are updated on update datastore (this.contentType.dataStore.set)
            // to avoid infinit loop, we do noting if the data label was updated
            return;
        }

        // Get the url to call
        var url = Config.getConfig("preview_url");
        const requestConfig = \{
            method: "POST",
            data: \{
                role: this.config.name
            }
        };

        jquery.ajax(url, requestConfig).done(function(response) \{
            this.flagAuthorUpdated = true;
            this.contentType.dataStore.set('label', response.data.message);

            // We can use this.data.main.html(response.data.message);
            // But this way, only the dom is updated.
            // The data is not be updated and not rendered on the master template
        }.bind(this));
    };


Il ne reste plus qu'à sauvegarder la page avec le composant pour observer le résultat.

Intégration d'un widget

Pour effectuer un rendu dynamique, il est possible de faire un appel Ajax et mettre à jour les données lors du retour de ce dernier.
Forcément, un affichage après un appel Ajax n'est pas instantané et peut avoir des impacts sur le confort de la navigation.
Pour effectuer un rendu dynamique, il est possible d'utiliser des Widgets.

Dans cet exemple, nous allons intégrer le widget natif de produit « catalog product link ».
Pour cela, il faudra que le rendu du master.html contienne un a appel du widget dont voici un exemple :

\{\{widget type="Magento\Catalog\Block\Product\Widget\Link" anchor_text="test" title="test" template="product/widget/link/link_inline.phtml" id_path="product/1"}}

Nous allons procéder en plusieurs étapes :

  • Création des templates
  • Définition des attributs
  • Mise en place de l'appel Ajax permettant la traduction du SKU en Id de l'entité
  • Traduction du DOM en id produit et inversement à l'aide d'un converter

L'exemple est téléchargeable ici 

Création des templates


Les templates sont très basiques. Le master.html est un simple div qui contiendra le widget vu plus haut :

view/adminhtml/web/template/content-type/tws-hello/default/master.html
<!--master.html-->
<div attr="data.main.attributes" html="data.main.html"></div>


Pour le preview.html permet le rendu du nom du produit avec son sku. Ces informations seront renvoyées par l'ajax :

view/adminhtml/web/template/content-type/tws-hello/default/preview.html
<!-- preview.html -->
<div
    attr="data.main.attributes"
    class="pagebuilder-content-type"
    event="\{ mouseover: onMouseOver, mouseout: onMouseOut }, mouseoverBubble: false"
>
    <render args="getOptions().template" ></render>
    <p ko-style="data.main.style" css="data.main.css" html="data.main.html"></p>
</div>

Définition des attributs


Afin de stocker le sku, nous allons rajouter un attribut data-sku. Nous renommons aussi le contenu HTML anciennement label en productid.
Pour cela, on modifie tws_hello.xml :

view/adminhtml/pagebuilder/content_type/tws_hello.xml
                …
                <elements>
                    <element name="main">
                        …
                        <attribute name="sku" source="data-sku"/>
                        <html name="productid" converter="Magento_PageBuilder/js/converter/html/tag-escaper"/>
                    </element>


Nous modifions le formulaire pagebuilder_tws_hello_form.xml pour rajouter le champ SKU :

view/adminhtml/ui_component/pagebuilder_tws_hello_form.xml
    …
    <fieldset name="general" sortOrder="20">
        <settings>
            <label/>
        </settings>
        <field name="sku" sortOrder="10" formElement="input">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="source" xsi:type="string">page</item>
                </item>
            </argument>
            <settings>
                <dataScope>sku</dataScope>
                <dataType>text</dataType>
                <label translate="true">Sku</label>
            </settings>
        </field>
    </fieldset>
</form>

Mise en place de l'appel Ajax permettant la traduction du SKU en Id de l'entité


Nous allons modifier maintenant le renderer pour prendre en compte le paramètre SKU et retourner les informations d'affichage :

Model/Stage/Renderer/TwsHello.php
<?php
namespace Tws\DemoPageBuilder\Model\Stage\Renderer;

class TwsHello implements \Magento\PageBuilder\Model\Stage\RendererInterface
\{
    protected $productRepository;

    public function __construct(
        \Magento\Catalog\Model\ProductRepository $productRepository
    ) \{
        $this->productRepository = $productRepository;
    }

    public function render(array $params): array
    \{
        try \{
            $product = $this->productRepository->get($params['sku']);
            return [
                'productId' => $product->getId(),
                'label' => sprintf('%s « %s »', $product->getName(), $product->getSku()),
            ];
        } catch(\Exception $e) \{
            return ['productId' => false];
        }
    }
}


Nous adaptons le preview.html pour questionner le backend et mettre à jour les informations affichées :

view/adminhtml/web/template/content-type/tws-hello/default/preview.html
    Preview.prototype.afterObservablesUpdated = function() \{
        $super.afterObservablesUpdated.call(this);

        if (this.label) \{
            // afterObservablesUpdated is called 2 times
            // first the ajax is launched, the data are updated
            // second the dom is updated with comprehensible data
            this.data.main.html(this.label);
            this.label = false;
            return;
        }

        const sku = this.contentType.dataStore.get('sku');

        if ('' == sku) \{
            this.label = 'Define a sku in the configuration';
            this.contentType.dataStore.set('productid', '0');
            return;
        } else \{
            // Get the url to call
            var url = Config.getConfig("preview_url");
            const requestConfig = \{
                method: "POST",
                data: \{
                    role: this.config.name,
                    sku: sku
                }
            };

            jquery.ajax(url, requestConfig).done(function(response) \{
                if (response.data.productId) \{
                    this.label = response.data.label;
                    this.contentType.dataStore.set('productid', response.data.productId);
                } else \{
                    this.data.main.html('Invalid SKU');
                }
            }.bind(this));
        }
    };


Ici, nous notons une petite subtilité. La méthode afterObservablesUpdated est appelée après que les champs observables sont mis à jour.

La condition this.label n'est pas vérifiée. Nous passons donc à la suite.

Si nous n'avons pas défini de sku (lors du premier chargement) alors, nous rentrons dans la condition suivante : '' = sku.
Le message « Define a sku in the configuration » est changé dans le this.label. La définition de productid permet le rappel de la méthode afterObservablesUpdated.
Comme this.label est défini, nous passons dans la première condition et le dom est mis à jour.

Si nous avons défini le sku, alors nous rentrons dans l'autre condition. L'appel Ajax est effectué. Le this.label est défini en fonction du retour ajax. Aussi l'attribut html (productid) est défini.

Traduction du DOM en id produit et inversement à l'aide d'un converter


Nous avons maintenant un composant qui permet d'afficher l'ID du produit à partir du SKU. Pour intégrer le widget, nous allons créer un converter.
Son rôle est de traduire le contenu de la node principale (data.main.html) lors de la récupération depuis la base de donnée et lors de l'écriture.

Un converteur a donc deux méthodes permettant d'effectuer ces deux opérations :

  • fromDom
  • toDom

Modifions le converter défini dans tws_hello.xml :

view/adminhtml/pagebuilder/content_type/tws_hello.xml
                        …
                        <html name="productid" converter="Tws_DemoPageBuilder/js/converter/html/catalog-product-link-widget"/>
                    </element>
                </elements>
            </appearance>
        </appearances>
    </type>
</config>


Créons maintenant le converter :

view/adminhtml/web/js/converter/html/catalog-product-link-widget.js
define([], function () \{
    var CatalogProductLinkWidget = function() \{
        "use strict";

        function CatalogProductLinkWidget() \{};

        return CatalogProductLinkWidget;
    }();

    return CatalogProductLinkWidget;
});


Ajoutons la méthode en charge de traduire l'id en l'entité appel du widget :

view/adminhtml/web/js/converter/html/catalog-product-link-widget.js
       CatalogProductLinkWidget.prototype.toDom = function toDom(name, data) \{
            if (data[name] && data[name] !== "") \{
                if (data[name] == 0) \{
                    return '';
                }

                const productId = data[name];
                return '\{\{widget type="Magento\\Catalog\\Block\\Product\\Widget\\Link" template="product/widget/link/link_inline.phtml" id_path="product/' + productId + '"}}';
            }

            return '';
        };


Ici, nous récupérons la donnée avec data[name]. Nous vérifions sont existence et qu'elle n'est pas nulle.

Ajoutons maintenant la méthode inverse :

view/adminhtml/web/js/converter/html/catalog-product-link-widget.js
        CatalogProductLinkWidget.prototype.fromDom = function fromDom(value) \{
            cons match = value.match(new RegExp('id_path="product/([0-9]+)"'));
            if (match) \{
                return match[1];
            }

            return '';
        };

À l'aide d'une expression régulière, nous récupèrons l'attribut productid.


L'intégration du widget est maintenant terminé. Il est naturellement possible d'aller plus loin. Par exemple en rajoutant des options pour les autres attributs du widget ou en améliorant la sélection du produit.

À vous de jouer !

Dans cet article nous avons découvert le Page Builder de Magento, en quoi ce dernier est puissant et comment créer des composants adaptés à vos besoins.

Prochainement nous vous dirons comment nous avons intégré Tailwind dans le PageBuilder.

C'est maintenant à vous de jouer !