Attributs et propriétés DOM

Comment reproduire l'association attribut / propriété existant dans un objet DOM pour un attribut personnalisé.

Présentation

Nous prendrons un élément quelconque, un paragraphe par exemple, sur lequel nous souhaitons définir un nouvel attribut « foo » que l'on puisse manipuler comme un attribut natif.

<p id="target">Lorem ipsum</p>
Elément HTML dont on veut personnaliser les attributs.
var target = document.getElementById('target');
Référence DOM de l'élément HTML.

Edition d'un attribut DOM existant

La modification d'un attribut défini dans les spécifications DOM, par exemple « title », peut se faire :

  • Par la méthode dédiée setAttribute.
  • A travers la propriété de l'objet DOM portant le même nom.
target.setAttribute('title', 'Hello World');

target.title = 'Hello World';
Changements de l'attribut « title ».

Récupérer ensuite la valeur de l'attribut peut se faire de 3 manières :

  • Par la méthode getAttribute.
  • En interrogeant l'objet attributes.
  • En lisant la propriété de l'objet associée à l'attribut.

Quelque soit la façon dont on aura enregistré l'attribut, le résultat de la lecture sera le même :

// Lectures de l'attribut
target.getAttribute('title'); // => Hello World
target.attributes.title.value; // => Hello World

// Lecture de la propriété
target.title; // => Hello World
Lecture de la valeur de l'attribut.

Il faut noter que ces 3 lectures ne sont pas équivalentes :

  • Si la méthode getAttribute et l'interrogation de l'objet attributes rendent le même résultat pour un attribut défini, la méthode renverra null si l'attribut n'existe pas alors que l'introspection de l'objet renverra undefined et émettra évidemment une erreur si on essaye de lire sa propriété value.
  • La lecture de la propriété DOM ne renvoit pas forcément la même chose que celle de l'attribut. Cela peut être le cas pour certains attributs string comme le href des liens mais surtout pour les attributs booléens. Dans ce dernier cas, la propriété est un booléen tandis que la méthode getAttribute renverra toujours une chaîne de caractères (éventuellement vide) ou null.

Edition d'un attribut ajouté

Essayons maintenant d'enregistrer puis de lire notre attribut « foo » par la méthode setAttribute :

target.setAttribute('foo', 'Hello World');
Définition d'un nouvel attribut « foo ».
// Lectures de l'attribut
target.getAttribute('foo'); // => Hello World
target.attributes.foo.value; // => Hello World

// Lecture de la propriété
target.foo; // => undefined
Lecture du nouvel attribut.

Que constatons-nous ? Que l'API qui interroge la liste des attributs répond correctement mais que la propriété de l'objet qui porte le même nom que l'attribut est indéfini. Et si on enregistrait celle-ci ?

target.foo = 'Hello World';
Définition de la propriété « foo ».
// Lectures de l'attribut
target.getAttribute('foo'); // => null
target.attributes.foo.value; // => TypeError

// Lecture de la propriété
target.foo; // => Hello World
Lecture du nouvel attribut.

Maintenant c'est l'inverse : la propriété est bien définie mais pas l'attribut portant le même nom.

Tout cela est logique : pour des attributs prédéfinis par les spécifications DOM, les navigateurs web définissent des associations entre ces attributs et une propriété de l'objet portant le même nom. Quand nous définissons nous-même l'attribut, l'association n'est pas créé, et si nous définissons la propriété, elle n'est qu'un membre « normal » de l'objet et en aucun cas la représentation d'un attribut.

Associer une propriété à un attribut

Pour associer une propriété de l'objet DOM à un attribut portant le même nom, il faut définir la propriété à l'aide d'un descripteur accesseur/mutateur où le getter et le setter pointeront vers la liste des attributs :

Object.defineProperty(target, 'foo', {
    enumerable: true,
    configurable: false,
    get: function () {
        return this.getAttribute('foo');
    },
    set: function (newValue) {
        this.setAttribute('foo', newValue);
    }
});
Associer une propriété « foo » à un attribut « foo » dans un objet DOM.

Pour un attribut de type booléen la définition sera légèrement différente car c'est la présence ou l'absence de l'attribut qui définit sa valeur :

Object.defineProperty(target, 'foo', {
    enumerable: true,
    configurable: false,
    get: function () {
        return this.getAttribute('foo') !== null;
    },
    set: function (newValue) {
        if (newValue) {
            this.setAttribute('foo', 'foo');
        } else {
            this.removeAttribute('foo');
        }
    }
});
Associer une propriété « foo » à un attribut booléen « foo » dans un objet DOM.

Cette fois-ci nous aurons un comportement avec un nouvel attribut identique à celui associé à un attribut « natif » du DOM.

Conclusion

La technique est évidemment à manipuler avec précaution afin d'éviter de définir des attributs qui seraient en conflit avec des attributs ou des méthodes DOM existants. Elle devrait aussi être limitée à des comportements gérés par des attributs définis dans les spécifications DOM et pour des navigateurs web qui n'implémentent pas le dit comportement.

Ressources et références

STENBACK, Johnny ; LE HÉGARET, Philippe ; LE HORS, Arnaud. Document Object Model (DOM) Level 2 HTML Specification. W3C, . 1.6.1. Property Attributes

LE HORS, Arnaud ; LE HÉGARET, Philippe ; WOOD, Lauren, et al.. Document Object Model (DOM) Level 2 Core Specification. W3C, . Interface Attr

VAN KESTEREN, Anne ; GREGOR, Aryeh ; HUNT, Lachlan, et al.. DOM4. W3C, . 5.8.1 Interface Attr

VAN KESTEREN, Anne ; GREGOR, Aryeh ; HUNT, Lachlan, et al.. DOM4. W3C, . 5.3 Mutation observers

HICKSON, Ian ; BERJON, Robin ; FAULKNER, Steve, et al.. HTML 5. W3C, . 2.7.1 Reflecting content attributes in IDL attributes

Object.defineProperty. Mozilla Developer Network,