React

How to filter all MDX articles by tags

How to create a button to filter articles by tag

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.

graphql
1query {
2 articles: allMdx(
3 filter: {fields: {slug: {regex: "/digital-garden/"}}},
4 sort: {fields: [frontmatter___date], order: DESC}) {
5 nodes {
6 frontmatter {
7 title
8 category
9 excerpt
10 tags
11 }
12 fields {
13 slug
14 }
15 }
16 }
17 tags: allMdx {
18 group(field: frontmatter___tags) {
19 fieldValue
20 }
21 }
22}

The Filtering Function

Filtering the articles is pretty straightforward. You can filter the articles, by a giving tag like this:

javascript
1const filterArticles = (tag) => {
2 const filtered = allArticles.filter(article => {
3 if (article.frontmatter.tags.includes(tag)) {
4 return article
5 }
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.

javascript
1const 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 article
8 }
9 }
10
11 })
12
13 setArticles(filtered)
14
15 return
16 }

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 } }.

javascript
1{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.

javascript
1const getAllArticles = () => {
2 setArticles(allArticles)
3
4 return
5 }

Then we also need to add another button so a user can get all the articles back.

javascript
1<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.

javascript
1import React, { useState } from "react"
2import { graphql } from "gatsby"
3
4import Layout from "../components/layout"
5import Article from "../components/article" // Component with styling for article
6
7
8const DigitalGarden = (props) => {
9
10 const allArticles = props.data.articles.nodes
11 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 article
20 }
21 return article
22 }
23
24 })
25
26 setArticles(filtered)
27
28 return
29 }
30
31 const getAllArticles = () => {
32 setArticles(allArticles)
33
34 return
35 }
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 DigitalGarden
61
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 title
70 category
71 excerpt
72 tags
73 }
74 fields {
75 slug
76 }
77 }
78 }
79 tags: allMdx {
80 group(field: frontmatter___tags) {
81 fieldValue
82 }
83 }
84}
85`

Webmentions

0 Like 0 Comment

You might also like these