Ajout et suppression d'éléments avec Javascript - Suppression de document.write

Introduction

L'ajout de code HTML peut être réalisé dynamiquement avec la fonction javascript write de l'objet document :

<script type="text/javascript">
    document.write("<div id='header'> \n");
    document.write("   <p class='certif'>Voici un paragraphe</p>  \n");
    document.write("</div>\n");
</script>

Si le type de document est HTML Transitional ou Strict, la fonction document.write est opérationnelle, en revanche si il s'agit d'un document de type XHTML, la méthode document.write n'est pas reconnue.

document.write est séduisant mais de sérieux problèmes existent avec cette méthode, méthode qu'il faut éradiquer, parmi ces problèmes :

  • document.write ne fonctionne pas si le document est de type XHTML.
  • le contenu écrit avec document.write peut ne pas apparaître dans le DOM (Document Object Model) du document, empêchant ainsi d'accéder et de manipuler ce contenu par programmation.
  • le traitement des nœuds est séquentiel et immédiat avec document.write, ce qui est conceptuellement faux.

SQLPAC est tombé dans ce piège avec un document XHTML : la section des liens de partage AddThis dans le document XHTML n'apparaissaient plus

bandeau addthis

et la mesure de l'audience Google Analytics était inopérante. Dans chacun de ces cas, la méthode document.write était utilisée.

Après un bref rappel sur le modèle DOM et les outils de débogage disponibles, cet article présente à travers des cas pratiques la migration des fonctions document.write vers les fonctions Javascript compatibles DOM pour ajouter/supprimer des éléments. Quelques cas particuliers sont également évoqués : les écueils avec IE 6 et 7 lors de l'utilisation de la méthode setAttribute ainsi que l'incompatibilité de Google AdSense avec la norme XHTML.

L'interface DOM (Document Object Model) et les outils (Chrome, FireFox/FireBug)

Le Document Object Model (ou DOM) est une recommandation du W3C qui décrit une interface indépendante de tout langage de programmation et de toute plate-forme.

DOM permet de construire une arborescence de la structure d'un document et de ses éléments. À partir d'un arbre DOM donné, il est possible de générer des documents dans le langage de balisage voulu (HTML, XML...), qui pourront à leur tour être manipulés par l'interface DOM. DOM est utilisé pour pouvoir modifier facilement des documents XML ou accéder au contenu des pages web HTML.

Deux outils précieux sont disponibles avec Chrome et FireFox (extension FireBug) pour inspecter et déboguer les éléments d'un document.

Dans Chrome : "OutilsOutils de développement" ou Ctrl+Maj+I.

Chrome outil de développement

Dans FireFox : "OutilsFirebugOuvrir Firebug" ou F12. Firebug est disponible comme extension : FireBug

Chrome outil de développement

L'ancien code de création de la barre d'outils AddThis avec la fonction document.write

La barre d'outils AddThis des fonctions de partage est générée avec la fonction javascript writeSharh-elinks dans le script ./js/header.js, fonction qui appelle les méthodes document.write à migrer.

bandeau addthis

./js/header.js
...
function writeSharh-elinks() {

  document.write("<div style='margin-top:5px;margin-bottom:10px;float:right;'>");
  document.write("<div class='addthis_toolbox addthis_default_style'>");

  document.write("<a class='addthis_button_facebook' rel='nofollow'></a>");
  document.write("<a class='addthis_button_linkedin' rel='nofollow'></a>");
  document.write("<a class='addthis_button_twitter' rel='nofollow'></a>");
  document.write("<a class='addthis_button_delicious' rel='nofollow'></a>");
  document.write("<a class='addthis_button_blogger' rel='nofollow'></a>");
  document.write("<a class='addthis_button_email' rel='nofollow'></a>");

  document.write("<span class='addthis_separator'>|</span>");
  document.write("<a class='addthis_button_expanded' href='http://www.addthis.com/bookmark.php?v=250&amp;pub=username' rel='nofollow'>");
  document.write("Partager...</a>");
  document.write("<script type='text/javascript' src='http://s7.addthis.com/js/250/addthis_widget.js#pub=username'></script>");

  document.write("</div>");
  document.write("</div>");

}
 ...

La fonction writeSharh-elinks est appelée dans le code HTML des articles :

...
<body>
   <script type="text/javascript" src="./js/header.js"></script>
   ...
  <script type="text/javascript">writeSharh-elinks();</script>
 ...

Création et insertion d'éléments par l'exemple sans utiliser document.write

Pré-requis d'un élément identifiable

Dans l'ancien code, aucun bloc avec un identifiant (ex. : <div id="iddiv">) existe entre la balise <body> et la fonction writeSharh-elinks. La création dynamique d'éléments ou de nœuds HTML peut être réalisée avec des identifiants de bloc grâce à la fonction getElementById. D'autres méthodes sont possibles (par classe ou par balise) mais ne sont pas abordées ici.

Le squelette des articles est donc modifié en conséquence afin d'introduire un bloc identifié (id=header), bloc dans lequel sera inséré dynamiquement la barre d'outils AddThis autrement qu'avec la fonction document.write :

<body>
   <script type="text/javascript" src="js/header.js"></script>   

   <div id="div-header" class="div-header"></div>
   <script type="text/javascript">writeSharh-elinks();</script>
</body>

Création d'un élément : createElement et setAttribute

Chaque élément (<div>, <p>, <a>, <img>, <script>, <span> etc...) est créé en Javascript avec la méthode createElement de l'objet document. La méthode createElement ne fait qu'instancier un objet, cette méthode n'insére pas l'élément immédiatement dans l'arbre DOM.

Pour instancier un élément div en javascript :

v_div = document.createElement("div");

La méthode setAttribute appliquée sur l'élément créé permet d'affecter des attributs à l'élément (id, class, src, style etc...) :

element.setAttribute(attribue,valeur);

Pour définir par exemple l'attribut id="shlinks" et style="width:300px;height:200px;background-color:blue;" à l'élément div précédemment créé :

v_div = document.createElement("div");
v_div.setAttribute("id","shlinks");
v_div.setAttribute("style","width:700px;height:200px;background-color:blue;");

Voici deux autres exemples qui instancient en javascript un objet de type img et script accompagné de quelques attributs :

v_img = document.createElement("img");
v_img.setAttribute("src","../imgsys/logo.jpg");
v_img.setAttribute("alt","logo");
v_img = document.createElement("script");
v_img.setAttribute("src","./js/footer.js");
v_img.setAttribute("type","text/javascript");

Insertion des éléments : appendChild, insertBefore, nextSibling

Le débogage de l'insertion des éléments se fait à partir des deux outils présentés au paragraphe 2.

Insérer un élément dans un élément avec la méthode appendChild

La méthode appendChild permet d'ajouter séquentiellement un élément fils dans un élément parent.

elementparent.appendChild(elementfils);

On désire par exemple obtenir l'arborescence ci-dessous :

<div id="header">
  <div id="enfant"></div>
</div>

En Javascript et le modèle DOM :

v_div_parent = document.getElementById("header");

v_div_enfant = document.createElement("div");
  v_div_enfant.setAttribute("id","enfant");

v_div_parent.appendChild(v_div_enfant);
  • l'élément parent <div id="header"> est récupéré grâce à son attribut id et la méthode getElementById de l'objet document.
  • l'élément fils <div id="enfant"> est créé avec la méthode createElement de l'objet document. L'attribut id="enfant" est défini grâce à la méthode setAttribute pour cet élément.
  • l'élément fils <div id="enfant"> est incorporé séquentiellement dans l'élément parent <div id="parent"> avec la méthode appendChild.

Insérer un élément avant un autre élément avec insertBefore

La méthode insertBefore permet d'insérer un élément fils element1 avant un autre élément fils element2 dans un élément parent elementparent.

elementparent.insertBefore(element1,element2);

On désire par exemple obtenir l'arborescence ci-dessous :

<div id="header"> 
  <div id="enfant1"></div>
  <div id="enfant2"></div>
</div>

En Javascript, l'élément enfant2 est déjà inséré dans l'élément parent dans le modèle DOM avec la méthode appendChild :

v_div_parent = document.getElementById ("header");

v_div_enfant2 = document.createElement("enfant2");
v_div_before.setAttribute("id","enfant2");

v_div_parent.appendChild(v_div_enfant2);

Ce qui donne dans l'arbre DOM :

<div id="header"> 
  <div id="enfant2"></div>
</div>

Pour insérer l'élément enfant1 avant l'élément enfant2 dans l'élément parent header, la méthode insertBefore est utilisée :

v_div_parent = document.getElementById ("header");
v_div_enfant2 = document.getElementById ("enfant2");

v_div_enfant1 = document.createElement("div");
  v_div_enfant1.setAttribute("id","enfant1");

v_div_parent.insertBefore(v_div_enfant1,v_div_enfant2);
  • l'élément parent <div id="header"> est récupéré grâce à son attribut id et la méthode getElementById de l'objet document.
  • l'élément fils <div id="enfant2"> est récupéré grâce à son attribut id et la méthode getElementById de l'objet document.
  • l'élément <div id="enfant1"> est créé avec la méthode createElement de l'objet document. L'attribut id="enfant1" est défini avec la méthode setAttribute pour cet élément.
  • l'élément <div id="enfant1"> est inséré avant l'élément <div id="enfant2"> dans l'élément <div id="header"> avec la méthode insertBefore.

Insérer un élément entre deux éléments avec insertBefore et nextSibling

La méthode insertAfter n'existe pas pour insérer un élément après un autre élément.

On désire par exemple obtenir l'arborescence ci-dessous :

<div id="header"> 
  <div id="enfant1"></div>
  <div id="enfant2"></div>
  <div id="enfant3"></div>
</div>

Les éléments enfant1 et enfant3 existent déjà dans l'arbre DOM :

<div id="header"> 
  <div id="enfant1"></div>
  <div id="enfant3"></div>
</div>

En Javascript, la méthode nextSibling appliquée à l'élément enfant1 renvoie l'élément enfant3 suivant dans l'arbre DOM. Pour le vérifier, coder une alerte pour récupérer l'id de l'élément suivant :

v_div_enfant1 = document.getElementById("enfant1");
alert(v_div_enfant1.nextSibling.id);
=> enfant3

L'insertion d'un élément après un autre élément sans se préoccuper de l'élément suivant est réalisée tout simplement en combinant les méthodes insertBefore et nextSibling :

elementparent.insertBefore(element2,element1.nextSibling);
v_div_parent = document.getElementById("header");
v_div_enfant1 = document.getElementById("enfant1");

v_div_enfant2 = document.createElement("div");
v_div_enfant2.setAttribute("id","enfant2");

v_div_parent.insertBefore(v_div_enfant2,v_div_enfant1.nextSibling);

Il faut bien entendu vérifier qu'il existe un élément suivant lorsque la méthode nextSibling est invoquée sur un élément. Si l'objet n'existe pas, la méthode appendChild doit être appliquée.

v_div_parent = document.getElementById("header");
v_div_enfant1 = document.getElementById("enfant1");

v_div_enfant2 = document.createElement("div");
v_div_enfant2.setAttribute("id","enfant2");

if (v_div_enfant1.nextSibling) {
    v_div_parent.insertBefore(v_div_enfant2,v_div_enfant1.nextSibling); }
else {
    v_div_parent.appendChild(v_div_enfant2);
}

Ajout de nœuds textes : createTextNode et appendChild

La méthode createTextNode de l'objet document créé des éléments texte avec Javascript :

myText = document.createTextNode("My Text");

Ces éléments texte peuvent alors être ajoutés à des éléments XML ou HTML acceptant du texte : <div>....</div>, <a> ... </a>, <span> ... </span>, <mabalisexml> ... <mabalisexml1> etc...

Exemple :

<div id="header">"My text"</div>

Code :

myText = document.createTextNode("My Text");
v_div_parent = document.getElementById("header");

v_div_parent.appendChild(myText);

Pour éviter l'utilisation de variables intensives dans le code Javascript, le code exemple ci-dessus peut être simplifié :

v_div_parent = document.getElementById("header");
v_div_parent.appendChild(document.createTextNode("My Text"));

Le nouveau code de création de la barre d'outils AddThis compatible HTML/XHTML sans la fonction document.write

Le nouveau code de la fonction javascript writeSharh-elinks dans le script ./js/header.js qui génère la barre d'outils AddThis est plus chargé mais il devient compatible HTML/XHTML avec la suppression des fonctions document.write :

bandeau addthis
./js/header.js
...
function writeSharh-elinks() {

    // Récupération de l'élement <div id="header">
    v_div_header = document.getElementById("header");

    // Création de l'élement <div id="addthis">
    v_div_addthis = document.createElement("div");
    v_div_addthis.setAttribute("id","addthis");
    v_div_addthis.setAttribute("class","addthis_toolbox addthis_default_style");


    // Définition d'un tableau des réseaux sociaux : facebook, linkedin...
    var array_socialnetwork_targets = new Array("facebook","linkedin","twitter","delicious",
                                        "blogger","email");
    
    // Pour tous les éléments du tableau, ajout dynamique des liens <a> </a> dans l'élément <div id="addthis">
    for (var i=0; i < array_socialnetwork_targets.length ; i++) {
       v_a_addthis_link = document.createElement("a");
       v_a_addthis_link.setAttribute("class","addthis_button_"+array_socialnetwork_targets[i]);
       v_a_addthis_link.setAttribute("rel","nofollow");
       v_div_addthis.appendChild(v_a_addthis_link);
    }

    // Ajout dynamique de l'élément <span> | </span> dans l'élément <div id="addthis">
    v_span_separator_addthis = document.createElement("span");
    v_span_separator_addthis.setAttribute("class","addthis_separator");
    v_span_separator_addthis.appendChild(document.createTextNode("|"));
    v_div_addthis.appendChild(v_span_separator_addthis);

    // Ajout dynamique de l'élément <a>Partager...</a> dans l'élément <div id="addthis">     
    v_a_addthis_share = document.createElement("a");
    v_a_addthis_share.setAttribute("class","addthis_button_expanded");
    v_a_addthis_share.setAttribute("rel","nofollow");
    v_a_addthis_share.setAttribute("onclick","return false");
    v_a_addthis_share.setAttribute("href","http://www.addthis.com/bookmark.php?v=250&amp;pub=<username>");
    v_a_addthis_share.appendChild(document.createTextNode("Partager..."));
    v_div_addthis.appendChild(v_a_addthis_share);

    // Ajout dynamique de l'élément <script src=...></script> dans l'élément <div id="addthis">
    v_script_addthis = document.createElement ("script");
    v_script_addthis.setAttribute("type","text/javascript");
    v_script_addthis.setAttribute("src","http://s7.addthis.com/js/250/addthis_widget.js#pub=<username>");
    v_div_addthis.appendChild(v_script_addthis);      

    // Ajout dynamique de l'élément <div id="addthis"></div> dans l'élément <div id="header">
    v_div_header.appendChild(v_div_addthis);
}
...

Ce fragment de code est parfait pour Internet Explorer 8, Chrome, FireFox, Safari et Opera. En revanche dès que l'on démarre la certification pour Internet Explorer 7 et 6, tout se corse comme d'habitude. Avec Internet Explorer 6 et 7, la méthode document.createElement("a") semble ne pas aboutir. Le paragraphe "Les écueils" dans cet article évoque ce point.

Suppression d'éléments : removeChild

La méthode pour supprimer des éléments est très simple avec la méthode removeChild appliquée sur l'élément parent pour supprimer un élément fils :

elementparent.removeChild(elementfils);

Exemple :

v_div_parent = document.getElementById("header");
v_div_enfant1 = document.getElementById("enfant1");

v_div_parent.removeChild(v_div_enfant1);

Les écueils

Compatibilités des navigateurs avec les versions de DOM (core et css) : exemple avec IE 6/7 et setAttribute

Lors des phases de validation multi-navigateurs, le code migré précédemment sans les fonctions document.write est inopérant pour les versions Internet Explorer 6 et Internet Explorer 7 : les icônes n'apparaissent pas. Il s'agit de versions anciennes d'Internet Explorer, certes, mais contrairement à FireFox et Google Chrome qui sont régulièrement mis à jour en automatique, de nombreux internautes utilisent encore les navigateurs IE 6 et IE 7, ce qui démontre les difficultés qui existent pour migrer les plateformes Windows, plateformes qui incorporent malheureusement IE dans le noyau.

Il est inenvisageable de négliger ces versions d'Internet Explorer : à titre d'exemple, pour le site SQLPAC en 2010, pour 9667 visites avec Internet Explorer : 41% des internautes utilisent la version Internet Explorer 6 et 32% la version Internet Explorer 7.

Proportion versions Internet Explorer

Quelques rares sites récapitulent les compatibilités des navigateurs avec les fonctions Javascript pour manipuler les arbres DOM (createElement, setAttribute...) :

Le site Quirksmode.org est le plus intéressant de tous car il remonte jusqu'aux versions IE 5.5. Une copie au format pdf a toutefois été générée ici car ce site n'a pas été mis à jour depuis Août 2010 : W3C DOM Compatibility- Core (format pdf)

Le débogage a donc été ardu (1 journée perdue) après de multiples tests et pérégrinations, et la cause fut enfin trouvée : la fonction setAttribute("class",valeur) est sans effet avec IE 6 et IE 7. Le site quirksmode.org confirme ce constat (Quirksmode.org : SetAttribute), tous les attributs de style (class, style...) appliqués sur un élément avec la méthode setAttribute sont systématiquement supprimés jusqu'à la version 7 d'Internet Explorer.

Pour contourner ce problème avec IE 6 et 7, l'attribut class doit être créé manuellement pour ces versions avec la méthode createAttribute de l'objet document, puis affecté à l'élément.

<div id="header">
 ...
   <a class="addthis_button_expanded"></a>
 ...
</div>
IE 6 et 7 IE 8


v_a_addthis_share = document.createElement("a");

v_attr_class = document.createAttribute("class");
v_attr_class.nodeValue = "addthis_button_expanded";
v_a_addthis_share.setAttributeNode(v_attr_class);

v_div_addthis.appendChild(v_a_addthis_share);
v_a_addthis_share = document.createElement("a");

v_a_addthis_share.setAttribute("class",
        "addthis_button_expanded");

v_div_addthis.appendChild(v_a_addthis_share);
  • L'attribut class, identifié par la variable v_attr_class, est créé avec la méthode document.createAttribute.
  • La valeur "addthis_button_expanded" est donnée à l'attribut avec la propriété nodeValue de l'objet attribut.
  • L'attribut v_attr_class est affecté à l'élément v_a_addthis_share avec la méthode setAttributeNode.

Une fois de plus, pour ne pas dire comme d'habitude, la version du navigateur Internet Explorer doit être vérifiée en Javascript pour gérer ces cas particuliers. Voici un exemple de vérification :

// agt = mozilla/4.0 (compatible; msie 7.0.... Récupération du navigateur
var agt=navigator.userAgent.toLowerCase();

// is_ie = true => MS Internet Explorer (msie)
var is_ie  = ((agt.indexOf("msie") != -1));

// version = 7 => Si is_ie, récupération de la version majeure
if (is_ie) {version = parseFloat(navigator.appVersion.split("MSIE")[1]);}

Lorsque les variables permettant d'identifier le navigateur et la version d'Internet Explorer sont récupérées, les instructions sont alors codées sous conditions :

if ( ! is_ie || (is_ie && version >=8)) {
   instructions pour IE >=8 et les autres navigateurs
} else {
   instructions pour IE 6 et 7
}

Incompatibilité de Google AdSense avec le format XHTML (document.write)

Inutile de s'escrimer pour l'heure actuelle à vouloir implémenter Google AdSense proprement avec les méthodes Javascript createElement et appendChild pour ajouter le script show_ads.js, Google AdSense utilise document.write en mode sérialisé pour afficher la bannière d'annonces :

<script type="text/javascript">
  google_ad_client ='pub-xxxxxxxxxxxxx';
  google_ad_slot = '3054549320';
  google_ad_width = 120;
  google_ad_height = 240;
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
show_ads.js


 ...
 document.write("<span id="+a+"></span>");
 ...

Google AdSense est incompatible avec la norme XHTML/DOM, norme dans laquelle la fonction document.write est inactive.

Pour délocaliser et centraliser la création de la bannière Google AdSense dans un script javascript afin de ne pas répéter le bout de code Javascript AdSense dans tous les articles au format HTML, il n'y a pas d'autres alternatives que d'utiliser également la fonction document.write :

document.write("<script type='text/javascript' src='http://pagead2.googlesyndication.com/pagead/show_ads.js'>\n");
document.write("</script>\n");

Pas d'autres alternatives ? Pas tout à fait, une solution existe : elle consiste à créer une page HTML statique pour les annonces AdSense et à incorporer celle-ci dynamiquement avec Javascript dans la page HTML parente grâce à la balise object. Voici le lien pour cette solution alternative : SQLPAC - Google Adsense, ajout dynamique avec Javascript et la méthode createElement. Malheureusement cette solution alternative n'est pas sans conséquences sur les statistiques Google Analytics, point qui est également abordé dans l'article précédemment cité.

Gageons que les futures APIs de Google AdSense supprimeront cette contrainte.

Un dernier exemple : appel du script Google Analytics ga.js avec insertBefore

Dans un article paru en novembre 2009 au sujet de l'incorporation de Google Analytics pour mesurer l'audience Web (Mesurer son audience Web et exploiter efficacement Google Analytics ), l'incorporation du script de Google Analytics ga.js est réalisée avec la méthode document.write :

<script type='text/javascript' src='http://www.google-analytics.com/ga.js'></script>
var gaJsHost = (("https:" == document.location.protocol) ?  "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));

Ce code contenant document.write empêche le suivi Google Analytics si il s'agit d'un document de type XHTML.

La fonction document.write peut être supprimée en créant classiquement un élément de type script avec la méthode createElement :

var v_script_ga = document.createElement('script');

v_script_ga.setAttribute("type","text/javascript");

var v_gaJsHost = (("https:" == document.location.protocol) ?  "https://ssl." : "http://www.");

v_script_ga.setAttribute("src",v_gaJsHost + "google-analytics.com/ga.js");

Mais où placer cet élément script ? Une solution simple peut consister à placer celui-ci juste avant le premier élément <script></script> de l'arbre DOM.

var v_script_first = document.getElementsByTagName('script')[0];
var v_parentNode = v_script_first.parentNode;
v_parentNode.insertBefore(v_src_ga, v_script_first);
  • La méthode document.getElementsByTagName('script') retourne un tableau des éléments <script...></script> dans l'arbre DOM. document.getElementsByTagName('script')[0] renvoie donc le premier élément <script...></script> présent dans l'arbre DOM.
  • v_parentNode renvoie le nœud (ou élément) parent de ce premier élément <script ...></script>.
  • L'élément script v_src_ga est inséré juste avant le premier élément <script ...></script> grâce à la méthode insertBefore appliquée sur son élément parent.

La lisibilité du code en pâtit un peu mais cette opération peut être réalisée en moins de lignes de code:

var v_script_first = document.getElementsByTagName('script')[0];
v_script_first.parentNode.insertBefore(v_src_ga,v_script_first);

Le nouveau code est compatible pour toutes les versions de navigateurs et assure une compatibilité de Google Analytics avec les documents de type XHTML :

var v_src_ga = document.createElement('script');

v_src_ga.setAttribute("type","text/javascript");

var v_gaJsHost = (("https:" == document.location.protocol) ?  "https://ssl." : "http://www.");

v_src_ga.setAttribute("src",v_gaJsHost + "google-analytics.com/ga.js");

var v_script_first = document.getElementsByTagName('script')[0];
v_script_first.parentNode.insertBefore(v_src_ga,v_script_first);