1

Is there an easier, better or cleaner way to filter a list of elements that utilize a data- attribute holding an array?

Currently we have a large list of items each containing 1 or more tags in an array stored in a "data-tags" attribute, as follows:

<div class="viewItem" data-tags='[{"id":1,"name":"Tag 01"},{"id":2,"name":"Tag 02"}]'></div>
<div class="viewItem" data-tags='[{"id":2,"name":"Tag 02"}]'></div>
<div class="viewItem" data-tags='[{"id":1,"name":"Tag 01"},{"id":3,"name":"Tag 03"}]'></div>

The object is to show only the divs that have a particular tag in the data-tags array. The following code works, but I feel it's very inefficient when dealing with a large number of items and would like to find a better answer, whether it's using jquery filter or grep or something else.

$(function () {
    //Instantiate variables
    var $viewItems = $('.viewItem');
    var filterId = 2;

    //Hide all items in object array.
    $viewItems.hide(); 

    //Loop thru EACH item and show only those with matching id in array
    $viewItems.each(function () {
        var thisItem = $(this);
        var array = thisItem.data("tags");

        $.each(array, function (i, obj) {
            if (obj.id == filterId) { thisItem.show(); return false; }
        });                    
    });
});
2
  • Note that the HTML is invalid (you need to deal with the nested quotes). Commented Jun 1, 2017 at 20:00
  • Corrected in example. (The array is dynamically generated. I copied the output html from Chrome which nested the quotes that way.) Commented Jun 1, 2017 at 20:15

4 Answers 4

3

First of all, the data method is rather fast: jQuery only reads the value from the DOM on the first access, but then keeps the values (objects in this case) in memory, and will not read the DOM's data attribute again.

So, if these JSON values are not very large, the filtering of the items by id will not be the part that takes most of the time. The most time-consuming part will be where you call .hide() and .show(), since it involves interaction with the DOM and rendering.

But, if you really need to optimise it, you could do some preprocessing and key your elements by this JSON id value, for instance like this:

$(function () {
    // Pre-processing: key all viewItems by the id in their data-tags:
    var hash = $('.viewItem').get().reduce(function (hash, div) {
        return $(div).data("tags").reduce(function (hash, o) {
            hash[o.id] = (hash[o.id] || []).concat(div);
            return hash;
        }, hash);
    }, {});

    // Actual filtering
    $('#apply').click(function() {
        var filterId = $('#filter').val();
        $('.viewItem').hide();
        $(hash[filterId]).show();
    });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Filter ID: <input id="filter"><button id="apply">Apply</button><br>

<div class="viewItem" data-tags='[{"id":1,"name":"Tag 01"},{"id":2,"name":"Tag 02"}]'>1,2</div>
<div class="viewItem" data-tags='[{"id":2,"name":"Tag 02"}]'>2</div>
<div class="viewItem" data-tags='[{"id":1,"name":"Tag 01"},{"id":3,"name":"Tag 03"}]'>1,3</div>

Imperative alternative

As the functional approach to create the hash may look confusing, I provide here the imperative alternative. But the resulting hash object will be exactly the same:

$(function () {
    // Pre-processing: key all viewItems by the id in their data-tags:
    var hash = [];
    $('.viewItem').each(function (i, div) {
        $.each($(div).data("tags"), function (j, obj) {
            if (!(obj.id in hash)) hash[obj.id] = [];
            hash[obj.id].push(div);
        });
    });

    // Actual filtering
    $('#apply').click(function() {
        var filterId = $('#filter').val();
        $('.viewItem').hide();
        $(hash[filterId]).show();
    });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Filter ID: <input id="filter"><button id="apply">Apply</button><br>

<div class="viewItem" data-tags='[{"id":1,"name":"Tag 01"},{"id":2,"name":"Tag 02"}]'>1,2</div>
<div class="viewItem" data-tags='[{"id":2,"name":"Tag 02"}]'>2</div>
<div class="viewItem" data-tags='[{"id":1,"name":"Tag 01"},{"id":3,"name":"Tag 03"}]'>1,3</div>

Sign up to request clarification or add additional context in comments.

9 Comments

Although quirimmo's answer is far easier to read and utilize, I selected this answer for 2 reasons: first because the detailed explanation really helped me with understanding where the bottleneck is in my code and how the data methods work, and second is even though the initial function that creates the 'hash' array is a bit complex and confusing, I really like the fact that I don't have to reloop through the list every time a new filter is selected. I can simply hide everything and show only the items by an ID. But for anyone looking at this, quirimmo's answer is just as good.
Following your comment, I have added an alternative (imperative vs. functional coding) to my answer, which you may find useful. I just want to note that a solution that uses attr instead of data will access the DOM each time that method executes, even when that same attribute had been read before, and thus will have some performance impact.
Thank you for making it much easier to read. And I will be using data instead of attr for now on. Does this also include when looping based on either a class or id? should I be using data-id and data-class to do searches instead of the regular id and class attribute?
Indeed, 100 is not a lot, so then it will not make a difference.
If you want faster, then avoid repeated interaction with the DOM, and limit it only to update the DOM to the final result.
|
2

Use jQuery grep function to filter your elements. Then inside the grep function, parse the data-tags attribute and find if there is an element with the given id:

var filterId = 1;
var dataTags;
var arr = $.grep($('.viewItem'), function( el ) {
  dataTags = JSON.parse($(el).attr('data-tags'));
  return dataTags.find(el => el.id === filterId);
});
console.log(arr);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="viewItem" data-tags='[{"id":1,"name":"Tag 01"},{"id":2,"name":"Tag 02"}]'></div>
<div class="viewItem" data-tags='[{"id":2,"name":"Tag 023"}]'></div>
<div class="viewItem" data-tags='[{"id":1,"name":"Tag 01"},{"id":3,"name":"Tag 03"}]'></div>

1 Comment

my use case was slightly different, but this answer worked best for me!
0

You can create css classes for every tag. Example: tag-1, tag-2, etc. Put tags inside class section like this: <div class="viewItem tag-1 tag2".../> So you can easily select/show/hide any combination of tags like this:

// Hide tag-1
$(".tag-1").css('display','none')
// Show tag-2
$(".tag-2").css('display','block');
// Select elements with tag-1 and tag-2
$(".tag-1 tag-2").css('background','pink');

1 Comment

Thank you for this option. Usually this is how I managed my lists, but for this instance I wanted to avoid this. Using classes, I will loose the ability of maintaining relationships that I can use with the array. (Id 2 is related to name Tag02). But also with the large number of entries i'll be dealing with, I simply wanted to use the IDs for locating elements. But classes cannot be just numbers. And lastly, I really want to move away from using classes for this because I want to separate styling (classes) from other forms of manipulation (using data- attributes). Thank you though.
0

How about jQuery filter?

var $viewItems = $('.viewItem');
var filterId = 2;

//Hide all items in object array.
$viewItems.hide(); 

//Loop thru EACH item and show only those with matching id in array
$viewItems.filter(function (i, el) {
  var dataTags = $(el).data("tags");
  return dataTags.filter(tag => tag.id === filterId).length
}).show()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.