L’intérêt de ce billet est de donner quelques astuces pour adapter rapidement du code javascript aux standards jQuery UI Widget pour une meilleure exploitation dans Magento 2.
Télécharger les sources: Magento2-jQuery-widget-example.
Dans cette archive au format ZIP, deux fichiers:
- la version « old » du code du module, fonctionnelle mais pas formatée à la sauce jQuery UI Widget Factory
- la version standardisée jQuery UI Widget Factory du code, pour une meilleure exploitation dans Magento 2
Note: très bonne ressource, en marge de la doc officielle, pour créer un widget jQuery UI pour Magento 2.
Découpage du squelette de base d’un widget jQuery UI pour Magento 2
Note: notre fichier d’exemple est placé dans un thème Magento 2 suivant le chemin: app/design/frontend/MyVendor/mytheme/web/js/attribute-carousel.js
On commence par un bon vieux define
qui nous servira à définir notre widget en tant que de module via RequireJS. Il pourra ensuite s’articuler avec d’autres modules définis en tant que tels, toujours via RequireJS:
1 2 3 4 5 6 7 8 |
define([ 'jquery', // obligatoire 'jquery-ui-modules/widget', // obligatoire 'autre module...', // facultatif 'autre module...', // facultatif 'slick' // le plugin tiers jQuery AMD Slick Carousel ], function($) { 'use strict'; |
Déclaration ici des variables globales pour ce widget (facultatif, votre widget n’en a peut-être pas besoin):
1 2 |
let ATTRIBUTE_SELECTOR_ELEMENT_ID, SELECTED_OPTION_INDEX; |
Déclaration du widget sous la forme $.widget('mage.<nomDuWidget>', {:
1 |
$.widget('mage.nomDuWidget', { |
Déclaration des options du widget sous forme d’objet options
, si il exploite des arguments (facultatif).
Le libellé options
ne doit pas être modifié!
(voir aussi les commentaires directement dans le bout de code ;))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
options: { // Option "simple": attributeSelectorId: '[id^="attribute"]', // Passage de plusieurs paramètres sous forme de sous-objet "slick" (par exemple, si on souhaite exploiter un plugin jQuery tiers qui propose des paramètres): slick: { infinite: false, slidesToShow: 3, slidesToScroll: 3, prevArrow: '<button type="button" class="clear slick-prev"><svg viewBox="0 0 100 100" class="icon icon-arrow-back-chevron"><use xlink:href="#icon-arrow-back-chevron"></use></svg></button>', nextArrow: '<button type="button" class="clear slick-next"><svg viewBox="0 0 100 100" class="icon icon-arrow-forward-chevron"><use xlink:href="#icon-arrow-forward-chevron"></use></svg></button>' } }, // <= ATTENTION A NE PAS OUBLIER LA VIRGULE JUSTE LA! |
La fonction privée _create"
(présence obligatoire), qui sera automatiquement exécutée à chaque initialisation de ce widget depuis un PHTML, un fichier JS, …
Le libellé _create
ne doit pas être modifié!
1 |
_create: function () { |
Variabiliser le sélecteur, présent dans le DOM, sur lequel est initialisé le widget. Remarquer ici le chemin this.options.attributeSelectorId
pour exploiter la valeur de l’option simple attributeSelectorId
déclarée plus haut dans notre objet options
.
Et variabiliser this
(ici sous l’alias that
) est important dès lors qu’on cherche à exécuter une fonction de ce widget (on le verra plus bas):
1 2 |
const $_THIS = $(this.options.attributeSelectorId), that = this; |
On précède chaque exécution d’une fonction de ce widget par notre « that » alias de « this »:
1 |
that.appendCarouselToDOM($_THIS, that.ulToCarousel(that.clonedSelectToUl(that.cloneSelect($_THIS)))); |
Commentaires directement dans le bout de code 😉
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$('.slick-slide').on('click', function () { const $_THIS = $(this); // Les mises à jour des valeurs de nos variables globales se font sous ce mode: ATTRIBUTE_SELECTOR_ELEMENT_ID = $_THIS.closest('ul').attr('id'); SELECTED_OPTION_INDEX = $_THIS.attr('data-slick-index'); // Systématiquement, on précède chaque exécution d'une fonction de ce widget par notre "that" alias de "this": // (si on oublie le "that", la fonction n'est pas exécutée) that.synchronizeCarouselWithSelect(ATTRIBUTE_SELECTOR_ELEMENT_ID, SELECTED_OPTION_INDEX); that.handleCarouselSelectedOptionClass(ATTRIBUTE_SELECTOR_ELEMENT_ID, SELECTED_OPTION_INDEX); }); }, // <= ATTENTION A NE PAS OUBLIER LA VIRGULE JUSTE LA! |
Manière de déclarer une fonction (<nomDeLaFontion>: function(<argument>, <argument>) { return <quelque chose> })
:
1 |
ulToCarousel: function(clonedSelectToUl) { |
Ici, on déclare le chemin this.options.slick
pour exploiter la valeur d’une option du sous-objet « slick » déclaré plus haut dans notre objet « options ».
Note: il y a probablement plus simple pour passer une série d’arguments que de repointer un à un tous les objets. Je manquais de temps sur le projet en question.
1 2 3 4 5 6 7 8 |
return clonedSelectToUl.slick({ infinite: this.options.slick.infinite, slidesToShow: this.options.slick.slidesToShow, slidesToScroll: this.options.slick.slidesToScroll, prevArrow: this.options.slick.prevArrow, nextArrow: this.options.slick.nextArrow }); }, // <= ATTENTION A NE PAS OUBLIER LA VIRGULE JUSTE LA! |
Manière de déclarer une fonction (
1 2 3 4 5 |
synchronizeSelectWithCarousel: function(relatedElementID, selectedOptionIndex) { $('ul#' + relatedElementID).slick('slickGoTo', parseInt(selectedOptionIndex)); }, // <= SAUF SI ON VIENT DE DECLARER LA TOUTE DERNIERE FONCTION DU WIDGET. }); |
On n’oublie pas de retourner le widget avant de refermer définitivement l’accolade et la parenthèse qui embrassent (c’est bô <3!) le widget:
1 2 |
return $.mage.nomDuWidget; }); |
Version finale (également disponible dans le ZIP):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
define([ 'jquery', 'jquery-ui-modules/widget', 'Magento_ConfigurableProduct/js/configurable', 'slick' ], function($, configurable) { 'use strict'; let ATTRIBUTE_SELECTOR_ELEMENT_ID, // Peut être un ID de <select> ou de <ul> (carousel) SELECTED_OPTION_INDEX; // Index de l'option sélectionnée par l'utilisateur dans un <select> ou un <ul> (carousel) $.widget('mage.attributeCarousel', { options: { attributeSelectorId: '[id^="attribute"]', slick: { infinite: false, slidesToShow: 3, slidesToScroll: 3, prevArrow: '<button type="button" class="clear slick-prev"><svg viewBox="0 0 100 100" class="icon icon-arrow-back-chevron"><use xlink:href="#icon-arrow-back-chevron"></use></svg></button>', nextArrow: '<button type="button" class="clear slick-next"><svg viewBox="0 0 100 100" class="icon icon-arrow-forward-chevron"><use xlink:href="#icon-arrow-forward-chevron"></use></svg></button>' } }, _create: function () { const $_THIS = $(this.options.attributeSelectorId), that = this; // 1. On clone le <select> // 2. On le transforme en <ul>/<li> // 3. On initialise le carousel sur le markup du <select> cloné et transformé en <ul>/<li> // 4. On injecte le carousel dans le DOM that.appendCarouselToDOM($_THIS, that.ulToCarousel(that.clonedSelectToUl(that.cloneSelect($_THIS)))); // Lorsqu'on change la valeur d'un attribut produit depuis le carousel $('.slick-slide').on('click', function () { const $_THIS = $(this); // Mise à jour de: ATTRIBUTE_SELECTOR_ELEMENT_ID = $_THIS.closest('ul').attr('id'); SELECTED_OPTION_INDEX = $_THIS.attr('data-slick-index'); that.synchronizeCarouselWithSelect(ATTRIBUTE_SELECTOR_ELEMENT_ID, SELECTED_OPTION_INDEX); that.handleCarouselSelectedOptionClass(ATTRIBUTE_SELECTOR_ELEMENT_ID, SELECTED_OPTION_INDEX); }); // Lorsqu'on change la valeur d'un attribut produit depuis le select $_THIS.on('change', function () { // Mise à jour de: ATTRIBUTE_SELECTOR_ELEMENT_ID = $_THIS.attr('id'); SELECTED_OPTION_INDEX = $_THIS.prop('selectedIndex'); that.synchronizeSelectWithCarousel(ATTRIBUTE_SELECTOR_ELEMENT_ID, SELECTED_OPTION_INDEX); that.handleCarouselSelectedOptionClass(ATTRIBUTE_SELECTOR_ELEMENT_ID, SELECTED_OPTION_INDEX); }); }, // 1. On clone le <select>, sous conditions... cloneSelect: function(thisObj) { const $_THIS = thisObj, $_THIS_ID = $_THIS.attr('id'); // Vérifier si le select est [disabled] ou si il a déjà été cloné // [TODO_DEV] Rajouter une condition qui vérifie si les options du select affichent des images. Si false, on ne créera pas de carousel pour ce champ. if ($_THIS.attr('disabled') || $('ul#' + $_THIS_ID).length) { // Si l'un des deux cas est true, on ne fait rien return false; } else { // Sinon return $_THIS.clone().prop('id', $_THIS_ID); } }, // 2. On transforme le <select> cloné en <ul>/<li> clonedSelectToUl: function(clonedSelect) { const $_CLONED_SELECT = $(clonedSelect); return $_CLONED_SELECT.find("option").map(function() { const $_THIS = $(this); return $("<li>").attr("value", $_THIS.attr("value")).text($_THIS.text()).get(); }).appendTo($("<ul>").attr({ id: $_CLONED_SELECT.attr("id"), name: $_CLONED_SELECT.attr("name") })).parent().replaceAll($_CLONED_SELECT); }, // 3. On initialise le carousel (slick) sur le markup du <select> cloné et transformé en <ul>/<li> ulToCarousel: function(clonedSelectToUl) { return clonedSelectToUl.slick({ infinite: this.options.slick.infinite, slidesToShow: this.options.slick.slidesToShow, slidesToScroll: this.options.slick.slidesToScroll, prevArrow: this.options.slick.prevArrow, nextArrow: this.options.slick.nextArrow }); }, // 4. On injecte le carousel dans le DOM appendCarouselToDOM: function(thisObj, carouselElement) { return carouselElement.insertBefore(thisObj); }, // On gère la classe CSS sur l'option sélectionnée par l'utilisateur dans le carousel handleCarouselSelectedOptionClass: function(relatedElementID, selectedOptionIndex) { $('ul#' + relatedElementID).find('.is-selected-option').removeClass('is-selected-option'); $('[data-slick-index="' + selectedOptionIndex + '"]', 'ul#' + relatedElementID).addClass('is-selected-option'); }, // Synchronization slick>select (select2 se gère tout seul) synchronizeCarouselWithSelect: function(relatedElementID, selectedOptionIndex) { const SELECTED_OPTION_VALUE = $('option', 'select#' + relatedElementID).eq(selectedOptionIndex).attr('value'); $('select#' + relatedElementID).val(SELECTED_OPTION_VALUE).trigger('change'); }, // synchronization select>slick (select2 se gère toujours encore tout seul :)) synchronizeSelectWithCarousel: function(relatedElementID, selectedOptionIndex) { $('ul#' + relatedElementID).slick('slickGoTo', parseInt(selectedOptionIndex)); // https://github.com/kenwheeler/slick/issues/235#issuecomment-43406956 $(window).trigger('resize'); } }); return $.mage.attributeCarousel; }); // https://www.siphor.com/different-ways-using-javascript-magento-2/ // https://jason.codes/2019/06/magento-2-create-jquery-ui-widget/ |
Déclaration de notre widget jQuery UI via RequireJS
Fichier app/design/frontend/MyVendor/mytheme/requirejs-config.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var config = { map: { "*": { attributeCarousel: 'js/attribute-carousel' } }, // Paths defines associations from library name (used to include the library, // for example when using "define") and the library file path. paths: { 'slick': 'js/vendor/slick/slick.min' }, // Shim: when you're loading your dependencies, requirejs loads them all // concurrently. You need to set up a shim to tell requirejs that the library // (e.g. a jQuery plugin) depends on another already being loaded (e.g. depends // on jQuery). shim: { 'slick': { deps: ['jquery'] } } }; |
Utilisation du widget dans un template PHTML de Magento 2
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<script type="text/x-magento-init"> { "#product_addtocart_form": { "attributeCarousel": { "attributeSelectorId": "#attribute76", "slick": { "slidesToShow": 4, "slidesToScroll": 4 } } } } </script> |