How to create a button to filter articles by tag
Table of Contents
One thing I wanted to add to my digital garden was a way for users to choose the topics that they want to see. The motive behind this is that at the moment every article is being shown on the page. You can't really see the tags, categories or excerpt, the only thing you can see is the title.
After adding the search bar to the digital garden, I thought about adding a button for each tag. When a user clicks the button, it will filter the articles and show only the ones related to that tag.
To build this, the only thing I had to rely on was on the filter
method provided by javascript.
Getting All the Articles
The first thing we need to do is to get all the articles. On the digital garden, I have two types of posts - books and the digital garden ones.
When querying the graphql, I want to get only the digital garden ones. I am filtering the articles to get the ones that contain digital-garden
in the slug. I am also sorting the entries by date to show the newest ones first.
graphql1query {2 articles: allMdx(3 filter: {fields: {slug: {regex: "/digital-garden/"}}}, 4 sort: {fields: [frontmatter___date], order: DESC}) {5 nodes {6 frontmatter {7 title8 category9 excerpt10 tags11 }12 fields {13 slug14 }15 }16 }17 tags: allMdx {18 group(field: frontmatter___tags) {19 fieldValue20 }21 }22}
The Filtering Function
Filtering the articles is pretty straightforward. You can filter the articles, by a giving tag like this:
javascript1const filterArticles = (tag) => {2 const filtered = allArticles.filter(article => {3 if (article.frontmatter.tags.includes(tag)) {4 return article5 }6}
Note: frontmatter.tags
is a list containing all the tags defined in the frontmatter.
This function will iterate over every article and check if the list of tags
contains the given tag. Now, something funky happens when you start filtering over and over again.
If you use the function as it is, you can probably notice that:
- If an article shares two or more tags with others, it will be repeated on the list.
- If there are repeated articles, the list will grow past the length of all articles.
To tackle those issues, we need to check if the length of the articles filtered isn't, larger than the one containing all articles. We also want to make sure no duplicates are on the list.
javascript1const filterArticles = (tag) => {2
3 const filtered = allArticles.filter(article => {4
5 if (article.frontmatter.tags.includes(tag)) {6 if (allArticles.length !== articles.length && !articles.includes(article) ) {7 return article8 }9 }10
11 })12 13 setArticles(filtered)14
15 return16 }
Note: I am using the name articles to keep the state of the filtered articles.
Creating Buttons per tag
We can create a button per tag, by mapping over frontmatter.tags
. For each tag, we will create a button. On the graphql query we are getting a list of all tags with the query allMdx { group(field: frontmatter___tags) { fieldValue } }
.
javascript1{props.data.tags.group.map(2 tag => <button 3 key={tag.fieldValue} 4 onClick={() => filterArticles(tag.fieldValue)} 5 className="article-category m-2">6 {tag.fieldValue}7 </button>)}
Now that we know how to generate a button for each tag, but there is one issue with it. What if a user wants to see all articles again? As it is, the articles will be filtered and that's it.
To fix the issue, we can create another function that will set the state of articles
to all the articles obtained from the graphql query.
javascript1const getAllArticles = () => {2 setArticles(allArticles)3
4 return5 }
Then we also need to add another button so a user can get all the articles back.
javascript1<button onClick={() => getAllArticles()} className="article-category m-2">All</button>
Tie it All Together
We can tie all of these concepts together. Let's create the page containing the buttons and the articles.
javascript1import React, { useState } from "react"2import { graphql } from "gatsby"3
4import Layout from "../components/layout"5import Article from "../components/article" // Component with styling for article6
7
8const DigitalGarden = (props) => {9 10 const allArticles = props.data.articles.nodes11 const [articles, setArticles] = useState(allArticles)12
13 const filterArticles = (tag) => {14
15 const filtered = allArticles.filter(article => {16
17 if (article.frontmatter.tags.includes(tag)) {18 if (allArticles.length !== articles.length && !articles.includes(article) ) {19 return article20 }21 return article22 }23
24 })25 26 setArticles(filtered)27
28 return29 }30
31 const getAllArticles = () => {32 setArticles(allArticles)33
34 return35 }36
37 return (38
39 <Layout>40 <h1 className="mb-5 mt-12 plane text-center">Digital Garden</h1>41 <section className="flex flex-wrap justify-center">42 <button onClick={() => getAllArticles()} className="article-category m-2">All</button>43 {props.data.tags.group.map(tag => 44 <button 45 key={tag.fieldValue} 46 onClick={() => filterArticles(tag.fieldValue)} 47 className="article-category m-2">48 {tag.fieldValue}49 </button>)}50 </section>51 <section className="flex flex-col py-12">52 {articles.map(article => <Article key={article.frontmatter.title} article={article} />)}53 54 </section>55 </Layout> 56 )57
58}59
60export default DigitalGarden61
62export const pageQuery = graphql`63{64 articles: allMdx(65 filter: {fields: {slug: {regex: "/digital-garden/"}}}, 66 sort: {fields: [frontmatter___date], order: DESC}) {67 nodes {68 frontmatter {69 title70 category71 excerpt72 tags73 }74 fields {75 slug76 }77 }78 }79 tags: allMdx {80 group(field: frontmatter___tags) {81 fieldValue82 }83 }84}85`