Cet article est une description didactique, phase par phase, de l'écriture du composant _p4008_google_search_ pris comme exemple. Par la même occasion, il permet de revoir quelques bases de la programmation Ruby et HAML ainsi que du passage de paramètres dans le logiciel _projet_a_.

Pour des raisons pédagogiques, les fonctionnalités du composant seront ajoutées les unes après les autres. À la fin de chaque phase et de chaque sous-phase, la version du composant obtenue sera pleinement opérationnelle et conforme à ses spécifications. De plus, chaque version successive permettra aussi de répondre aux spécifications des versions précédentes et de pouvoir être appelée avec leurs interfaces.

Le composant p4008_google_search sera pris comme exemple. L'objectif initial de celui-ci était de permettre une recherche plein texte sur un site. Pour simplifier la réalisation, cette opération a été déléguée au moteur de recherche Google en exploitant le fait qu'il limite son exploration à un site quand on lui passe parmi les mots-clés cherchés un nom de domaine précédé des caractères "site:" (ex. : site:a-io.eu).

La réalisation du composant doit bien sûr adopter les conventions et utiliser les outils définis dans le logiciel projet_a pour les composants. Le code sera écrit dans le langage de vues HAML, concis et clair, surtout ici pour une présentation élément par élément. L'appel du composant dans un générateur de pages sera lui soit de préférence au format GAL du projet A, soit au formar HAML.

Conventions d'écriture :

Dans un fichier source, les différents langages sont souvent intriqués. Pour faciliter leur compréhension, les lignes de code seront mises en couleurs :

       vert
gris
orange    
rouge
rouge vif
bleu
Commentaires (tous langages)
Code HTML sous HAML
Code CSS
Code Ruby sous HAML
Code GAL
Code Javascript et JQuery

1. Phase 1 : Recherche sur le site courant

1.1. Interface graphique :

La partie visible du composant comportera trois éléments : un bloc englobant, un champ de saisie de texte et un élément d'activation.

Composant p4008 réduit

Le bloc englobant sera un bloc HTML <div> avec comme classe CSS le nom du composant. Cette convention est respectée pour chaque composant pour faciliter le ciblage d'un style CSS. La balise <div> étant par défaut en HAML, le code sera simplement :

.p4008

Le champ de saisie sera défini par une balise <input> de type text :

  %input(type=text)

L'élément d'activation de la commande de recherche peut être soit un bouton, soit un lien sur un texte ou sur une image. Pour obtenir une interface simple, légère et claire, le lien sur un texte a été retenu. Le lien sera produit par une balise <a> sans attributs, car ceux-ci seront ajoutés dynamiquement par du code Javascript. Le texte sera le mot Rechercher déjà défini dans les fichiers d'internationalisation et appelé par la fonction I18n::translate, alias t tout simplement :

  %a= t('general.go_search')

Pour améliorer un peu l'ergonomie, on peut ajouter un texte en infobulle au survol du lien, c'est-à-dire comme attribut title de la balise <a>. Le texte, "Rechercher sur le site" en français, sera défini dans la sous-section du composant dans la section components des fichiers de langue. La valeur d'un attribut en HAML devant être une chaine de caractères, la fonction t sera interpolé sur la ligne de code qui devient :

  %a(title="#{t('components.p4008.site_search')}")= t('general.go_search')

Le code HAML de l'interface :

.p4008
  %input(type=text)
  %a(title="#{t('components.p4008.site_search')}")= t('general.go_search')

Le fichier de styles a_components.css dispose les éléments graphiques alignés et cadré à droite ; il fixe aussi la taille du champ de saisie ; le curseur sur le mot d'activation est forcé, car une balise <a> sans attribut href n'est pas considérée comme un lien actif par la plupart des navigateurs. Cette présentation pourra bien sûr être ajustée par la feuille de styles de chaque site utilisateur du composant :

.p4008 {text-align:right;}
.p4008 input {display:inline; width:200px;}
.p4008 a {cursor:pointer;}

1.2. Code Javascript :

La dynamique du composant est assurée par du code Javascript utilisant la bibliothèque JQuery. Bien qu'il soit conseillé de ne pas placer de code Javascript dans les sources HTML, les instructions du composant seront placées à la fin du fichier HAML, dans un bloc <script> ouvert par la ligne de code :javascript.

Le code à exécuter sur l'activation du lien Rechercher doit à minima récupérer le texte saisi par l'internaute dans le champ, puis ouvrir une nouvelle fenêtre pour l'affichage du résultat de la recherche.

Le texte est la valeur du champ <input> du composant :

var text = $(".p4008 input").val();

Comme cette valeur sera incluse dans la requête passée à Google dans l'URL d'appel, elle doit être encodée pour les cas où l'internaute taperait certains caractères spéciaux. La ligne précédente est donc à remplacer par :

var value = encodeURIComponent($(".p4008 input").val());

L'ouverture de la fenêtre ou de l'onglet pour afficher le résultat est la méthode open de l'objet window du navigateur qui comporte un seul paramètre obligatoire, l'URL de la requête :

open(URL);

L'URL est constituée de l'URL de la page de recherche de Google suivie en paramètre q des critères de recherche : les mots-clés tapés par l'utilisateur et encodés dans la variable value ainsi que le nom de domaine du site courant précédé de "site:". L'instruction devient :

open("http://www.google.com/search?q=" + value + " site:#{@site.domain}");

Vous noterez que les deux valeurs incluses sont traitées différemment :

  • value est une variable Javascript dont le contenu n'est défini qu'au moment de l'activation du lien Rechercher, elle est concaténée au préfixe de l'URL ;
  • @site.domain est la méthode qui fournit le nom de domaine du site courant, défini par la variable Ruby @site lors de la génération de la page, elle est interpolée dans le code du Javascript à ce moment-là.

Ces instructions à exécuter sur l'activation du lien Rechercher sont à placer dans le gestionnaire de l'évènement du clic sur le lien :

$('.p4008 a').click(function(){
  ...
};

Ce gestionnaire d'évènement ne doit être déclaré qu'après la fin du chargement de la page HTML. Son code doit lui-même être inséré dans le gestionnaire de l'évènement load de la fenêtre du navigateur :

$(window).load(function(){
  ...
};

Les lignes à ajouter à la fin du fichier HAML du composant sont donc les suivantes :

:javascript
  $(window).load(function(){
    $(.p4008 a).click(function(){
      var value = encodeURIComponent($(".p4008 input").val());
      open("http://www.google.com/search?q=" + value + " site:#{@site.domain}");
    });
  });

Là aussi, on va améliorer l'ergonomie en refusant de lancer la requête de recherche si l'internaute n'a pas saisi de critères. Il nous faut tester la valeur lue du champ <input> après suppression des espaces d'extrémité par la fonction JQuery $.trim. Si la valeur est vide, alors la saisie obligatoire sera notifiée à l'utilisateur avant de replacer le focus sur le champ <input>.

Par ailleurs, si on lance plusieurs recherches successives depuis le composant, les résultats s'affichent à chaque fois dans une nouvelle fenêtre ou un nouvel onglet. Pour éviter ce comportement envahissant, il suffit d'ajouter un nom de fenêtre constant comme deuxième paramètre de la méthode open :

open("http://www.google.com/search?q=" + value + " site:#{@site.domain}", 'p4008');

1.3. Source 1 :

En ajoutant en ligne de tête un commentaire d'identification, le fichier source du composant devient :

-# _p4008_google_search.html.haml - Phase 1
.p4008
  %input(type=text)
  %a(title="#{t('components.p4008.site_search')}")= t('general.go_search')

:javascript
  $(window).load(function(){
    $(.p4008 a).click(function(){
      var input = $(".p4008 input");
      var value = encodeURIComponent($.trim(input.val()));
      if (value == '') {
        alert("#{t('general.oblig')}");
        input.focus();
      } else {
        open("http://www.google.com/search?q=" + value + " site:#{@site.domain}", 'p4008');
      }
    });
  });

Le composant peut être appelé depuis un générateur de pages au format HAML par l'instruction suivante :

= render 'p4008_google_search'

ou plus simplement, au format GAL du projet A :

=> p4008_google_search

2. Phase 2 : Recherche sur un site donné

La première évolution du composant permettra de lancer une recherche dans les pages d'un site dont le nom de domaine sera fourni soit en paramètre du composant, soit en paramètre de génération de l'espace. Comme plusieurs instances du composant peuvent être déclarées dans le code d'un générateur pour permettre les recherches sur autant de sites différents, notre code devra être adapté en conséquence.

2.1. Site défini comme paramètre du composant

Le nom de domaine pour la recherche pourra être précisé dans la variable locale domain. L'appel HAML au composant sera alors de la forme :

= render :partial => 'p4008_google_search', :locals => { domain: 'a-io.eu' }

soit, en langage GAL :

=> p4008_google_search, domain: 'a-io.eu'

Pour garder la compatibilité avec la version précédente, le code du composant commencera par l'instruction :

- domain ||= @site.domain

qui, en dialecte Ruby, signifie que la variable domain sera initialisée au nom de domaine du site courant si cette variable n'est pas définie (ou contient false ou nil).

Dans le code du composant, seule l'URL de la requête de recherche doit être modifiée. Elle devient :

        open("http://www.google.com/search?q=" + value + " site:#{domain}", 'p4008');

C'est également plus sympathique pour les internautes de préciser le site de recherche dans l'infobulle sur le lien Rechercher, ce qui donne :

  %a(title="#{t('components.p4008.site_search')} #{domain}")= t('general.go_search')

2.2. Source 2.1 :

Le code du composant devient alors :

-# _p4008_google_search.html.haml - Phase 2.1
- domain ||= @site.domain
.p4008
  %input(type=text)
  %a(title="#{t('components.p4008.site_search')} #{domain}")= t('general.go_search')
:javascript
  $(window).load(function(){
    $(.p4008 a).click(function(){
      var input = $(".p4008 input");
      var value = encodeURIComponent($.trim(input.val()));
      if (value == '') {
        alert("#{t('general.oblig')}");
        input.focus();
      } else {
        open("http://www.google.com/search?q=" + value + " site:#{domain}", 'p4008');
      }
    });
  });

2.3. Site défini comme paramètre de génération de l'espace

Pour définir le domaine de recherche comme paramètre de génération search_domain de l'espace courant, on modifiera la première instruction du composant :

- domain ||= @gen_params[:search_domain] || @site.domain

Elle signifie que la valeur de la variable domain est celle du paramètre d'appel domain s'il est précisé, sinon celle du paramètre de génération search_domain de l'espace courant, sinon le nom de domaine du site courant.

2.4. Utilisation multiple du composant sur une page

Tel qu'il est écrit comme ci-dessus, le composant présente deux défauts majeurs quand on le place plusieurs fois sur la même page, pour des sites de recherche différents : le premier champ de saisie est toujours lu et l'exécution est multiple.

2.5. Sélection imprécise

L'instruction JQuery $(".p4008 input") sélectionne en réalité tous les éléments input placés derrière un élément de la classe p4008. Appliquée à un tableau d'éléments JQuery, la méthode val() retourne la valeur du premier élément. Pour résoudre ce problème, la sélection du champ de saisie doit être plus précise pour ne retourner que l'élément situé dans le même bloc que le lien Rechercher cliqué.

Pour cela, partons de cet élément qui est référencé par $(this) dans le gestionnaire d'évènements et recherchons parmi ses frères – les balises de même niveau que la balise <a> dans le bloc <div> – les éléments input. Comme notre composant n'en contient qu'un seul, ce sera celui cherché :

var input = $(this).siblings("input");

2.6. Exécution multiple

Dans le code du générateur, chaque appel au composant génère dans la page HTML un bloc <div class="p4008"> et le bloc Javascript. Chacun déclare un gestionnaire d'évènements sur les clics sur les balises <a> des blocs <div class="p4008">. Comme dans le paragraphe précédent, la sélection $(".p4008 a") est imprécise et englobe à chaque fois toutes les balises correspondantes de la page qui se voient chacune dotées de plusieurs gestionnaires de clics (autant que d'instanciations du composant). Contrairement au cas précédent, il n'est pas ici possible d'assurer une sélection plus précise, car dans le gestionnaire $(windows).load(), $(this) référence la fenêtre toute entière.

La solution est de ne générer qu'une seule fois le code Javascript, lors de la première génération du composant. Dans ce but, l'instruction :javascript et tout le code Javascript qui la suit seront inclus dans une instruction conditionnelle Ruby qui testera l'existence d'une variable spécifique au composant. Si elle n'est pas définie, alors elle le sera immédiatement, puis le Javascript sera généré :

- unless defined?(@p4008_generated)
  - @p4008_generated = true
  :javascript
    ...

La génération unique du code Javascript a un effet de bord malencontreux : dans l'URL d'appel de Google, l'interpolation de la variable Ruby domain se fera à partir de la valeur du domaine de recherche du premier appel du composant. Pour éviter ça, cette interpolation qui permet de passer au Javascript une variable de la génération Ruby doit être déplacée sur l'une des trois lignes du composant générées à chaque instanciation, c'est-à-dire celles qui produisent le bloc <div> et son contenu. Le plus simple et le plus clair est d'ajouter un attribut data à la balise <div> qui devient :

.p4008(data-domain=domain)

NB : Pour la valeur d'un attribut d'une balise HTML, le compilateur HAML effectue automatiquement l'interpolation de la variable Ruby sans devoir l'écrire dans le traditionnel "#{...}". Cette forme a aussi un avantage qui sera exploité ultérieurement lors de la phase 3 de la conception du composant.

Il ne reste plus qu'à récupérer cette valeur lors de l'exécution pour la concaténer lors du calcul de l'URL après le clic sur le lien Rechercher. Ici aussi, il faut faire attention de sélectionner le bon bloc <div> en partant de $(this) et en remontant à son père :

var site = $(this).parent().data('domain');
open("http://www.google.com/search?q=" + value + " site:" + site, 'p4008');

2.7. Source 2.3 :

Le code du composant est le suivant :

-# _p4008_google_search.html.haml - Phase 2.3
- domain ||= @gen_params[:search_domain] || @site.domain
.p4008(data-domain=domain)
  %input(type=text)
  %a(title="#{t('components.p4008.site_search')} #{domain}")= t('general.go_search')
- unless defined?(@p4008_generated)
  - @p4008_generated = true
  :javascript
    $(window).load(function(){
      $(.p4008 a).click(function(){
        var site = $(this).parent().data('domain');
        var input = $(this).siblings("input");
        var value = encodeURIComponent($.trim(input.val()));
        if (value == '') {
          alert("#{t('general.oblig')}");
          input.focus();
        } else {
          open("http://www.google.com/search?q=" + value + " site:" + site, 'p4008');
        }
      });
    })

3. Phase 3 : Recherche sur un site choisi dans une liste

Dans le cas du besoin de proposer la recherche d'informations dans les pages de plusieurs sites, plutôt que d'implémenter le composant plusieurs fois, il est le plus souvent judicieux de donner à choisir un site dans une liste.

3.1. Interface d'appel :

Dans la version précédente, la variable d'appel domain pouvait définir le nom de domaine du site de recherche, ou par défaut le paramètre search_domain de génération de l'espace, ou par défaut encore le site courant. Pour respecter la règle de compatibilité avec les versions antérieures, cette règle est conservée, ce qui revient à maintenir sans changement la première ligne de code :

- domain ||= @gen_params[:search_domain] || @site.domain

L'objectif de la nouvelle version implique d'accepter plusieurs noms de domaine, ce qui peux être réalisé en autorisant un tableau de valeurs en paramètre. Dans ce cas, les noms de paramètres domain et search_domain au singulier sont sémantiquement incorrects, donc sources d'erreurs potentielles. Les formes au pluriel, respectivement domains et search_domains, seront donc aussi acceptées, avec une préférence du pluriel sur le singulier au cas où les deux formes seraient fournies lors d'un appel du composant (il faut bien faire un choix...). Ceci se traduit par la ligne suivante :

- domains ||= @gen_params[:search_domains] || domain

NB : Avec ces deux lignes, il est possible de déclarer plusieurs sites dans domain ou un seul dans domains. Ce n'est pas grave, le résultat défini dans l'une des formes de paramétrage, sinon la valeur par défaut, se retrouvera finalement dans la variable domains.

Ne serait-ce que pour le site portail http://a-io.eu de l'association, ce serait bien aussi d'accepter une valeur spéciale sélectionnant automatiquement tous les sites déclarés, ou plus précisément tous les sites visibles par l'internaute en fonction de ses droits et de son état de connexion. La valeur spéciale d'appel sera la valeur booléenne true ou le symbole :all ou la chaine de carractères 'all'. La méthode all_visible_by de la classe Site sélectionne les sites convenables ; la méthode map du module Ruby Enumerable construit alors un tableau dont chaque valeur est défini par le bloc suivant (current_user désigne l'utilisateur dans le logiciel) :

- if [true, :all, 'all'].include?(domains)
  - domains = Site.all_visible_by(current_user).map {|site| site.domain}

Enfin, pour pouvoir utiliser au mieux le code de la version précédente du composant, nous restaurons la variable domain dans le cas où une simple chaine est fournie, sinon nous l'initialisons à nil :

- domain = domains.is_a?(String) ? domains : nil

Avec la valeur nil dans domain, l'attribut data-domain de la balise <div> n'est pas généré par la ligne ".p4008(data-domain=domain)".

3.2. Interface graphique :

Composant p4008 complet

Dans le cas où un tableau de valeurs est fourni, une liste déroulante qui contiendra les noms de domaine de recherche est simplement ajoutée devant le champ de saisie des critères. Cette liste est produite par un bloc HTML <select>, lui-même généré par les assistants (helpers) Rails select_tag et options_for_select. Dans notre cas, ce dernier ne prend comme paramètre que le tableau des noms de domaines enregistré dans la variable domains.

  = select_tag :domains, options_for_select(domains) if domains.class == Array

3.3. Impact sur le code Javascript :

Visiblement, l'ajout d'une liste de valeur a deux impacts sur la dynamique Javascript du composant : la nécessité du calcul de l'infobulle sur le lien Rechercher et bien sûr un calcul adapté de la requête vers Google.

L'infobulle s'affiche lors du survol du lien Rechercher, un gestionnaire d'évènements mouseover peut calculer le texte de l'infobulle en fonction de l'élément positionné de la liste des sites. Cette action ne doit être effectuée que si cette liste est présente :

      $('.p4008 a').mouseover(function() {
        var select = $(this).siblings("select");
        if (select.size() > 0) {
          var domain = select.val();
          $(this).attr('title', "#{t('components.p4008.site_search')} " + domain);
        }
      });

La sélection de l'élément liste <select> est similaire à celle de l'<input> dans la version précédente. Nous profitons du fait qu'une sélection JQuery retourne toujours un tableau d'éléments, vide dans le cas d'une sélection infructueuse, comme quand le composant ne doit gérer qu'un seul site de recherche.

En ce qui concerne le calcul de l'URL de requête vers Google, remarquons que le gestionnaire d'évènements click déjà écrit extrait le nom de domaine de l'attribut data-domain du bloc <div> du composant. Comme le lien Rechercher doit avoir été survolé avant d'être cliqué, en positionnant data-domain dans le gestionnaire mouseover le gestionnaire click sera capable en l'état de produire la bonne URL. Ajoutons donc la ligne suivante à la fin du if du bloc de code cité précédemment :

          $(this).parent().data('domain', domain);

3.4. Source 3 :

En chainant les deux gestionnaires d'évènements appliqués au(x) lien(s) Rechercher, le code du composant est maintenant le suivant :

-# _p4008_google_search.html.haml - Phase 3
- domain ||= @gen_params[:search_domain] || @site.domain
- domains ||= @gen_params[:search_domains] || domain
- if [true, :all, 'all'].include?(domains)
  - domains = Site.all_visible_by(current_user).map {|site| site.domain}
- domain = domains.is_a?(String) ? domains : nil

.p4008(data-domain=domain)
  = select_tag :domains, options_for_select(domains) if domains.is_a?(Array)
  %input(type=text)
  %a(title="#{t('components.p4008.site_search')} #{domain}")= t('general.go_search')
- unless defined?(@p4008_generated)
  - @p4008_generated = true
  :javascript
    $(window).load(function(){
      $(.p4008 a)
       .mouseover(function() {
        var select = $(this).siblings("select");
        if (select.size() > 0) {
          var domain = select.val();
          $(this).attr('title', "#{t('components.p4008.site_search')} " + domain);
        }
      })

       .click(function(){

        var site = $(this).parent().data('domain');
        var input = $(this).siblings("input");
        var value = encodeURIComponent($.trim(input.val()));
        if (value == '') {
          alert("#{t('general.oblig')}");
          input.focus();
        } else {
          open("http://www.google.com/search?q=" + value
+ " site:" + site, 'p4008');
        }
      });
    })

version 0.9.5-0442-150125

↑ Haut