You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

16 KiB

Table of Contents

[TOC]

Utilisation de l'API JS/DOM en Brython

Brython a un accès complet de l'API JS/DOM via window.

Si vous utilisez components.py, le contenu de window est automatiquement importé dans l'espace global de Brython.

Sinon, vous pouvez soit :

  1. Importer window :

    from browser import window
    
    window.document
    
  2. Importer le contenu de window dans l'espace global de Brython :

    from browser import window
    
    g = globals()
    for x in window.Object.getOwnPropertyNames(window):
    
       if x in g or x.startswith("on"):
           continue
    
       g[x] = getattr(window, x, None)
    

Manipulation du DOM

Sélectionner des éléments HTML

  • Sélectionner un élément à partir d'un sélecteur CSS :

    document.querySelector($CSS_SELECTOR) // retourne un noeud ou undefined.
    
  • Sélectionner tous les éléments à partir d'un sélecteur CSS :

    document.querySelectorAll($CSS_SELECTOR) // retourne une liste
    
  • Sélectionner un élément descendant de $ELEM à partir d'un sélecteur CSS :

    $ELEM.querySelector($CSS_SELECTOR) // retourne un noeud ou undefined.
    
  • Sélectionner un élément ancestre de $ELEM à partir d'un sélecteur CSS :

    $ELEM.closest($CSS_SELECTOR) // retourne un noeud ou undefined.
    
  • Est-ce que $ELEM correspond au sélecteur CSS ?

    $ELEM.matches($CSS_SELECTOR) // retourne un booléen.
    

💡 En Typescript, vous pouvez préciser le type de retour :

document.querySelector<HTMLDivElement>($CSS_SELECTOR) // retourne un DIV

Propriétés d'un élément HTML

  • Classes :

    $ELEM.classList.add($CLASSNAME)
    $ELEM.classList.remove($CLASSNAME)
    $ELEM.classList.toggle($CLASSNAME [, $FORCE]) // "alterner"
    $ELEM.classList.contains($CLASSNAME) // retourne un booléen
    
  • Attributs :

    $ELEM.getAttribute($ATTR_NAME)
    $ELEM.setAttribute($ATTR_NAME, $ATTR_VALUE)
    $ELEM.removeAttribute($ATTR_NAME)
    $ELEM.toggleAttribute($ATTR_NAME [, $FORCE]) // "alterner"
    $ELEM.hasAttribute($ATTR_NAME) // retourne un booléen
    
  • Interface dataset:

    // data-toto-foo ($ATTR_NAME) => totoFoo ($DATA_NAME)
    $ELEM.dataset[$DATA_NAME] // retourne la valeur
    $ELEM.dataset[$DATA_NAME] = $DATA_VALUE
    delete $ELEM.dataset[$DATA_NAME] // [JS] Javascript
    del $ELEM.dataset[$DATA_NAME]    // [🐍] Python
    ($DATA_NAME in $ELEM.dataset) // retourne un booléen
    

💡 Vous pouvez parcourir $ELEM.classList, $ELEM.dataset, et $ELEM.getAttributeNames() :

// [JS] Javascript
for( let name in $ELEM.getAttributeNames() )
    $ELEM.getAttribute(name)
# [🐍] Python
for name in $ELEM.getAttributeNames():
    $ELEM.getAttribute(name)
  • Modifier le contenu (texte) : $ELEM.textContent = $STR

  • Modifier une variable CSS : $ELEM.style.setProperty($CSS_VARNAME, $VALUE)

  • Obtenir la valeur d'une varable CSS: $ELEM.style.getProperty($CSS_VARNAME)

💡 Pour modifier dynamiquement la mise en forme d'un élément via Typescript/Brython, il est recommandé de lui ajouter (ou retirer) une classe CSS.

📖 Plus de méthodes disponibles dans la documentation.

Enfants d'un élément HTML

  • Ajouter un fils à un élément :

    $ELEM.append($CHILD_ELEM)
    
  • Remplacer les fils d'un élément :

    $ELEM.replaceChildren($CHILD_ELEM_1 [,$CHILD_ELEM_2,...] );
    
  • Retirer un élément du DOM (et donc de son parent) :

    $ELEM.remove()
    
  • Obtenir la liste des enfants d'un élément :

    $ELEM.children   // seulement les Element.
    $ELEM.childNodes // tous les Node (noeuds, e.g. texte).
    
  • Obtenir le parent d'un élément :

    $ELEM.parentElement // returne le parent Element ou null.
    

Créer un élément HTML

  • Cloner un élément :

    $ELEM.cloneNode(true) // retourne le clone.
    
  • Créer un élément HTML à partir de son nom :

    document.createElement($TAGNAME) // e.g. "div".
    
  • Créer un élément HTML en l'initialisant :

    // [TS] TypeScript
    let elem = create("div", {
        classes  : ["c1", "c2"],
        listeners: {
            click: (ev) => { ... }
        }
    });
    
    # [🐍] Python
    def clickHandler(ev):
        pass
    
    elem = create("div",
        classes  = ("c1", "c2"),
        listeners= {
            "click": clickHandler
        }
    });
    
  • Créer un élément à partir d'un string (à éviter) :

    // [JS] Javascript
    function str2df(str) {
        const template = document.createElement('template');
        template.innerHTML = str; // en général à éviter
        return template.content;
    }
    function str2elem<T extends Element>(str): T {
        return str2df(str).children[0] as T;
    }
    
    str2df($HTML_STRING)   // retourne un DocumentFragment (hérite Node)
    str2elem<$ELEM_TYPE>($HTML_STRING) // retourne un $ELEM_TYPE (hérite Element)
    
    # [🐍] Python
    def str2df(str):
        const template = document.createElement('template');
        template.innerHTML = str; # en général à éviter
        return template.content;
    
    def str2elem(str):
        return str2df(str).children[0];
    
    str2df($HTML_STRING)   # retourne un DocumentFragment (hérite Node)
    str2elem($HTML_STRING) # retourne un Element
    

📖 Plus d'informations dans la documentation.

Implémentations de create()

// [TS] TypeScript
type CreateOpts = Partial<{
      content    : string|Element|readonly Element[],
      id         : string,
      classes    : readonly string[],
      attrs      : readonly Record<string, string|boolean>,
      dataset    : readonly Record<string, string|boolean>,
      cssvars    : readonly Record<string, string>,
      listeners  : readonly Record<string, (ev: Event) => void>
      parent     : Element
}>;
function create<T extends keyof HTMLElementTagNameMap>(tagname: T,
                      {
                          content     = [],
                          id          = null,
                          classes     = [],
                          attrs       = {},
                          dataset     = [],
                          cssvars     = {},
                          listeners   = {},
                          parent      = null
                      } = {}): HTMLElementTagNameMap[T] {

  let elem = document.createElement(tagname) as HTMLElementTagNameMap[T];

  if( ! Array.isArray(content) )
    content = [content];
  elem.replaceChildren(...content);

  if( id !== null )
      elem.id = id;

  elem.classList.add(...classes);

  for(let name in attrs) {
    const value = attrs[name];
    if( typeof value === "boolean")
      elem.toggleAttribute(name, value);
    else
      elem.setAttribute(name, value);
  }
      

  for(let name in dataset) {
      const value = dataset[name];
      if( value === true )
        elem.dataset[dataname] = "";
      else if(value === false)
        delete elem.dataset[name];
      else
        elem.dataset[dataname] = value;
  }

  for(let cssvarname in cssvars)
      elem.style.setProperty(`--${cssvarname}`, cssvars[cssvarname])

  for(let event in listeners)
      elem.addEventListener(event, listeners[event]);

  if( parent !== null )
      parent.append(elem);

  return elem;
}
# [🐍] Python
def create( tagname,
            *, # force kw args
            children    = None,
            textContent = None,
            id          = None,
            classes     = (),
            attrs       = {},
            cssvars     = {},
            listeners   = {},
            parent      = None ) {

  elem = document.createElement(tagname);

  if children is not None:
      elem.replaceChildren(*children);

  if textContent is not None:
      elem.textContent = textContent;

  if id is not None:
      elem.id = id;

  elem.classList.add(*classes);

  for attrname in attrs:
      elem.setAttribute(attrname, attrs[attrname]);

  for dataname in dataset:
      elem.dataset[dataname] = dataset[dataname];

  for cssvarname in cssvars:
      elem.style.setProperty(f"--{cssvarname}", cssvars[cssvarname])

  for event in listeners:
      elem.addEventListener(event, listeners[event]);

  if parent is not None:
      parent.append(elem);

  return elem;
}

Événements

Bubble vs Capture

Dans les navigateurs, les interactions sont gérées via des événements, qui se composent de 2 phases :

  • capture

  • bubble

Capture : La phase de capture est descendante, elle part du noeud racine et descend vers le noeud cible (target) final. Par exemple, lorsque vous cliquez sur un élément, l'événement de clic va d'abord partir du document, puis par le body. Le navigateur va propager l'événement en se demandant quel fils de l'élément courant est la cible de l'événement, afin de lui transmettre l'événement, puis recommence de manière récursive, l'élément fils cible devenant l'élément courant.

Bubble : La phase de buble est montante, elle part du noeud cible (target) final et remonte vers le noeud racine. Si l'événement se propage dans le DOM (bubble: true), le navigateur va transmettre l'événement à l'élément père de l'élément courant, puis recommence de manière récursive, l'élément père devant l'élément courant.

Écouter un événement

// [JS] Javascript

function handler(ev) {
    // ev.currentTarget : $ELEM
    // ev.target : source of the event.
    // ev.type   : $EVENT_TYPE
    // ev.detail : $EVENT_DATA (si CustomEvent, cf plus bas)
    // ev.preventDefault() : annuler l'action par défaut du navigateur
    // e.g. lorsqu'on clique sur un lien, empêcher d'aller vers la page.
    // ev.stopImmediatePropagation() : ne pas appeler les autres
    // handleurs pour cet événement.
}

$ELEM.addEventListener($EVENT_TYPE, handler);
// ou
$ELEM.addEventListener($EVENT_TYPE, ev => ... );
# [🐍] Python

def handler(ev):
    # ev.currentTarget : $ELEM
    # ev.target : source of the event.
    # ev.type   : $EVENT_TYPE
    # ev.detail : $EVENT_DATA (si CustomEvent, cf plus bas)
    # ev.preventDefault() : annuler l'action par défaut du navigateur
    # e.g. lorsqu'on clique sur un lien, empêcher d'aller vers la page.
    # ev.stopImmediatePropagation() : ne pas appeler les autres
    # handleurs pour cet événement.

$ELEM.addEventListener($EVENT_TYPE, handler)

📖 Plus d'informations dans la documentation.

Écouteur délégué

Il arrive qu'on souhaite écouter des événements sur les descendants d'un élément, qui peuvent être ajoutés, déplacés, supprimés, etc. Le problème est que cela nécessiterait de créer un écouteur pour chaque éléments et de les supprimer/ajouter à chaque modifications du DOM.

Heureusement, il est possible d'utiliser un écouteur délégué, i.e. d'écouter l'événement sur le descendant (nécessite que l'événement soit bubble, i.e. se propage dans le DOM).

// [JS] Javascript

function handler(ev) {
    if( ev.target.matches($CSS_SELECTOR) ) {
        // ...
    }
    // or
    let target;
    if( target = ev.target.closest($CSS_SELECTOR) ) {
        // ...
    }
}

$ELEM.addEventListener($EVENT_TYPE, handler);
# [🐍] Python

def handler(ev):
    if ev.target.matches($CSS_SELECTOR):
        # ...
    # or
    if target := ev.target.closest($CSS_SELECTOR):
        # ...

$ELEM.addEventListener($EVENT_TYPE, handler)

Créer un événement

// [JS] Javascript

$ELEM.dispatchEvent( new Event($EVENT_TYPE) );
// ou
$ELEM.dispatchEvent( new CustomEvent($EVENT_TYPE, {detail: $EVENT_DATA}) );
# [🐍] Python

$ELEM.dispatchEvent( Event.new($EVENT_TYPE) )
# ou
$ELEM.dispatchEvent( CustomEvent.new($EVENT_TYPE, {"detail": $EVENT_DATA}) )

💡 Vous pouvez aussi ajouter, au 2ème argument, l'option bubble: true pour faire en sorte que l'événement soit bubble, i.e. se propage dans le DOM (par default bubble: false).

Envoyer une requête REST

// [JS] Javascript
async query() {
    const anwser = await fetch($URL);
    // ou
    const answer = await fetch($URL, {method: "POST", body: $PARAMS);

    if( ! answer.ok )
        throw new Error(`${answer.status}: ${answer.statusText}`);

    const json = await answer.json(); // récupérer du JSON
    const text = await answer.text(); // récupérer du texte
    // autres formats possibles dans la doc.
}

query();
# [🐍] Python
async query():
    answer = fetch($URL)
    # ou
    answer = fetch($URL, {"method": "POST", "body": $PARAMS)

    if not answer.ok:
        raise Error(f"{answer.status}: {answer.statusText}");

    json = await answer.json() # récupérer du JSON
    text = await answer.text() # récupérer du texte
    # autres formats possibles dans la doc.


aio.run( query() )

📖 Les requêtes de type "GET" (type par défaut) ont leurs paramètres dans l'URL (cf ci-dessous).

📖 Les requêtes de type "POST" ont leurs paramètres dans le corps de la requête (body), i.e. sont chiffrées en HTTPS. Elles peuvent être écrites sous n'importe quelle format : une chaîne de paramètre (comme GET), du JSON, du texte, etc.

📖 Plus d'informations dans la documentation.

Pour construire la chaîne de paramètre :

// [JS] Javascript

// client
const params = new URLSearchParams();
params.set($NAME, $VALUE)
fetch( `${URL}?${params.toString()}` );

// serveur
const params = new URLSearchParams($STR);
for(let key in params.keys() )
    params.get(key); // retourne undefined si pas trouvé.

params.has($NAME); // retourne un booléen
params.get($NAME) ?? $DEFAULT_VALUE; // avec une valeur par défaut
# [🐍] Python

# client
params = URLSearchParams.new()
params.set($NAME, $VALUE)
fetch( f"{URL}?{params.toString()}" )

# serveur
params = URLSearchParams.new($STR);
for key in params.keys():
    params.get(key) # retourne undefined si pas trouvé.

params.has($NAME) # retourne un booléen

📖 Plus d'informations dans la documentation.

💡 location contient les informations relatives à l'URL de la page actuelle. Vous pouvez ainsi récupérer la chaîne de paramètres via location.search, et l'exploiter en la donnant au constructeur de URLSearchParams.

Stocker des données côté client

Précedemment, les données enregistrées côté client étaient stockées sous la forme de cookies, inclues dans l'en-tête de chaque requêtes HTTP envoyées au serveur. Cette technologie est désormais obsolète au profit de localStorage, sessionStorage, et IndexDB. Contrairement aux cookies, les données enregistrées par un site Web (identifié par son nom de domaine) ne peuvent pas être accedées via d'autres noms de domaines.

LocalStorage/SessionStorage

localStorage.setItem($NAME, JSON.stringify( $VALUE ) );
JSON.parse( localStorage.getItem($NAME) ); // returne null si inexistant
localStorage.removeItem($NAME);

La différence entre localStorage et sessionStorage est que le dernier est unique à chaque onglet du navigateur et sera supprimé lorsque l'onglet sera fermé. Les données stockées par un site Web ne peuvent généralement pas dépasser 10Mo.

📖 Plus d'informations dans la documentation.

IndexDB

L'usage d'indexDB est bien plus complexe que localStorage ou sessionStorage, mais permet de stocker des données plus volumineuses. Vous pouvez le voir comme une base de données stockée côté client.

Son usage étant complexe et peu fréquent, nous ne l'étudierons pas en cours.

📖 Plus d'informations dans la documentation.