Beyond Console.log, Episode III: Custom Formatters in Devtools

This article is part of a series of papers on the Chrome Console;
Previously on “Beyond console.log”
Are you tired of seeing your objects always being logged in the same way in the console? You like the Chrome Dev Tools, but you would like the name of the instances of Eleve is displayed in red, orange or green depending on its value? Stay on this page, you don't need to go any further.

Prerequisite: enable Custom Formatters

In DevTools, go to settings, then check the “enable custom formatters” box in the “Console” section.

What are Custom Formatters?

The Custom formatters are an array of object stored in object window. They allow us to customize the display of a particular object as we wish. Thus, they can greatly improve the readability of our logs. Or better : make our colleagues' eyes bleed (see below).
screenshot of when we log() an object with the custom formatters: here, the letters appear on fire!
Despite their usefulness, I don't feel like this is a widely known topic, there are a very small number of articles on the subject on the net, all in English. Without further ado, let's fix this injustice by getting our hands dirty.

The objective

  • First, we will ensure that the instances of our future Student class display their name in the format firstname + NAME, and this one will have to be colored red to green according to the average of its marks.
  • Then we will make sure to have an easily maintainable code

let’s code

First, let's code the Student class:

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;
    }
}

… Then let’s declare our formatter and add it to the array:

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 ];

Now we can log an array of students:

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]);

screenshot of devtools when logging an object with Custom Formatters

Brief code analysis

moss excited
Wow, it's magic, the names of the students appear in capitals and it's colored according to the average! How could I live without so far? My life as a programmer is changed forever!
Each object in the custom formatters array must have three methods: header, which must render a JSONML object which will be used to display the name of the object when it is collapsed, hasBody, which indicates whether the object is unfoldable and finally body which must render the HTML template of the unfolded body of the object, in JSONML format. However, the only allowed HTML elements are table, tr, ts, ol, li, div et chip
Here I check if x is an instance of Student with instanceof. Note that this will return true for any subclass of Student. If we wanted to only enable the formatter if the object is exactly an instance of Student, we could have written:
if (x.constructor === Student)

Go further

All that really isn't bad. But in a large project, I don't see people adding the class they just created to the window.devtoolsFormatters array by hand. It would be better if Student's CustomFormatter was defined in the class itself. But that would pose a drawback: it would pollute our students, it could cause collisions. It is with this kind of trouble in mind that we will take advantage of Symbols:

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"]

trainers
Well, that's all for today. See you soon for new adventures with Chromes Dev Tools!