Custom Element: meter

Introduction à l'utilisation du nouvel élément HTML 5 meter et à la manière de lui assurer un rendu sur les navigateurs qui ne le supportent pas.

Présentation

Les spécifications

The meter element represents a scalar measurement within a known range, or a fractional value; for example disk usage, the relevance of a query result, or the fraction of a voting population to have selected a particular candidate.

ref-html5-meter

L'élément meter devrait être utilisé quand nous avons une mesure qui peut prendre une valeur dans un intervalle défini. Autrement dit : « une jauge ». Nous disposons de deux séries de propriétés pour caractériser cette mesure :

Propriétés pour gérer l'intervalle de valeurs
Propriétés Fonctions
min Valeur minimale qui peut prendre la mesure (0 par défaut).
max Valeur maximale qui peut prendre la mesure (1 par défaut).
value Valeur courante.
Propriétés pour définir les zones basse, médiane et haute de l'intervalle de valeurs
Propriétés Fonctions
low Valeur supérieure de la zone basse (min par défaut).
high Valeur inférieure de la zone haute (max par défaut).
optimum Valeur optimum (médiane entre min et max par défaut).

Ces propriétés sont des nombres flottants qui doivent obéir aux règles suivantes :

  • minvaluemax
  • minlowmax
  • minhighmax
  • minoptimummax
  • lowhigh
Règles arithmétiques liant les propriétés de meter.

Images

Représentation graphique des propriétés de l'élément meter.

En fonction de la position de la valeur optimum, les zones délimitées par les valeurs de low et high seront définies comme « optimale », « sub-optimale » ou « encore moins bonne ».

Images

Rprésentation graphiques des zones d'un l'élément meter.

Il aurait peut-être été plus judicieux de définir la valeur par défaut de optimum comme la valeur médiane de la zone délimitée par low et high et non pas celle délimitée par min et max.

Rendu par défaut

<meter min="0" max="100" low="25" high="80" value="40">40%</meter>
Code HTML d'un élément meter.

Pour illustrer les différents états de la jauge, créons une série d'éléments pour lesquels nous ferons varier la valeur et/ou l'optimum :

Value: 40 (low: 25 ; high: 80; optimum: 50)
40%
Value: 20 (low: 25 ; high: 80; optimum: 50)
20%
Value: 90 (low: 25 ; high: 80; optimum: 50)
90%
Value: 90 (low: 25 ; high: 80; optimum: 20)
90%
Rendu d'élément meter.

Si vous affichez cette page avec Chromium ≥ 17, Opera ≥ 11 ou Firefox ≥ 16, vous verrez une jauge graphique en lieu et place du texte des éléments meter ; une jauge verte pour les valeurs dans la zone optimale, jaune pour celles se trouvant dans une zone sub-optimale et rouge si elles sont dans une zone encore moins bonne. Seul le texte est visible avec les autres navigateurs.

Un élément plus riche

Une première évolution de l'élément meter en 2012 a consisté à suppléer les lacunes de certains navigateurs web en injectant du code quand le rendu graphique le nécessitait et en développant un modèle de données permettant de gérer un intervalle de valeurs. Une deuxième implémentation abordait les choses différemment en décorant l'élément meter par un élément span qui héritait de ses propriétés afin (1) d'harmoniser le rendu graphique et (2) de gérer une propriété qui n'existe pas encore : l'orientation. L'implémentation actuelle reprend ces mêmes idées mais sous la forme d'un Custom Element.

Le code

Code HTML et javascript

<rnb-meter value="3" min="0" max="5">3/5</rnb-meter>
Code HTML
/**
 *  Flag pour l'état optimum.
 *  @private
 *  @type {String}
 */
const OPTIMUM = 'optimum-value';

/**
 *  Flag pour l'état sub-optimum.
 *  @private
 *  @type {String}
 */
const SUB_OPTIMUM = 'sub-optimum-value';

/**
 *  Flag pour l'état even-less-good
 *  @private
 *  @type {String}
 */
const EVEN_LESS_GOOD = 'even-less-good-value';

/**
 *  Regex pour identifier la classe définissant l'état optimum
 *  @private
 *  @type {RegExp}
 */
const RE_STATE = `/${OPTIMUM}|${SUB_OPTIMUM}|${EVEN_LESS_GOOD}/`;

const OBSERVER_CONFIG = {
    attributes: true
};

const ORIENTATION_ATTRIBUTE = 'aria-orientation';
const ORIENTATION_HORIZONTAL = 'horizontal';
const ORIENTATION_VERTICAL = 'vertical';

class Meter extends HTMLElement {

    constructor () {
        super();

        this.meter = document.createElement('meter');
        this.meter.min = this.getAttribute('min') || 0;
        this.meter.max = this.getAttribute('max') || 1;
        this.meter.low = this.getAttribute('low') || this.min;
        this.meter.high = this.getAttribute('high') || this.max;
        this.meter.value = this.getAttribute('value') || 0;

        this.observer = new MutationObserver(() => this.render());
        this.observer.observe(this.meter, OBSERVER_CONFIG);

        const content = this.textContent;
        const shadow = this.attachShadow({'mode': 'open'});

        this.textContent = '';

        // styling
        const style = document.createElement('style');
        style.textContent = `
            :host {
                position: relative;
                display: inline-block;
                border: 1px solid rgba(0,0,0,0.2);
                border-radius: 0;
                vertical-align: middle;
                padding: 0;
                width: 10rem;
                height: 1rem;
                background: rgba(0,0,0,0.2);
                font-size: 0;
                line-height: 1rem;
            }
            :host([aria-orientation=vertical]) {
                height: 10rem;
                width: 1rem;
            }
            :host > span {
                display: block;
                position: absolute;
                bottom: 0;
                left: 0;
                background-color: var(--rnb-meter-value-background, #77aa33);
                height: 100%;
            }
            :host([aria-orientation=vertical]) > span {
                width: 100%;
            }
        `;

        shadow.appendChild(style);

        // value
        this.valueElement = document.createElement('span');
        this.valueElement.textContent = content;
        shadow.appendChild(this.valueElement);

        this.render();
    }
    
    get value () {
        return this.meter.value;
    }

    set value (newValue) {
        this.meter.value = newValue;
    }

    get min () {
        return this.meter.min;
    }

    set min (newValue) {
        this.meter.min = newValue;
    }

    get max () {
        return this.meter.max;
    }

    set max (newValue) {
        this.meter.max = newValue;
    }

    get low () {
        return this.meter.low;
    }

    set high (newValue) {
        this.meter.high = newValue;
    }

    get orientation () {
        return this.getAttribute(ORIENTATION_ATTRIBUTE) || ORIENTATION_HORIZONTAL;
    }
    set orientation (newValue) {
        if (newValue !== ORIENTATION_HORIZONTAL && newValue !== ORIENTATION_VERTICAL ||
                newValue === this.orientation) {
            return;
        }
        this.setAttribute(ORIENTATION_ATTRIBUTE, newValue);
        this.render();
    }

    render () {
        let value = this.value,
            cls = OPTIMUM,
            optimum = this.optimum,
            low = this.low,
            high = this.high,
            current = this.valueElement.className.match(RE_STATE),
            sizeValue = (value / (this.max - this.min)) * 100,
            sizeRule = 'width',
            oppositeRule = 'height';
        
        if (this.orientation === 'vertical') {
            sizeRule = 'height';
            oppositeRule = 'width';
        }

        // Taille de l'élément valeur.
        this.valueElement.style[sizeRule] = sizeValue + '%';
        this.valueElement.style[oppositeRule] = '';
        // gestion du look en fonction de la position de la valeur
        if (optimum >= low && optimum <= high) {
            if (value < low || value > high) {
                cls = SUB_OPTIMUM;
            }
        } else if (optimum < low) {
            if (value > high) {
                cls = EVEN_LESS_GOOD;
            } else if (value > low) {
                cls = SUB_OPTIMUM;
            }
        } else {
            if (value < low) {
                cls = EVEN_LESS_GOOD;
            } else if (value < high) {
                cls = SUB_OPTIMUM;
            }
        }
        if (!current) {
            this.valueElement.classList.add(cls);
        } else if (current[1] !== cls) {
            this.valueElement.classList.remove(current[1]);
            this.valueElement.classList.add(cls);
        }
    }
}

customElements.define('rnb-meter', Meter);

export {};
Code javascript

Exemples de rendu

  • 40%
  • 20%
  • 90%
  • 90%
Rendu d'éléments meter horizontaux.

40% 20% 90% 90%

Rendu d'éléments meter verticaux.

Quelques remarques

  • Toute l'API liée à la notion de jauge est basée sur un élément meter "interne".
  • La notion d'orientation est portée par l'attribut aria « aria-orientation ». La propriété « orient » n'est pour l'instant pas standardisée.

Pourquoi un <rnb-meter> et pas un <meter is="rnb-meter> ? Parce qu'il semble impossible d'afficher des éléments enfants d'un meter, affichage indispensable pour illustrer la valeur de la jauge.

Autre chose qu'une jauge : notation

L'élément meter est rendu par défaut sous forme de jauge mais il peut très bien convenir à un système de notation sous forme d'étoiles par exemple grâce à des images de fond.

<meter class="rating" min="0" max="5" value="3">3/5</meter>
Code HTML d'un élément meter utilisé pour une notation (3/5).

3/5

Rendu de l'élement meter utilisé pour une notation.

Capture d'écran

Affichage d'un élément meter utilisé pour la notation sous Firefox.
/*

Rating
=====================

La classe rating permet de remplacer le contenu de l'élément portant cette classe
par une série d'étoiles. Le rendu particulier pour chaque notation doit être
géré dynamiquement en javascript.

----
°°stx-html°°
    <meter class="rating" value="3" min="0" max="5">3/5</meter>
-----
Code HTML pur.

----
°°stx-html°°
    <span class="meter rating">
        <span class="value">3/5</span>
        <meter value="3" min="0" max="5" aria-hidden="true">3/5</meter>
    </span>
-----
Code HTML géré par javascript.

*/

.rating {
    line-height: 1rem;
    background: url(../img/star-16.png) repeat-x 0 0;
    height: 1rem;
    width: 5rem;
    border-width: 0;
}

.rating > meter[max='5'] {
    width: 5rem;
}
.rating > meter[max='10'] {
    width: 10rem;
}

.rating::-webkit-meter-bar {
    background: transparent;
}

/* Metre les 2 sélecteurs (webkit et moz) dans un seul set fait planter moz */
.rating::-webkit-meter-even-less-good-value,
.rating::-webkit-meter-optimum-value,
.rating::-webkit-meter-suboptimum-value {
    background: url(../img/star-16.png) repeat-x 0 -16px !important; 
}
.rating > .value,
.rating::-moz-meter-bar { 
    background: url(../img/star-16.png) repeat-x 0 -16px !important; 
}
Code css pour un système de notation.

Ressources et références

Titre
HTML 5
Chapitre
4.10.15 The meter element
Auteurs
Hickson IAN
Auteurs
Berjon ROBIN
Auteurs
Faulkner STEVE
Auteurs
Leithead TRAVIS
Auteurs
Doyle Navara ERIKA
Auteurs
O'Connor EDWARD
Auteurs
Pfeiffer SILVIA
Editeur
W3C
Date
Titre
meter
Editeur
Mozilla Developer Network
Date
Titre
Styling Form Controls Using Pseudo Classes
Éditeur
trac.webkit.org
Date
Titre
HTML : progress
Editeur
Omacronides
Date

Historique

2019-03-06
  • upd publication de la version Custom Element.
2015-07-07
  • upd Refonte complète avec abandon des vieux hacks.
  • add gestion de la propriété orientation.
2012-10-12
  • upd Hack générique pour Opera et Firefox 16.
2012-03-25
  • add Création de l'article.