Tag Item Collection

Web Component
JavaScript
Currently a raw JS file on this site

What It Is

A web component to allow on-page filtering of elements by tag

Status

Under active development in October 2024

Details

This web component allows you to specify a set of elements on a page that have “tags” associated with them, then filter that set of elements by tag.

See a demo here (viewing the source of this is helpful)

There are six tags in the library (terms in bold are used on this page as nomenclature), only one of which you have to use

At it’s most basic, it looks like this:

<tag-item-collection>
  <img data-tags="foo" src="foo.jpg"/>
  <img data-tags="foo,bar" src="foo-and-bar.jpg"/>
</tag-item-collection>

The immediate children of tag-item-collection are considered “items.” They are associated with one or more tags, and they will be shown or hidden based on what tag the tag-item-collection is displaying at any given moment.

One way the tags can be specified on the items is using a data-tags attribute, containing a comma-delimited list of tags. (There’s another way; see below.)

Then, setting the show attribute on the collection will only show those items associated with that tag.

<tag-item-collection show="bar">
  <img data-tags="foo" src="foo.jpg"/><! -- this will not display -->
  <img data-tags="foo,bar" src="foo-and-bar.jpg"/>
</tag-item-collection>

Obviously, manually coding that in HTML is not particularly helpful. However, the show attribute is watched by the component, and it will dynamically change the filter when the attribute changes.

myCollection.setAttribute("show", "foo");

To wire this up to the click of a button:

document.querySelectorAll("button.showTag").forEach(b => {
  b.addEventListener('click', (e) => {
    myCollection.show = b.innerText;
  });
});

To clear the tag filter and show all the items again, set show to an empty string.

To avoid having to wire up the JavaScript, there is a second web component for “tag triggers” which will actively filter the collection when clicked. These triggers can be internal or external to the collection.

<tag-trigger>bar</tag-trigger>
<tag-trigger>foo</tag-trigger>

<tag-item-collection>
  <img data-tags="foo" src="foo.jpg"/>
  <img data-tags="foo,bar" src="foo-and-bar.jpg"/>
</tag-item-collection>

Normally, the innerText of the trigger will specify the tag to filter for, but you can explicitly specify it in an attribute if you want the text to be something else.

<tag-trigger tag="foo">Things tagged with "foo"</tag-trigger>

If tag-trigger elements are present inside a collection item, they can also provide the tags for the item, without having to specify them in a data-tags attribute. (This will only be used when a data-tags attribute is not present.)

<tag-item-collection>
  <div>
    Kitten 1
    <tag-trigger>foo</tag-trigger>
  </div>
  <div>
    Kitten 2
    <tag-trigger>foo</tag-trigger>
    <tag-trigger>bar</tag-trigger>
  </div>
</tag-item-collection>

The contents of the tag-trigger elements will be used for the tags of each item (foo for the first item; foo and bar for the second).

The collection exposes an event called TagChanged which is raised whenever the tag being displayed changes:

myTagItemCollection.addEventListener('tagChanged', () => {
  alert("Let's see some items for " + e.detail.displayedTag);
})

e.detail.displayedTag will have the name of the new active tag.

The collection also has a property for isFiltering which will return true if an active filter is in place.

To provide a summary of the tagging status, the tag-is-filtering will only show if the collection is actively filtering for something – it will display when the collection is filtering, then hide when it’s reset to all items.

Additionally, the tag-item-count and tag-current-filter will display the current state of the collection.

<tag-is-filtering>
  There are <tag-item-count></tag-item-count> item(s)
  tagged with "<tag-current-filter></tag-current-filter>"
</tag-is-filtering>

Notes…

Some random stuff that didn’t fit anywhere else.

Can I put the current tag in the URL?

Yes, but I’m not going to build this in, because you might want to do other stuff with your URL hashes.

This works for me:

myCollection.addEventListener('TagChanged', (e) => {
  window.location.hash = e.detail.displayedTag;
});

if(window.location.hash != '') {
  myCollection.show = window.location.hash.replace('#','');
}

If you refresh the page, the collection will immediately filter for the hash in the URL.

Can I “reflect” the collection?

Yes, on page load, the collection forms an index of all the items – it’s an array that contains a key for each tag, and the value is an array of references to all the item nodes for that tag.

It’s available – you can sort through myCollection.index to find all the tags and everything associated with each one.

It’s a bit easier to call myCollection.tags to get an array of objects. Each object will have a value property with the value of the tag, and a count property with the number of items tagged with it.

What if there’s stuff in the collection that doesn’t have tags?

It doesn’t really matter. It will be moved to the bullpen, and it won’t ever show up in a filter (because it has no tags).

If you reset the collection (by searching for an empty tag), the bullpen will empty and it will all come back out the same way it went in. Essentially, the collection will “reset” the way it was before you filtered, including all your untagged stuff.

What if I dynamically change the items in the collection?

It’s not really designed to work like this. Ideally, everything is on the page when it loads.

But if the collection is empty on page load – like if you’re adding everything after the page loads – then add your stuff and call myCollection.buildIndex() to re-build the index (that’s what the constructor does…)

However, if you’ve already filtered, this can be a problem because not everything is in the collection anymore, and the bullpen is loaded with stuff.

So you can call myCollection.reset() and it will basically start everything over – clear the bullpen and move everything back into the collection, just like when the page loaded. Then you can add your new items and call myCollection.buildIndex() to rebuild the index based on the new stuff.

What if I have more than one collection on a page? What do the triggers and other tags affect?

All of the elements except for the collection itself need to be associated with a collection – they need to know what collection to operate on or draw data from.

In most cases, you will only have one collection in the DOM, so you don’t have to specify anything. When any tag needs to find its collection, it will just use the first (and probably only) one on the page.

If you have more than one collection on a page, all of the tags accept a collection attribute which should contain the ID of the specific collection on which they should operate or reference.

What if I want to change the animation?

You need to change the code.

I’ve hard-coded a fade effect/animation into it. I’m not very good with things like this, and I realize it should probably be handled externally via CSS or some callback function.

But, I needed it to fade, and I couldn’t figure out the CSS, so here we are…

Can I filter for a combination of more than one tag?

Nope.

How efficient is it?

I’m no expert in browser performance, but I think it’s pretty good.

It works by by moving all the item nodes to a hidden template tag (the “bullpen”), then consults the index and clones the nodes back into the collection that match the tag.

I have done no testing on it. For the Friends Episode Tagging Project it’s filtering 236 items with about a thousand tag assignments (as of this writing), and it does fine.

I’m trying to speculate what might slow it down. Clearly, the more items there are, the longer it would take to compute the index, but that’s done async and would likely always complete before the user tried to filter.

When the items are copied back in, they’re already grouped in the index, and the DOM doesn’t re-render for each.

…I don’t know. It seems pretty fast, no matter how I look at it.