Closed ItsBenedict closed 1 year ago
@ItsBenedict — It's hard to tell for sure, but I assume you checkout this example for Filtering Hashes? I"m not sure what you mean by "using selects"? Do you just mean that the HTML element you're using is a <select>
element?
Yes, I've looked at the example but my code is different since I am using
@ItsBenedict — Can you make a CodePen of your code that you've tried? That would make it a lot easier to help you. I really need to see how you're using the select menus, but I should be able to help provided that. Thanks!
Sure, thank you. Here is the pen with the modified code: https://codepen.io/ItsBenedict/pen/mdLbbRr
Looks like something is wrong with the filtering and the hash is also not working since it puts a #filter=undefined into the URL.
@ItsBenedict — Thanks so much for creating the CodePen for me; very helpful.
So I spent a little time on this trying to whip you something up that would work. Here is what I have:
(function($) {
let $grid = $('.grid')
let filters = initializeFilters()
let isIsotopeInit = false
/**
* Temporary: For CodePen ONLY —
* This just allows us to start with a hash, which can be
* heplful since you cannot actually *see* the hash on CodePen,
* but you definitely should delete this after debugging.
*/
window.location.hash = 'location=location-01&category=cat-01'
/**
* On Select Menu Change
* - Get filter group from element
* - Set filters object @ 'group-value' to filter value (from select/option)
* - Set the hash for the new filter/value
*/
$('.filters').on('change', '.filters-select', function() {
let filterGroup = $(this).attr('data-filter-group');
filters[filterGroup] = $(this).val();
window.location.hash = setHashFilters(filters)
});
/**
* Bind event listener and call once on load.
*/
$(window).on('hashchange', onHashchange);
onHashchange();
/**
* On Hash Change
*
* Anytime the hash changes we want to —
* - Set global filters object to new filters object from getHashFilter()
* - Generate a filters object from the hashes
* - Generate Isotope filters values for filtering
* - Set the select menus to the matching values
* - Run the Isotope Layout/Filtering
*
* @return {void}
*/
function onHashchange() {
filters = getHashFilter()
let filter = getIsotopeFiltersFromFilterObject(filters)
setSelectedValueOnMenus(filters)
isIsotopeInit = true
$grid.isotope({
itemSelector: '.grid-item',
layoutMode: 'fitRows',
masonry: {
columnWidth: '.grid-item',
percentPosition: true,
gutter: 30
},
filter,
});
}
/**
* Initialize Filter Object
*
* @return {object}
*/
function initializeFilters() {
return Array.from(document.getElementsByClassName( 'filters-select' ))
.reduce((filters, element) => {
/**
* Set each group value to 'all' ('*') on initialization
*/
filters[ element.dataset.filterGroup ] = '*'
return filters
}, {})
}
/**
* Get filters via hashes
*
* @return {object}
*/
function getHashFilter() {
/**
* Get the hash and remove '#'
*/
let hash = window.location.hash.slice(1)
/**
* If the hash is invalid, set all filters to '*'
*/
if (hash === '' || typeof hash == 'undefined') {
return Object.keys(filters).reduce((filters, group) => ({ ...filters, [group]: '*' }), {})
}
/**
* Split hash group/value by separator ('='), then add
* each valid value to a new filters object
*
* @return {object}
*/
let nextFilterState = hash.split('&').reduce((acc, val) => {
let [ group, value ] = val.split('=')
/**
* In the obscure case that a hash group/value gets added
* which doesn't belong, filter it out of being added to
* the filters to prevent errors
*
* Additionally, we check for the filter value equal to '*' (all)
* because adding a secondary '*' filter to a group won't change
* the result; i.e., if the hash is, for example "#location=*&category=something",
* then this will be concated to filters of "*.something", which is the
* same as just ".something". It's important to note, we especially don't want
* to end up with this replacing the hash values, since having '*' added to
* your hashes will likely only make this more complicated.
*/
if (filters.hasOwnProperty(group) && value !== '*') {
acc[ group ] = value
}
return acc
}, {})
/**
* We return a new object: filters merged with the next state —
*
* We don't want to just return the nextFilterState object here
* because if the current set of hash filters doesn't have
* one of the groups, it will return a partial object.
*/
return Object.assign({}, filters, nextFilterState)
}
/**
* Get all values from object for each group, then map over those
* values to add the classname identifier (`.${filter}`), and finally,
* join the values using whichever join property suits your filters.
* If you want the values to be intersected, use '', otherwise for union
* joins, use ','.
*
* @param {object} filters
* @return string
*/
function getIsotopeFiltersFromFilterObject(filters) {
let filter = Object.values(filters)
.filter(filter => filter !== '*')
.map(filter => `.${filter}`).join(',')
return filter === '' ? '*' : filter
}
/**
* Set new hashes based on mutated/updated isotope filters
*
* @param {object} filters
* @return {string}
*/
function setHashFilters(filters) {
return Object.entries(filters)
.filter(([ group, value ]) => value !== '*')
.map(([ group, value ]) => {
/**
* If the value still has a classname identifier prefixed
* to it, remove it for the hash conversion
*/
if (value.includes('.')) {
value = value.slice(1)
}
return `${group}=${value}`
})
.join('&')
}
/**
* Macth the current filter/selected state to the select menus
* via hash change. (On load, the select menus will not know
* their values have changed, so we need to do it manually.)
*/
function setSelectedValueOnMenus(filters) {
Object.entries(filters).forEach(([ group, value ]) => {
let $element = $(`select[data-filter-group="${group}"]`)
if ($element.val() !== value) {
let $option = $element.find(`option[value=".${value}"]`)
$option.prop('selected', true)
}
})
}
}(jQuery))
You should be able to paste this into your CodePen and have it just work, essentially. I tried to comment as much as I possibly could, since I'm not sure what your experience level is. If you have any questions, please don't hesitate to ask. I'm happy to help.
I'm sure there are parts of this that could be a lot better; I just had a bit of extra time this morning and once I started it working, I had some momentum and wanted to try to help you out. Some general notes—
Wow, you are a legend! I've just tried it and it is working, thanks so much! :) I appreciate the comments, it does help me a lot to understand the code. And don't worry about Vanilla JS, I prefer it over jquery, too. You can close this topic. :)
Awesome—glad I could help!
Hi there again! Just noticed a little bug with the filter logic: https://codepen.io/ItsBenedict/pen/WNJeOdW If you click on "Location 1" it displays all items with "Location 1". So far so good. But if you now select "Category 2" or "Category 1" it ignores "Location 1" and also displays items with "Location 2".
It was working before with the previous code without the URLs: https://codepen.io/ItsBenedict/pen/oNdvyog Do you know where the issue is?
I think that's coming from this function —
/**
* Get all values from object for each group, then map over those
* values to add the classname identifier (`.${filter}`), and finally,
* join the values using whichever join property suits your filters.
* If you want the values to be intersected, use '', otherwise for union
* joins, use ','.
*
* @param {object} filters
* @return string
*/
function getIsotopeFiltersFromFilterObject(filters) {
let filter = Object.values(filters)
.filter(filter => filter !== '*')
.map(filter => `.${filter}`).join(',')
return filter === '' ? '*' : filter
}
In the comment it's talking about joining the filters by intersection or union. Intersection is an AND join, and union is an OR join. Currently it's using a comma, which is an OR join, so it will give you any item that has filter-a OR filter-b. If you change the comma to just ''
, it will create an AND join, such that it will only display items that have filters for filter-a AND filter-b.
Union (OR) : .filter-a
, .filter-b
=> .filter-a, .filter-b
Intersection (AND): .filter-a
, .filter-b
=> .filter-a.filter-b
(I hope this ^^ helps the explanation. If you think about the way you're joining the classnames/strings in the same way you think about CSS selectors, it might help.
Also, as a follow-up note, I would add that the current configuration works well because the input is simple: two categories, each with only one active classname/filter at a time. If you expanded this to more selectable filters and/or more categories, you would need to permute the different combinations of categories and classnames.
I see, thanks for the explanation, this is working! :) Okay, I will try to re-write the code if I need more categories and classes.
Hello there and thanks for the plugin!
I'm a JS beginner but with the help of the examples and the documentation my filter is working nicely with WordPress and Custom Posttypes.
Now I need to extend the filters to work with URL hashes. I took a look at this example: https://cdpn.io/pen/debug/vErxXj#filter=.metal
Unfortunately I am using select-filters and with my basic JS knowledge I don't know how to re-write my code in order to achieve this. This is my current code:
JS:
HTML:
Thanks for any help! :)