Webflow CMS & MixItUp 3 | filter + sort + search + hash URL

Hi there !

For those interested, here is a script I tweaked and rewrote to fit my ongoing CMS project
Using the latest MixItUp 3 library, the script performs the following on Webflow dynamic CMS items:

  1. filter “featuring” elements on load (via Webflow CMS switch)
  2. filter by category (via CMS “categ” text field)
  3. sort (via native MixItUp 3)
  4. search by text input (via CMS “title” text field)
  5. manage button checked state on click & hash URL load

Have a look at the live codepen, reproducing the Webflow HTML structure.
The script is inspired by @sabanna and patrickkunka and is for one dimension deep only.

Hope that helps ! :smiling_face_with_three_hearts:

JavaScript

/**
 * one dimensional filtering module
 * MixItUp 3 & wf CMS
 */
// 🌮 on DOM loaded
document.addEventListener("DOMContentLoaded", event => {
  // 🥑 manage button state
  filterChecked();
  // 🍅 convert wf switch "featuring" into CSS comboclass
  featToClass();
  // 🌽 convert wf text field "category" into CSS comboclass
  categToClass();
  // 🍆 convert wf text field "title" into CSS comboclass
  titleToClass();
  // 🍋 initialize MixItUp 3 (incl. search module + hash URL filtering)
  mixItUp();
});

// 🥑 manage button state
function filterChecked() {
  const controls = document.getElementById("controlsMixItUp");
  const buttons = document.getElementsByClassName("filter");
  for (let i = 0; i < buttons.length; i++) {
    let button = buttons[i];
    button.addEventListener("click", event => {
      // use "currentTarget" to select parent trigger not its children...
      let target = event.currentTarget;
      if (target.classList != "filter checked") {
        // button not yet checked, remove "old" class + add new one
        controls.querySelector(".filter.checked").classList.remove("checked");
        target.classList.add("checked");
      } else {
        // button already checked
        return;
      } // end if
    }); // end eventlistener
  } // end for loop
} // end checked()

// 🍅 convert wf switch "featuring" into CSS comboclass
function featToClass() {
  const mixes = document.getElementsByClassName("mix");
  for (let i = 0; i < mixes.length; i++) {
    let mix = mixes[i];
    let stringFeat = mix.querySelector(".feat").innerHTML;
    let classNameFeat = stringFeat.split(" ").join("");
    // if wf switch is on, then add classNameFeat as combo class
    let feat = mix.querySelector(".feat");
    if (mix.querySelector(".feat").classList != "feat w-condition-invisible") {
      mix.classList.add(classNameFeat.toLowerCase().trim());
    }
  }
}

// 🌽 convert wf text field "category" into CSS comboclass
function categToClass() {
  const mixes = document.getElementsByClassName("mix");
  for (let i = 0; i < mixes.length; i++) {
    let mix = mixes[i];
    let stringCateg = mix.querySelector(".categ").innerHTML;
    let classNameCateg = stringCateg.split(" ").join("");
    mix.classList.add(classNameCateg.toLowerCase().trim());
  }
}

// 🍆 convert wf text field "title" into CSS comboclass
function titleToClass() {
  const mixes = document.getElementsByClassName("mix");
  for (let i = 0; i < mixes.length; i++) {
    let mix = mixes[i];
    let stringTitle = mix.querySelector(".title").innerHTML;
    let classNameTitle = stringTitle.split(" ").join("");
    mix.classList.add(classNameTitle.toLowerCase().trim());
  }
}

// 🍋 initialize MixItUp 3 (with search module)
function mixItUp() {
  const container = document.getElementById("containerMixItUp");
  const inputSearch = document.getElementById("inputSearch");
  let keyupTimeout, searchValue;

  // mixer options
  let mixer = mixitup(container, {
    load: {
      filter: ".feat"
    },
    animation: {
      duration: 450,
      nudge: true,
      reverseOut: true,
      effects: "fade scale(0.77) translateZ(-68px) stagger(6ms)"
    },
    callbacks: {
      onMixClick: function() {
        // reset the search if a filter is clicked
        if (this.matches("[data-filter]")) {
          inputSearch.value = "";
        }
      }
    }
  });

  // 🍧 handle hash URL filtering
  (function setHashFromFilter() {
    let filterValue,
        filterValueCleaned,
        filterFromHash;

    let filters = document.getElementsByClassName("filter");
    for (let i = 0; i < filters.length; i++) {
      let filter = filters[i];
      filter.addEventListener("click", event => {
        // get the "data-filter" respectively "data-sort" attribute         
        if (event.currentTarget.hasAttribute("data-filter")) {
          filterValue = event.currentTarget.getAttribute("data-filter");
        }        
        // handle the "all" data-filter edge case
        if (!filterValue.includes(".")) {
          filterValue = `.${filterValue}`;
        }     
        filterValueCleaned = filterValue.split(".")[1];        
        location.hash = "filter=" + encodeURIComponent(filterValueCleaned);
      }); // end listener

      // 🍪 if hash exists
      if (location.hash) {
        // handling of the data-filter="all" misssing the "."
        if (location.hash == "#filter=all") {
          filterFromHash = "all";
        } else {
          filterFromHash = location.hash.replace("#filter=", ".");
        } // end if
        // update mixer on hash exists
        mixer.filter(filterFromHash);
        // handle button state on hash exists
        let oldFilter = document
          .querySelector(".filter.checked")
          .classList.remove("checked");
        let newFilter = document
          .querySelector(`[data-filter="${filterFromHash}"]`)
          .classList.add("checked");
      }
    } // end for loop
  })(); // end setHashFromFilter()

  // 🥤 set up a handler to listen for "keyup" events
  inputSearch.addEventListener("keyup", event => {
    if (inputSearch.value.length < 1) {
      searchValue = "";
    } else {
      searchValue = inputSearch.value.toLowerCase().trim();
    }

    // basic throttling to prevent mixer thrashing
    clearTimeout(keyupTimeout);
    keyupTimeout = setTimeout(function() {
      filterByString(searchValue);
    }, 350);
  });
  
  // 🍸 update mixitup mixer.filter method
  function filterByString(searchValue) {
    if (searchValue) {
      // use an attribute wildcard selector to check for matches
      mixer.filter(`[class*="${searchValue}"]`);
    } else {
      // update current category button state 
      document.querySelector(".filter.checked").classList.remove("checked");
      document.querySelector("[data-filter='.feat']").classList.add("checked");
      // update mixer's filter
      mixer.filter(".feat");
    }
  }
}

HTML reproducing Webflow structure

<!-- mixitup controls -->
<div id="controlsMixItUp">
  <button type="button" class="filter" data-filter="all">all</button>
  <button type="button" class="filter checked" data-filter=".feat">featuring</button>
  <button type="button" class="filter" data-filter=".green">green</button>
  <button type="button" class="filter" data-filter=".blue">blue</button>
  <button type="button" class="filter" data-filter=".pink">pink</button>
  <button type="button" class="filter" data-sort="default:asc">Asc</button>
  <button type="button" class="filter" data-sort="default:desc">Desc</button>
  <button type="button" class="filter" data-sort="random">random 🤪</button>
  <input type="text" id="inputSearch" placeholder="🔮 Search category or title" />
</div>

<!-- mixitup elements -->
<div id="containerMixItUp">
  <div class="mix w-dyn-item">
    <div class="wrap_helpers">
      <div class="feat w-condition-invisible">feat</div>
      <div class="categ">green</div>
      <div class="title">Leeloo Dallas Multipass</div>
    </div>
  </div>
  <div class="mix w-dyn-item">
    <div class="wrap_helpers">
      <div class="feat">feat</div>
      <div class="categ">green</div>
      <div class="title">Korben Dallas</div>
    </div>
  </div>
  <div class="mix w-dyn-item">
    <div class="wrap_helpers">
      <div class="feat w-condition-invisible">feat</div>
      <div class="categ">blue</div>
      <div class="title">Jessica Alba</div>
    </div>
  </div>
  <div class="mix w-dyn-item">
    <div class="wrap_helpers">
      <div class="feat">feat</div>
      <div class="categ">pink</div>
      <div class="title">Léon The Professional</div>
    </div>
  </div>
  <div class="mix w-dyn-item">
    <div class="wrap_helpers">
      <div class="feat w-condition-invisible">feat</div>
      <div class="categ">blue</div>
      <div class="title">Jim Carrey</div>
    </div>
  </div>
</div>
8 Likes

Thanks for sharing with the community @anthonysalamin

2 Likes

Hello! This is awesome - for the text search is this part of mixitup or something else? I finally got my mixitup dropdowns working but it would be great to be able to include a text search function like yours too!

Hi @DrNinjamonkey,

Yes the search function is a simple input field (like one provided natively by Webflow) which passes its value to the mixitup mixer.filter() method via the function filterByString() as you can see at the very bottom of the javascript snippet.

1 Like

Back to this…my code looks very different to yours! I have 3 dropdowns filtering my CMS items - one for location / category / jobtype. Next i want a keyword search box that searches those but also the job description which is not view-able until you click the job. I was planning on adding the job description to each job but setting it to hidden and then letting the text field search it all. This is my code now:

// Reusable function to convert any string/text to css-friendly format
var conv = function (str) {
if (!str) {
str = ‘empty’;
}
return str.replace(/[!"#$%&'()*+,./:;<=>?@[\]^`{|}~]/g, ‘’)
.replace(/ /g, “-”)
.toLowerCase()
.trim();
};

// Creating dynamic elements classes from its categories:
var catArray = document.querySelectorAll(‘.w-dyn-item .categ’);
catArray.forEach( function(elem) {
var text = elem.innerText || elem.innerContent;
var className = conv(text);
if (!isNaN(parseInt(className.charAt(0), 10))) {
className = (“_” + className);
}
elem.parentElement.classList.add(className);
});

// Creating a custom data-order attributes from blog titles:

var filterGroups = document.querySelectorAll(‘.filter-group’);
filterGroups.forEach( function(group) {
group.setAttribute(‘data-filter-group’,‘’);
});

var containerEl = document.querySelector(‘.container’);
var selectFilter = document.querySelector(‘.filter_select’);
var mixer = mixitup(containerEl, {
multifilter: {
enable: true
}
});

	selectFilter.addEventListener('change', function() {
var selector = selectFilter.value;
mixer.filter(selector);

});

Is this close enough to yours that i can use the same text search functionality? (Is that everything from // :cup_with_straw: set up a handler to listen for “keyup” events down?)

Thank you so much for your help :slight_smile:

Hi there @anthonysalamin

Thanks for posting! I think your solution may partially work for something I am working on. I am using MixitUp to filter in a more ‘dropdown’ type motion with 3 categories. I want to only display 9 featured items on the page, but I want the filters to filter through all 32 options when clicked ( not uploaded yet). Would the option to ‘show featured on load’ solve this issue? If so, would you mind pointing me in the right direction on your codepen?

Very beginner at javascript, would so appreciate any advice you could give!

thank you!

Read-only

Published

Hi, thanks for your work, I’m wondering if this could work with pagination. Because I have over 100 CMS items, Webflow is limit to only show 100 items in one page. Without pagination, I won’t be able to show all the items. But with the current code, I can only search within the items I showed in one page.

Hei @anthonysalamin, maybe you got some ideas on how to solve this
MixItUP Sorting price value :+1:

Really need some help, would appreciate it!!

Solved it by myself! :+1: You can close this one!