Au-delà de console.log, épisode III : les Custom Formatters dans Devtools

Cet article fait partie d’une série de papiers sur la console de Chrome;
Previously on « Beyond console.log »
Vous êtes las de voir vos objets être logués toujours de la même façon dans la console ? Vous aimez le Chrome Dev Tools, mais vous aimeriez bien que le nom des instances de Eleve s’affiche en rouge, orange ou verte selon sa valeur ? Restez sur cette page, vous n’avez pas besoin d’aller plus loin.

Prérequis : activer les Custom Formatters

Dans DevTools, allez dans settings, puis cochez la case “enable custom formatters” dans la rubrique “Console”.

C’est quoi, les Custom Formatters ?

Les Custom formatters sont un tableau d’objet stocké dans l’objet window. Ils permettent de personnaliser à notre guise l’affichage de tel ou tel objet. Ainsi, ils peuvent améliorer grandement la lisibilité de nos logs. Ou mieux : faire saigner les yeux de nos collègues (voir ci-dessous).
capture d'écran de quand on log() un objet avec les custom formatter : ici, les lettres apparaissent en feu !
Malgré leur utilité, je n’ai pas l’impression que ce soit un sujet largement connu, il y a un très faible nombre d’articles sur le sujet sur le net, tous en anglais. Sans plus tarder, réparons cette injustice en mettant les mains dans le cambouis.

L’objectif

  • D’abord, nous ferons en sorte que les instances de notre future class Student affichent leur nom au format prenom + NOM, et celui-ci devra être de couleur rouge à verte selon la moyenne de ses notes.
  • Puis nous ferons en sorte d’avoir un code aisément maintenable

Let’s code

D’abord, codons la classe Eleve :

class Student {
    constructor(firstname, lastname, notations) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.notations = notations;
    }
    getAverageNotation() {
        return this.notations.reduce((total, notation) => total += notation, 0) / this.notations.length;
    }
}

… Puis déclarons notre formateur et ajoutons-le au tableau :

const studentFormatter = {
    header: (x) => {
        if (x instanceof Student) {
            const moyenne = x.getAverageNotation();
            const color = (moyenne > 15) ? 'limegreen' : (moyenne > 5) ? 'orangered' : 'firebrick';
            return [
                'span',
                {
                    style: 'color: ' + color
                },
                x.lastname.toUpperCase() + ' ' + x.firstname
            ];
        }
    },
    hasBody: x => x instanceof Student,
    body: (x) => {
        return [
            'table',
            {},
            ['tr', {},
                ['td', {style: 'color: burlywood'}, 'Prénom: '],
                ['td', {}, `${x.firstname}`],
            ],
            ['tr', {},
                ['td', {style: 'color: burlywood'}, 'Nom: '],
                ['td', {}, `${x.lastname}`],
            ],
            ['tr', {},
                ['td', {style: 'color: burlywood'}, 'Notes: '],
                ['object', {object: x.notes}],
            ],
        ];
    }
};
window.devtoolsFormatters = [ studentFormatter ];

À présent, nous pouvons loguer un tableau d’élèves :

const ducobu = new Student('Élève', 'Ducobu', [2, 5, 3]);
const willHunting = new Student('William', 'Hunt', [18, 20, 19]);
const nicolasLePetit = new Student('Nicolas', 'Le Petit', [10, 10, 10]);
console.info([ducobu, willHunting, nicolasLePetit]);

capture d'écran de devtools quand on logue un objet avec les Custom Formatters

Brève analyse du code

moss-excited
Wahou, c’est magique, le nom des élèves apparaît en capitales et c’est coloré en fonction de la moyenne ! Comment ai-je pu vivre sans jusqu’ici ? Ma vie de programmeur est changée à jamais !
Chaque objet du tableau des custom formatters doit avoir trois méthodes : header, qui doit rendre un objet JSONML qui servira à afficher le nom de l’objet quand il est replié, hasBody, qui indique si l’objet est dépliable et enfin body qui doit rendre le template HTML du corps déplié de l’objet, au format JSONML. Cependant, les seuls éléments HTML autorisés sont table, tr, ts, ol, li, div et span
Ici, je vérifie si x est une instance de Student avec instanceof. Notez que ça renverra true pour toute sous-classe de Student. Si nous souhaitions n’activer le formateur que si l’objet est exactement une instance de Student, nous aurions pu écrire :
if (x.constructor === Student)

Aller plus loiiiiin

Tout ça, c’est vraiment pas mal. Mais dans un grand projet, je vois mal les gens ajouter à la main la classe qu’ils viennent de créer à l’array window.devtoolsFormatters. Ce serait mieux si le CustomFormatter de Student était défini dans la classe elle-même. Mais ça poserait un inconvénient : ça polluerait nos student, ça pourrait provoquer des collisions. C’est avec ce genre d’ennuis en tête que nous allons tirer profit des Symboles :

const CustomFormatterToken = Symbol('CustomFormatter');
// en ajoutant cette propriété à Symbol, ça permet à notre
// fonctionnalité d'être accessible partout :
Symbol.customFormatter = CustomFormatterToken;
// Voici la nouvelle version de la classe Eleve :
class Student {
    constructor(firstname, lastname, notations) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.notations = notations;
    }
    get [CustomFormatterToken]() {
        return {
            header: () => {
                const averageNotation = this.getAverageNotation();
                const color = (moyenne > 15) ? 'limegreen' : (moyenne > 5) ? 'orangered' : 'firebrick';
                return [
                    'span',
                    {
                        style: 'color: '+ color + '}'
                    },
                    this.lastname.toUpperCase() + 'this.firstname'
                ];
            },
            hasBody: () => true,
            body: () => [
                'table',
                {},
                ['tr', {},
                    ['td', {style: 'color: burlywood'}, 'prenom: '],
                    ['td', {}, `${this.firstname}`],
                ],
                ['tr', {},
                    ['td', {style: 'color: burlywood'}, 'nom: '],
                    ['td', {}, `${this.lastname}`],
                ],
                ['tr', {},
                    ['td', {style: 'color: burlywood'}, 'notes: '],
                    ['object', {object: this.notations}],
                ],
            ]
        };
    }
    getAverageNotation() {
        return this.notations.reduce((total, notation) => total += notation, 0) / this.notations.length;
    }
}
// Dorénavant, devtoolsFormatters n'aura qu'une seule valeur : AllPurposeFormatter
const AllPurposeFormatter = {
    header: x => {
        const formatter = x[Symbol.customFormatter];
        if (formatter) {
            return formatter.header();
        }
    },
    hasBody: x => {
        const formatter = x[Symbol.customFormatter];
        if (formatter) {
            return formatter.hasBody();
        }
    },
    body: x => {
        const formatter = x[Symbol.customFormatter];
        if (formatter) {
            return formatter.body();
        }
    },
};
window.devtoolsFormatters = [ AllPurposeFormatter ];
const ducobu = new Student('Élève', 'Ducobu', [2, 5, 3]);
const willHunting = new Student('William', 'Hunt', [18, 20, 19]);
const nicolasLePetit = new Student('Nicolas', 'Le Petit', [10, 10, 10]);
console.info([ducobu, willHunting, nicolasLePetit]);
// les propriétés qui ont pour clé un symbole n'apparaissent pas...
// Notre objet n'est pas pollué \o/
console.log(Object.keys(ducobu)); // => ["firstname", "lastname", "notations"]

formateurs
Voilà, c’est tout pour aujourd’hui. À bientôt pour de nouvelles aventures avec les Chromes Dev Tools !