Dynamically group list items with a GROQ filter
In this article, we'll use the documentList() method to dynamically group documents with a GROQ filter.
It's often useful to group documents automatically by some field's value or a combination of field values. Common examples are grouping documents by author, publishing date periods, editorial status, category, or even the dominant background color in a document’s main image. In this article, we'll create lists of filtered blog posts to allow for quicker discovery and editing.
If you're unfamiliar with setting up the Structure Builder API, be sure to check out the previous articles in this series.
Learning the Structure Builder API
This collection of articles will walk you through all the basics of using Structure Builder to create custom editing experiences.
In this article, we'll need some basic schema for a blog. For the sake of simplicity, we'll use the default schema that comes from creating a new Sanity project from the Sanity CLI.
To get the schema, run npx sanity init
and create a new project. When prompted, select yes
to Use default dataset configuration?
and Blog
from the Select project template
options.
This will give you a project structure that contains the schema for post
, which contains references for author
and category
schema. Combining this with the singletons made in the previous articles, we should have a desk structure that looks like this:
To start creating our filters, we'll first create a manual group to house our two dynamic lists. For a review, read this article on creating a manual group with Structure Builder.
First, we'll create a new listItem()
for our "Base" list. We'll give it the title "Filtered Posts" and a .child()
node that will be a static list with the title "Filters."
This list will have two items, our filtered lists "Posts by Category" and "Posts by Author."
// /deskStructure.js
// ./deskStructure
export const myStructure = (S) =>
S.list()
.title('Base')
.items([
S.listItem()
.title('Filtered Posts')
.child(
S.list()
.title('Filters')
.items([
S.listItem().title('Posts By Category').child(),
S.listItem().title('Posts By Author').child(),
])
),
// The rest of this document is from the original manual grouping in this series of articles
...S.documentTypeListItems().filter(
(listItem) => !['siteSettings', 'navigation', 'colors'].includes(listItem.getId())
),
S.listItem()
.title('Settings')
.child(
S.list()
.title('Settings Documents')
.items([
S.listItem()
.title('Metadata')
.child(S.document().schemaType('siteSettings').documentId('siteSettings')),
S.listItem()
.title('Site Colors')
.child(S.document().schemaType('colors').documentId('colors')),
S.listItem()
.title('Main Navigation')
.child(S.document().schemaType('navigation').documentId('navigation')),
])
),
])
This will create a list of two items. Neither of those items will have children yet. To populate them, we'll use dynamic lists using GROQ queries.
To grab blog posts by category, we need to create a child for our listItem
that will pull a documentTypeList
. This list will show all categories in the dataset.
// /deskStructure.js
S.listItem()
.title('Posts By Category')
.child(
S.documentTypeList('category')
.title('Posts by Category')
.child(),
)
This will create a list of items with a document type that matches the string 'category'
. From here, we need to fill in what this item's child will be. In our case, we want to create a list of all the documents that match the category clicked.
// /deskStructure.js
S.listItem()
.title('Posts By Category')
.child(
S.documentTypeList('category')
.title('Posts by Category')
.child(categoryId =>
S.documentList()
.title('Posts')
.filter('_type == "post" && $categoryId in categories[]._ref')
.params({ categoryId })
)
),
The .child()
method can accept an anonymous "arrow function", which will have the _id
of the current item passed into it. From there, we need to define what type of child we're creating.
The .documentList()
method will pull a list of documents given a filter. It accepts most of the same chained methods as the .list()
method but has a few special methods.
The .filter()
method is not the normal JavaScript filter method. In this case, it's a function that will accept a GROQ query as a string and return an array of documents that match that query. We can optionally chain a .parameter()
method to pass a parameter into our query. In this case, the categoryId
from our current function scope.
The GROQ query here will match all documents with a _type
of post
, containing the $categoryId
as a reference in its categories
array.
At this point, we have the post documents that match our query pulling into the next pane.
Now, let's do the same process to pull posts by author reference into the "Post by Author" node.
// /deskStructure.js
S.listItem()
.title('Posts By Author')
.child(
S.documentTypeList('author')
.title('Posts by Author')
.child(authorId =>
S.documentList()
.title('Posts')
.filter('_type == "post" && $authorId == author._ref')
.params({ authorId })
)
),
Now that we have a "Filtered Posts" group, let's rename our "Post" document type list. To do this, we'll create a new manual list item in the "Base" list group. For a more in-depth explanation, see this article on creating singleton documents. In this example, we'll create a new listItem()
for the post document type, give it a new title, and create a child panel with a document list filtering all posts. From there, we'll add the 'post'
ID to our exclusion filter for all other document types in this list.
// /deskStructure.js
S.list()
.title('Base')
.items([
S.listItem()
.title('Filtered Posts')
.child(/* Dynamic lists */ ),
S.listItem()
.title('All Posts')
.child(
/* Create a list of all posts */
S.documentList()
.title('All Posts')
.filter('_type == "post"')
),
/* List the other document types adding 'post' to the list to exclude */
...S.documentTypeListItems().filter(listItem => !['post', 'siteSettings', 'navigation', 'colors'].includes(listItem.getId())),
/* Finish with our Settings item */
S.listItem()
.title('Settings')
.child()
])
This is beginning to look finished. We can help increase an editor's understanding of the grouping by adding dividers between the various sections.
To group things together, we'll use the S.divider()
method in our .items()
array. We want to group "All Posts" and "Filtered Posts" together, then allow the rest of our document types to flow in the middle, then our "Settings." To do this, we'll insert the divider method in the order we want it to appear.
// /deskStructure.js
S.list()
.title('Base')
.items([
S.listItem()
.title('Filtered Posts')
.child(/* Dynamic lists */ ),
S.listItem()
.title('All Posts')
.child(
/* Create a list of all posts */
S.documentList()
.title('All Posts')
.filter('_type == "post"')
),
S.divider(),
...S.documentTypeListItems().filter(listItem => !['post', 'siteSettings', 'navigation', 'colors'].includes(listItem.getId())),
S.divider(),
S.listItem()
.title('Settings')
.child()
])
Putting together all the examples from this series of articles we get the following desk structure.
// ./deskStructure.js
export const deskStructure = (S) =>
S.list()
.title('Base')
.items([
S.listItem()
.title('Site Config')
.child(
S.list()
// Sets a title for our new list
.title('Settings Documents')
// Add items to the array
// Each will pull one of our new singletons
.items([
S.listItem()
.title('Metadata')
.child(S.document().schemaType('settings').documentId('siteSettings')),
S.listItem()
.title('Site Colors')
.child(S.document().schemaType('colors').documentId('colors')),
S.listItem()
.title('Main Navigation')
.child(S.document().schemaType('navigation').documentId('navigation')),
])
),
S.divider(),
S.listItem()
.title('Filtered Posts')
.child(
S.list()
.title('Filters')
.items([
S.listItem()
.title('Posts By Category')
.child(
S.documentTypeList('category')
.title('Posts by Category')
.child((categoryId) =>
S.documentList()
.title('Posts')
.filter('_type == "post" && $categoryId in categories[]._ref')
.params({categoryId})
)
),
S.listItem()
.title('Posts By Author')
.child(
S.documentTypeList('author')
.title('Posts by Author')
.child((authorId) =>
S.documentList()
.title('Posts')
.filter('_type == "post" && $authorId == author._ref')
.params({authorId})
)
),
])
),
S.listItem().title('All Posts').child(
/* Create a list of all posts */
S.documentList().title('All Posts').filter('_type == "post"')
),
S.divider(),
...S.documentTypeListItems().filter(
(listItem) => !['settings', 'post', 'colors', 'navigation'].includes(listItem.getId())
),
])
Now that we've created singletons, static lists, and dynamic lists, we need to look at creating tabs and custom previews for our document views.