Using HTML5 Data Attributes

As you might know, HTML5 introduced custom data attributes. These are global attributes that may be set on any HTML element, and their main purpose is to allow for data exchange between HTML and scripts.

In this post, I would like to discuss working with data attributes, especially reading data in your HTML from within a JavaScript context in order to efficiently process it later on—either still in JavaScript, or within some PHP script.

Defining Data in Your HTML

Attaching custom data to an HTML element is as simple as setting an attribute with a value, although there are some restrictions regarding the name of the attribute.

It follows an example button element with an ID attribute and two custom data attributes:

<button id="magic" data-spell="alohomora" data-force="40">Click for Magic</button>

Accessing Data Attributes

The value of a specific data attribute may be accessed (i.e., both written and read) in various ways. The following sections illustrate the, in my opinion, three most well-known ones.

Accessing Data via the Element Interface

Since custom data attributes are regular (global) HTML attributes, they can be accessed via the Element interface, more precisely: by using the setAttribute() and getAttribute() methods.

For the above button, reading the initial spell, and then changing and confirming it would look something like this:

const button = document.getElementById( 'magic' );

console.log( button.getAttribute( 'data-spell' ) );
// alohomora

button.setAttribute( 'data-spell', 'lumos' );

console.log( button.getAttribute( 'data-spell' ) );
// lumos

Accessing Data via jQuery

When using jQuery, both reading and writing attributes is done by using the attr() method.

Reading, and then changing and confirming the force of the above button would look like so:

const $button = jQuery( '#magic' );

console.log( $button.attr( 'data-force' ) );
// 40

$button.attr( 'data-force', 60 );

console.log( $button.attr( 'data-force' ) );
// 60

For HTML5 data attributes, jQuery comes with a special method: data(). It works almost like attr(), although the name has to be passed without the data- prefix.

Changing the force again, now by using data(), would look like this:

console.log( $button.data( 'force' ) );
// 60

$button.data( 'force', 'max' );

console.log( $button.data( 'force' ) );
// max

Accessing Data via the dataset Property

HTML5 also introduced the dataset property. It is a DOMStringMap, defined for any (even custom) HTML element, and provides access to all custom data of the individual element.

For the above example, access to spell and force is possible by either bracket notation, or the preferable dot notation.

console.log( button.dataset['spell'] );
// lumos

button.dataset['spell'] = 'bombada';

console.log( button.dataset.spell );
// bombada

Uh oh! 😀

One great advantage of using dataset compared to individual data attribute access is that you can pass all custom data of a specific element to some consumer (e.g., an HTTP request handler in PHP, or a JavaScript event listener) in one go.

A second advantage is due to the nature of DOMStringMap: it can be destructured. So, instead of explicitly declaring and initializing every variable individually:

const spell = button.dataset.spell;
const force = button.dataset.force;

you can just do this:

const { spell, force } = button.dataset;

The above is, of course, even more efficient and comfortable if you have more than just two data attributes.

Data Attributes with More Complex Names

Oftentimes, (attribute) names consist of more than one word. Say, we now have a custom data attribute data-something-special="special".

To access the value, you might expect you just have to adapt the things from before. And this is true for almost anything we’ve seen so far. For example, all of the following just works:

/**
 * Element interface.
 */
console.log( button.getAttribute( 'data-something-special' ) );
// special

button.setAttribute( 'data-something-special', 'special 2' );

console.log( button.getAttribute( 'data-something-special' ) );
// special 2

/**
 * jQuery attr() method.
 */
console.log( $button.attr( 'data-something-special' ) );
// special 2

$button.attr( 'data-something-special', 'special 3' );

console.log( $button.attr( 'data-something-special' ) );
// special 3

/**
 * jQuery data() method.
 */
console.log( $button.data( 'something-special' ) );
// special 3

$button.data( 'something-special', 'special 4' );

console.log( $button.data( 'something-special' ) );
// special 4

So, what about dataset then?

Property Names

As we all know, for example, from JavaScript object literals, property names with special characters (e.g., a dash, as in our case) can only be accessed via bracket notation. Or in other words: you cannot use dot notation for access. Something like button.dataset.something-special is just invalid and will trigger a ReferenceError or similar.

Alright. We can only use bracket notation then, which we try right away.

console.log( button.dataset['something-special'] );
// undefined

Wait, what? Why is it that we get undefined as value, meaning: no value at all? We just checked in various ways before, and there should be a value.

Let’s try writing something to the attribute then:

button.dataset['something-special'] = 'working?';

Surprisingly, the above code triggers an error (about an invalid or illegal string). Why is that?

Name Conversion

In order to find out what’s going on here, we just log the whole data:

console.log( button.dataset );
// DOMStringMap { spell='bombada', force='max', somethingSpecial='special 4' }

Aha! There is no something-special key  in the map at all, but we do have a somethingSpecial instead. The reason for this is the underlying name conversion between custom data attributes defined in the HTML and their individual counterpart inside the dataset map.

Using Custom Data in JavaScript

Once again, this is great not only because of destructuring. When you follow the WordPress JavaScript Coding Standards (e.g., by using the ESLint config by Inpsyde), all your variables are expected to be in camelCase. And this is easily done by just destructuring. This means that you just can go ahead and work with the dataset property, and your linter is always happy. No extra work to be done. Great!

Here is a short (incomplete) example of a button that shows a modal when clicked:

$button.click( ( e ) => {
    const { postId, postTitle } = e.target.dataset;
    showModal( postId, postTitle );
} );

In case all of the data attached to the button should be passed, this can be shortened to this:

$button.click( ( e ) => {
    showModal( e.target.dataset );
} );

Depending on what you are doing with the data, you might want to use a copy instead of the original object (reference). This can be done by using, for example, the spread operator:

const copy = { ...e.target.dataset };

Using Custom Data in PHP

Reading custom data in JavaScript and then sending it to some PHP script, is, most probably, another story.

Depending on your PHP code style, variables or array keys might follow a different format (i.e., case) than in your JavaScript code. In WordPress context, for example, JavaScript variables are expected in camelCase, while PHP variables are expected in lower_case_with_underscores. If you prepare data in JavaScript and then send it to some PHP request handler, and if you are using some linter for both JavaScript and PHP, then one of these might complain. Most linters can very easily be disabled and re-enabled, though.

Here is a short (and incomplete) example of a button that fires an AJAX request when clicked:

$button.click( ( e ) => {
    const dataset = e.target.dataset;

    $.ajax( 'http://example.com/some-url', {
        data: {
            /* eslint-disable camelcase */
            'post_id': dataset.postId,
            'site_id': dataset.siteId
            /* eslint-enable camelcase */
        },
        success: this.someCallbackHere
    } );
} );

The above example code includes two ESLint-specific comments to disable and re-enable the camelcase enforcing rule.

Different Naming Convention

In case you don’t want/have to work with the data in JavaScript, but only need to pass it to some PHP script, and if it’s OK that the script gets all the data that is attached to your element, then you can do this in another way. It is perfectly valid to have a data attribute like data-post_id="42" (i.e., with underscores). And if you have a look at the rules for the name conversion, you will see that names like this will end up in the dataset property exactly like they are given, so no conversion or removal or underscores, and also no changing cases.

You can then just grab all of the data, like so:

$button.click( ( e ) => {
    $.ajax( 'http://example.com/some-url', {
        data: e.target.dataset,
        success: this.someCallbackHere
    } );
} );

The advantage of the above code is that you don’t have to provide any ESLint-specific comments due to incorrect variable names. Inside the map, all the property names are given with underscores. But in the JavaScript code, no variable name is explicitly stated.

Call to Action

If you read my blog regularly, you know that I like to conclude with a Call to Action. Oftentimes, this is just leaving a comment about the post, whether or not you liked it, if I missed an important thing, or gt things wrong etc.

Even though I, of course, would like to receive one or another comment for this post as well, the Call to Action is different this time.

Spend some merry Christmassy days! 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *