skip to Main Content

I have two content type areas which contain unique filter options. These are:

  1. type
  2. tag

I’m trying to utilise isotope.js, to achieve a dual filtering layout, but it always gives the last clicked filter priority.

The way I want it to function is:

  1. If only one tag is selected, then show results with that tag
  2. If more than one tag is selected, then show results that have any of those tags (both do not need to exist together on a card)
  3. If a type is selected, only show results that fall under that type
  4. If one type is selected and one tag, show results for posts where the tag exists on the selected type
  5. If one type is selected and multiple tags, then show posts that fall under that type that have either of those tags.
  6. If more than one type is selected and one or more tags, then show posts where the tags exist on either type

For example, using my demo below, here are some use cases:

  1. If I click on "Video Demos & Tours", I should see two video posts (card 1 and 3) – WORKS
  2. If I click on "Video Demos & Tours" and then "Ansible", I should see only see card 3 – DOESN’T WORK
  3. If I click on "Blog & News", I should see 3 cards (card 2, 4, 5) – WORKS
  4. If I click on "Blog & News" and then "Ansible", I should see cards 4 and 5
  5. If I click on "Blog & News", "Ansible" and then "Automation", I should see cards 2,4 and 5

However, in my current demo, though the console log seems to be on the right lines, it doesn’t perform the way I intent it to.

document.addEventListener("DOMContentLoaded", function () {
  var container = document.querySelector(".grid");
  var gridItems = container.querySelectorAll(".grid-item");
  const optionLinks = document.querySelectorAll(".rSidebar__options-li");

  var iso = new Isotope(container, {
    itemSelector: ".resourceCard",
    layoutMode: "fitRows",
    transitionDuration: "0.5s"
  });

  var filters = {};

  function concatValues(obj) {
    var value = [];
    for (var prop in obj) {
      value.push(obj[prop]);
    }
    return value.flat().join(", ");
  }

  function handleFilterClick(event, filters) {
    var listItem = event;
    var filterGroup = listItem
      .closest(".rSidebar__options")
      .getAttribute("data-filter-group");
    var data_filter = listItem.getAttribute("data-filter");

    if (filterGroup === "type") {
      filters[filterGroup] = [data_filter];
    } else {
      if (!filters[filterGroup]) {
        filters[filterGroup] = [];
      }

      if (listItem.classList.contains("selected")) {
        filters[filterGroup].push(data_filter);
      } else {
        filters[filterGroup] = filters[filterGroup].filter(
          (tag) => tag !== data_filter
        );
      }
    }

    // Combine the type filter with the selected tag filters using an AND relationship
    var filterValues = [];

    // Handle type filter
    if (filters["type"]) {
      filterValues.push("." + filters["type"][0]);
    }

    // Handle tags filter if it's defined
    if (filters["tag"]) {
      var selectedType = filters["type"][0];
      filters["tag"].forEach((tag) => {
        filterValues.push("." + selectedType + "." + tag);
      });
    }

    var finalFilter = filterValues.join(", ");
    console.log(finalFilter);

    iso.arrange({
      filter: finalFilter
    });
  }

  optionLinks.forEach(function (optionLink) {
    optionLink.addEventListener("click", function (event) {
      event.preventDefault();
      this.classList.toggle("selected");
      handleFilterClick(this, filters);
    });
  });
});
.post {
  padding: 100px;
}

.rSidebar__box {
  margin-bottom: 30px;
}
.rSidebar__options {
  padding-left: 0;
}
.rSidebar__options-li {
  margin-bottom: 17px;
  display: flex;
  align-items: center;
  cursor: pointer;
  width: fit-content;
}
.rSidebar__options-li.selected .rSidebar__options-square {
  background-color: #185A7D;
}
.rSidebar__options-square {
  height: 20px;
  width: 20px;
  transition: all 0.5s ease;
  border: 2px solid #000000;
}
.rSidebar__options-label {
  margin-left: 10px;
}

.grid {
  display: flex;
  flex-wrap: wrap;
  margin: -14px 0 0 -14px;
}
.grid-item {
  box-sizing: border-box;
  width: calc(45% - 14px);
  margin: 14px 0 18px 14px;
  border: 2px solid black;
  padding: 20px;
}
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<script src="https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.min.js"></script>


<div class="post">
  <div class="container">
    <div class="row justify-content-between">

      <!-- SIDEBAR -->
      <div class="col-3">
        <div class="rSidebar">

          <!-- tags -->
          <div class="rSidebar__box">
            <span class="rSidebar__label d-block fw-bold">Filter by tag</span>
            <ul class="rSidebar__options button-group" data-filter-group="tag">
              <li class="rSidebar__options-li" data-filter="ansible">
                <span class="rSidebar__options-square"></span>
                <span class="rSidebar__options-label d-block" data-filter="ansible">Ansible</span>
              </li>
                <li class="rSidebar__options-li" data-filter="automation">
                <span class="rSidebar__options-square"></span>
                <span class="rSidebar__options-label d-block" data-filter="automation">Automation</span>
              </li>
            </ul>
          </div>

          <!--  type -->
          <div class="rSidebar__box">
            <span class="rSidebar__label d-block fw-bold">Filter by type</span>
            <ul class="rSidebar__options button-group" data-filter-group="type">
              <li class="rSidebar__options-li" data-filter="blogs-and-news">
                <span class="rSidebar__options-square"></span>
                <span class="rSidebar__options-label d-block" data-filter="blogs-and-news">Blog & News</span>
              </li>
              <li class="rSidebar__options-li" data-filter="video-demos-tour">
                <span class="rSidebar__options-square"></span>
                <span class="rSidebar__options-label d-block" data-filter="video-demos-tours">Video Demos & Tours</span>
              </li>
            </ul>
          </div>
          <!-- end -->
        </div>
      </div>
      <!-- END -->

      <!-- GRID -->
      <div class="col-7">
        <div class="grid">
          <article class="resourceCard grid-item video-demos-tour automation"><span class="resourceCard__body-title">Card 1<br>Type: Video Demo & Tour<br>Tag: Automation</span></article>
          <article class="resourceCard grid-item blogs-and-news"><span class="resourceCard__body-title">Card 2<br>Type: Blog & News<br>Tag: Automation</span></article>
          <article class="resourceCard grid-item video-demos-tour automation ansible"><span class="resourceCard__body-title">Card 3<br>Type: Video Demo & Tour<br>Tags: Automation, Ansible</span></article>
          <article class="resourceCard grid-item blogs-and-news ansible"><span class="resourceCard__body-title">Card 4<br>Type: Blog & News<br>Tag: Ansible</span></article>
          <article class="resourceCard grid-item blogs-and-news ansible"><span class="resourceCard__body-title">Card 5<br>Type: Blog & News<br>Tags: Ansible, Automations</span></article>
        </div>
      </div>
      <!-- END -->

    </div>
  </div>
</div>

2

Answers


  1. If I understand correctly, you want to combine filters of the same group with OR and between groups with AND:

    {a,b} x {1,2} -> a1|a2|b1|b2
    

    In selectors (as used by Isotope), that would be:

    .a.1, .a.2, .b.1, .b.2
    

    You can build this by going over each group, combining every element with the previously build selectors:

    function buildFilterValues(){
      return Object.values(filters)  // extract filter arrays
        .reduce(
          (selectors, groupValues) => 
            groupValues.length === 0 ? selectors :  // skip empty groups
            groupValues.flatMap(v => selectors.map(s => `${s}.${v}`)) // combine group elements with existing selectors into new list
          , [''])
        .join(', ')
    }
    

    Some general hints and recommendations:

    • Card 2 and Card 5 miss the automation class, not sure if that is intentional
    • Instantiate the filter object with empty arrays for all existing groups, it spares you figuring out if the group property already exists later on
    • Separate DOM operations from filter data operations, it makes the code more maintainable
    • Consider creating a class for filter to further split up the code by concern

    Have a look at the updated snippet:

    class Filter{
      groups = {
        tag: [],
        type: [],
      }
      
      updateGroup(group, value, isSelected) {
        const isActiveValue = this.groups[group].includes(value)
        if (isSelected && !isActiveValue){
           this.groups[group].push(value)
        }
        if (!isSelected && isActiveValue){
           this.groups[group] = this.groups[group].filter(v => v !== value)
        }
      }
    
      buildSelector(){
        return Object.values(this.groups).reduce(
          (selectors, groupValues) => 
            groupValues.length === 0 ? selectors :
            groupValues.flatMap(v => selectors.map(s => `${s}.${v}`))
          , [''])
        .join(', ')
      }
      
      updateSelector(group, value, isSelected){
        this.updateGroup(group, value, isSelected)
        return this.buildSelector()
      }
    }
    
    document.addEventListener("DOMContentLoaded", function () {
    
      const iso = new Isotope('.grid', {
        itemSelector: ".resourceCard",
        layoutMode: "fitRows",
        transitionDuration: "0.5s"
      });
    
      const activeFilter = new Filter()
      
      function onOptionLinkClick(event){
        event.preventDefault();
        
        this.classList.toggle("selected");
        const isSelected = this.classList.contains("selected")
        
        const group = this
          .closest(".rSidebar__options")
          .getAttribute("data-filter-group");
        const value = this.getAttribute("data-filter");
    
        const filter = activeFilter.updateSelector(group, value, isSelected)
        iso.arrange({filter})
        
        console.clear()
        console.log('Current filter:', filter)
      }
    
      const optionLinks = document.querySelectorAll(".rSidebar__options-li");
      optionLinks.forEach(optionLink => optionLink.addEventListener("click", onOptionLinkClick));
    })
    .post {
      padding: 10px;
    }
    
    .rSidebar__box {
      margin-bottom: 30px;
    }
    .rSidebar__options {
      padding-left: 0;
    }
    .rSidebar__options-li {
      margin-bottom: 17px;
      display: flex;
      align-items: center;
      cursor: pointer;
      width: fit-content;
    }
    .rSidebar__options-li.selected .rSidebar__options-square {
      background-color: #185A7D;
    }
    .rSidebar__options-square {
      height: 20px;
      width: 20px;
      transition: all 0.5s ease;
      border: 2px solid #000000;
    }
    .rSidebar__options-label {
      margin-left: 10px;
    }
    
    .grid {
      display: flex;
      flex-wrap: wrap;
      margin: -14px 0 0 -14px;
    }
    .grid-item {
      box-sizing: border-box;
      width: calc(45% - 14px);
      margin: 14px 0 18px 14px;
      border: 2px solid black;
      padding: 20px;
    }
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <script src="https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.min.js"></script>
    
    
    <div class="post">
      <div class="container">
        <div class="row justify-content-between">
    
          <!-- SIDEBAR -->
          <div class="col-3">
            <div class="rSidebar">
              
              <!-- tags -->
              <div class="rSidebar__box">
                <span class="rSidebar__label d-block fw-bold">Filter by tag</span>
                <ul class="rSidebar__options button-group" data-filter-group="tag">
                  <li class="rSidebar__options-li" data-filter="ansible">
                    <span class="rSidebar__options-square"></span>
                    <span class="rSidebar__options-label d-block" data-filter="ansible">Ansible</span>
                  </li>
                    <li class="rSidebar__options-li" data-filter="automation">
                    <span class="rSidebar__options-square"></span>
                    <span class="rSidebar__options-label d-block" data-filter="automation">Automation</span>
                  </li>
                </ul>
              </div>
    
              <!--  type -->
              <div class="rSidebar__box">
                <span class="rSidebar__label d-block fw-bold">Filter by type</span>
                <ul class="rSidebar__options button-group" data-filter-group="type">
                  <li class="rSidebar__options-li" data-filter="blogs-and-news">
                    <span class="rSidebar__options-square"></span>
                    <span class="rSidebar__options-label d-block" data-filter="blogs-and-news">Blog & News</span>
                  </li>
                  <li class="rSidebar__options-li" data-filter="video-demos-tour">
                    <span class="rSidebar__options-square"></span>
                    <span class="rSidebar__options-label d-block" data-filter="video-demos-tours">Video Demos & Tours</span>
                  </li>
                </ul>
              </div>
              <!-- end -->
            </div>
          </div>
          <!-- END -->
    
          <!-- GRID -->
          <div class="col-7">
            <div class="grid">
              <article class="resourceCard grid-item video-demos-tour automation"><span class="resourceCard__body-title">Card 1<br>Type: Video<br>Tag: Automation</span></article>
              <article class="resourceCard grid-item blogs-and-news automation"><span class="resourceCard__body-title">Card 2<br>Type: Blog<br>Tag: Automation</span></article>
              <article class="resourceCard grid-item video-demos-tour automation ansible"><span class="resourceCard__body-title">Card 3<br>Type: Video<br>Tags: Automation, Ansible</span></article>
              <article class="resourceCard grid-item blogs-and-news ansible"><span class="resourceCard__body-title">Card 4<br>Type: Blog<br>Tag: Ansible</span></article>
              <article class="resourceCard grid-item blogs-and-news ansible automation"><span class="resourceCard__body-title">Card 5<br>Type: Blog<br>Tags: Ansible, Automation</span></article>
              <article class="resourceCard grid-item video-demos-tour ansible"><span class="resourceCard__body-title">Card 6<br>Type: Video <br>Tags: Ansible</span></article>
            </div>
          </div>
          <!-- END -->
    
        </div>
      </div>
    </div>

    Does that make sense, does it help?

    Login or Signup to reply.
  2. The question is: Let S = (0,1) ∪ (1,2) ∪ (3,4) and T = {0,1,2,3}. Then which of the following statements is(are) true?

    There are infinitely many functions from S to T.
    There are infinitely many strictly increasing functions from S to T.
    The number of continuous functions from S to T is at most 120.
    Every continuous function from S to T is differentiable.
    Let’s analyze each statement:

    There are infinitely many functions from S to T: This statement is true. A function from S to T is a rule that assigns each element in S to an element in T. Since there are 4 elements in T and each element in S can be assigned to any element in T, there are infinitely many such rules or functions.

    There are infinitely many strictly increasing functions from S to T: This statement is false. A strictly increasing function is one where if x1 < x2 then f(x1) < f(x2). Given the sets S and T, there are no strictly increasing functions because the set S has infinitely many elements while set T has only 4 elements.

    The number of continuous functions from S to T is at most 120: This statement is false. A continuous function is one where small changes in the input result in small changes in the output. Given the sets S and T, there are no continuous functions because the set S is a subset of real numbers (which is uncountable) while set T has only 4 elements (which is countable).

    Every continuous function from S to T is differentiable: This statement is false. Differentiability implies continuity but not vice versa. That means if a function is differentiable at a point then it must be continuous at that point but if a function is continuous at a point it need not be differentiable at that point. But as we established earlier, there are no continuous functions from S to T given the nature of sets S and T.

    So, the only true statement among these options is “There are infinitely many functions from S to T”.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search