0

I am writing a class using ES2016 and I don't understand why, the console throws back at me this error.

I've looked around for a possible cause but:

  • it can't be a typo in the function name, I have checked again and again and the function declaration and the function name I use to invoke it match;
  • highlightSelected does not share its name with any of the pre-existing properties;
  • all the operation I perform inside the highlightSelected function are allowed on the variable type passed as parameter.

So what is going on? Why isn't this working?

Uncaught TypeError: this.highlightSelected is not a function
    at HTMLAnchorElement.<anonymous> (app.js:40) 

window.document.addEventListener('DOMContentLoaded', function() {
  new Filter("nf");
});

class Filter {
  constructor(category) {
    this.products = [...document.querySelectorAll('.product')];
    this.filterButtons = [...document.querySelectorAll('a.filter')];
    this.lastClicked = null;
    this.registerListeners();
    console.log(this.filterProducts(category));
  }

  filterProducts(category) {
    let filtered = this.products.filter(function(item) {
      return (item.dataset[category] === "true");
    });

    return filtered;
  }

  registerListeners() {
    this.filterButtons.map(function(button) {
      button.addEventListener("click", function(event) {
        console.log(event.target.dataset.cat + " has been clicked!");
        if (this.lastClicked !== event.target.dataset.cat) {
          this.highlightSelected(event.target)
          this.lastClicked = event.target;
        }
      });
    });
  }

  highlightSelected(clickedButton) {
    if ((this.lastClicked !== undefined) && (this.lastClicked !== null)) {
      if (this.lastClicked.classList.contains('currently-selected')) {
        this.lastClicked.classList.remove('currently-selected');
      }

      if (!clickedButton.classList.contains('currently-selected')) {
        clickedButton.classList.add('currently-selected');
      }
    }
  }
}
html {
  font-size: 16px;
}

.container {
  width: 85%;
  margin: 4% auto;
}

#button-area {
  display: flex;
  justify-content: space-around;
  margin-bottom: 4rem;
}

#button-area a {
  text-decoration: none;
  font-family: sans-serif;
  background-color: mistyrose;
  color: #000;
  padding: .8rem 2rem;
}

#button-area a.currently-selected {
  background-color: purple;
  color: white;
}

#product-area {
  display: flex;
  justify-content: space-around;
}

.product {
  font-family: sans-serif;
  font-size: 1rem;
  background-color: goldenrod;
  padding: .8rem 2rem;
}
<html>

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>Query Selector</title>
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div class="container">

    <div id="button-area">
      <a class="filter" href="#" data-cat="ef"> Cat E </a>
      <a class="filter" href="#" data-cat="lf"> Cat L </a>
      <a class="filter" href="#" data-cat="gf"> Cat G </a>
      <a class="filter" href="#" data-cat="nf"> Cat N </a>
    </div>

    <div id="product-area">
      <div class="product" data-ef="true" data-lf="true" data-nf="true" data-gf="true">
        <p> Car </p>
      </div>
      <!-- /. product -->

      <div class="product" data-ef="false" data-lf="false" data-nf="false" data-gf="false">
        <p> Airplane </p>
      </div>
      <!-- /. product -->

      <div class="product" data-ef="true" data-lf="false" data-nf="false" data-gf="true">
        <p> Pizza </p>
      </div>
      <!-- /. product -->

      <div class="product" data-ef="true" data-lf="false" data-nf="false" data-gf="true">
        <p> Ficus </p>
      </div>
      <!-- /. product -->

      <div class="product" data-ef="false" data-lf="false" data-nf="true" data-gf="true">
        <p> Keyboard </p>
      </div>
      <!-- /. product -->

      <div class="product" data-ef="true" data-lf="false" data-nf="false" data-gf="false">
        <p> Shirt </p>
      </div>
      <!-- /. product -->

      <div class="product" data-ef="true" data-lf="false" data-nf="true" data-gf="true">
        <p> Vanilla </p>
      </div>
      <!-- /. product -->
    </div>

  </div>

</body>

<script src="app.js"></script>

</html>
haunted85
  • 1,601
  • 6
  • 24
  • 40

2 Answers2

1

When you bind a method to an event, this is no longer the class. To preserve your context, you can use an intermediate variable:

This example shows the problem...

window.setTimeout(this.showText, 50);

The showText method will be correctly called, but any use of this inside the method will fall... fix:

let _this = this;
window.setTimeout(function () { _this.showText(); }, 50);

You could also use bind, apply, or arrow functions for the same effect. This is one cat that can be skinned in many ways. Arrow functions have similar browser support to ECMAScript classes, so you won't be losing any browsers of you do that.

Fenton
  • 241,084
  • 71
  • 387
  • 401
0

Your problem lies in this chunk of code:

registerListeners() {
  this.filterButtons.map(function(button) {
    button.addEventListener("click", function(event) {
      console.log(event.target.dataset.cat + " has been clicked!");
      if (this.lastClicked !== event.target.dataset.cat) {
        this.highlightSelected(event.target)
        this.lastClicked = event.target;
      }
    });
  });
}

The problem is that JavaScript uses function scoping so this inside of map and addEventListener are not the instance of Filter. There are a few ways of fixing this but for you, the easiest would be to use arrow functions since they retain the same lexical this.

registerListeners() {
  this.filterButtons.map((button) => {
    button.addEventListener("click", (event) => {
      console.log(event.target.dataset.cat + " has been clicked!");
      if (this.lastClicked !== event.target.dataset.cat) {
        this.highlightSelected(event.target)
        this.lastClicked = event.target;
      }
    });
  });
}

For more in-depth information on this, check out How to access the correct this inside a callback?

Mike Cluck
  • 31,869
  • 13
  • 80
  • 91