Créer un widget personnalisé pour Elementor (WordPress)

Créer un widget personnalisé pour Elementor (WordPress)
Categories:
Posté le

Elementor,  constructeur de sites Web bien connu des développeurs WordPress, adoré ou détesté, il a incontestablement changé la manière de concevoir des sites avec WordPress.

Son principal atout, pour moi, étant l'apport de possibilités pour des utilisateurs non technique d'effectuer un certain nombre de modifications de manière autonome.

Elementor se démarque aussi par un écosystème très riche qui apporte toutes sortes d'extensions afin de l'intégrer à de multiples solutions.

Le sujet de l'article d'aujourd'hui, porte sur un autre avantage d'Elementor, son extensibilité, comme la création de widgets personnalisés, nous allons utiliser comme exemple un widget affichant la liste des services d'un développeur web freelance.
Liste de services qui sera récupérée en JSON depuis une API REST par exemple.

Je me suis lancé le défi de créer mon premier Widget personnalisé et de vous partager la démarche.

Voici le rendu que l'on veut obtenir :


Ainsi que le processus d'ajout du widget à une page, une fois créé, en vidéo :


(le slider ne paraît pas très fluide sur la vidéo, mais ce n'est que le rendu sur la vidéo)

Nous allons maintenant voir les étapes qui nous permettent d’atteindre ce résultat.

Initialisation de l’environnement de développement :

Afin de nous simplifier la tâche dans la création d'une installation d'un site WordPress d'exemple qui servira d’environnement de développement nous allons utiliser Bedrock (https://roots.io/bedrock/).

Bedrock permet d'initialiser une installation WordPress grâce à Composer, mais apporte également beaucoup de flexibilité dans la gestion de la configuration et notamment de l’environnement (variables d’environnement).

Afin d’initialiser un projet, lancez la commande suivante :

composer create-project roots/bedrock elementor-custom-element

Il faut ensuite modifier un peu la configuration de Bedrock par la modification du ficher .env, voici un exemple :

DB_NAME='elementorelem'
DB_USER='elementorelem'
DB_PASSWORD='elementorelem'

# Optionally, you can use a data source name (DSN)
# When using a DSN, you can remove the DB_NAME, DB_USER, DB_PASSWORD, and DB_HOST variables
# DATABASE_URL='mysql://database_user:database_password@database_host:database_port/database_name'

# Optional database variables
# DB_HOST='localhost'
# DB_PREFIX='wp_'

WP_ENV='development'
WP_HOME='http://localhost:9000'
WP_SITEURL="${WP_HOME}/wp"

# Specify optional debug.log path
# WP_DEBUG_LOG='/path/to/debug.log'

...

Cela permet de configurer l'adresse du site, la configuration de la base de données, ...

Puis pour compléter le processus d'installation de WordPress, nous allons utiliser le petit serveur intégré de PHP

 php -S 0.0.0.0:9000 -t web/

Complétez ensuite les différentes étapes de l'installation de WordPress.

Installer ensuite les éléments suivants depuis l'interface de WordPress :

  • Plugin : Elementor
  • Thème : Hello Elementor

Puis installer le thème enfant de Hello Elementor, que vous pouvez trouver ici : https://github.com/elementor/hello-theme-child

Thème enfant qui est à mettre à l'emplacement suivant, puis à activer :

web/app/themes

Créez ensuite une page, par exemple 'Nos services', avec comme adresse : http://localhost:9000/nos-services et comme template Elementor Pleine Largeur.

Création du custom Widget :

La structure d'un Widget est la suivante :

<?php
class Elementor_Test_Widget extends \Elementor\Widget_Base {

	public function get_name() {}

	public function get_title() {}

	public function get_icon() {}

	public function get_categories() {}

	public function get_script_depends() {}

	public function get_style_depends() {}

	protected function _register_controls() {}

	protected function render() {}

	protected function content_template() {}

}

Le lien vers la documentation est le suivant : https://developers.elementor.com/creating-a-new-widget/documentation qui ne semble pas à jour, on a ainsi content_template à la place du _content_template.

Nous allons ainsi commencer par créer un fichier nommé freelanceServices.php à la racine de notre thème enfant.

<?php

namespace Elementor;

defined('ABSPATH') || die();

use Elementor\Widget_Base;
use Elementor\Controls_Manager;

class FreelanceServices extends Widget_Base
{

    public function __construct($data = array(), $args = null)
    {
        parent::__construct($data, $args);

        wp_register_style('lightslidercss', get_stylesheet_directory_uri() . '/assets/css/lightslider.min.css', array(), '1.0.0', 'all');
        wp_register_script('lightsliderjs', get_stylesheet_directory_uri() . '/assets/js/lightslider.min.js', array('jquery'), '1.0.0', true);
        wp_register_script('lightsliderinit', get_stylesheet_directory_uri() . '/assets/js/lightslider-init.js', array('jquery', 'lightsliderjs'), '1.0.0', true);
    }

    public function get_style_depends()
    {
        $styles = ['lightslidercss'];

        return $styles;
    }

    public function get_script_depends()
    {
        $scripts = ['lightsliderjs', 'lightsliderinit'];

        return $scripts;
    }

    public function get_name()
    {
        return 'freelanceservice';
    }

    public function get_title()
    {
        return 'Freelance Services';
    }

    public function get_icon()
    {
        return 'eicon-favorite';
    }

    public function get_categories()
    {
        return ['basic'];
    }

....

Dans ce début d'implémentation, nous donnons un nom à notre widget, une icône, une catégorie, ainsi qu'initialiser et inclure les dépendances JS et CSS.

Il vous faut :

Nous allons ensuite ajouter les "contrôles'  et ce dans la fonction _register_controls, la liste des contrôles disponibles se trouve ici : https://developers.elementor.com/elementor-controls/

La fonction est la suivante :

protected function _register_controls()
{
        $this->start_controls_section(
            'section_content',
            array(
                'label' => 'Les services',
            )
        );

        $this->add_control(
            'title',
            array(
                'label'   => 'Titre',
                'type'    => Controls_Manager::TEXT,
                'default' => 'Titre',
            )
        );

        $this->add_control(
            'text_center',
            array(
                'label'   => "Alignement du text",
                'type'    => \Elementor\Controls_Manager::SELECT,
                'default' => 'left',
                'options' => array(
                    'left'  => 'Gauche',
                    'center' => 'Centré',
                    'right' => 'Droite'
                ),
            )
        );

        $this->end_controls_section();

        $this->start_controls_section(
            'service_content',
            [
                'label' => "Affichage des services",
                'tab' => \Elementor\Controls_Manager::TAB_CONTENT
            ]
        );

        $this->add_group_control(
            \Elementor\Group_Control_Border::get_type(),
            [
                'name' => 'border',
                'label' => 'Bordure',
                'selector' => '{{WRAPPER}} .wrapper',
            ]
        );

        $this->add_control(
            'margin',
            [
                'label' => "Marges",
                'type' => Controls_Manager::DIMENSIONS,
                'size_units' => ['px', '%', 'em'],
                'selectors' => [
                    '{{WRAPPER}} .margin-services' => 'margin: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
                ],
            ]
        );

        $this->add_control(
            'text_service_center',
            array(
                'label'   => "Alignement du text",
                'type'    => \Elementor\Controls_Manager::SELECT,
                'default' => 'left',
                'options' => array(
                    'left'  => 'Gauche',
                    'center' => 'Centré',
                    'right' => 'Droite'
                ),
            )
        );

        $services = wp_remote_get(getenv('SERVICE_URL'));

        if (!is_wp_error($services)) {
            $services_array = $this->get_services($services);

            $this->add_control(
                'services_array',
                [
                    'label' => "Les services",
                    'type' => \Elementor\Controls_Manager::HIDDEN,
                    'default' => $services_array,
                ]
            );
        }

        $this->add_control(
            'color_alternate',
            [
                'label' => "Alterner les couleurs",
                'type' => \Elementor\Controls_Manager::SWITCHER,
                'label_on' => "Oui",
                'label_off' => "Non",
                'return_value' => 'yes',
                'default' => 'yes',
            ]
        );

        $this->add_control(
            'color_1',
            [
                'label' => "Couleur impaire",
                'type' => \Elementor\Controls_Manager::COLOR,
                'scheme' => [
                    'type' => \Elementor\Core\Schemes\Color::get_type(),
                    'value' => \Elementor\Core\Schemes\Color::COLOR_2,
                ],
                'selectors' => [
                    '{{WRAPPER}} .odd-bg' => 'background-color: {{VALUE}}',
                ],
            ]
        );

        $this->add_control(
            'color_2',
            [
                'label' => "Couleur paire",
                'type' => \Elementor\Controls_Manager::COLOR,
                'scheme' => [
                    'type' => \Elementor\Core\Schemes\Color::get_type(),
                    'value' => \Elementor\Core\Schemes\Color::COLOR_3,
                ],
                'selectors' => [
                    '{{WRAPPER}} .even-bg' => 'background-color: {{VALUE}}',
                ],
            ]
        );

        $this->add_control(
            'color_3',
            [
                'label' => "Couleur du text",
                'type' => \Elementor\Controls_Manager::COLOR,
                'scheme' => [
                    'type' => \Elementor\Core\Schemes\Color::get_type(),
                    'value' => \Elementor\Core\Schemes\Color::COLOR_2,
                ],
                'selectors' => [
                    '{{WRAPPER}} .text-color' => 'color: {{VALUE}}',
                ],
            ]
        );

        $this->add_control(
            'margin_services',
            [
                'label' => "Marges entre les services",
                'type' => Controls_Manager::DIMENSIONS,
                'size_units' => ['px', '%', 'em'],
                'selectors' => [
                    '{{WRAPPER}} .margin-service' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
                ],
            ]
        );

        $this->end_controls_section();
}

Les contrôles sont séparés en groupes, il y en a ici 2 :

Une section commence par start_controls_section et se termine par end_controls_section.

Si vous vous posez la question sur cette partie de code :

        $services = wp_remote_get(getenv('SERVICE_URL'));

        if (!is_wp_error($services)) {
            $services_array = $this->get_services($services);

            $this->add_control(
                'services_array',
                [
                    'label' => "Les services",
                    'type' => \Elementor\Controls_Manager::HIDDEN,
                    'default' => $services_array,
                ]
            );
        }

La liste de services est récupérée depuis avec une requête HTTP et la puissance de Bedrock permet ici d'injecter une variable d’environnement SERVICE_URL. La réponse est stockée dans un champ HIDDEN.

Il faut rajouter cette variable d'environnement dans le fichier .env :

SERVICE_URL='http://127.0.0.1:5152/services.json'

Pour simplifier l'exemple il s'agit ici d'un simple fichier JSON mais en réalité cela pourrait faire appel à une API REST / SOAP ou autre, le ficher services.json à la structure suivante :

{
    "services": [
        {
            "name": "Création de site internet",
            "images": [
                "http://127.0.0.1:5151/img/media/logo.png",
                "http://127.0.0.1:5151/img/media/banner-bg.jpg"
            ],
            "description": "<ul style=\"list-style-type: none;\"><li>Sites vitrines</li><li>Sites catalogues</li></ul>"
        },
        {
            "name": "Développements spécifiques",
            "images": [
                "http://127.0.0.1:5151/img/media/banner-bg.jpg"
            ],
            "description": "<p>Des développements qui s'adaptent aux besoins</p><ul style=\"list-style-type: none;\"><li>API REST</li><li>Node.js</li><li>...</li></ul>"
        }
    ]
}

Le petit serveur HTTP, http-server, nous permet de servir facilement le fichier, comme ceci :

http-server -p 5152 --cors

Nous allons également ajouter une fonction à la class FreelanceServices afin de récupérer la liste des services depuis la réponse à la requête :

private function get_services($request)
{
    return json_decode(wp_remote_retrieve_body($request), true)['services'];
}

L'affichage du Widget :

Le rendu / affichage du widget se décompose en 2 fonctions, render et content_template, la première contenant le code PHP qui servira pour le rendu final du site (HTML) et la deuxième en JS avec Backbone, servant d'affichage pour la preview live dans l'éditeur d'Elementor.
D’où la différence d'affichage que l'on peut constater dans la vidéo notamment avec les sliders que je n'ai pas réussi à faire fonctionner en mode Live ainsi que d'autres éléments que ne fonctionnent pas parfaitement sur ce mode, mais l'essentiel est l'affichage final.

Plus d'informations sur le sujet ici : https://developers.elementor.com/render-widget-template/

Nous avons ainsi la fonction render :

protected function render()
{
        $settings = $this->get_settings_for_display();

        $services_array = [];

        if (array_key_exists('services_array', $settings)) {
            $services_array = $settings['services_array'];
        }

        $this->add_inline_editing_attributes('title', 'none');
        ?>
        <div style="max-width: 1140px; margin: auto;">
            <h2 style="text-align: <?= esc_attr($settings['text_center']) ?>;" 
            <?= $this->get_render_attribute_string('title'); ?>><?= wp_kses($settings['title'], array()); ?></h2>
        </div>
        <div class="wrapper margin-services">
            <?php if (count($services_array)) { ?>
                <div style="width: 100%; min-height: 350px;">
                    <?php $i = 0;
                    foreach ($services_array as $s) { ?>
                        <?php if ('yes' == $settings['color_alternate']) { ?>
                            <div class="margin-service <?= ($i % 2 == 0) ? 'odd-bg' : 'even-bg' ?>"
                             style="background-color: <?= ($i % 2 == 0) ? $settings['color_1'] : $settings['color_2'] ?>">
                            <?php } else { ?>
                                <div class="margin-service">
                                <?php } ?>
                                <div style="max-width: 1140px; margin: auto;">
                                    <div style="width: 50%; display: inline-block; height: 100%;">
                                        <ul id="light-slider" class="image-gallery" style="width: 100%; text-align:center;">
                                            <?php foreach ($s['images'] as $image) { ?>
                                                <li data-thumb="<?= $image ?>">
                                                    <a href="<?= $image ?>" data-sub-html="#caption2">
                                                    <img src="<?= $image ?>" <?= $s['name'] ?> style="height:300px;" />
                                                </a>
                                                </li>
                                            <?php } ?>
                                        </ul>
                                    </div>
                                    <div style="width: 48%; display: inline-block; height: 100%; vertical-align: top; text-align: 
                                        <?= esc_attr($settings['text_service_center']) ?>;">
                                        <h3 class="text-color" style="color: <?= $settings['color_3'] ?>"><?= $s['name'] ?></h3>
                                        <p class="text-color" style="color: <?= $settings['color_3'] ?>"><?= $s['description'] ?></p>
                                    </div>
                                </div>
                                </div>
                            <?php $i++;
                        } ?>
                            </div>
                        <?php } ?>
                </div>
        <?php
}

Fonction qui va afficher les différents éléments avec les options de styles définies dans l'éditeur du widget Elementor.

La logique odd et even permet une alternance dans le background entre chaque service comme le montre le rendu.

Il serait évidement pertinent de créer des class CSS spécifiques plutôt que du CSS inline mais dans la cadre d'un exemple cela permet de simplifier les choses. 

Et la fonction content_template :

protected function content_template()
{
        ?>
        <# view.addInlineEditingAttributes( 'title' , 'none' ); #>
        <div style="max-width: 1140px; margin: auto;">
            <h2 style="text-align: {{ settings.text_center }};" {{{ view.getRenderAttributeString( 'title' ) }}}>{{{ settings.title }}}</h2>
        </div>
        <div class="wrapper margin-services" id="freelance-services">
            <# if (settings.services_array) { #>
                <div style="width: 100%; min-height: 350px;">
                    <#
                    let i = 0;
                    #>
                    <# _.each(settings.services_array, function(s) { #>
                        <# if ('yes' == settings.color_alternate) { #>
                            <# if (i % 2 == 0) { #>
                                <div class="margin-service odd-bg" style="background-color: {{ settings.color_1 }};">
                            <# } else { #>
                                <div class="margin-service even-bg" style="background-color: {{ settings.color_2 }};">
                            <# } #>
                        <# } else { #>
                            <div class="margin-service">
                        <# } #>
                        <div style="max-width: 1140px; margin: auto;">
                            <div style="width: 50%; display: inline-block; height: 100%;">
                                <ul id="light-slider" class="image-gallery" style="width: 100%; text-align:center;">
                                    <# _.each(s.images, function(image) { #>
                                        <li data-thumb="{{ image }}">
                                            <a href="{{ image }}" data-sub-html="#caption2"> <img src="{{ image }}" alt="{{ s.name }}" /></a>
                                        </li>
                                        <# }); #>
                                </ul>
                            </div>
                            <div style="width: 48%; display: inline-block; height: 100%; vertical-align: top; text-align: {{ settings.text_service_center }};">
                                <h3 class="text-color" style="color: {{ settings.color_3 }};">{{{ s.name }}}</h3>
                                <p class="text-color" style="color: {{ settings.color_3 }};">{{{ s.description }}}</p>
                            </div>
                        </div>
                    </div>
                    <# ++i; #>
                    <# }); #>
            </div>
            <# } #>
        </div>
        <?php
}  

Qui va reproduire globalement le même comportement, mais cette fois-ci en JS.

Afin de faire fonctionner les sliders, il vous faut créer dans le dossier assets/js le fichier lightslider-init.js avec le contenu :

jQuery(document).ready(function ($) {

    $('.image-gallery').lightSlider({
        gallery: false,
        item: 1,
        auto: true,
        loop: true,
        caption: true,
        speed: 1000,
        pause: 2500
    });

});

Fichier qui va vous permettre de configurer le comportement des sliders, comme par exemple le temps entre chaque slide, ...

Inscrire le widget dans Elementor :

Afin de pouvoir vous service du widget FreelanceServices, vous devez le déclarer à Elementor comme ceci, par exemple dans votre fichier function.php :

<?php

/**
 * Theme functions and definitions
 *
 * @package HelloElementorChild
 */


/**
 * Load child theme css and optional scripts
 *
 * @return void
 */
function hello_elementor_child_enqueue_scripts()
{
	wp_enqueue_style(
		'hello-elementor-child-style',
		get_stylesheet_directory_uri() . '/style.css',
		[
			'hello-elementor-theme-style',
		],
		'1.0.0'
	);
}
add_action('wp_enqueue_scripts', 'hello_elementor_child_enqueue_scripts', 20);


function load_elementor_widget()
{
	require_once __DIR__ . '/freelanceServices.php';
	\Elementor\Plugin::instance()->widgets_manager->register_widget_type(new \Elementor\FreelanceServices());
}
add_action('init', 'load_elementor_widget');

La partie concernant la déclaration du widget se trouvant dans la fonction load_elementor_widget.

Conclusion :

Voici pour la création de mon premier widget personnalisé sur Elementor, qui peut sembler assez complexe dans un premier temps, mais qui au final relève d'une grande flexibilité notamment au niveau des contrôles.

Le point avec lequel j'ai rencontré le plus de difficulté est la notion de live preview et la fonction content_template, le résultat est par ailleurs largement améliorable, n'ayant pas réussi à reproduire l'affichage à l’identique du rendu final et le live preview ne fonctionnant plus / pas parfaitement une fois le widget créé.

Les sources de cet article se trouvent ici : https://github.com/Yann-Carlen/elementor-custom-widget

Si vous cherchez un développeur WordPress afin de réaliser votre site ou de vous accompagner sur une autre problématique n'hésitez pas à me contacter !

Les ressources qui m'ont aidé dans la rédaction de cet article et dont je remercie grandement les auteurs :