loading...
Cover image for Click Event Filtering on a JSON Rendered List in Vue.js

Click Event Filtering on a JSON Rendered List in Vue.js

alexmourer profile image Alex Mourer ・5 min read

In this tutorial, we'll explore basic click event filtering on a rendered list using JSON data in Vue.js. This is a basic tutorial, but it should be a solid foundation to start building something more complex.

TL;DR
Here is an example of a working CodeSandbox
https://codesandbox.io/s/kkvr7z5rr3
working-example

Why would we need to do this?

Filtering data is a part of my everyday life as a front-end dev. I'm often presented with a large piece of data that needs to be looped through, filtered, and displayed. In this tutorial, we will fetch() a JSON file, build a rendered list from the data, and filter the list with click event controls.

We will not be using any dependencies outside of Vue.js.

How do we do this?

First, we'll need to have Vue.js up and running. This is covered here, or you can set up a CodeSandbox.

If you used the Vue CLI or CodeSandbox to set up your app, you'll likely already have the initial structure of your project in place. In case you don't, you'll need a place to display our future component.

// App.vue file
<template>
  <div id="app">
  </div>
</template>
<script>
  export default {
    name: "App"
  }
</script>

The above code is a good starting point for this tutorial. In the provided CodeSandbox example, this file is named App.vue. The code shown here will serve as the foundation for displaying the component we will build.

In this tutorial, we will be placing the filter controls and rendered list inside the same component. If this were a larger application or a longer tutorial, I would likely split them in two and share data between. I've written a separate tutorial on sharing data between multiple components here.

Okay, let's start building out our component.

In the provided example the file we are working with is named DataDisplay.vue.

// DataDisplay.vue file
<template>
  <div>
  </div>
</template>

<script>
  export default {
    name: "DataDisplay"
  };
</script>

The first thing we'll work on is data. I've created sample JSON data through a service called JSON Generator. To bring the data into our DataDisplay.vue file, we'll fetch() the data in the created lifecycle hook. Information on lifecycle hooks can be found here. All of the data returned will be stored in the data property users.

// DataDisplay.vue file
<template>
  <div>
  </div>
</template>

<script>
  export default {
    name: "DataDisplay"
    data: function() {
      return {
        users: []
      }
    },
    created() {
      var apiURL = "https://next.json-generator.com/api/json/get/4JCnNiTCr";
      fetch(apiURL)
        .then(res => res.json())
        .then(res => (this.users = res))
        .catch(error => console.log(error));
    }
  }
</script>

Now that we have our data stored, we can work on displaying it.

Let's loop through the data stored in the users property with Vue's v-for directive. The v-for directive requires a syntax structure of x in y or (x, i) in y. You can also use of as the delimiter instead of in. In this example, our syntax is "(entry, index) in users", where users is the data source, entry is an alias for the element being iterated on, and index is the index of the item in the rendered list.

The test data provided in the example is a list of developers with some associated information for each. We'll render their name and main coding language in an unordered list. You can view the full JSON file here.

// DataDisplay.vue file
<template>
  <div>
    <ul class="userWrap">
      <li
        v-for="(entry, index) in users"
        :item="entry"
        :key="index"
        class="user"
      >
        <h2>{{ entry.name }}</h2>
        <span>
            Primary Language: <strong>{{ entry.mainLanguage }}</strong>
        </span>
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    name: "DataDisplay"
    data: function() {
      return {
        users: []
      }
    },
    created() {
      var apiURL = "https://next.json-generator.com/api/json/get/4JCnNiTCr";
      fetch(apiURL)
        .then(res => res.json())
        .then(res => (this.users = res))
        .catch(error => console.log(error));
    }
  }
</script>

You should see a list of developers and their main coding language. We can now build out our buttons to filter this list.

We will be adding a list of buttons, a series of data properties, and the v-if directive to our results list.

Starting with the data properties. The fkey property is the data field that we will be keying our filtering on. filterList is an array of filter values we'll be checking our data against. The list of filter buttons will be built off filterList as well. filter contains the value of the current set filter. By default, we want All of the users to show.

data: function() {
  return {
    fkey: "mainLanguage",
    filterList: ["JavaScript", "Python", "PHP", "Java", "All"],
    filter: "All",
    users: []
  }
}

Let's build out our filter control buttons.

We will use the v-for directive again. This time, to iterate through the filterList array and generate our filtering values. You'll notice two new pieces in our rendered list properties, @click & :class directives. The @click will set the value for filter when the button is clicked. :class will set the button's class as active when entry === filter.

<button
  v-for="(entry, index) in filterList"
  :item="entry"
  :key="index"
  @click="filter = entry;"
  :class="{ active: entry == filter }"
>
  {{ entry }}
</button>

Next, we will connect our filtering buttons to our rendered user list.

To do this, we will add Vue's v-if directive to our list's properties. Our example uses v-if="entry[fkey] === filter || filter === 'All'". If our entry's mainLaguage is equal to filter or if filter is set to 'All', it will return true and show the entry.

<ul class="userWrap">
  <li
    v-for="(entry, index) in users"
    v-if="entry[fkey] === filter || filter === 'All'"
    :item="entry"
    :key="index"
    class="user"
  >
    <h2 class="title">{{ entry.name }}</h2>
    <span class="language">
      Primary Language: <strong>{{ entry.mainLanguage }}</strong>
    </span>
  </li>
</ul>

This is the full DataDisplay.vue file. I added in some CSS for fun.

// DataDisplay.vue
<template>
  <div>
    <div>
      <button
        v-for="(entry, index) in filterList"
        :item="entry"
        :key="index"
        @click="filter = entry; active = index;"
        :class="{ active: entry == filter }"
      >
        {{ entry }}
      </button>
    </div>
    <ul class="userWrap">
      <li
        v-for="(entry, index) in users"
        v-if="entry[fkey] === filter || filter === 'All'"
        :item="entry"
        :key="index"
        class="user"
      >
        <h2 class="title">{{ entry.name }}</h2>
        <span class="language">
          Primary Language: <strong>{{ entry.mainLanguage }}</strong>
        </span>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "DataDisplay",
  data: function() {
    return {
      fkey: "mainLanguage",
      filterList: ["JavaScript", "Python", "PHP", "Java", "All"],
      filter: "All",
      users: []
    };
  },
  created() {
    var apiURL = "https://next.json-generator.com/api/json/get/4JCnNiTCr";
    fetch(apiURL)
      .then(res => res.json())
      .then(res => (this.users = res))
      .catch(error => console.log(error));
  }
};
</script>

<style scoped>
button {
  background: #74b6cc;
  border: none;
  color: #fff;
  padding: 10px;
  margin: 5px;
}
button.active {
  background: #0089ba;
}
.userWrap {
  list-style-type: none;
  padding: 2%;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  flex-direction: row;
}
.user {
  padding: 10px;
  margin: 1% 0;
  border: 1px solid #ddd;
  border-radius: 3px;
  width: 45%;
  text-align: left;
}
h2.title {
  font-size: 1.3rem;
  font-weight: bold;
  margin: 0;
}
.language {
  display: block;
  font-size: 0.9rem;
}
</style>

The last step is to import our DataDisplay component into our App.vue file.

// App.vue
<template>
  <div id="app">
    <DataDisplay />
  </div>
</template>

<script>
import DataDisplay from "./components/DataDisplay";

export default {
  name: "App",
  components: {
    DataDisplay
  }
};
</script>

🍻

Posted on by:

alexmourer profile

Alex Mourer

@alexmourer

Alex is an award-winning designer, Senior Front-End Dev @Healthx, and an avid sticker maker. He has worked for over 10 years as a web developer and designer.

Discussion

pic
Editor guide
 

I'm not sure but this also could be like this:

<template>
  <div>
    <div>
      <button
        v-for="(entry, index) in filterList"
        :item="entry"
        :key="index"
        @click="
          filter = entry;
          active = index;
        "
        :class="{ active: entry == filter }"
      >
        {{ entry }}
      </button>
    </div>
    <ul class="userWrap">
      <li v-for="(entry, index) in users" :key="index" class="user">
        <h2 class="title">{{ entry.name }}</h2>
        <span class="language"
          >Primary Language: <strong>{{ entry.mainLanguage }}</strong></span
        >
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "DataDisplay",
  data: function() {
    return {
      fkey: "mainLanguage",
      filterList: ["JavaScript", "Python", "PHP", "Java", "All"],
      filter: "All",
      users: [],
      fetchedUsers: []
    };
  },
  created() {
    var apiURL = "https://next.json-generator.com/api/json/get/4JCnNiTCr";
    fetch(apiURL)
      .then(res => res.json())
      .then(res => (this.fetchedUsers = res))
      .catch(error => console.log(error));
  },
  watch: {
    fetchedUsers: function() {
      this.users = this.fetchedUsers;
    },
    filter: function(val, oldVal) {
      this.users =
        val != "All"
          ? this.fetchedUsers.filter(f => f.mainLanguage === val)
          : this.fetchedUsers;
    }
  }
};
</script>

I used watch property.

 

I was expecting to read a similar implementation πŸ˜…
I guess separating all the filtering logic from markup feels more maintainable to me.

 

how to solve this ...

[Vue warn]: Property or method "enter" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: vuejs.org/v2/guide/reactivity.html....

found in

---> at src/components/DataDisplay.vue
at src/App.vue

 

Well written, thank you for sharing your knowledge, it is really helpful

 

Great tutorial! Any suggestions on adding multiple filter sets based on the json data?