Création d'un contrôle personnalisé à l'aide de la bibliothèque Windows pour JavaScript (WinJS)

Si vous avez déjà développé des applications Windows Store en JavaScript, vous connaissez sans doute déjà la bibliothèque Windows pour JavaScript (WinJS). Cette bibliothèque met à votre disposition des styles CSS, ainsi que des contrôles et des utilitaires JavaScript qui vous permettent de créer rapidement des applications respectant les recommandations relatives à l'expérience utilisateur du Windows Store. WinJS propose notamment différentes fonctions que vous pouvez utiliser pour créer des contrôles personnalisés au sein de votre application.

Pour développer vos contrôles JavaScript, vous pouvez utiliser les modèles et les bibliothèques que vous souhaitez : les fonctions de bibliothèque fournies dans WinJS ne constituent qu'une des possibilités qui s'offrent à vous. En créant vos contrôles avec WinJS, vous profitez néanmoins d'un avantage important : vous créez des contrôles personnalisés qui fonctionneront en parfaite cohérence avec les autres contrôles de la bibliothèque. Les modèles de développement et d'utilisation des contrôles personnalisés sont identiques à ceux des autres contrôles de l'espace de noms WinJS.UI.

Dans ce billet, je vais vous expliquer comment créer vos propres contrôles prenant en charge des options configurables, des événements et des méthodes publiques. Certains d'entre vous souhaiteront peut-être savoir comment faire de même avec les contrôles XAML. Sachez que nous aborderons ce sujet dans un prochain billet !

Insertion d'un contrôle JavaScript dans une page HTML

Tout d'abord, rappelons comment insérer un contrôle WinJS dans une page. Deux possibilités s'offrent à vous : vous pouvez procéder à une insertion impérative (en utilisant exclusivement JavaScript, de façon non obtrusive) ou à une insertion déclarative (en insérant les contrôles dans votre page HTML via des attributs complémentaires appliqués aux éléments HTML). Avec la deuxième méthode, les outils fournissent une expérience utilisateur au moment de la conception, ce qui permet par exemple de faire glisser des contrôles à partir d'une boîte à outils. Pour en savoir plus, consultez l'article MSDN suivant : Démarrage rapide : ajout de contrôles et de styles WinJS.

Dans cet article, je vais vous expliquer comment générer un contrôle JavaScript exploitant le modèle de traitement déclaratif de WinJS. Pour insérer un contrôle dans votre page de façon déclarative, suivez la procédure ci-dessous :

  1. Insérez des références WinJS dans votre page HTML. Elles permettront à votre contrôle d'utiliser les API de ces fichiers.

     <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
    
  2. Après les références de balise de script ci-dessus, insérez une référence à un fichier de script contenant votre contrôle.

     <script src="js/hello-world-control.js"></script>
    
  3. Appelez WinJS.UI.processAll() dans le code JavaScript de votre application. Cette fonction analyse votre code HTML et instancie les contrôles déclaratifs détectés. Si vous utilisez les modèles d'application de Visual Studio, la fonction WinJS.UI.processAll() est appelée automatiquement dans default.js.

  4. Insérez le contrôle dans votre page de façon déclarative.

     <div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}"></div>
    

Contrôle JavaScript simple

Créons maintenant un contrôle très simple : le « Hello World » des contrôles ! Voici le code JavaScript utilisé pour définir le contrôle. Créez un fichier appelé hello-world-control.js dans votre projet, en utilisant le code suivant :

 function HelloWorld(element) {
    if (element) {
        element.textContent = "Hello, World!";
    }
};

WinJS.Utilities.markSupportedForProcessing(HelloWorld);

Ensuite, dans le corps de votre page, insérez le contrôle en utilisant le code suivant :

 <div data-win-control="HelloWorld"></div>

Si vous exécutez votre application, vous pouvez constater que le contrôle a été chargé et qu'il affiche le texte « Hello, World! » dans le corps de votre page.

Le seul fragment de code propre à WinJS est l'appel de WinJS.Utilities.markSupportedForProcessing, qui décrit le code comme compatible avec un traitement déclaratif. Ceci vous permet d'indiquer à WinJS que vous autorisez ce code à injecter du contenu dans votre page. Pour en savoir plus à ce sujet, consultez la documentation MSDN consacrée à la fonction WinJS.Utilities.markSupportedForProcessing.

Pourquoi faire appel aux utilitaires WinJS ou à une bibliothèque pour créer un contrôle ?

Je viens de vous montrer comment créer un contrôle déclaratif sans vraiment utiliser WinJS. Examinons maintenant le fragment de code suivant, qui n'utilise toujours pas WinJS pour la majeure partie de ses implémentations. Il s'agit là d'un contrôle plus complexe, possédant des événements, des options configurables et des méthodes publiques :

 (function (Contoso) {
    Contoso.UI = Contoso.UI || {};

    Contoso.UI.HelloWorld = function (element, options) {
        this.element = element;
        this.element.winControl = this;

        this.blink = (options && options.blink) ? true : false;
        this._onblink = null;
        this._blinking = 0;

        element.textContent = "Hello, World!";
    };

    var proto = Contoso.UI.HelloWorld.prototype;

    proto.doBlink = function () {
        var customEvent = document.createEvent("Event");
        customEvent.initEvent("blink", false, false);

        if (this.element.style.display === "none") {
            this.element.style.display = "block";
        } else {
            this.element.style.display = "none";
        }

        this.element.dispatchEvent(customEvent);
    };

    proto.addEventListener = function (type, listener, useCapture) {
        this.element.addEventListener(type, listener, useCapture);
    };

    proto.removeEventListener = function (type, listener, useCapture) {
        this.element.removeEventListener(type, listener, useCapture);
    };

    Object.defineProperties(proto, {
        blink: {
            get: function () {
                return this._blink;
            },

            set: function (value) {
                if (this._blinking) {
                    clearInterval(this._blinking);
                    this._blinking = 0;
                }
                this._blink = value;
                if (this._blink) {
                    this._blinking = setInterval(this.doBlink.bind(this), 500);
                }
            },
            enumerable: true,
            configurable: true
        },

        onblink: {
            get: function () {
                return this._onblink;
            },
            set: function (eventHandler) {
                if (this._onblink) {
                    this.removeEventListener("blink", this._onblink);
                    this._onblink = null;
                }
                this._onblink = eventHandler;
                this.addEventListener("blink", this._onblink);
            }
        }
    });

    WinJS.Utilities.markSupportedForProcessing(Contoso.UI.HelloWorld);
})(window.Contoso = window.Contoso || {}); 

De nombreux développeurs construisent les contrôles de cette manière (à l'aide de fonctions anonymes, de fonctions de constructeur, de propriétés et d'événements personnalisés). Si votre équipe de développement est à l'aise avec cette méthode, inutile de changer vos habitudes ! Cependant, pour certains développeurs, ce code peut s'avérer quelque peu déroutant. En effet, de nombreux développeurs Web ne maîtrisent pas les techniques utilisées. C'est là tout l'intérêt des bibliothèques : elles rendent l'écriture du code moins complexe et moins déroutante.

En plus d'améliorer la lecture du code, WinJS et les autres bibliothèques gèrent pour vous de nombreux éléments subtils dont vous n'avez plus à vous préoccuper (les prototypes, propriétés et événements personnalisés sont utilisés de façon efficace). Elles optimisent l'utilisation de la mémoire et vous évitent de tomber dans les pièges courants. WinJS n'est qu'un exemple : c'est vous qui choisissez ! Pour découvrir plus concrètement les avantages liés à l'utilisation d'une bibliothèque, une fois que vous aurez terminé la lecture de ce billet, je vous invite à réexaminer le code qui y figure et à comparer l'implémentation précédente avec le même contrôle implémenté à la fin de l'article à l'aide des utilitaires de WinJS.

Modèle de base pour les contrôles JavaScript dans WinJS

Le code suivant offre un modèle recommandé pour la création d'un contrôle JavaScript avec WinJS.

 (function () {
    "use strict";

    var controlClass = WinJS.Class.define(
            function Control_ctor(element) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                this.element.textContent = "Hello, World!"
            });
    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });
})();

Dans votre, page, insérez le contrôle de façon déclarative :

 <div data-win-control="Contoso.UI.HelloWorld"></div>

Certaines parties de ce code vous sembleront sans doute inconnues, en particulier si vous n'avez jamais utilisé WinJS. Examinons plus en détail le processus.

  1. Dans cet exemple, nous incluons le code dans un wrapper en utilisant un modèle JavaScript courant : une fonction à exécution automatique.

     (function () {
    …
    })();
    

    Ainsi, nous sommes certains que notre code est intégré et qu'il ne laisse pas traîner derrière lui des variables ou des affectations globales involontaires. Notons qu'il s'agit là d'une recommandation générale et qu'il peut en pratique être utile d'apporter cette modification à la source d'origine.

  2. Le mode strict ECMAScript 5 est activé par le biais de la déclaration "use strict" qui figure au début de la fonction. Nous recommandons cette pratique dans l'ensemble des modèles d'applications Windows Store, afin d'améliorer la détection des erreurs et la compatibilité avec les futures versions de JavaScript. Là encore, il s'agit d'une recommandation générale, qu'il peut également être utile d'appliquer à la source d'origine.

  3. Examinons maintenant le code propre à WinJS. La fonction WinJS.Class.define() est appelée pour créer une classe pour le contrôle. Cette classe permettra notamment de gérer l'appel de la fonction markSupportedForProcessing() et facilitera la création de propriétés sur le contrôle. Il s'agit uniquement d'une application auxiliaire simple axée autour de la fonction standard Object.defineProperties.

  4. Un constructeur appelé Control_ctor est défini. Lorsque la fonction WinJS.UI.processAll() est appelée à partir de default.js, elle analyse le code de la page pour détecter les éventuels contrôles référencés par l'attribut data-win-control, trouve notre contrôle et appelle ce constructeur.

  5. Dans le constructeur, une référence à l'élément figurant sur la page est stockée avec l'objet de contrôle, et une référence à cet objet de contrôle est stockée aux côtés de l'élément.

    • Si vous vous demandez à quoi sert la partie element || document.createElement("div") , sachez qu'elle permet de prendre en charge le modèle impératif. Ainsi, l'utilisateur peut associer par la suite un contrôle à un élément figurant sur la page.
    • Il est en général judicieux de gérer de cette manière la référence à l'élément figurant dans la page, mais aussi de gérer une référence à l'élément associé à l'objet de contrôle en définissant element.winControl. Si vous ajoutez des fonctionnalités telles que des événements, certaines fonctions de bibliothèque pourront ainsi fonctionner. Ne vous inquiétez pas des éventuelles fuites de mémoire pouvant résulter d'une référence à un objet circulaire ou à un élément du DOM : Internet Explorer 10 s'en occupera !
    • Le constructeur modifie le contenu textuel du contrôle afin de définir le texte « Hello, World! » qui s'affiche à l'écran.
  6. Enfin, WinJS.Namespace.define() permet de publier la classe de contrôle et d'exposer le contrôle publiquement pour que l'ensemble du code de notre application puisse y accéder. Sans cet appel, nous serions obligés de trouver une autre solution pour exposer le contrôle à l'aide de l'espace de noms global, afin de coder à l'extérieur de la fonction inline dans laquelle nous travaillons.

Définition des options du contrôle

Pour rendre notre exemple un peu plus intéressant, ajoutons à notre contrôle la prise en charge d'options configurables. Dans ce cas, nous allons ajouter une option permettant à l'utilisateur de faire clignoter le contenu.

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this._doBlink.bind(this), 500);
                        }
                    }
                },

                _doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                },
            });

    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

Cette fois-ci, lorsque vous insérez le contrôle dans votre page, vous pouvez configurer l'option de clignotement en utilisant l'attribut data-win-options :

 <div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}">
</div>

Pour ajouter la prise en charge d'options, apportons les modifications suivantes au code :

  1. Les options sont transmises au contrôle par le biais d'un paramètre (options) appliqué à la fonction du constructeur.
  2. Les paramètres par défaut sont configurés en appliquant des propriétés privées à la classe.
  3. La fonction WinJS.UI.setOptions() est appelée pour transmettre votre objet de contrôle. Cet appel remplace les valeurs par défaut des options configurables du contrôle.
  4. Une propriété publique (blink) est ajoutée à la nouvelle option.
  5. Nous avons ajouté une fonctionnalité en faisant clignoter le texte à l'écran (en pratique, il vaut mieux ne pas coder les styles en dur et utiliser plutôt une classe CSS).

Dans cet exemple, le gros du travail correspond à l'appel de la fonction WinJS.UI.setOptions(). Une fonction utilitaire, setOptions, passe en revue chacun des champs de l'objet options et attribue sa valeur à un champ du même nom sur l'objet cible, qui est le premier paramètre de setOptions.

Dans notre exemple, nous configurons l'objet options par le biais de l'argument data-win-options de notre contrôle win-control, afin de transmettre la valeur true pour le champ « blink ». L'appel de setOptions() dans notre fonction de constructeur détecte ensuite le champ « blink » et copie sa valeur dans un champ portant le même nom dans notre objet de contrôle. Nous avons défini une propriété blink qui fournit une fonction setter. Cette fonction est appelée par setOptions(), ce qui a pour effet de définir le membre _blink de notre contrôle.

Ajout de la prise en charge des événements

Maintenant que nous avons implémenté notre option blink, ajoutons la prise en charge des événements, afin de pouvoir répondre chaque fois qu'un événement blink a lieu :

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,
                _blinkCount: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this._doBlink.bind(this), 500);
                        }
                    }
                },

                _doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                    this._blinkCount++;
                    this.dispatchEvent("blink", {
                        count: this._blinkCount
                    });
                },
            });

    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

    // Set up event handlers for the control
    WinJS.Class.mix(Contoso.UI.HelloWorld,
        WinJS.Utilities.createEventProperties("blink"),
        WinJS.UI.DOMEventMixin);

Insérez le contrôle dans la page, comme nous l'avons déjà expliqué. Vous remarquerez que nous avons ajouté un identifiant à l'élément, pour pouvoir le retrouver facilement par la suite :

 <div id="hello-world-with-events"
    data-win-control="Contoso.UI.HelloWorld"
    data-win-options="{blink: true}"></div>

Ces modifications effectuées, nous pouvons maintenant associer un détecteur d'événements afin d'écouter l'événement « blink » (remarque : dans cet exemple, l'alias $ correspond à document.getElementById) :

 $("hello-world-with-events").addEventListener("blink",
        function (event) {
            console.log("blinked element this many times: " + event.count);
        });

Lorsque vous exécutez ce code, vous voyez un message s'afficher toutes les 500 millisecondes dans la fenêtre JS Console de Visual Studio.

Pour prendre en charge ce comportement, trois modifications ont été apportées au contrôle :

  1. Un appel est émis vers WinJS.Class.mix(Contoso.UI.HelloWorld, WinJS.Utilities.createEventProperties("blink"));, ce qui a pour effet de créer une propriété « onblink » que les utilisateurs peuvent définir par programmation, ou à laquelle les utilisateurs peuvent associer des éléments de façon déclarative dans la page HTML.
  2. L'appel de WinJS.Class.mix(Contoso.UI.HelloWorld, WinJS.UI.DOMEventMixin) ajoute les fonctions addEventListener, removeEventListener et dispatchEvent au contrôle.
  3. L'événement blink est déclenché en appelant that.dispatchEvent("blink", {element: that.element});, et un objet d'événement personnalisé est créé avec un champ d'élément.
  4. Un gestionnaire d'événements est attaché afin de détecter l'événement blink. En réponse, il accède au champ d'élément de l'objet d'événement personnalisé.

Signalons ici que les appels émis vers dispatchEvent() fonctionnent uniquement si vous avez défini this.element dans le constructeur de votre contrôle. Les éléments internes de l'événement mix-in ont besoin de cet élément pour accéder à l'élément dans le DOM. Ce cas fait partie des situations que j'ai déjà évoquées ci-dessus, dans lesquelles un membre élément est requis sur l'objet de contrôle. Ainsi, les événements peuvent être regroupés au sein d'éléments parents dans la page, dans un modèle d'événement DOM de niveau 3.

Exposition de méthodes publiques

Pour terminer la modification de notre contrôle, ajoutons une fonction publique doBlink() qui peut être appelée à tout moment pour forcer le clignotement.

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,
                _blinkCount: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this.doBlink.bind(this), 500);
                        }
                    }
                },

                doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                    this._blinkCount++;
                    this.dispatchEvent("blink", {
                        count: this._blinkCount
                    });
                },
            });
    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

    // Set up event handlers for the control
    WinJS.Class.mix(Contoso.UI.HelloWorld,
        WinJS.Utilities.createEventProperties("blink"),
        WinJS.UI.DOMEventMixin);

Il s'agit là d'une simple convention : nous pouvons en effet remplacer le nom de notre fonction _doBlink par doBlink.

Pour appeler la fonction doBlink() via JavaScript, vous devez disposer d'une référence à l'objet de votre contrôle. Si vous créez votre contrôle de façon impérative, vous disposez peut-être déjà d'une référence. Si vous utilisez un traitement déclaratif, vous pouvez accéder à l'objet de contrôle en utilisant une propriété winControl sur l'élément HTML de votre contrôle. En reprenant par exemple le même code qu'auparavant, vous pouvez accéder à l'objet de contrôle à l'aide du code suivant :

$("hello-world-with-events").winControl.doBlink();

Pour résumer...

Nous venons de passer en revue les aspects les plus courants liés à l'implémentation d'un contrôle :

  1. Insertion du contrôle dans une page.
  2. Transmission d'options de configuration.
  3. Distribution et réponse aux événements.
  4. Exposition de fonctionnalités par le biais de méthodes publiques.

J'espère que ce didacticiel vous permettra de mieux comprendre comment créer un contrôle JavaScript personnalisé relativement simple. Pour toute question sur la gestion de vos propres contrôles personnalisés, consultez le Centre de développement Windows et posez votre question dans les forums. Si vous développez en XAML, sachez que nous publierons prochainement un billet expliquant les mêmes procédures pour le développement de contrôles XAML.

Jordan Matthiesen 
Chef de projet, Microsoft Visual Studio