Reputation: 783
I'm starting to use more Vanilla JS and rewriting old code that was in jQuery into Vanilla JS.
So, I have a list of filters. Under that, I have a list of stories under those filters. The idea is that when you click on a filter, you will only see the stories that match. If you click the "All" filter, you will see all the stories.
With jQuery, this is the old code:
var $filters = $('.filters').click(function(e) {
e.preventDefault();
if ($(this).data('filter') == 'all') {
$('.directory .item').fadeIn(450);
} else {
var $el = $('.' + $(this).data('filter')).fadeIn(450);
$('.directory .item').not($el).hide();
}
$filters.removeClass('selected');
$(this).addClass('selected');
})
I'm rebuilding this using Vanilla JS and have what I built over on CodePen: https://codepen.io/rmlumley/pen/ExjVxgw
You can also see the JS here:
// Find list of items with a class of filters
const filters = document.querySelectorAll('.filters');
function toggleFilter(e) {
e.preventDefault(); // Prevent link from working.
// Remove Selected class from other filters.
for (var i = 0; i < filters.length; i++) {
filters[i].classList.remove('selected');
}
// Add selected class to this filter.
this.classList.add('selected');
// Grab all Items.
const all = document.querySelectorAll('.directory .item');
// If selecting filter "All", then show all items.
if (this.dataset.filter == 'all') {
for (var i = 0; i < all.length; i++) {
all[i].classList.remove('hide');
}
// Otherwise, filter by the data-attribute of filter that is set.
} else {
const filter = this.dataset.filter;
// First off, hide all elements.
for (var i = 0; i < all.length; i++) {
all[i].classList.add('hide');
}
// Now show all elements that match.
let selected = document.querySelectorAll(`.directory .${filter}`);
for (var i = 0; i < selected.length; i++) {
selected[i].classList.remove('hide');
}
}
}
// Event Listener on any Filter that is clicked.
filters.forEach(filter => filter.addEventListener('click', toggleFilter));
Upvotes: 1
Views: 146
Reputation: 17511
Just one of several ways to do it:
The fading behavior can be emulated purely with css using transition
. In this case, if a style attribute is changed due to the addition or removal of a class, there's an fluid transition from the initial state to the changed one. For example, let's say the hidden items will have opacity:0;
and height:0
.item {
opacity:1;
height:initial;
transition: height 0.5s ease-out, opacity 0.3s ease-out;
}
.item.hide {
height:0;
opacity:0;
}
Toggling the .hide
class will fade the elements in or out and also smoothly change their height. It's not perfect tho, you'll need to fine tune those timings and easing functions.
Regarding the function itself, you can pick the items that should be hidden vs the selected ones separately:
const toHide = document.querySelectorAll(`.directory .item:not(.${filter})`),
toShow = document.querySelectorAll(`.directory .${filter}`);
On the other hand, given they all have the .item
class, I would use that classname to adress all of them, because:
document.querySelectorAll(`.directory .item`);
does indeed matches every item.
I don't quite get what is this.dataset.filter
but assuming it's irrelevant, I'll say the filter is taken from the rel
attribute.
This is my humble POC:
// Find list of items with a class of filters
const filters = document.querySelectorAll('.filter');
function toggleFilter(e) {
e.preventDefault();
// Remove Selected class from other filters.
for (var i = 0; i < filters.length; i++) {
filters[i].classList.remove('selected');
}
// Add selected class to this filter.
this.classList.add('selected');
const filter = this.attributes.rel.value;
const toHide = document.querySelectorAll(`.directory .item:not(.${filter})`),
toShow = document.querySelectorAll(`.directory .${filter}`);
// hide elements that do not match
for (var i = 0; i < toHide.length; i++) {
toHide[i].classList.add('hide');
}
// show all elements that match.
for (var i = 0; i < toShow.length; i++) {
toShow[i].classList.remove('hide');
}
}
// Event Listener on any Filter that is clicked.
filters.forEach(filter => filter.addEventListener('click', toggleFilter))
.filters {
display: flex;
}
.filter {
display: flex;
flex-direction: column;
padding: 4px;
border: 1px solid cyan;
margin: 3px;
cursor: pointer;
width: 50px;
}
.filter.selected {
color: white;
background: blue;
}
.item {
opacity:1;
height:initial;
transition: height 0.5s ease-out, opacity 0.3s ease-out;
}
.item.hide {
height:0;
opacity:0;
}
<div class="filters">
<div class="filter" rel="even">even</div>
<div class="filter" rel="odd">odd</div>
<div class="filter selected" rel="item">all</div>
</div>
<ul class="directory">
<li class="item odd">one</li>
<li class="item even">two</li>
<li class="item odd">three</li>
<li class="item even">four</li>
</ul>
Edit: Simpler way
If you knew the categories beforehand, you could do this without modifying the items's classList.
Let's say the .directory
element had a class with defines the filter, and the only the children items having that same class will remain visible:
/* hidden by default */
.directory .item {
transition: height 0.5s ease-out, opacity 0.3s ease-out;
height:0;
opacity:0;
}
/* visible if filter is "all" or filter matches their className */
.directory.all .item,
.directory.even .item.even,
.directory.odd .item.odd {
opacity:1;
height:initial;
}
// Find list of items with a class of filters
const filters = document.querySelectorAll('.filter');
function toggleFilter(e) {
e.preventDefault();
// Remove Selected class from other filters.
for (var i = 0; i < filters.length; i++) {
filters[i].classList.remove('selected');
}
// Add selected class to this filter.
this.classList.add('selected');
const filter = this.attributes.rel.value,
directoryContainer=document.querySelector(`.directory`);
directoryContainer.className=`directory ${filter}`;
}
// Event Listener on any Filter that is clicked.
filters.forEach(filter => filter.addEventListener('click', toggleFilter))
.filters {
display: flex;
}
.filter {
display: flex;
flex-direction: column;
padding: 4px;
border: 1px solid cyan;
margin: 3px;
cursor: pointer;
width: 50px;
}
.filter.selected {
color: white;
background: blue;
}
.directory .item {
transition: height 0.5s ease-out, opacity 0.3s ease-out;
height:0;
opacity:0;
}
.directory.all .item,
.directory.even .item.even,
.directory.odd .item.odd {
opacity:1;
height:initial;
}
<div class="filters">
<div class="filter" rel="even">even</div>
<div class="filter" rel="odd">odd</div>
<div class="filter selected" rel="all">all</div>
</div>
<ul class="directory all">
<li class="item odd">one</li>
<li class="item even">two</li>
<li class="item odd">three</li>
<li class="item even">four</li>
</ul>
Upvotes: 2
Reputation: 1003
How do I animate it similar to the fadeIn I was using with jQuery?
Since you're adding the class hidden
to non-active items, you can fade them in with a CSS animation of opacity like so:
.item {
animation: fade 0.75s ease-in-out;
}
.hide {
display: none;
}
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
You can skip !important
for .hidden
It's not needed.
Is there a better way to write this function?
There's always a better way to write a piece of code and it can be very subjective. A few things I would recommend:
That said, here's how I would write that JS
const directory = document.querySelector('.directory')
const allItems = [...directory.querySelectorAll('.item')];
const filterWrapper = document.getElementById('isotope-filters')
const toggleFilter = event => {
if (!event.target.dataset.filter) return;
event.preventDefault();
const filter = event.target.dataset.filter;
const oldSelected = filterWrapper.querySelector('.selected');
const currentSlected = [...directory.querySelectorAll(`.${filter}`)];
oldSelected.classList.remove('selected');
event.target.classList.add('selected');
if (filter === 'all') {
allItems.forEach(item => {
item.classList.remove('hide');
})
return
}
allItems.forEach(item => {
item.classList.add('hide');
})
currentSlected.forEach(item => {
item.classList.remove('hide');
})
}
filterWrapper.addEventListener('click', toggleFilter)
and here's a working snippet of the entire thing
const directory = document.querySelector('.directory')
const allItems = [...directory.querySelectorAll('.item')];
const filterWrapper = document.getElementById('isotope-filters')
const toggleFilter = event => {
if (!event.target.dataset.filter) return;
event.preventDefault();
const filter = event.target.dataset.filter;
const oldSelected = filterWrapper.querySelector('.selected');
const currentSlected = [...directory.querySelectorAll(`.${filter}`)];
oldSelected.classList.remove('selected');
event.target.classList.add('selected');
if (filter === 'all') {
allItems.forEach(item => {
item.classList.remove('hide');
})
return
}
allItems.forEach(item => {
item.classList.add('hide');
})
currentSlected.forEach(item => {
item.classList.remove('hide');
})
}
filterWrapper.addEventListener('click', toggleFilter)
.selected {
border: 1px solid black;
background-color: gray;
}
.item {
border: 1px solid black;
margin: 1em;
padding: 1em;
animation: fade 0.75s ease-in-out;
}
.hide {
display: none;
}
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
<ul id="isotope-filters">
<li><a href="#" data-filter="all" class="selected filters">All</a></li>
<li><a href="#" data-filter="bioethics" class="filters">Bioethics</a></li>
<li><a href="#" data-filter="medical-engineering" class="filters">Medical Engineering</a></li>
<li><a href="#" data-filter="metabolism" class="filters">Metabolism</a></li>
<li><a href="#" data-filter="outreach" class="filters">Outreach</a></li>
<li><a href="#" data-filter="regenerative-biology" class="filters">Regenerative Biology</a></li>
<li><a href="#" data-filter="virology" class="filters">Virology</a></li>
</ul>
<div class="directory">
<ul>
<li class="item medical-engineering">Story 1</li>
<li class="item bioethics">Story 2</li>
<li class="item medical-engineering">Story 3</li>
<li class="item outreach">Story 4</li>
<li class="item medical-engineering">Story 5</li>
<li class="item regenerative-biology">Story 6</li>
<li class="item medical-engineering">Story 7</li>
<li class="item virology">Story 8</li>
<li class="item metabolism">Story 9</li>
<li class="item regenerative-biology">Story 10</li>
<li class="item outreach">Story 11</li>
<li class="item virology">Story 12</li>
</ul>
</div>
Upvotes: 2