Javascript non-intrusif, chapitre 3 : créer du contenu et le détruire
Cet article est le troisième d'une série qui en compte 5.
La principale force du DOM est la possibilité non seulement de lire, mais aussi de modifier le contenu et la structure du document. Nous disposons de plusieurs méthodes pour effectuer tout cela.
Créer du nouveau contenu
createElement(element)
- Crée un nouvel élément
createTextNode(chaine)
- Crée un nouveau nœud de texte dont la valeur est la chaîne de caractères
chaine
.
Les éléments nouvellement créés ne sont pas ajoutés au document tout de suite, ils restent in limbo jusqu'à ce qu'on les ajoute quelque part dans l'arbre à nœuds. Ces fonctions doivent être appliquées à l'objet document plutôt qu'à un nœud.
Javascript : monNouveauParagraphe=document.createElement('p'); monNouveauTexte=document.createTextNode('ceci est un nouveau paragraphe');
Modifier le contenu existant
setAttribute(attribut,valeur)
- Ajoute un nouvel
attribut
avec lavaleur
à l'objet. appendChild(enfant)
- Ajoute
enfant
en tant que nœud enfant (childNode
) à l'objet.enfant
doit être un objet, vous ne pouvez pas utiliser de chaîne de caractères. cloneNode()
- Copie le nœud entier avec tous les nœuds enfants.
hasChildNodes()
- Vérifie si un objet possède des nœuds enfants (
childNodes
), et renvoietrue
si c'est le cas. insertBefore(nouvelEnfant,ancienEnfant)
- Insère
nouvelEnfant
avantancienEnfant
dans l'arbre du document. removeChild(ancienEnfant)
- Supprime le nœud enfant
ancienEnfant
. replaceChild(nouvelEnfant,ancienEnfant)
- Remplace
ancienEnfant
parnouvelEnfant
. removeAttribute(attribut)
- Supprime l'
attribut
de l'objet.
Affichage d'images
Admettons que nous ayons des liens vers quelques images, et que nous voulions qu'ils s'ouvrent dans une nouvelle fenêtre en l'absence de Javascript, ou sous les liens si Javascript est activé.
HTML : <ul id="imglist"> <li><a href="home.gif" target="_blank">Accueil (nouvelle fenêtre)</a></li> <li><a href="home_on.gif" target="_blank">Accueil actif (nouvelle fenêtre)</a></li> <li><a href="jscsshtml.gif" target="_blank">HTML-CSS-Javascript (nouvelle fenêtre)</a></li> </ul>
Maintenant, quand Javascript et DOM sont disponibles, nous voulons :
- nous débarrasser du terme « (nouvelle fenêtre) » dans les liens ;
- ajouter un gestionnaire d'événements qui appelle une fonction popw().
Cette fonction devrait :
- montrer l'image sous le lien qui l'appelle quand elle n'est pas déjà affichée ;
- supprimer l'image si elle est déjà là (pour éviter qu'une nouvelle image ne soit ajoutée à chaque fois qu'on clique sur le lien) ;
- faire en sorte que l'image disparaisse quand l'utilisateur clique dessus.
Le premier problème n'est pas difficile à résoudre :
Javascript : function imgpop() { var il,imga,imgatxt; // récupère toutes les balises <li> dans la liste d'images, // et boucle dessus il=document.getElementById('imglist').getElementsByTagName('li'); for(i=0;i<il.length;i++) { // récupère le premier lien dans les balises <li> imga=il[i].getElementsByTagName('a')[0]; // supprime les mots (nouvelle fenêtre) dans le texte du lien // (qui est la valeur (nodeValue) du premier nœud) imgatxt=imga.firstChild; imgatxt.nodeValue=imgatxt.nodeValue.replace(/ \(nouvelle fenêtre\)/,''); // ajoute le gestionnaire d'événements pour appeler popw(); imga.onclick=function(){return popw(this);} //imga.onkeypress=function(){return popw(this);} } }
En ce qui concerne la fonction popw(), nous allons utiliser quelques-unes des méthodes expliquées plus haut :
Javascript : function popw(o) { var newimg; // s'il y a déjà une image dans le nœud parent (parentNode) (li) if(o.parentNode.getElementsByTagName('img').length>0) { // on la supprime o.parentNode.removeChild(o.parentNode.getElementsByTagName('img')[0]); } else { // sinon, on crée une nouvelle image et on ajoute un gestionnaire qui // la supprime quand on clique dessus newimg=document.createElement('img'); newimg.style.display='block'; newimg.onclick=function(){this.parentNode.removeChild(this);}; newimg.src=o.href; o.parentNode.appendChild(newimg) } return false; }
Voir l'afficheur d'images en action.
Sélecteur de dates
Imaginons cette fois qu'on ait un formulaire qui contienne des champs de dates, et qu'on veuille offrir à l'utilisateur équipé de Javascript un sélecteur de dates, les autres utilisateurs devant simplement entrer la date à la main. Mettons de côté la fonction du sélecteur de dates pour l'instant, et voyons d'abord comment l'appeler.
Nous commençons par le code HTML nécessaire. Pour savoir à quels éléments ajouter le lien du sélecteur de dates, on leur ajoute des classes portant le nom date
.
HTML : <h1>Réservation de vol</h1> <form action="nosend.php" method="post" onsubmit="return check(this);"> <p>Étape 1 sur 4</p> <h2>Veuillez sélectionner les dates</h2> <p> <label for="dateDepart">Date de départ</label> <input type="text" class="date" id="dateDepart" name="dateDepart" /> </p> <p> <label for="dateRetour">Date de retour</label> <input type="text" class="date" id="dateRetour" name="dateRetour" /> </p> <p> <input type="submit" value="Envoyer" /> </p> </form>
On va boucler sur tous les champs de saisie du document, et repérer ceux qui ont un nom de classe correspondant à date
(souvenez-vous que les éléments HTML peuvent avoir plus d'un nom de classe dans leur attribut class !).
Si tel est le cas, on crée un nouveau lien, et un texte pour celui-ci. On ajoute le texte en tant qu'enfant du lien, et on ajoute un gestionnaire d'événements qui va lancer notre script de sélection de dates.
Une fois que le lien est créé, on l'ajoute juste après le champ de saisie.
Javascript : function ajouteLienSelecteur() { var champsSaisie,selectLien,selectTexte; // boucle sur tous les champs de saisie champsSaisie=document.getElementsByTagName('input'); for(i=0;i<champsSaisie.length;i++) { // si la classe contient 'date' if(/date/.test(champsSaisie[i].className)) { // créer un nouveau lien et un nouveau texte selectLien=document.createElement('a'); selectTexte=document.createTextNode('sélectionner une date'); // ajoute le texte en tant qu'enfant du lien selectLien.appendChild(selectTexte); // change l'adresse de destination href à # et appelle le sélecteur // lors d'un clic ou d'un focus sur le lien selectLien.setAttribute('href','#'); selectLien.onclick=function(){selecteur(this);return false;}; //selectLien.onkeypress=function(){selecteur(this);return false;}; // ajoute le nouveau lien au parent du champ de saisie (ici, le paragraphe <p>) champsSaisie[i].parentNode.appendChild(selectLien) } } }
Tous les champs contenant des dates ont désormais après eux un lien qui pointe vers le sélecteur.
Tout ce dont nous avons besoin maintenant, c'est de dire à la fonction de sélection où attribuer sa valeur de retour.
Comme on envoie le lien lui-même en tant qu'objet à notre fonction de sélection, on a besoin d'accéder au champ de saisie qui le précède.
Javascript : function selecteur(o) { alert('Ceci est juste une simulation.') // pas de vraie fonction o.previousSibling.value='26/04/1975'; }
On y est presque. Comme nous avons ajouté le lien en tant que dernier enfant du nœud parent du champ de saisie, il se pourrait très bien que le nœud qui précède (previousSibling
) notre nouveau lien ne soit pas le champ de saisie lui-même mais une espace ! Du coup, il faut boucler sur les nœuds précédents jusqu'à ce qu'on tombe sur un élément.
Javascript : function selecteur(o) { alert('Ceci est juste une simulation.') // pas de vraie fonction while(o.previousSibling.nodeType!=1) { o=o.previousSibling; } o.previousSibling.value='26/04/1975'; }
L'utilisation de boucles tient toujours un peu du bricolage et peut être assez lente. Pour éviter de boucler, nous devons changer notre fonction.
Changer la fonction ajouteLienSelecteur()
Il est très facile d'utiliser appendChild()
, mais cela nous rend dépendant du balisage HTML. Que se passerait-il si, par exemple, nous devions ajouter par la suite une balise <span>
avec une astérisque à côté du champ pour indiquer qu'il s'agit d'une information à remplir obligatoirement ?
L'astuce consiste à utiliser insertBefore()
sur le nœud suivant notre champ de saisie (nextSibling
).
Javascript : function ajouteLienSelecteur() { var champsSaisie,selectLien,selectTexte; // boucle sur tous les champs de saisie champsSaisie=document.getElementsByTagName('input'); for(i=0;i<champsSaisie.length;i++) { // si la classe contient 'date' if(/date/.test(champsSaisie[i].className)) { // créer un nouveau lien et un nouveau texte selectLien=document.createElement('a'); selectTexte=document.createTextNode('sélectionner une date'); // ajoute le texte en tant qu'enfant du lien selectLien.appendChild(selectTexte); // change l'adresse de destination href à # et appelle le sélecteur // lors d'un clic ou d'un focus sur le lien selectLien.setAttribute('href','#'); selectLien.onclick=function(){ajouteLienSelecteur(this)}; //selectLien.onkeypress=function(){ajouteLienSelecteur(this)}; // ajoute le nouveau lien directement après le champ de saisiechampsSaisie[i].parentNode.appendChild(selectLien)champsSaisie[i].parentNode.insertBefore(selectLien,champsSaisie[i].nextSibling); } } }
Voir le sélecteur amélioré en action.
Choses à savoir
Voilà, c'est à peu près tout. Avec ces quelques outils nous sommes désormais capables d'accéder à n'importe quel élément d'un document et de le modifier, et nous pouvons améliorer l'expérience utilisateur sans devenir dépendant de Javascript.
C'est un peu déroutant de prime abord. Mais une fois que vous vous serez un peu habitués au DOM, plus vous l'utiliserez et plus ça deviendra facile.
Quelques obstacles habituels :
- Assurez-vous de vérifier que les éléments auxquels vous tentez d'accéder existent. Si certains navigateurs ne voient pas d'inconvénients à vérifier l'existence de
object.nextSibling.nodeName
et à renvoyerfalse
lorsqu'il n'y a pas de nœud suivant ou s'il s'agit d'un nœud de texte, d'autres en revanche renvoient une erreur expliquant que vous essayez d'accéder à l'attribut d'un élément qui n'existe pas. - Faites en sorte de ne pas trop vous reposer sur le balisage, car les sauts de ligne peuvent être vus comme de nouveaux nœuds, ou encore parce que le balisage est susceptible de changer, et que vous ne voulez pas changer votre script à chaque fois que cela se produit.
- Pour lire le contenu d'un élément, il faut lire les valeurs de ses nœuds enfants (childNodes), et non la valeur de l'élément lui-même !
document.getElementsByTagName('h2')[0].nodeValue
est vide, tandis quedocument.getElementsByTagName('h2')[0].firstChild.nodeValue
ne l'est pas. - Quand vous cherchez le nom de nœuds (nodeNames) et leurs attributs, assurez-vous de rester insensible à la casse, car certains navigateurs transforment les éléments en majuscules, d'autres en minuscules.
- Le HTML généré par le DOM est dans la plupart des cas mal formé, donc si vous voulez réutiliser le code HTML généré par un navigateur, il faudra sans doute le nettoyer.
- Évitez de trop utiliser les boucles ; si vous avez la chance de créer vous-même le balisage dont vous avez besoin pour travailler, utilisez plutôt les champs
id
à la place. - Maîtrisez votre syntaxe. Bien souvent, l'appel à
getElementById
peut entraîner une grosse refonte de votre code. - Maîtrisez vos objets Javascript et vos attributs HTML ; il est inutile de commencer par chercher un attribut qui, de toutes façons, n'est pas censé être présent.
- Ne croyez pas que votre propre façon de baliser un document est la façon la plus communément utilisée. Par exemple, il vaut mieux chercher si l'attribut
className
contient la chaîne que vous cherchez plutôt que chercher s'il est exactement cette chaîne, car certains développeurs aiment utiliser des classes multiples.
Et innerHTML, alors ?
innerHTML
est né lorsqu'Internet Explorer 4 est sorti, et c'était à l'époque une façon rapide de générer et de changer du contenu. Il s'agit en fait de lire le contenu d'un élément d'une façon beaucoup plus simple qu'en utilisant les recommandations originelles du W3C. C'est particulièrement probant lorsqu'un élément contient des nœuds enfants (childNodes
) qui sont eux-mêmes des éléments, et qu'on veut lire la totalité du contenu. Pour faire cela en utilisant seulement le DOM, vous devez subir les terribles épreuves[1] qui consistent à vérifier les types des nœuds (nodeTypes
) et lire les valeurs de chaque nœud enfant. innerHTML
est beaucoup plus souple à utiliser, mais possède lui aussi des inconvénients. Vous n'avez par exemple aucune référence sur les éléments que vous créez avec, puisque la valeur complète est une chaîne de caractères au lieu d'être une série d'objets. En outre, innerHTML
est uniquement lié à HTML, et pas à XML, tandis que le DOM est défini pour n'importe quelle sorte de balisage. Si vous voulez une comparaison des techniques et du support de chaque navigateur, allez faire un tour dans la section DOM de Quirksmode.org[2] ou lisez la longue discussion de Developer-x[3].
Liens
- [1] innerHTML pour mozilla : http://www.webfx.nu/dhtml/mozInnerHTML/mozInnerHtml.html
- [2] Une très bonne série de tableaux sur le support de DOM par les différents navigateurs, ainsi qu'une comparaison avec innerHTML : http://www.quirksmode.org
- [3] DOM contre innerHTML : http://www.developer-x.com/content/innerhtml/
» À suivre : « Javascript non-intrusif, chapitre 4 : l'appel des scripts de la forêt ».
http://www.onlinetools.org/articles/unobtrusivejavascript/chapter3.html