JavaScript dans le navigateur Web

Michel Buffa
Avril 2012

buffa@unice.fr

Introduction

BOM, DOM, Evènements, XMLHttpRequest...

Introcuction

Les programmes JavaScript ont besoin d'un environnement d'exécution.

Il existe du server-side JavaScript, du JavaScript par exemple

Dans ce chapitre nous allons nous consacrer au cas du navigateur Web, et allons étudier :

  • Le BOM (Browser Object Model),
  • Le DOM (Document Object Model)
  • Comment gérer les évènements du navigateur,
  • L'objet XMLHttpRequest au cour de la technologie AJAX

Inclure JavaScript dans une page HTML

Exemple pour HTML5 (pas besoin de typer le script)

<!DOCTYPE html>
<html>
<head>
    <title>JS test</title>
    <script src="somefile.js"></script>
</head>
<body>
<script>
    var a = 1;
    a++;
</script>
</body>
</html>         
    

Le navigateur exécute le code JavaScript dans un ordre séquentiel. Le code de la balise <script> pourra référencer celui du script inclu puisqu'il sera exécuté après.

Les objets BOM et DOM

Le code JavaScript dans une page HTML a accès à divers objets qui peuvent être :

  1. En rapport avec la page (le document)
  2. Sans rapport avec la page : fenêtre du navigateur, bureau, etc.

Les objets de la première catégorie forment le DOM (Document Object Model).

Ceux de la seconde le BOM (Browser Object Model.

Le DOM et le BOM sont ils standards ?

Le DOM est un standard du W3C, il existe en plusieurs versions appelés niveaux. Il y a le "DOM level 1", le "DOM level 2" et enfn le "DOM level 3".

Avant qu'il ne devienne standard il y avait un "DOM level 0" dont certaines parties sont devenues des standards de facto.

Le support de ces niveaux par le navigateur est variable mais tous supportent entièrement le DOM level 1.

Le BOM n'est pas un standard officiel mais une grande partie est commune à tous les navigateurs.

Nous allons surtout étudier les parties du DOM et du BOM implémentées par les principaux navigateurs.

Le BOM (Browser Object Model)

Le BOM fournit un ensemble d'objets utiles, que l'on manipulera à travers deux objets bien connus : window et window.screen

Il existe toujours un "objet global" qui correspond à l'environnement d'exécution. Dans le cas du navigateur, il s'agit de l'objet window.

Toutes les variables globales sont des propriétés de cet objet :

>>> window.somevar = 1;
       1
>>> somevar
       1
   

L'objet window, suite...

Toutes les fonctions JavaScript prédéfinies sont des méthodes de cet objet :

>>> parseInt('123a456')
       123
>>> window.parseInt('123a456')
       123
    

En plus d'être le container des variables globales et fonctions prédéfinies, window fournit également les données sur l'environnement du navigateur. Il y a un objet window par frame, iframe, popup ou onglet ouvert.

window.navigator (ou navigator tout court)

Tester le navigateur sur lequel le code est en train de tourner :

>>> window.navigator.userAgent
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.12) Gecko/20080201
    Firefox/2.0.0.12"
>>> navigator.userAgent
       Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET
       CLR 2.0.50727; .NET CLR 3.0.04506.30)
if (navigator.userAgent.indexOf('MSIE') !== -1) {
  // this is IE
} else {
  // not IE
}
    

Meilleure manière de tester le navigateur

Il est plutôt conseillé de tester si une feature est supportée par le navigateur :

if (navigator.geolocation) { // eq à window.navigator.geolocation
  // feature supportée, utilisons-là
} else {
  // feature non supportée, faisons autrement...
}
    

En effet, il est difficile de tester tous les navigateurs et la valeur de userAgent peut être modifiée par des extensions.

Pour analyser le BOM ou le DOM : utiliser la console de votre navigateur !

Que ce soit Firebug/FF ou les consoles intégrées dans Chrome ou Opera (f12 ou ctrl-shift-i ou ctrl-shift-j sur mac), elles permettent d'explorer le BOM ou le DOM

Utiliser console.dir(navigator) pour le BOM :

BOM : différences entre navigateurs

window.location ou location tout court

Permet de connaitre l'URL de la page courante, permet d'en changer

Nombreuses propriétés, comme location.href, etc.

http://search.phpied.com:8080/search?p=javascript#results
>>> for(var i in location) {console.log(i + ' = "' + location[i] +
                            '"')}
       href = "http://search.phpied.com:8080/search?p=javascript#results"
       hash = "#results"
       host = "search.phpied.com:8080"
       hostname = "search.phpied.com"
       pathname = "/search"
       port = "8080"
       protocol = "http:"
       search = "?p=javascript"
    

Changer la page courante en modifiant location

Si on modifie la valeur de certaines propriétés de location cela donne l'ordre au navigateur de charger un autre document.

Plusieurs possibilités :

>>> window.location.href = 'http://www.packtpub.com'
>>> location.href = 'http://www.packtpub.com'
>>> location = 'http://www.packtpub.com'
>>> location.assign('http://www.packtpub.com')
>>> location.replace('http://www.yahoo.com')

L'utilisation de replace() ne modifie pas l'historique du navigateur.

Rafraichir une page avec window.location

Trois possibilités :

>>> location.reload()

>>> window.location.href = window.location.href

>>> location = location
    

window.history ou history tout court

Donne accès à l'historique de navigation, uniquement dans la session !

Connaitre le nombre de pages consultées avant d'arriver à la page courante :

>>> window.history.length
       5

On ne peut pas connaitre le nom des URLs visités cependant, l'accès aux valeur ne donne rien

>>> alert(history[0]);
    

window.history : navigation avant et arrière

On peut donner l'ordre au navigateur d'aller à la page précédente/suivante (comme si on avait cliqué sur les flèches de navigation).

On peut aussi aller à un endroit précis dans l'historique de navigation.

>>> history.forward()
>>> history.back()
>>> history.go(-1); // identique à précédent
>>> history.go(-2);
>>> history.go(0);  // identique à location.reload()
    

window.frames

Une page peut contenir une ou plusieurs frames ou iframes

Chaque frame ou iframe a son propre objet window

On a donc une hiérarchie de frames/iframes avec une hiérarchie de window associée.

window.frames est cette collection/hiérarchie, elle est égale à l'objet window lui-même

>>> window.frames === window
       true
    

window.frames, suite...

On peut tester si une page contient plusieurs frames/iframes à l'aide de frames.length :

>>> frames.length
       1
    

Chaque frame contient une page qui a son propre objet window. Imaginons que la page courante contienne cette instruction :

<iframe name="maFrame" src="http://www.google.com"/>
    

window.frames, suite...

Depuis la page courante, pour accèder à l'objet window de l'iframe incluse, plusieurs possibilités :

>>> window.frames[0]
>>> window.frames[0].window
>>> frames[0].window
>>> frames[0]
    

Accès aux propriétés de l'objet window de l'iframe

>>> frames[0].window.location.reload()
    

window.frames, suite...

Depuis la page fille on peut avoir accès à l'objet window de la page parente :

>>> frames[0].parent === window
     true
    

Sommet de la hiérarchie : window.top ou top tout court :

>>> window.frames[0].window.top === window
       true
>>> window.frames[0].window.top === window.top
       true
>>> window.frames[0].window.top === top
       true
    

window.frame, accès par nom

Si une frame ou iframe a un attribut 'name' positionné, on peut y acccèder par son nom :

>>> window.frames['maFrame'] === window.frames[0]
       true
    

window.screen

Fournit des informations sur l'écran : nombre de couleurs, résolution, etc.

>>> window.screen.colorDepth
    32
>>> screen.width
       1440
>>> screen.availWidth
       1440
>>> screen.height
       900
>>> screen.availHeight
       847
    

La différence entre height et availHeight : cette dernière enlève la taille de la barre d'icones windows, par ex.

Méthodes de window : window.open() et window.close()

window.open() permet de créer des popups (à éviter please...). Si vous ouvrez des popups au chargement de la page (onload event), ils risquent d'être filtrés (extensions anti popups par ex).

Paramètres : URL du document à afficher, nom, largeur, hauteur, status bar visible ou pas, etc...

var win = window.open('http://www.packtpub.com', 'packt',  
                      'width=300,height=300,resizable=yes');
    

window.open() renvoie l'objet window du popup.

window.moveTo, window.moveBy window.resizeTo

A éviter si possible, perturbant pour l'utilisateur...

>>> window.moveTo(100, 100);
>>> window.moveBy(10, -10);   // déplacement relatif
>>> window.resizeTo(800, 800);
    

Interactions avec l'utilisateur : window.alert(), window.confirm() et window.prompt()

Oui, window.alert() ou alert() est une fonction du BOM !

confirm() demande à l'utilisateur de choisir entre Ok et Cancel

    >>> var answer = confirm('Are you cool?');
                         console.log(answer);
    

window.confirm() suite...

On peut noter que :

  • Rien ne s'affiche dans la console dans que le popup n'est pas fermé,
  • Toute l'exécution est bloquée...
  • Cliquer Ok renvoie true, cancel ou croix ou ESC renvoie false.

Pratique pour tester une confirmation :

if (confirm('êtes vous sur de vouloir tout effacer ?')) {
  // on supprime tout
} else {
  // cancel
}
    

window.prompt() ou prompt() tout court

Demande une entrée clavier :

>>> var answer = prompt('Quel est votre nom ?');
>>> console.log(answer);

Exécution bloquée tant qu'on a pas répondu,

Renvoie null si cancel ou ESC ou clic sur la croix, "" si Ok mais aucune entrée...

Renvoie la chaine saisie sinon...

window.setTimeOut(), window.setInterval()

setTimeOut() sert à exécuter une fonction après un certain délai,

setInterval() sert à exécuter une fonction de manière répétée...

Pour des animations graphiques on préfèrera requestWindowFrame() de HTML5.

>>> function boo(){alert('Boo!');}
>>> setTimeout(boo, 2000); // exécuté après 2 s
       4
        

4 est l'identificateur du TimeOut

setTimeOut() et setInterval(), suite...

A l'aide de l'id du TimeOut et de la fonction clearTimeOut() on peut l'interrompre :

>>> var id = setTimeout(boo, 20000);
        ....
>>> clearTimeout(id); // Le code de boo ne sera pas exécuté
    

Avec setInterval() on peut exécuter de manière périodique une fonction

>>> function boo() {console.log('boo')};
>>> var id = setInterval(boo, 2000);
       boo
       boo
       boo
       boo
       boo
       boo
>>> clearInterval(id); // annule l'exécution périodique
    

window.document

window.document est un objet du BOM qui correspond au document courant (page).

Ses méthodes et propriétés appartiennent au DOM (Document Object Model)...

Respirez un grand coup, nous plongeons dans le DOM !

Le DOM (Document Object Model)...

DOM = représentation d'un document XML ou XHTML sous forme d'un arbre de noeuds

A l'aide des méthodes et propriétés du DOM on pourra accèder/modifier/supprimer ou créer n'importe quel élément dans la page.

Il existe des implémentations du DOM dans plusieurs langages (PHP par exemple, Java, etc.)

<!DOCTYPE html>
<html>
<head>
    <title>Ma page</title>
</head>
<body>
<p class="opener">premier paragraphe</p>
<p><em>second</em> paragraphe</p>
<p id="closer">final</p>
<!-- et voilà  -->
</body>
</html>

Le DOM est une hiérarchie

Le second paragraphe (<p><em>second</em> paragraph</p>) est un tag <p>

...qui est un fils (child) de <body>,

...lui-même fils de <html> qui est son parent, etc.

Le second paragraphe est un frère (sibling en anglais)

Ces relations forment un arbre que l'on appelle le DOM tree

On peut le vidualiser graphiquement !

Commentaires...

  • On voit des noeuds de type #text, c'est par exemple le texte d'un paragraphe comme "premier paragraphe",
  • Les espaces non sécants (whitespaces) forment aussi des noeuds comme entre <body> et le premier <p>
  • Les commentaires forment aussi des noeuds de type #comment

Voyons maintenant comment faire le CRUD sur les éléments du DOM.

Accès aux éléments du DOM

Pour les exemples suivants on travaillera sur cette page, qui contient le document déjà présenté :

<!DOCTYPE html>
<html>
<head>
    <title>Ma page</title>
</head>
<body>
<p class="opener">premier paragraphe</p>
<p><em>second</em> paragraphe</p>
<p id="closer">final</p>
<!-- et voilà  -->
</body>
</html>
    

Accès via firebug ou console Chrome ou Opera

Tapez F12, puis dans la console "document", puis cliquez sur le lien obtenu :

Que remarquer dans cet affichage ?

Tous les noeuds ont une propriété nodeType, nodeName et nodetValue, y compris le document :

>>> document.nodeType
  9        
    

Il existe 12 types différents, qui ont des valeurs numériques. Les plus utiles sont 1 (element), 2 (attribute) et 3 (text).

Les noeuds ont un nom : le nom du tag pour un élément (propriété tagName), #text pour les noeuds de type texte et #document pour le document.

>>> document.nodeName
       "#document"
    

Que remarquer dans cet affichage ?

Les noeuds ont une valeur : pour les noeuds #text, c'est le texte entre les balises, pour le document, c'es null :

>>> document.nodeValue
  null
        

document.documentElement

document.documentElement représente la racine du DOM tree :

>>> document.documentElement
   <html>
>>> document.documentElement.nodeType
       1
>>> document.documentElement.nodeName
       "HTML"
>>> document.documentElement.tagName
       "HTML"
    

remarque : dans le cas d'un élément (type = 1), les propriétés nodeName et tagName ont la même valeur.

Accès aux enfants : propriété childNodes d'un noeud du DOM

On peut aussi tester si un noeud a des enfants avec la méthode hasChildNodes():

>>> document.documentElement.hasChildNodes()
       true
>>> document.documentElement.childNodes.length
       2
>>> document.documentElement.childNodes[0]
       <head>
>>> document.documentElement.childNodes[1]
       <body>
    

Accès au parent : propriété parent

>>> document.documentElement.childNodes[1].parentNode
       <html>
>>> var leBody = document.documentElement.childNodes[1];
>>> leBody.childNodes.length
       9
    

9 enfants ! Vérifions ensembles !

<body>
<p class="opener">first paragraph</p>
<p><em>second</em> paragraph</p>
<p id="closer">final</p>
<!-- and that's about it -->
</body>
    

Attributs d'un noeud

On peut vérifier si un noeud a des attributs avec hasAttributes() :

>>> leBody.childNodes[1]
       <p class="opener">
>>> leBody.childNodes[1].hasAttributes()
        true
>>> leBody.childNodes[1].attributes.length; // combien d'attributs ?
        1
>>> leBody.childNodes[1].attributes[0].nodeName
       "class"
>>> leBody.childNodes[1].attributes[0].nodeValue
       "opener"
>>> leBody.childNodes[1].attributes['class'].nodeValue
       "opener"
>>> leBody.childNodes[1].getAttribute('class')
       "opener"
    

Contenu d'un tag HTML

Regardons le premier paragraphe de notre exemple :

>>> leBody.childNodes[1].textContent
       "first paragraph"
>>> leBody.childNodes[1].innerHTML
       "first paragraph"
>>> leBody.childNodes[3].innerHTML
       "<em>second</em> paragraph"
>>> leBody.childNodes[3].textContent
       "second paragraph"
    

innerHTML n'est pas dans le standard du DOM mais il est utilisé partout ! Il renvoie le sous-arbre sous forme de String.

Contenu d'un tag HTML

On peut aussi utiliser la propriété nodeValue

>>> leBody.childNodes[1].childNodes.length
        1
>>> leBody.childNodes[1].childNodes[0].nodeName
       "#text"
>>> leBody.childNodes[1].childNodes[0].nodeValue
       "first paragraph"
    

Raccourcis pour manipuler le DOM

Les transparents précédents sont intéressants pour la compréhension de l'API du DOM, mais le fait qu'un blanc non sécable (whitespace entre body et le premier paragraphe, dans l'exemple), créée un noeud, pose des problèmes de portabilité du code.

Si le document change légèrement, il se peut que les fonctions d'adressage des noeuds ne pointent plus vers ce que l'on attend.

Pour ce faire, l'API du DOM propose aussi des méthodes utilitaires incontournables.

Ces fonctions sont : getElementsByTagName(), getElementsByName(), et getElementById(), auxquelles HTML5 a récemment ajouté getElementByClass() et querySelector().

document.getElementByTagName()

Renvoie une collection (array object) de noeuds du type de celui passé en paramètre :

 >>> document.getElementsByTagName('p').length
        3
 >>> document.getElementsByTagName('p')[0]
        <p class="opener">
 >>> document.getElementsByTagName('p').item(0)
        <p class="opener">
 >>> document.getElementsByTagName('p')[1]
        <p>
 >>> document.getElementsByTagName('p')[0].innerHTML; // contenu
       "first paragraph"
 >>> document.getElementsByTagName('p')[2]
        <p id="closer">
    

document.getElementByTagName(), suite...

Autres exemples, accès aux attributs par leur nom :

 >>> document.getElementsByTagName('p')[2].id
       "closer"
    

Ne marche pas avec un attribut qui s'appelle "class" car c'est un mot réservé, dans ce cas :

 >>> document.getElementsByTagName('p')[0].className
  "opener"
    

document.getElementById()

Sans doute le plus populaire des raccourcis, permet d'accèder à un noeud par son Id :

>>> document.getElementById('closer')
        <p id="closer">
>>> var p3 = document.getElementById('closer');
>>> p3.innerHTML="hello";
    

Essayez donc ce code sur la page de l'exemple ! Que se passe-t-il ?

Plus gros défaut : beaucoup de caractères à écrire, jQuery a exploité ce défaut $("#closer").html("hello");

Réunion de famille anglaise : siblings, body, firstChild, et lastChild !

Visite chez les frères et soeurs :

>>> var para = document.getElementById('closer')
>>> para.nextSibling
       "\n "
>>> para.previousSibling
       "\n "
>>> para.previousSibling.previousSibling
        <p>
>>> para.previousSibling.previousSibling.previousSibling
        "\n "
>>> para.previousSibling.previousSibling.nextSibling.nextSibling
        <p id="closer">
    

On voit les \n (ou whitespaces) avec Firebug :

Cas particulier de body

L'élément body est si utilisé qu'il a son propre raccourci :

>>> document.body
      <body>
>>> document.body.nextSibling; // prochain frère
      null
>>> document.body.previousSibling // frère précédent
      <head>
    

firstChild et lastChild

Ce sont deux propriétés très pratiques.

firstChild est équivalent à childNodes[0] et lastChild à childNodes[childNodes.length - 1].

>>> document.body.firstChild
       "\n "
>>> document.body.lastChild
       "\n "
>>> document.body.lastChild.previousSibling
        Comment length=21 nodeName=#comment
>>> document.body.lastChild.previousSibling.nodeValue
       " and that's about it "
}

Récapitulatif de l'exemple

Parcours du DOM

En utilisant ce que nous venons de voir, on peut écrire un petit bout de code qui parcourt le DOM :

function walkDOM(n) {
  do {
    console.log(n);
    if (n.hasChildNodes()) {
      walkDOM(n.firstChild)
    }
  } while (n = n.nextSibling)
}
    
>>> walkDOM(document.documentElement);
>>> walkDOM(document.body);
    

Cela doit donner...

Modification de noeuds du DOM

Par exemple, modifier le texte d'une page :

>>> var my = document.getElementById('closer');
>>> my.innerHTML = 'final!!!';
       "final!!!"
    

Comme innerHTML accepte un sous arbre sous forme de String, on peut aller plus loin encore :

>>> my.innerHTML = 'my final';
 "<em>my</em> final"
    

Modifications de noeuds, suite...

Le dernier exemple a aussi ajouté des noeuds dans le DOM (un tag <em>) :

    >>> my.firstChild
    <em>
>>> my.firstChild.firstChild
    "my"
    

On aurait aussi pu utiliser nodeValue en écriture :

>>> my.firstChild.firstChild.nodeValue = 'your';
       "your"
    

Modification de styles CSS de noeuds du DOM

Les noeuds du DOM ont une propriété style qui a son tour a de nombreuses propriétés qui correspondent à des propriétés CSS.

Exemple : ajouter une bordure rouge à un élément :

>>> my.style.border = "1px solid red";
    "1px solid red"
    

Modification de styles CSS de noeuds du DOM, suite...

Les propriétés CSS ont souvent des tirets dans leurs noms, par exemple : padding-top, margin-left, font-weight,

En JavaScript on supprime le tiret et on capitalise le mot qui suit : paddingTop, maginLeft, fontWeight, etc.

>>> my.style.fontWeight = 'bold';
       "bold" "bold"
    

Modification d'attributs :

On peut les modifier mêmes s'ils n'avaient pas été initialisés avant...

>>> my.align = "right";
       "right"
>>> my.name
>>> my.name = 'myname';
       "myname"
>>> my.id
       "closer"
>>> my.id = 'further'
       "further"
>>> my
<p id="further" align="right" style="border: 1px solid red;
        font-weight: bold;">
    

Jouons un peu avec la page de Google et son formulaire !

Allez sur la page http://www.google.fr et ouvrez Firebug !

Identifier la zone de saisie de la requête

Cherchons dans les champs <input> de cette page celui de la requête (son nom est "q", on le retrouve dans la query string HTTP)

>>> var inputs = document.getElementsByTagName('input');
>>> inputs.length;
   12
>>> inputs[1].name;
   "output"
>>> inputs[0].name;
   "hl"
>>> inputs[2].name;
   "sclient"
>>> inputs[3].name;
   "q"  // Ah le voilà !
>>> inputs[3].value='ma requête à moi !';
   "ma requête à moi !"
    

Que se passe-t-il ?

Modifier le texte du bouton "J'ai de la chance"

Utilisons un moyen plus pratique, la console Firebug peut nous aider à trouver l'id de cet élément...

Modifier le texte du bouton "J'ai de la chance"

On va juste utiliser getElementById() :

>>> var b = document.getElementById('gbqfsb');
>>> b.innerHTML="Je pue des pieds";
    "Je pue des pieds"
    

Pas mal non ?

Faire clignoter le texte de ce bouton !

function toggle(){
  var st= document.getElementById('gbqfsb').style;
  st.visibility = (st.visibility === 'hidden') ? 'visible': 'hidden';
}
>>> var myint = setInterval(toggle, 1000);
>>> clearInterval(myint);
    

Ajout de nouveaux noeuds dans le DOM

Principe : on créée des noeuds avec createElement() ou createTextNode() et on les ajoute à un noeud de l'arbre avec appendChild()

Création d'un paragraphe :

>>> var myp = document.createElement('p');
>>> myp.innerHTML = 'yet another';
       "yet another"
>>> myp.style
       CSSStyleDeclaration length=0
>>> myp.style.border = '2px dotted blue'
       "2px dotted blue"
>>> document.body.appendChild(myp)
     <p style="border: 2px dotted blue;">
    

Appel de la fonction :

>>> var result = somme(1, 2);
>>> result;
       3
    

Page avec l'ajout de ce paragraphe...

Autres fonctions utiles

cloneNode(booleen) copie un noeud. Si on l'appelle avec le paramètre true, c'est une copie de tout le sous-arbre qui est effectuée (deep copy).

insertBefore(noeudAInserer, frere) permet d'insérer un noeud juste avant un autre. Le noeud inséré sera un frère de celui passé en paramètre. appenChild() n'insère qu'à la fin, en tant que dernier fils.

Exemple d'insertion en tant que premier fils :

document.body.insertBefore(
  document.createTextNode('boo!'),
  document.body.firstChild
);
    

Supprimer des noeuds avec removeChild()

<body>
<p class="opener">first paragraph</p>
<p><em>second</em> paragraph</p>
<p id="closer">final</p>
<!-- and that's about it -->
</body>
    

Supprimons le second paragraphe :

>>> var myp = document.getElementsByTagName('p')[1];
>>> var removed = document.body.removeChild(myp);
>>> removed
    <p>
>>> removed.firstChild
    <em>
    

Remplacer un noeud avec replaceChild()

Le document sans le second paragraphe ressemble à ceci :

<body>
<p class="opener">first paragraph</p>
<p id="closer">final</p>
<!-- and that's about it -->
</body>
    

Remplaçons le second paragraphe par celui qu'on avait enlevé :

>>> var p = document.getElementsByTagName('p')[1];
>>> p
       <p id="closer">
>>> var replaced = document.body.replaceChild(removed, p);
>>> replaced
       <p id="closer">
    

replaceChild() suite...()

Le document ressemble à ceci maintenant :

<body>
<p class="opener">first paragraph</p>
<p><em>second</em> paragraph</p>
<!-- and that's about it -->
</body>
    

Supprimer tout un sous-arbre du DOM

Avec innerHTML :

>>> document.body.innerHTML = '';
       ""
>>> document.body.firstChild
       null
    

Avec les fonctions classiques :

function removeAll(n) {
  while (n.firstChild) {
    n.removeChild(n.firstChild);
  }
}
>>> removeAll(document.body);
    

Objets du DOM propres à HTML

Nous avons vu jusqu'à présent des objets propres au DOM, quel que soit le langage (XML ou XHTML). Maintenant nous allons étudier les objets propres à HTML.

Collections spécialisées :

  • document.images : équivalent de document.getElementsByTagName('img');
  • document.applets : équivalent de document.getElementsByTagName('applets');
  • document.links, document.anchors, documents.forms

Collections spécialisées, suite...

document.links est la collection de tous les <a href="...">...</a>

document.anchors est la liste de tous les <a name="...">...</a>

document.forms contient l'ensemble des formulaires de la page.

>>> document.forms[0]
>>> // identique à
>>> document.getElementsByTagName('forms')[0]
>>> // premier <input> du premier formulaire
>>> >>> document.forms[0].elements[0]
    

Manipulation de formulaires, suite...

Imaginons le premier champ <input> du premier formulaire :

<input name="email" id="email" type="text" size="50"
       maxlength="255" value="Entrer votre email..." />
    

On peut changer la valeur qu'il contient :

>>> document.forms[0].elements[0].value = 'toto@toto.com'
       "toto@toto.com"
>>> document.forms[0].elements.email.value = 'toto@toto.com'
    

Dans le dernier cas on a utilisé la valeur de l'attribut name.

Ou le rendre grisé / disabled :

>>> document.forms[0].elements.email.disabled = true;
    

document.write() pour insérer dynamiquement des valeurs dans le document au rendu

<p>It is now <script>document.write("<em>" + new Date()
        + "</em>");</script></p>
    

Est comme si on avait entré la date dans le texte :

<p>Nous sommes le <em>Wed May 09 2012 22:04:04 GMT-0800</em></p>
    

Il existe document.write() et document.writeln() :

>>> document.write('boo!\n');
>>> document.writeln('boo!');        

Cookies, Title, Referrer, Domain

document.cookies contient les cookies que le navigateur a positionnées lors du chargement de la page, et qu'il renverra au serveur en cas de requête HTTP

C'est la valeur qu'on retrouvera dans la partie Set-Cookie du header HTTP.

Supposons qu'on va sur CNN.com et qu'on exécute ceci dans la console Firebug :

>>> document.cookie
       "CNNid=Ga50a0c6f-14404-1198821758-6; SelectedEdition=www;
        s_sess=%20s_dslv%...
    

On peut modifier la valeur de document.cookies, c'est cette nouvelle valeur qui sera renvoyée au serveur.

document.title

Utile pour changer le titre de la page courante

>>> document.title = 'My title'
       "My title"
    

Note : ne change pas la valeur du noeud <title> dans le DOM, juste la barre de titre.

document.referrer

Contient l'URL de la page qui a amené à la page courante

Par exemple si on va sur google.fr, que l'on fait une recherche sur "Michel Buffa", que l'on clique sur le premier lien, et qu'on ouvre alors la console Firebug :

>>> document.referrer
"http://www.google.fr/url?sa=t&rct=j&q=michel%20buffa&source=web&cd=1&..."
    

Cela donne l'URL de la page de résultats de recherche de Google...

document.domain

Donne le domaine de la page courante.

Utile pour ce qu'on appelle "la relaxation de domaines"

Imaginons la page www.yahoo.com qui contient une iframe sur music.yahoo.com

  • Pour des raisons de sécurité, la fenêtre ne pourra pas communiquer avec l'iframe.
  • On pourra résoudre de problème en mettant document.domain pour l'iframe et pour la page égal à yahoo.com
  • Limitation : on ne peut changer que pour un domaine moins spécifique.
>>> document.domain
       "www.yahoo.com"
>>> document.domain = 'yahoo.com'
      "yahoo.com"
>>> document.domain = 'www.yahoo.com'
      Illegal document.domain value" code: "1009
>>> document.domain = 'www.example.org'
      Illegal document.domain value" code: "1009
    

Gestion des événements !

Nous allons voir comment gérer les principaux événements


Exemples :
  • L'utilisateur clique sur un bouton,
  • L'utilisateur a entré un caractère dans un champs de saisie,
  • La page vient d'être chargée, etc.

Le principe : attacher une fonction appelée un callback, un écouteur d'évènement, un event handler en anglais, à un ou plusieurs événements, et cette fonction sera appelée automatiquement lorsque l'événement arrive.

Exemple le plus simple : attribut spécialisé des tags HTML

<div onclick="alert('Ouch!')">click</div>
    

Ici, lorsqu'on clique sur le mot "click", le code JavaScript valeur de l'attribut onClick est exécuté.

Un callback anonyme est en fait créé en coulisses.

Il existe de nomnbreux attributs pour les différents types d'événements, nous les verrons un peu plus loin...

Autre possibilité : utiliser des propriétés des éléments du DOM :

<div id="my-div">click</div>

<script>
    var myelement = document.getElementById('my-div');
    myelement.onclick = function() {
        alert('Aie!');
        alert('Ouille ! Ouille ! Ouille !');
    }
</script>
    

Cet exemple est plus intéressant : on ne mélange pas HTML et JavaScript sur la même ligne.


HTML : contenu,

JavaScript : comportement,

CSS : présentation

Autre possibilité : DOM event listeners

Le dernier exemple a un défaut cependant, on ne peut mettre qu'un seul écouteur à l'événement. Voici une méthode qui permet de mettre plusieurs écouteurs :

On reprend cette page, qui contient un document déjà utilisé par des exemples précédents :

<!DOCTYPE html>
<html>
<head>
    <title>Ma page</title>
</head>
<body>
<p class="opener">premier paragraphe</p>
<p><em>second</em> paragraphe</p>
<p id="closer">final</p>
<!-- et voilà  -->
</body>
</html>
    

DOM event listeners

>>> var mypara = document.getElementById('closer');
>>> mypara.addEventListener('click', function()
                             {alert('Boo!')}, false);
>>> mypara.addEventListener('click', console.log, false);
    

Le deuxième écouteur exécute console.log et lui passe en paramètre l'événement lui-même.

Allez sur la page, ouvre Firebug, exécutez le code ci-dessus et cliquez sur le mot "final"...

Suite de l'exemple...

Si on clique sur l'événement...

addEventListerner() et le troisième paramètre "false" ?

Ah ! Vous avez remarqué ! Il sert à indiquer la manière dont l'événement se propage.

Voyons avec un exemple :

<body>
<ul>
    <li><a href="http://toto.com">mon blog</a></li>
</ul>
</body>
    

Quand on clique le lien, on clique aussi le <li>, le <ul>, le <body> et éventuellement le document lui-même...

Propagation des événements

Et oui, ces petites bêtes se propagent...

Dans les navigateurs, cela s'implémente de deux manières différentes :

  1. Event capturing : le clic est pris par le document, puis se propage au body, au ul, au li, et au lien...
  2. Event bubbling : c'est l'inverse ! Il remonte de a vers le li, vers le ul etc. comme une bulle dans l'eau!

La spécification des événements dans DOM level 2 dit que l'événement doit se propager en trois phases : 1) être capturé au niveau du document, 2) descendre jusqu'à la cible (le a) et 3) bubbler (remonter) à nouveau jusqu'au document

Phases de propagation du DOM Level 2

Firefox, Opera, et Safari implémentent les trois phases, mais IE uniquement le bubbling, ce serait trop beau...

Bon, alors, ce troisième paramètre ?

En le mettant à false on indique qu'on ne veut qu'une propagation de type "bubbling", donc qui marche de la même manière dans tous les navigateurs.

On peut stopper la propagation en appelant stopPropagation() sur l'évènement.

Si on a dix boutons dans un div, on peut mettre un seul écouteur sur le div et tester event.target pour voir quel bouton a été cliqué.

Exemple de propagation et d'arrêt de propagation

Nous travaillons toujours sur la même page avec "closer" etc...

>>> function paraHandler(){alert('clicked paragraph');}
        
>>> var para = document.getElementById('closer');

>>> para.addEventListener('click', paraHandler, false);

>>> document.body.addEventListener('click', function(){alert
                                    ('clicked body')}, false);
>>> document.addEventListener('click', function(){alert
                               ('clicked doc')}, false);
>>> window.addEventListener('click', function(){alert
                             ('clicked window')}, false);
    

Exemple, suite...

Si on clique sur le paragraphe dont l'id vaut "closer", ceci apparait dans Firebug :

clicked paragraph
clicked body
clicked doc
clicked window
    

C'est l'illustration du bubbling ! L'événement "remonte" dans le DOM...

Exemple, suite...

Si on modifie le code de l'écouteur pour le paragraphe :

function paraHandler(e){
  alert('clicked paragraph');
  e.stopPropagation();
}
    

Et qu'on re-exécute, la propagation a disparu !

clicked paragraph
    

Supprimer le comportement par défaut du navigateur pour certains événements

Dans certains cas, le navigateur implémente déjà un écouteur pour certains événements sur certains noeuds du DOM, par exemple, quand on clique sur un lien dans une page, le navigateur charge la page pointée par le lien.

On peut modifier ce comportement. Regardons cet exemple (à tester sur une page de résultats de Google par exemple) :

var all_links = document.getElementsByTagName('a');
for (var i = 0; i < all_links.length; i++) {
  all_links[i].addEventListener(
    'click',
    function(e){ // écouteur
      if (!confirm('Etes vous sûr de vouloir suivre ce lien ?')){
        e.preventDefault();
      }
    },
    false); // que du bubbling
}
    

Types d'événements

Souris

  • mouseup, mousedown, click, dblclick
  • mouseover, mousemove

Clavier

  • keydown, keypress, keyup

Fenêtre / Chargement

  • load (une page ou une image est entièrement chargée), unload (on quitte la page), beforeunload

Formulaires

  • focus (s'applique à un champ de formulaire), blur (quand on quitte un champ de formulaire)
  • change (on quitte un champs dont le contenu a changé), select (de texte dans un champ)
  • reset, submit

XMLHttpRequest (XHR en abrégé)

C'est un objet qui permet d'envoyer des requêtes HTTP en tâche de fond, depuis du code JavaScript.

C'est un objet natif dans les navigateurs.

Avec l'API du DOM, c'est l'autre composante fondamentale d'AJAX :

AJAX signifie Asynchronous JAvaScript XML

Le mot clé important ici est "Asynchronous" ! Le code JS de la page continue de s'exécuter une fois la requête envoyée.

Principe d'AJAX : 1) on envoie la requête en passant un écouteur pour la réponse (un callback), 2) Le callback traite la réponse et peut modifier le DOM pour mettre à jour la page.

Envoi d'une requête XHR

// création de la requête
var xhr = new XMLHttpRequest();
// ajout d'un callback pour la réponse
xhr.onreadystatechange = traiteReponse;
// spécification de l'URL à appeler, de la méthode...
xhr.open('GET', url, true);
// envoi de la requête
xhr.send('');
    

Le troisème paramètre de xhr.open indique si on envoie la requête en asyncrhone (true) ou pas (false)

Traitement de la réponse

On a attaché le callback de la réponse à l'événement onreadystatechange, de quoi s'agit-il ?

La requête XHR a un statut qui peut valoir plusieurs valeurs. A chaque changement, le callback est appelé.

  • 0 : uninitialized
  • 1 : loading
  • 2 : loaded
  • 3 : interactive
  • 4 : complete

C'est évidemment la valeur 4 qui nous intéresse !

Traitement de la réponse, suite...

Il faut également tester le statut de la réponse afin d'être sûr que tout va bien. Un statut=404 ce n'est pas bon par exemple, alors qu'un statut=200 qui signifie Ok, est bon.

function traiteReponse() {
  if (xhr.readyState < 4) {
    return; // On ne fait rien, la réponse n'est pas revenue
  }
  if (xhr.status !== 200) {
    alert('Error!'); // erreur HTTP
    return;
  }
  //  Tout va bien, la réponse est arrivée, on l'affiche
  alert(xhr.responseText);
}
    

Traitement de la réponse, suite...

Dans l'exemple précédent on a juste affiché la réponse dans un alert() mais le plus souvent on va modifier le DOM de la page avec une des méthodes vues dans ce chapitre (avec innerHTML=... par exemple) ou à l'aide de bibliothèques de plus haut niveau comme jQuery.

Le format le plus populaire pour les réponses a longtemps été XML (le "X" de Ajax) mais il est supplanté aujourd'hui par le format JSON (JavaScript Object Notation), que nous verrons un peu plus loin.

La plupart du temps on fait de l'AJAX à l'aide d'une bibliothèque JavaScript simplifiant le travail, comme jQuery, Dojo, Prototype, etc Voyons néanmoins les points délicats qui sont traités en coulisses par ces librairies.

JavaScript est asynchrone !

Avec l'exemple précédent que se passe-t-il si on envoie plusieurs requêtes en même temps ?

Le callback référence un opbjet xhr qui est une variable globale et teste son statut, son readystatechange, etc...

Ca ne peut pas marcher !!!

Une solution classique consiste à mettre le callback dans une closure (voir cours sur les bases de JavaScript) :

var xhr = new XMLHttpRequest();

// Le callback est une fonction retournée par
// la fonction auto-appelante, c'est une closure !
xhr.onreadystatechange = (function(myxhr){
   function(){myCallback(myxhr);}
})(xhr);

xhr.open('GET', url, true);
xhr.send('');
    

Utiliser une bibliothèque de haut niveau ?

Il reste de nombreuses choses à dire sur AJAX, notamment comment gérer les différents types de requête HTTP (on a vu GET mais il y a POST, PUT, DELETE, etc.), les types Mime (comment envoyer un fichier ?), l'encodage/décodage des données échangées (souvent en JSON), etc.

Les bibliothèques les plus populaires simplifient ce travail. En cours cette année nous allons voir jQuery, et l'an prochain Dojo.