Grégory Bourguin
SysReIC - LISIC - ULCO
Javascript (DOM)

Document Object Model (DOM)

Le Document Object Model (DOM) est une interface de programmation normalisée par le W3C, qui permet à des scripts d'examiner et de modifier le contenu du navigateur web.

(Wikipedia)

Le DOM est une représentation objet de ce qui est affiché dans le navigateur.
Javascript permet d'accéder et de manipuler dynamiquement cette structure.
Toute modification du DOM est immédiatement répercutée dans le navigateur.

Tip: La fonctionnalité "inspecter l'élément" des outils de développement du navigateur donne accès au DOM de la page en cours d'affichage. Ces outils permettent généralement de manipuler le DOM dynamiquement. Il est alors possible de modifier "en live" les propriétés des objets affichés (css, ...): très utile pour faire des tests !
(NB: les modifications faites dans l'inspecteur ne sont pas sauvegardées dans le source.)

Les sélecteurs

Identifiant : document.getElementById(...)

En HTML, il est possible de donner un identifiant (unique) aux éléments.

Pour nommer un élément HTML, utiliser l'attribut id des balises.

ATTENTION: il est important de ne pas donner le même id à plusieurs éléments !

<!-- Insertion d'un nommé mon_bouton -->
<button id="mon_bouton">Bouton</button>

En Javascript, il est possible de récupérer et manipuler l'objet correspondant à un id.

Utiliser document.getElementById(id) pour récupérer l'objet dans un script.

<!-- Insertion du bounton nommé bouton_1 -->
<button id="bouton_1">Bouton 1</button>

<script>   
(function (){
    
    // récupération de l'objet bouton_1 grâce à son identifiant
    let bouton = document.getElementById("bouton_1")
    
    // optionnel : affichage du bouton1 dans la console (pour éventuel débuggage)
    console.log(bouton) 
    
    // changement (dynamique) de la couleur du bouton1
    bouton.style.color = 'blue' 
    
})()
</script>
Resultat

Pour que document.getElementById(id) fonctionne, il faut que l'élément HTML correspondant ait bien été déclaré/téléchargé dans la page avant son déclenchement.
Dans le cas contraire, l'objet ne sera pas trouvé, et la méthode renverra null.

Le code ci-dessous génère une erreur :

<!-- Insertion du bouton_2 avant l'exécution du script : ok ! -->
<button id="bouton_2">Bouton 2</button>

<script> 
(function (){
    
    let bouton2 = document.getElementById("bouton_2")
    console.log(bouton2) // affichage du bouton_2 dans la console
    bouton2.style.color = 'blue' // changement de la couleur du bouton_2

    let bouton3 = document.getElementById("bouton_3") 
    console.log(bouton3) // affiche null car le bouton_3 n'existe pas encore...
    bouton3.style.color = 'red' // ERREUR gb : bouton_3 n'existe pas encore !
    
})()
</script>

<!-- Insertion du bouton_3... mais après l'exécution du script : pas bon ! -->
<button id="bouton_3">Bouton 3</button>

Attendre que le document soit prêt : DOMContentLoaded

Comme l'indique l'exemple précédent, il faut être certain que les éléments du DOM ont bien eu le temps d'être chargés dans la page avant d'utiliser les sélecteurs.

Pour ce faire, nous allons mettre en place un nouveau wrapping : la fonction de wrapping ne sera plus déclenchée directement, mais elle attendra que le navigateur la prévienne que tout est bien en place.

Afin d'être certains que le DOM ait bien été chargé avant d'utiliser les sélecteurs, utiliser un wrapping déclenché par l'évènement 'DOMContentLoaded' de l'objet document.

L'exemple ci-dessous présente donc une meilleur manière d'utiliser getElementById.

<!-- Insertion du bounton nommé bouton_1 -->
<button id="bouton_1_bis">Bouton 1</button>

<script>   
// le nouveau wrapping déclenché parf l'évènement 'DOMContentLoaded'
document.addEventListener('DOMContentLoaded', function () {
    
    // récupération de l'objet bouton_1 grâce à son identifiant
    let bouton = document.getElementById("bouton_1_bis")
    
    // optionnel : affichage du bouton1 dans la console (pour éventuel débuggage)
    console.log(bouton) 
    
    // changement (dynamique) de la couleur du bouton1
    bouton.style.color = 'blue' 
    
})
</script>
Resultat

NB: il s'agit ici d'écouter un évènement sur l'objet document, mais en version DOM.
Pour plus d'informations sur les évènements du DOM, c'est ici.

Classe CSS : .getElementsByClassName(...)

Il est possible de récupérer tous les éléments qui ont une certaine classe CSS.
La méthode renvoie la liste des éléments trouvés.

Attention : cette méthode renvoie une HTMLCollection et ce type de liste est automatiquement mise à jour quand le document concerné change !

-> ne pas faire une boucle qui retire la classe des éléments qui ont été récupérés par cette méthode en ciblant cette même classe...

<div class="classe1 classe2 classe3">Du texte</div>
<div class="classe2">Encore du texte</div>
<div class="classe1">Toujours du texte</div>
<div class="classe1 classe3">Et un peu plus de texte</div>

<script>       
document.addEventListener('DOMContentLoaded', function () {
    
    let elements = document.getElementsByClassName("classe1")
    console.log(elements) // optionnel
    for (let elem of elements){
        elem.style.color = 'blue'
        elem.style['font-weight'] = 'bold' 
    }
    
    elements = document.getElementsByClassName("classe3")
    for (let elem of elements){ 
        elem.style['background'] = 'orange' 
    }
    
})
</script>
Resultat
Du texte
Encore du texte
Toujours du texte
Et un peu plus de texte

Nom de balise (Tag) : .getElementsByTagName(...)

Même principe que précédemment, mais pour les noms de balises.

Il est possible d'appeler ces méthodes non pas obligatoirement sur document, mais aussi sur des sous-noeuds du DOM. Dans ce cas, la recherche est limitée aux sous-noeuds de l'objet visé.

<!-- création d'un noeud/objet nommé mon_conteneur-->
<div id="mon_conteneur">
    <div>Du texte</div>
    <p>Encore du texte</p>
    <div>Toujours du texte</div>
    <p>Et un peu plus de texte</p>
</div>

<script>  
document.addEventListener('DOMContentLoaded', function () {    
    
    // récupération du noeud/objet nommé mon_conteneur
    let conteneur = document.getElementById("mon_conteneur")

    // récupération LIMITÉE aux <p> contenus (sous-noeuds) dans le conteneur
    let elements = conteneur.getElementsByTagName("p")
    for (let elem of elements){
        elem.style.color = 'blue'
        elem.style.fontWeight = 'bold' 
    }
    
    // récupération LIMITÉE aux <div> contenus (sous-noeuds) dans le conteneur
    elements = conteneur.getElementsByTagName("div")
    for (let elem of elements){ 
        elem.style.background = 'orange' 
    }
    
})
</script>
Resultat
Du texte

Encore du texte

Toujours du texte

Et un peu plus de texte

Note: Si je n'avais pas limité la recherche des élément au conteneur, tous les <p> de cette page de cours seraient maintenant en bleu, et tous les <div> en orange... :)

Groupe de sélecteurs : .querySelectorAll(...)

Même principe que précédemment, mais la méthode renvoie la liste des éléments qui correspondent à un groupe de sélecteurs spécifiques.

Cette méthode renvoie une NodeList statique !

A la différence de HTMLCollection, une NodeList n'est pas modifiée dynamiquement si on change les éléments -> bon pour les boucles qui voudraient modifier les éléments !

<div id="autre_conteneur">
    <div class="classe_01">
        <p>Du texte</p>
        <p class="classe_01">Un peu texte</p>
        <p>Un peu texte en plus</p>
    </div>
    
    <div class="classe_02">
        <p class="classe_01">Encore du texte</p>
        <p>Toujours du texte</p>
        <p>Et un peu plus de texte</p>
    </div>
</div>

<script>  
document.addEventListener('DOMContentLoaded', function () {
    
    // récupère le conteneur
    let conteneur = document.getElementById("autre_conteneur")
    
    // sélectionne :
    // -> les p ayant la classe_01
    // -> le dernier p fils direct des div ayant la classe_02
    let elements = conteneur.querySelectorAll("p.classe_01, div.classe_02 > p:last-child")
    
    for (let elem of elements){
        elem.style.color = 'blue'
        elem.style.fontWeight = 'bold' 
    }
    
})
</script>
Resultat

Du texte

Un peu texte

Un peu texte en plus

Encore du texte

Toujours du texte

Et un peu plus de texte

NB: .querySelector(...) vous permet de récupérer uniquement le 1er éléments.

Pour une liste exhaustive des patterns de sélecteurs : https://www.w3schools.com/cssref/css_selectors.asp

Les évènements version DOM

Maintenant que nous savons récupérer un élément sur la page, nous pouvons lui ajouter (et retirer) des écouteurs afin de gérer les réactions aux actions de l'utilisateur.

La plupart des éléments HTML ont des gestionnaires d'évènements de tous types.
(Ex. il est possible d'écouter un click sur un simple span, un keydown sur un input, un resize ..., un scroll..., ...)

Voir https://developer.mozilla.org/en-US/docs/Web/Events pour une liste exhaustive.

Ajouter un écouteur : .addEventListener(...)

.addEventListener(type, callback) permet d'ajouter un écouteur d'évènement de type type sur un élément. Lorsque l'évènement se produit, la fonction de callback est déclenchée.

Ex. Écouter un click sur un bouton
<!-- Insertion du bouton en lui donnant un identifiant -->
<button id="bouton_coucou">Fais Coucou !</button>

<script>
document.addEventListener('DOMContentLoaded', function () {
    
    // récupération de l'objet bouton grâce à son identifiant
    let bouton = document.getElementById("bouton_coucou")
    
    // ajout d'un écouteur d'évènement dans le gestionnaire de clicks du bouton
    bouton.addEventListener('click', function (){
        alert('Coucou !')
    })
    
})
</script>
Resultat

Retirer un écouteur : .removeEventListener(...)

.removeEventListener(type, callback) permet de retirer le callback du gestionnaire d'évènements de type type sur un élément.

Ex. Écouter 1 click, puis retirer l'écouter
<!-- Insertion du bouton en lui donnant un identifiant -->
<button id="capricieux" style="color: blue ; font-weight: bold">J'écoute !</button>

<script>
document.addEventListener('DOMContentLoaded', function () {
    
    // récupération de l'objet bouton grâce à son identifiant
    let bouton = document.getElementById("capricieux")
    
    // il est possible de déclarer les fonctions comme des variables
    let mon_traitement = function (){
        
        // NB: le mot clé this fait référence à l'élément visé par l'évènement
        alert("Je suis " + this + ", et c'est bon, j'ai entendu !")
        
        // retrait de l'écouteur : le bouton n'écoutera donc qu'une fois...
        this.removeEventListener('click', mon_traitement)
        // pour le fun : changement de l'aspect du bouton
        this.innerHTML ="Je n'écoute plus !"
        this.style.color = "red"
    }

    // ajout d'un écouteur d'évènement dans le gestionnaire de clicks du bouton
    bouton.addEventListener('click', mon_traitement)

})
</script>
Resultat

NB: après avoir retiré un écouteur avec removeEventListener, il est toujours possible de le remettre avec un nouvel appel à addEventListener.

Détails des évènements

Il est souvent pratique (ou nécessaire) d'obtenir des détails sur un évènement. C'est par exemple le cas lors d'un click pour savoir si l'utilisateur a cliqué avec le bouton droit, ou le bouton gauche. De la même manière, lors d'un keyup, on souhaite en général connaitre la touche sur laquelle l'action a été effectuée.

Un exemple :

<!-- Création d'un élément -->
<!-- NB: pour tester le click droit : on désactive le menu contextuel du navigateur -->
<div id="div_evt_detail" oncontextmenu="return false"
    style="width: 100%; height: 100px ;
           display:flex ; align-items: center ; justify-content: center ; 
           border: 1px solid grey; ">
    Play with me ! 
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {
    
    // récupération de l'élément
    let element = document.getElementById("div_evt_detail")
    // sauvegarde du texte initial
    let initial_txt = element.innerHTML
    
    // on écoute quand la souris se déplace sur l'élément, 
    // et on récupère l'évènement (event) en paramètre du callback, 
    // ce qui permet de l'utiliser dans la fonction
    element.addEventListener('mousemove', function (event){
        
        // un petit message avec des détails sur l'évènement...
        let txt = event.type + " : " + event.offsetX + ", " + event.offsetY
        
        // ... qu'on affiche dans le div (lui-même)      
        this.innerHTML = txt
        
        // ... en changeant sa couleur 
        this.style.background = 'lightgreen'
    })
    
    // Quand la souris est enfoncée sur l'élément...
    element.addEventListener('mousedown', function (event){
        
        // optionnel : pour voir les détails dans la console
        console.log(event)
        
        // infos sur les touches enfoncées lors du click
        let txt = event.type + " : \n"
            + "Bouton : " + event.button + "\n"
            + "Touche Alt  : " + event.altKey + "\n"  
            + "Touche Ctrl : " + event.ctrlKey
        window.alert(txt)
    })
    
    // Quand la souris sort de l'élément...
    element.addEventListener('mouseout', function (event){
        
        // ... on remet le texte de départ ...
        this.innerHTML = initial_txt
        
        // ... et la couleur initiale
        this.style.background = 'initial'
    })
    
})
</script>
Cliquer pour plus d'infos
Play with me !

Gérer les comportements par défaut

Lors de la création d'une application Javascript, il faut parfois désactiver certains comportements qui existent par défaut dans le navigateur (c'était par exemple le cas dans l'exemple précédent avec le menu contextuel).

Grace au DOM, il est possible de désactiver, voire remplacer, certains de ces comportements.

.preventDefault()

Comme son nom l'indique, .preventDefault() permet d'empêcher un évèvement de déclencher son action par défaut (comme par exemple le fait que lorsqu'on clique sur une ancre <a href='...'>, le navigateur dirige automatiquement vers l'attribut href associé).

Un exemple simple

    <ul>
        <li>
            Ancre sur laquelle on va laisser le fonctionnement <b>par défaut</b> : 
            <a href="https://www.w3.org/TR/dom41/" target="_blank"> 
                W3C
            </a>
        </li>
        <li>
            Ancre sur laquelle on va greffer un fonctionnement <b>custom</b> : 
            <a id="dummy_anchor" 
               href="https://www.w3.org/TR/dom41/">
                W3C
            </a>
        </li>
    </ul>
    
    <script>
    document.addEventListener('DOMContentLoaded', function () {
        
        // récupération de l'élément
        let dummy = document.getElementById("dummy_anchor")
        
        // un écouteur sur les clicks
        dummy.addEventListener('click', function (event){
            
            // SUPPRESSION DU COMPORTEMENT PAR DEFAUT
            // (ici une balise <a> : le navigateur n'ouvrira plus la page ciblée par le href) 
            event.preventDefault()
            
            // traitement customisé
            alert("Vous avez cliqué sur " + event.target.href)
        })
    
    })
    </script>
Resultat
  • Ancre sur laquelle on va laisser le fonctionnement par défaut : W3C
  • Ancre sur laquelle on va greffer un fonctionnement custom : W3C

Exemple avec un menu Bootstrap

Tip: Pour que bootstrap fonctionne dans votre page, il faut compléter le <head> comme suit :
(cf. Boostrap - Getting started)

<head>
    <!-- ici le contenu de votre balise head-->
    
    <!-- partie à ajouter pour que bootstrap fonctionne -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" 
        integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" 
        crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" 
        integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" 
        crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" 
        integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" 
        crossorigin="anonymous"></script>
</head>
<div class="dropdown" id="test_preventdefault">
    <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
        Moteurs de Recherche <span class="caret"></span>
    </button>
    <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
        <li><a class="dropdown-item" href="https://www.qwant.com/">Qwant</a></li>
        <li><a class="dropdown-item" href="https://www.google.com/">Google</a></li>
        <li><a class="dropdown-item" href="https://fr.yahoo.com/">Yahoo</a></li>
    </div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
    
    // récupération du conteneur
    let container = document.getElementById("test_preventdefault")
    
    // récupération des ancres du conteneur
    let anchors = container.getElementsByTagName("a")
    
    // pour chaque ancre : écraser le comportement par défaut, et mettre celui souhaité
    for (item of anchors){
        item.addEventListener('click', function (e){
            
            // suppression comportement par défaut
            e.preventDefault()
            
            // comportement custom
            let cible = e.target.href
            let rep = confirm("Voulez-vous vraiment aller à " + cible + " ?")
            if (rep){ 
                window.open(cible) // permet d'ouvrir une nouvelle fenêtre
            }
        })
    }
    
})
</script>
Resultat

Parcourir/Modifier la hiérarchie

Comme on peut le constater sur le schéma, le DOM correspond à une hiérarchie de noeuds.

Chaque noeud a un parent unique, et peut avoir un ou des enfant(s)(child(ren)), ainsi qu'un ou des frère(s) (sibling), i.e. enfants du même parent.

NB: quand le DOM n'a pas été modifié, l'odre des noeuds correspond à celui de leur déclaration dans le code source (HTML).

Les noeuds peuvent être de 2 types :

  • element : correspond aux balises HTML
  • text : correspond aux textes à l'intérieur des balises

Parcourir la hiérarchie

Différents attributs permettent de récupérer les fils, frères, parent, ...

<style>
    #test_hierarchy *{
        margin: 5px;
    }
</style>

<div id="test_hierarchy" style="background: lightgray ; padding: 3px">
    Texte 1.
    <div style="background: lightgreen">Texte 2.
        <span style="background: lightcoral">Un span</span>
        Texte 3.
    </div>
    <div style="background: lightblue">
        Texte 4.
    </div>
    Texte 5.
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {
    
    let container = document.getElementById("test_hierarchy")
    
    // 1er fils (text ou element)
    let texte1 = container.firstChild
    console.log(texte1.textContent)
    
    // 1er element
    let div0 = container.firstElementChild
    console.log(div0 )
    
    // le parent (element)
    console.log(div0.parentElement)
    
    // dernier fils (text ou element)
    let div_text3 = div0.lastChild
    console.log(div_text3.textContent)
    
    // dernier element
    let div_p = div0.lastElementChild
    console.log(div_p)
    
    // frère précédent (text ou element)
    let div_prev_sibling = div0.previousSibling
    console.log(div_prev_sibling.textContent)
    
    // element suivant 
    let div_next_sibling = div0.nextElementSibling
    console.log(div_next_sibling.textContent)
    
    // tous les elements
    let children = container.children
    console.log("Il y a " + children.length + " element(s)")
    
    // tous les enfants (text ou element)
    let child_nodes = container.childNodes
    console.log("Il y a " + child_nodes.length + " noeud(s) (text et element)")
    
})
</script>
Resultat
Texte 1.
Texte 2. Un span Texte 3.
Texte 4.
Texte 5.

Modifier la hiérarchie

Il est aussi possible d'ajouter/retirer dynamiquement des enfants, de modifier le contenu html, ...

<style>
    #modify_hierarchy *{
        margin: 5px;
    }
</style>

<div id="modify_hierarchy" style="background: lightgray ; padding: 5px">
    text
    <div style="background: lightcoral">div 1</div>
    <div style="background: lightgreen">div 2</div>
    <div style="background: lightcyan">div 3</div>
    <button class="btn btn-warning">Modifier</button>    
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {
    
    let container = document.getElementById("modify_hierarchy")

    let button = container.lastElementChild
    button.addEventListener('click', function (){
        
        // retrait du bouton
        container.removeChild(button)
        
        // retrait du 2nd élément
        let target = container.children[1]
        container.removeChild(target)
        
        // creation d'un nouveau div
        let new_node = document.createElement('div')
        new_node.style.background = 'lightyellow'
        new_node.innerHTML = "<b>div 4</b>"
        // insertion en tant que 1er fils du conteneur
        container.insertBefore(new_node, container.firstChild)
        
        // création et insertion d'un <br> à la fin
        new_node = document.createElement('br')
        container.appendChild(new_node)
        
        // insertion d'un nouveau texte à la fin
        container.append("Voir : ")
        
        // création et insertion d'une ancre à la fin
        new_node = document.createElement('a')
        new_node.innerText = 'HTML DOM appendChild() Method'
        new_node.href = 'https://www.w3schools.com/jsref/met_node_appendchild.asp'
        new_node.target = '_blank'
        container.appendChild(new_node)
              
    })      
        
})
</script>
Resultat
text
div 1
div 2
div 3

Éditer les attributs

La structure d'une balise telle qu'on l'écrit dans le code HTML est :
<name attributes> innerHTML </name>
(ex. <a href='http://www.google.fr'>Google</a> )

Les attributs correspondent à des paires key=value.
key correspond au nom de l'attribut visé, et value est une chaine.
(ex. href='http://www.google.fr').

La chaine value peut être "complexe" et contenir des (sous-)paires key: value.
(ex. style="background: 'blue' ; color: 'red' ; font-weight: 'bold'").

Les objets element du DOM correspondent aux balises HTML.
Les attributs d'une balise HTML correspondent aux attributs de l'objet element qui la représente.
Lorsque la valeur d'attribut d'une balise contient des sous-paires key: value, l'attribut key de l'objet element est lui même un objet.

Javascript permet de lire/modifier dynamiquement les attributs des element.
C'est d'ailleurs ce que nous avons fait à plusieurs reprises en transformant l'attribut style de certains éléments.

Accès par notation Objet

La plupart des attributs standard HTML sont accessible (en lecture et écriture) en Javascript grâce à la notation 'pointée' des objets.

<div id="edit_dom_attr">
    <h2>Un exemple d'accès aux attributs</h2>
    <a href="http://www.google.fr" target="_blank">Moteur de recherche</a><br>
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {

    let container = document.getElementById("edit_dom_attr")
    
    let a = container.getElementsByTagName('a')[0]
    // lecture de l'attribut href de l'ancre
    console.log("L'ancre pointait vers : " + a.href)
    // modification...
    a.href = 'https://www.qwant.com'
    console.log("Maintenant, elle pointe vers : " + a.href)
    
    let h2 = container.getElementsByTagName('h2')[0]
    // modification de l'attribut fontSize de l'attribut style du h2
    h2.style.fontSize = '0.8em'
    // modification de l'attribut margin-top de l'attribut style du h2
    h2.style['margin-top'] = '0px'
    // ... la même écrite autrement :
    h2['style'].marginTop = '0px'
    
})
</script>
Resultat

Un exemple d'accès aux attributs

Moteur de recherche

Accès par .getAttribute(key) et .setAttribute(key, value)

En HTML, il est possible créer ses propres (clés d') attributs.
(ex. <div mon_attribut='une valeur qui me parle'>

Ces attributs 'non standards' permettent de faire des traitements custom en Javascript. (C'est par exemple le cas dans certains Frameworks comme Bootstrap.)

Les méthodes .getAttribute(key) et .setAttribute(key, value) permettent d'accéder à tout attribut, qu'il soit standard (ex. href) ou non standard (ex. mon_attribut).

<div id="get_set_dom_attr">
    <div perso="Bill">div Bill</div>
    <div perso="Greg">div Greg</div>
    <div perso="inconnu">div inconnu 1</div>
    <div perso="Toto">div Toto</div>
    <div perso="inconnu">div inconnu 2</div>
    <br>
    <button class="btn btn-default">Trouver les inconnus</button>
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {
    
    let container = document.getElementById("get_set_dom_attr")
    
    // Accès à l'attribut 'perso' du 2nd div
    let i = 2
    let div = container.children[i-1]
    let perso = div.getAttribute('perso')
    console.log("Le div n° " + i + " parle de " + perso)
    
    let button = container.getElementsByTagName('button')[0]
    // Un click sur bouton va surligner les elements avec un attribut perso='inconnu'
    button.addEventListener('click', function (event){
        
        // div[perso='inconnu'] cible tous les div qui ont un attribut perso='inconnu'
        let targets = container.querySelectorAll("div[perso='inconnu']")
        
        // changement de style pour tous les éléments ciblés
        for (t of targets){
            let new_style = 'background: darkred ; color: white'
            t.setAttribute('style', new_style)
        }
    }) 
    
})
</script>
Resultat
div Bill
div Greg
div inconnu 1
div Toto
div inconnu 2

Attributs spéciaux

Il existe des accesseurs d'attributs que je qualifierais de 'spéciaux' du fait qu'il n'ont pas une représentation aussi 'directe' dans les balises.

Ajouter/retirer des classes CSS : .classList

Permet de lire/modifier la liste des classes CSS qui sont appliquées à l'élément.

Il s'agit littéralement d'ajouter ou retirer dynamiquement des classes CSS !

Cet attribut est très pratique car, contrairement à ce que nous avons fait jusqu'à présent, il permet de modifier le style d'un élément sans avoir à énumérer toutes les sous-propriétés une par une à chaque fois.

<style>
    #class_dom_attr{
        display: inline-block;
        padding: 5px;
        color: blue;
    }
    .ok{
        background: greenyellow;
        font-family: Arial;
    }
    .dangereux{
        font-weight: bold;
        border: 5px solid red ;
        border-radius: 3px;
    }
</style>

<div id="class_dom_attr" class="ok">Ce cours est important</div>
<br><br>
<button class="btn btn-default" onclick="goToDanger()">Go to Danger</button>

<script>
    function goToDanger(){
        let elem = document.getElementById("class_dom_attr")
        elem.classList.remove('ok')
        elem.classList.add('dangereux')    
    }
</script>
Resultat
Ce cours est important


NB: vous pouvez aussi ajouter/supprimer une classe en boucle grâce à classList.toggle(...)


La position, la taille , etc.

Il serait fastidieux de toutes les passer en revue, mais Javascript donne accès à d'autres propriétés bien pratiques, comme celles qui permettent de connaitre la position d'un élément, sa taille, etc.

Je vous invite à les découvrir par vous même par exemple ici : element.offsetHeight, element.offsetLeft, element.offsetTop, ...

Exceptions

Comme en Java, le try ... catch ... finally de Javascript permet d'intercepter et de traiter les éventuelles erreurs qui peuvent se produire pendant l'exécution d'un script.

La structure du code est en 3 parties : la dernières est optionnelle.

  • Le bloc try permet d'entourer un code sensible susceptible de générer des erreurs.
    (ex. accès à un élément du DOM qui n'existe pas, pb réseau, ...)
  • Le bloc catch définit les instructions à exécuter en cas d'erreur.
  • Le bloc finally (optionnel) définit les instructions à exécuter après le bloc try (et éventuellement catch), qu'il y ait eu erreur ou non.

try...catch

try{

    // on tente de récupérer un élément qui n'existe pas...
    let element = document.getElementById('Un div inexistant')
    
    // l'accès à style est null, et donc l'accès à color déclenche un erreur !
    element.style.color = 'red'
    
    // du fait de l'erreur, le code ci-dessous ne sera pas exécuté
    window.alert("je n'ai servi à rien")

} catch(e) { // l'erreur est interceptée
    console.log("Erreur interceptée : " + e)     
} 

console.log('Tout va bien, je continue...')

try...catch...finally


<div id="div_tcf" style="margin: 10px 0 ; border: 1px solid black ; padding: 5px">Je suis un div</div>

<button id="btn_tcf_01" class="btn btn-primary">Ce bouton fonctionne</button>
<button id="btn_tcf_02" class="btn btn-danger">Ce bouton génère une erreur</button>

<!-- un div pour afficher les message -->
<div id="tcf_output" 
     style="margin: 10px 0 ; min-height: 50px ; padding: 5px ; background: black ; color: greenyellow">   
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {
    
    let btn1 = document.getElementById('btn_tcf_01')
    btn1.addEventListener('click', function (){
        changerCouleur('div_tcf') // cherche un id existant
    })
    
    let btn2 = document.getElementById('btn_tcf_02')
    btn2.addEventListener('click', function (){
        changerCouleur('un div inexistant') // cherche un id inexistant
    })
    
    function changerCouleur(identifiant){
        
        // si l'élément avec un id=identifiant n'existe pas, div sera null
        let div = document.getElementById(identifiant)
        
        try{
            // si le div n'existe pas : génère une erreur
            div.style.background = 'yellow'
            
            // quand il n'y a pas eu d'erreur :
            afficherMsg("Tout s'est bien passé.")
        } catch(e) { 
            // quand l'erreur est interceptée
            afficherMsg("Erreur interceptée : " + e, 'red')     
        } finally {
            // ce bloc s'exécute quoi qu'il arrive
            afficherMsg("Le finally est exécuté dans tous les cas.", 'lightblue')
        }
    }
    
    // Juste pour afficher les messages
    let output = document.getElementById('tcf_output')
    function afficherMsg(msg, color='inherit'){
        let txt = document.createElement('div')
        txt.innerText =  msg
        txt.style.color = color
        output.appendChild(txt) 
    }
    
})
</script>
Resultat
Je suis un div

Timers

L'objet window fournit des méthodes permettant de retarder ou répéter des instructions au bout d'un certain temps.

Retarder une exécution : setTimeout / clearTimeout

setTimeout

window.setTimeout permet de déclencher l'exécution d'une fonction au bout d'un certain temps. La fonction exécutée et le délai à attendre (en ms) sont passés en paramètres.
La méthode renvoie aussi un objet qui permettra de le stopper (cf. clearTimeout).

ex. setTimeout 'simple'

<button id="sto_btn" class="btn btn-default">Test exécution différée</button>

<div id="sto_out" 
     style="min-height: 50px ; margin: 10px 0 ; padding: 5px ; background: black ; color: greenyellow">     
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {
    
    let button = document.getElementById('sto_btn')
    button.addEventListener('click', function (){
        
        // exécution instantanée (dès qu'on clique)
        afficher(new Date().toLocaleTimeString() + ' : click') 
        
        // exécution différée...
        window.setTimeout(function (){ 
            afficher(new Date().toLocaleTimeString() + ' : message différé', 'aqua')
        }, 1000) // ... 1000ms plus tard
        
    })
    
    // Juste pour afficher des messages
    let output = document.getElementById('sto_out')
    function afficher(msg, color='greenyellow'){
        let element = document.createElement('div')
        element.innerText = msg
        element.style.color = color
        output.append(element)
    }
    
})
</script>
Resultat

clearTimeout

window.clearTimeout permet de stopper un timeout avant qu'il ne se déclenche.
La méthode prend en paramètre l'objet renvoyé par setTimeout.

ex. setTimeout + clearTimeout

<button id="cto_btn" class="btn btn-default">Test exécution différée</button>
<button id="cto_btn_stopper" class="btn btn-warning" style="visibility: hidden">Stopper le timer</button>

<div id="cto_out" 
     style="min-height: 50px ; margin: 10px 0 ; padding: 5px ; background: black ; color: greenyellow">     
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {    
    
    // servira à stocker le timeout
    let timer = undefined ;
    
    let button = document.getElementById('cto_btn')
    let button_stop = document.getElementById('cto_btn_stopper')
    
    let delay = 2000 // on attendra 2s 
    
    // évènement permettant de lancer le timeout
    button.addEventListener('click', function (){
        
        // exécution instantanée (dès qu'on clique)
        afficher(new Date().toLocaleTimeString() + 
                 " : le message différé s'affichera dans " + delay/1000 + "s...") 
        
        // exécution différée...
        // (on stocke le timer pour pouvoir le bloquer au cas où...)
        timer = window.setTimeout(function (){ 
            afficher(new Date().toLocaleTimeString() + ' : message différé', 'aqua')
            // cacher les bouton stop quand le message a été affiché
            button_stop.style.visibility = 'hidden'
        }, delay) 
        
        // affichage du bouton permettant de stopper le timer            
        button_stop.style.visibility = 'visible'
    })
    
    // évènement permettant de stopper le timeout (avant qu'il ne se déclenche)
    button_stop.addEventListener('click', function (){
        window.clearTimeout(timer)
        afficher('Timer STOPPED', 'red')
        button_stop.style.visibility = 'hidden'
    })
    
    // Juste pour afficher des messages
    let output = document.getElementById('cto_out')
    function afficher(msg, color='greenyellow'){
        let element = document.createElement('div')
        element.innerText = msg
        element.style.color = color
        output.append(element)
    }
    
})
</script>
Resultat

Répéter une exécution : setInterval / clearInterval

window.setInterval permet de répéter l'exécution d'une fonction au bout d'un certain temps. La fonction répétée et le délai entre 2 exécutions (en ms) sont passés en paramètres. La méthode renvoie aussi un objet qui permettra de stopper la répétition.

window.clearInterval permet de stopper un Interval. La méthode prend en paramètre l'objet renvoyé par setInterval.

ex. setInterval + clearInterval


<button id="si_btn" class="btn btn-default">start</button>

<script>
document.addEventListener('DOMContentLoaded', function () {
    
    // pour stocker le timer quand il sera actif
    let timer = undefined
    
    // récupération du bouton 'horloge'
    let clock = document.getElementById('si_btn')
    
    // écouter les click pour : 
    // -- démarrer le timer s'il ne tourne pas (timer == undefined)
    // -- stopper le timer s'il est déjà actif (timer != undefined)
    clock.addEventListener('click', function (){
        
        if(timer === undefined){ // il n'y a pas de timer actif
            clock.innerHTML = new Date().toLocaleTimeString()
    
            // mise à jour de l'horloge toutes les secondes
            // (on stocke le timer pour pouvoir l'arrêter au prochain click)
            timer = window.setInterval(function (){
                clock.innerHTML = new Date().toLocaleTimeString()        
            }, 1000) // 1000ms = 1s
            
        }else{ // le timer existe : il faut l'arrêter
            window.clearInterval(timer)
            timer = undefined
            clock.innerHTML = 'Restart'
        }
    })
    
})
</script>
Cliquer pour démarrer/arrêter l'horloge

Technique du "callback"

La technique du callback ("fonction de rappel"), n'est pas uniquement liée aux timers, mais elle peut s'avérer particulièrement utile dans ce contexte.

Un callback est une fonction envoyée en paramètre d'une autre fonction pour que cette dernière puisse appeler le callback quand elle le souhaite (par exemple, à la fin de son exécution).

Exemple de Callback avec un setInterval

Pour exemplifier, le code ci-dessous crée 2 boutons avec chacun leur traitement. Cependant, un click sur un bouton ne lance pas ce traitement immédiatement : l'exécution est différée et exécutée seuelment après un compte à rebours.

Cette exécution différée est mise en oeuvre par une fonction générique launch qui prend en paramètre un callback, c.a.d. une autre fonction qui sera appelée uniquement lorsqu'un compte à rebours est terminé.

Exemple de callback.

<button id="button-01">Traitement 01</button> 
&nbsp;
<button id="button-02">Traitement 02</button>
<br><br>
<div id="messages"></div> <!-- zone d'affichage des messages -->

<script>
    let messages = undefined // sera initialisée dans le 'DOMContentLoaded'

    // traitement du bouton 01
    function traitement_01(){
        messages.innerHTML += "<div style='color: blue'>TRAITEMENT 01 : TERMINÉ !</div>"
    }

    // traitement du bouton 02
    function traitement_02(){
        messages.innerHTML += "<div style='color: red'>TRAITEMENT 02 : TERMINÉ !</div>"
    }

    // Fonction qui prend en paramètre un 'callback', i.e. une autre fonction.
    // Le callback sera déclenché quand le compte à rebours est terminé 
    function launch(callback){
        let counter = 5 // on décompte de 5 à 0 
        const delay = 500 // vitesse du compte à rebours

        let timer = window.setInterval(function (){
            
            // traitement à chaque décompte
            messages.innerHTML += "<span style='color: grey'>"+ counter + "</span> "
            counter--
            if (counter === -1){ // quand le compte à rebours est terminé
                window.clearInterval(timer) // arrêt du timer
                callback() // !!! déclenchement du callback !!!
            }
            
        }, delay)
    }

    document.addEventListener('DOMContentLoaded', function () {

        messages = document.getElementById("messages")

        let launcher01 = document.getElementById("button-01")
        console.log(launcher01)
        launcher01.addEventListener('click', function (){
            launch(traitement_01) // la fonction traitement_01 est envoyée en callback
        })

        let launcher02 = document.getElementById("button-02")
        launcher02.addEventListener('click', function (){
            launch(traitement_02) // la fonction traitement_02 est envoyée en callback
        })
    })
</script>
Resultat