Tag Item Collection
What It Is
A web component to allow on-page filtering of elements by tagStatus
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 three tags in the library (terms in bold are used on this page as nomenclature), only one of which you have to use
tag-item-collection
: this is a container of child elements – the collection – each of which is considered an item and has one or more tags associated with it. The purpose of this library is to allow visual filtering of these items.The items themselves have no special tag – they just need to be top-level child nodes of the collection. (I’ve been using
section
, but whatever.)tag-trigger
: this is a trigger element that, when clicked, will trigger the associated collection to show only a subset of elementstag-list
: this will render trigger tags for all available tags in the collection
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.
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.
The index is pre-computed, so the collection “knows” in advance what content goes with what tag
All the items are copied to and stored in a
template
tag, which is not rendered or even parsed by the DOMCopying items back into the collection uses a
DocumentFragment
, so it will only re-render the DOM once each time you filter
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.