Open-Source Pattern Library Tutorial: Part 4 — Searchable List

Marcello Paiva
cross.team
Published in
5 min readDec 20, 2019

In this portion of the tutorial, we are going to build a searchable list that the user can select patterns from. To do this we’ll need to install a new plugin, and use that plugin to build a wrapper that will ultimately hold our search bar and list of patterns.

Elasticlunr Plugin

Elasticlunr is a lightweight search engine built in JavaScript. We are going to be using it via a Gatsby plugin called @gatsby-contrib/gatsby-plugin-elasticlunr-search. So the first thing to do is install that plugin. Run the following command in your root directory:

yarn add @gatsby-contrib/gatsby-plugin-elasticlunr-search

Now we have to tell the Gatsby config how to handle this plugin. Add the following object to the plugins array in gatsby-config.js:

{
resolve: `@gatsby-contrib/gatsby-plugin-elasticlunr-search`,
options: {
// Fields to index
fields: [`title`, `tags`, `caption`, `category`, `subcategory`],
// How to resolve each field`s value for a supported node type
resolvers: {
// For any node of type MarkdownRemark, list how to resolve the fields` values
MarkdownRemark: {
title: node => node.frontmatter.title,
slug: node => node.fields.slug,
tags: node => node.frontmatter.tags,
caption: node => node.frontmatter.caption,
category: node => node.frontmatter.category,
subcategory: node => node.frontmatter.subcategory,
}
}
}
},

The fields array in the plugin's options sets which fields will be indexed to be searched. The resolvers object connects each field to the actual data node in GraphQL. If you want to know more about how this plugin works you can go here to learn more.

Modifying Index.js

Go to your src/pages/index.js file and paste the following code:

import React from 'react'
import '../styles/index.css'
import { graphql } from 'gatsby'
import Layout from '../components/layout'
import SearchWrapper from '../components/search-wrapper'
import SEO from "../components/seo"

export default ({ data }) => {
const mapMarkdown = ({allMarkdownRemark}) => allMarkdownRemark.edges.map(({node}) => ({
id: node.id,
title: node.frontmatter.title,
tags: node.frontmatter.tags,
slug: node.fields.slug,
assets: node.frontmatter.assets,
caption: node.frontmatter.caption,
category: node.frontmatter.category,
subcategory: node.frontmatter.subcategory
}))
return (
<Layout >
<SEO title="Pattern Library" />
<SearchWrapper data={ mapMarkdown(data) } />
</Layout>
)
}

export const query = graphql`
query IndexQuery {
allMarkdownRemark {
edges {
node {
frontmatter {
title
tags
caption
category
subcategory
assets {
asset {
image,
caption
}
}
}
id
fields {
slug
}
html
}
}
}
}
`

The first thing worth noting in this new index.js file is the GraphQL query that is now sitting at the bottom of it. We are querying for all relevant fields of every markdown file in our src/pattern/ directory. We then take the data from that array as a prop and pass it through our mapMarkdown() function, which maps over it and returns a new array with an easy to reference object with a key for each pattern field. We then pass that as a prop to our SearchWrapper component and render that.

Creating SearchWrapper

The next step is to create a new file in src/components/ called search-wrapper.js. This file will contain some of the elasticlunr indexing logic. Paste the following code inside the file:

import React from 'react'
import { graphql, StaticQuery } from 'gatsby'
import Search from './search'

const SearchWrapper = ({ data }) => {
return (
<StaticQuery
query={graphql`
query SearchIndexQuery {
siteSearchIndex {
index
}
}
`}
render={ indexData => (
<Search data={ data } searchIndex={indexData.siteSearchIndex.index} />
)}
/>
)
}

export default SearchWrapper

In this component we utilize Gatsby’s StaticQuery to query for the search index and we pass that index data as a prop to the Search component that we render, along with the data from from index.js.

Creating Search

Now we need to create a new file in src/components/ called search.js. In this file, we build out the UI for the list view and we finish writing in the rest of the search logic. Paste the following code inside the file:

import React, { useState } from 'react'
import Link from 'gatsby-link'
import {
makeStyles,
TextField,
List,
Divider,
Typography,
Chip,
Hidden,
} from '@material-ui/core'
import { Index } from 'elasticlunr'

const useStyles = makeStyles(theme => ({
textField: {
width: '100%',
margin: theme.spacing(4),
backgroundColor: '#fff',
},
list: {
width: '100%',
backgroundColor: '#fff',
border: '1px solid rgba(0, 0, 0, 0.12)',
},
link: {
textDecoration: 'none',
color: 'inherit',
},
listItem: {
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'wrap',
paddingLeft: '16px',
paddingRight: '16px',
width: '100%',
position: 'relative',
boxSizing: 'border-box',
textAlign: 'left',
alignItems: 'center',
paddingTop: '8px',
paddingBottom: '8px',
textDecoration: 'none'
},
titleLine: {
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
},
listTitle: {
marginRight: theme.spacing(2),
},
category: {
fontWeight: 'bold',
marginRight: theme.spacing(1),
},
rightMargin: {
marginRight: theme.spacing(1),
},
}))

const Search = ({ data, searchIndex }) => {
const [query, setQuery] = useState(``)
const [results, setResults] = useState([])
let index

const classes = useStyles()

let patterns = data || []

const listItems = items => items.map(({ title, id, tags, slug, caption, category, subcategory }, pIndex) => {
const chips = tags ? tags.map((tag) =>
<Chip label={tag} color="primary" className={classes.rightMargin} />
) : []
return (
<li key={id}>
<Link to={slug} className={classes.link}>
<div className={classes.listItem}>
<div>
<div className={classes.titleLine}>
<Typography className={classes.listTitle}>{title}</Typography>
<div>
<Typography variant='caption' className={classes.category}>{category}:</Typography>
<Typography variant='caption' >{subcategory}</Typography>
</div>
</div>
<Typography variant='caption'>{caption}</Typography>
</div>
<Hidden xsDown><div>{chips}</div></Hidden>
</div>
{pIndex < items.length - 1 && <Divider />}
</Link>
</li>
)})

const getOrCreateIndex = () => {
return index ? index : Index.load(searchIndex)
}

const search = query => index.search(`${query}`, { expand: true })
const mapResults = results => results.map(({ ref }) => index.documentStore.getDoc(ref))
const handleSearch = evt => {
const query = evt.target.value
index = getOrCreateIndex()
setQuery(query)
const mappedResults = mapResults(search(query))
setResults(mappedResults)
}

return (
<>
<TextField
id="search"
className={classes.textField}
label='Search'
placeholder='Search'
variant="outlined"
value={query}
onChange={handleSearch}
/>
<List className={classes.list}>
{ query ? listItems(results) : listItems(patterns) }
</List>
</>
)
}

export default Search

This file uses two different data sets to render our list of patterns, which is generated by the listItems function. Whenever the value within the searchBar is changed the handleSearch function is triggered and it saves it to the query state with the new value and also creates a new index to query the string against. It then saves the result of that query in the results state.

If there is a query in the search bar, the Search component will use results to generate the list of patters. If not, it will use the data that was passed from the query in the index.js file.

Now that the searchable list view is complete, push your code up to master and wait for your Netlify site to deploy. Once it’s done building, go to your site and you should see a page that looks like this:

Pattern library list view

In the next part of the tutorial we’re going to use React contexts to allow us to switch between our list view that we just built and a new card view.

Part 5 — Card View

If you are having issues or want to look at the source code for this part, you can checkout the part4 branch of the plib-tutorial repo:

--

--

Marcello Paiva
cross.team

I am a front-end web developer with a passion for accessibility!