0

I am using javascript to process DOM attributes on page load.

I have markup with elements which contain data-* attributes:

<section>
<h2 data-fruit-apples="sdlfls">Heading</h2>
<p data-vegetables-carrots="sdjjasd">Paragraph</p>
<ul>
<li data-fruit-cherries="sdfada" data-fruit-bananas="adada">List Item</li>
<li>List Item</li>
</ul>
<p data-fruit-pears="rtfadds">Paragraph</p>
<p>Paragraph</p>
</section>

I'd like to process all the data-fruit-* attributes.

But how can I select all the elements with data-* attributes beginning with data-fruit?

I know I can use ^= to pattern match the start of a data-* attribute value.

Is there anything I can use to pattern match the start of a data-* attribute name?


My attempt would be something like this:

// Set up variables
const mySection = document.getElementsByTagName('section')[0];
let allDataElements = [];
let nonFruitDataElements = [];

// Build allDataElements
[... mySection.querySelectorAll('*')].forEach((element) => {

  if (Object.keys(element.dataset).length > 0) {

    allDataElements.push(...(Object.keys(element.dataset)));
  }

});

// Build nonFruitDataElements
allDataElements.forEach((element) => {

  if (element.substr(0, 5) !== 'fruit') {

    nonFruitDataElements.push(element);

  }

});

// Subtract nonFruitDataElements from allDataElements
fruitDataElements = allDataElements.filter(x => !nonFruitDataElements.includes(x));

// Log fruitDataElements
console.log(fruitDataElements);
<section>
<h2 data-fruit-apples="sdlfls">Heading</h2>
<p data-vegetables-carrots="sdjjasd">Paragraph</p>
<ul>
<li data-fruit-cherries="sdfada" data-fruit-bananas="adada">List Item</li>
<li>List Item</li>
</ul>
<p data-fruit-pears="rtfadds">Paragraph</p>
<p>Paragraph</p>
</section>

But this seems really longwinded.

Rounin
  • 27,134
  • 9
  • 83
  • 108
  • [There is no css selector for this](https://stackoverflow.com/questions/21222375/css-selector-for-attribute-names-based-on-a-wildcard), but there may be an XPATH that would do it (slowly), is it an acceptable solution for you (meaning do you only want to select by JS?) – Kaiido Dec 20 '19 at 09:31
  • I am using javascript to process DOM attributes on page load. – Rounin Dec 20 '19 at 09:32
  • 1
    Sorry to be "that guy" but the intended behaviour of custom attributes (using `data` or `aria`, etc) are intended to be predetermined and static. Especially to prevent conflicts with other code that may want to use `data-fruit-xyz*`. I would suggest either assigning a class to elements containing a `data-fruit-*` attribute or else to give yourself a key variable such as `data-fruit-index="cherries"` and then you know that you have access to `data-fruit-cherries="sdfada"` – Matt Dec 20 '19 at 09:40
  • 1
    Thanks for this smart suggestion, @Matt. Your idea of having a `data-fruit-index` inspired me to realise that actually I could just have a consistent `data-fruit` attribute which contained a pseudo-JSON string: `data-fruit="{'cherries' : 'sdfada'}"`. Then, the index will be `Object.keys(JSON.parse(el.dataset.fruit.replace(/'/g, '"')))`. – Rounin Dec 20 '19 at 12:35

3 Answers3

0

Get all elements, spread to an array, filter them by taking all dataset keys, and checking if some of them begin with the key you are looking for.

Note: the dataKey would be need to be a camel case string "fruitApples" for example.

const getElementsByDataAttr = dataKey =>
  [...document.querySelectorAll('*')]
  .filter(el => 
    Object.keys(el.dataset)
    .some(k => k.startsWith(dataKey))
  )
  
const result = getElementsByDataAttr('fruit')

console.log(result)
<section>
  <h2 data-fruit-apples="sdlfls">Heading</h2>
  <p data-vegetables-carrots="sdjjasd">Paragraph</p>
  <ul>
    <li data-fruit-cherries="sdfada" data-fruit-bananas="adada">List Item</li>
    <li>List Item</li>
  </ul>
  <p data-fruit-pears="rtfadds">Paragraph</p>
  <p>Paragraph</p>
</section>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • 1
    +1 for answering the question. But I don't think I'd be able to sleep at night knowing that this many iterations exist on page load. With a large (not so large) enough DOM. This will eventually make your initial payload completely freeze your browser (even if just temporarily). – Matt Dec 20 '19 at 09:42
  • 1
    I agree. I would include the key in the value and not the attribute name to start with. However, since I've encountered several cases of annoying parsing that actually has merits in real life (coming from clients, other systems, etc...), this might be useful. – Ori Drori Dec 20 '19 at 09:46
  • 1
    Your javascript is certainly a lot more sophisticated than mine. But I'd be tempted to say, this is essentally the same approach that I included in my question. The key is to start with something which isn't `document.querySelectorAll('*')`. – Rounin Dec 20 '19 at 10:17
  • Indeed. The basic idea is the same, since there are no selectors for parts of attributes' names. – Ori Drori Dec 20 '19 at 10:19
0

You can alternatively use an XPath expression with a carefully crafted expression that looks for any element with some attribute that begins with "data-fruit" ~ like so:

<!DOCTYPE html>
<html lang='en'>
    <head>
        <meta charset='utf-8' />
        <title>XPath: match attribute using wildcard</title>
    </head>
    <body>
        <section>
            <h2 data-fruit-apples="sdlfls">Heading</h2>
            <p data-vegetables-carrots="sdjjasd">Paragraph</p>
            <ul>
                <li data-fruit-cherries="sdfada" data-fruit-bananas="adada">List Item</li>
                <li>List Item</li>
            </ul>
            <p data-fruit-pears="rtfadds">Paragraph</p>
            <p>Paragraph</p>
        </section>
        <script>
            let query='//*[@*[starts-with(name(),"data-fruit")]]';
            let col=document.evaluate( query, document, null, XPathResult.ANY_TYPE, null );
            while( node=col.iterateNext() ){
                console.info( node, node.tagName, node.textContent )
            }
        </script>
    </body>
</html>

Unfortunately this doesn't work in the Stackoverflow snippet console :(

let query='//*[@*[starts-with(name(),"data-fruit")]]';
   let col=document.evaluate( query, document, null, XPathResult.ANY_TYPE, null );
   while( node=col.iterateNext() ){
    alert( node+'\n'+node.tagName+'\n'+node.textContent )
   }
<section>
   <h2 data-fruit-apples="sdlfls">Heading</h2>
   <p data-vegetables-carrots="sdjjasd">Paragraph</p>
   <ul>
    <li data-fruit-cherries="sdfada" data-fruit-bananas="adada">List Item</li>
    <li>List Item</li>
   </ul>
   <p data-fruit-pears="rtfadds">Paragraph</p>
   <p data-fruit-banana='curvy yellow thing with slippery skin'>Paragraph</p>
  </section>
Professor Abronsius
  • 33,063
  • 5
  • 32
  • 46
0

One approach that it occurs to me - inspired by @Matt's comment above - is something like the following:

<section>
<h2 data-fruit="{'apples' : 'sdlfls'}">Heading</h2>
<p data-vegetables="{'carrots' : 'sdjjasd'}">Paragraph</p>
<ul>
<li data-fruit="{'cherries' : 'sdfada', 'bananas' : 'adada'}">List Item</li>
<li>List Item</li>
</ul>
<p data-fruit="{'pears' : 'rtfadds'}">Paragraph</p>
<p>Paragraph</p>
</section>

Then I can grab all the elements with data-fruit attributes simply with:

let fruitElements = document.querySelectorAll('[data-fruit]');

Once I have a data-fruit attribute value for a given <element>, such as:

"{'cherries' : 'sdfada', 'bananas' : 'adada'}"

I can:

  • convert it to JSON, using el.dataset.fruit.replace(/'/g, '"')
  • convert the JSON to a javascript object using JSON.parse

Then I have an object for that <element> equivalent to el.dataset but only including fruit names and values (not vegetable names and values):

elementFruitObject = {
  cherries : 'adada',
  bananas : 'sdfada'
}
Rounin
  • 27,134
  • 9
  • 83
  • 108
  • that basically changes the entire approach that one takes. Matching a wildcard attribute name is tricky but matching on known attribute is easy – Professor Abronsius Dec 20 '19 at 10:30
  • Yes. Given the constraints we're working with, creating one overarching attribute per element and then labelling the various data as **sub-values** _inside_ the overarching attribute's value seems a smarter approach. – Rounin Dec 20 '19 at 14:02
  • 1
    I don't disagree but it does essentially render the question null and void somewhat – Professor Abronsius Dec 20 '19 at 14:45
  • I couldn't have proposed the solution above if @Matt hadn't already made the fundamental conceptual leap that finding a way to focus on the `data-*` attribute value (not the attribute name) actually _was_ the right way to approach answering the question. Lateral thinking is quite legitimate. – Rounin Dec 20 '19 at 22:45