Sat, Sep 21

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 !

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>
5 replies