React - How to Display a List of Schemas/Content Types but Only Allow the User to Select One

16 replies
Last updated: May 9, 2024
Hey šŸ‘‹ Just a quick question, is it possible to display a list of schemas/content types but only allow the user to select one?
AI Update

Yes! It's definitely possible to display a list of schemas/content types and restrict selection to just one. This is exactly what the reference field type is designed for in Sanity Studio.

How it works

When you set up a reference field, you specify which document types can be referenced using the to property. Here's a basic example:

{
  name: 'myReference',
  type: 'reference',
  to: [
    {type: 'person'},
    {type: 'company'},
    {type: 'organization'}
  ]
}

This will show the user a list of all three document types (person, company, organization) to choose from, but they can only select one document from those types.

Key points

  • The reference field UI lets users search, browse, and select from the allowed document types
  • Users can only select one document at a time (unless you wrap it in an array)
  • You can filter which documents appear using the filter option if you need more control
  • By default, references are "strong" - meaning they enforce referential integrity

If you need multiple selections

If you actually want users to select multiple documents, you'd wrap the reference in an array:

{
  name: 'myReferences',
  type: 'array',
  of: [
    {
      type: 'reference',
      to: [{type: 'person'}, {type: 'company'}]
    }
  ]
}

But based on your question about selecting "only one," the standard reference field should be exactly what you need!

Like radio inputs somewhat?
Just made a quick example and attached a screenshot (ignore the names they make little sense šŸ˜†). Hoping to allow a user to select one, and only one, of the two options (in this example, Navigation Item or Link). I am just using an array type at the moment so can limit it to 1 using validation but this catches the errors after they’ve added too much content (they assume they did it by accident) which doesn’t seem great. Any help is super appreciated šŸ˜„
Ah! I see.
We have exactly the same thing at work and we solved it with a
.max(1)
validation rule.
It may seem annoying that you can effectively have several items in a draft, but it’s actually pretty handy for editors, rather than having to delete their item first before creating a new one.
And you ensure that a production document can’t have more than 1 with the validation rule. Win-win. šŸ™‚
Yeh good points! Thanks for the quick reply šŸ™‚. It would be cool to see how others have dealt with it
user A
has previously helped me with a custom array part which should do what you’re after! Once the array has the maximum number of items (set using
validation: (Rule) => Rule.max(x)
), the ā€˜Add item…’ button is removed:
Here’s the
LimitArray.js
part:
import React from "react";
import { isReferenceSchemaType } from "@sanity/types";
import { AddIcon } from "@sanity/icons";
import { Button, Grid, Menu, MenuButton, MenuItem } from "@sanity/ui";
import { useId } from "@reach/auto-id";

const LimitArray = React.forwardRef((props, ref) => {
    const { type, readOnly, children, onCreateValue, onAppendItem, value } = props;
    const menuButtonId = useId();
    const insertItem = React.useCallback(
        (itemType) => {
            const item = onCreateValue(itemType);
            onAppendItem(item);
        },
        [onCreateValue, onAppendItem]
    );
    const handleAddBtnClick = React.useCallback(() => {
        insertItem(type.of[0]);
    }, [type, insertItem]);
    if (readOnly) {
        return null;
    }
    const maxLength = type.validation[0]._rules.find((rule) => rule.flag === "max");
    if (maxLength && value && value.length >= maxLength.constraint) {
        return null;
    }
    return (
        <Grid
            gap={1}
            style={{ gridTemplateColumns: "repeat(auto-fit, minmax(100px, 1fr))" }}
            ref={ref}
        >
            {type.of.length === 1 ? (
                <Button icon={AddIcon} mode="ghost" onClick={handleAddBtnClick} text="Add item" />
            ) : (
                <MenuButton
                    button={<Button icon={AddIcon} mode="ghost" text="Add item…" />}
                    id={menuButtonId || ""}
                    menu={
                        <Menu>
                            {type.of.map((memberDef, i) => {
                                const referenceIcon =
                                    isReferenceSchemaType(memberDef) &&
                                    (<http://memberDef.to|memberDef.to> || []).length === 1 &&
                                    <http://memberDef.to[0].icon;|memberDef.to[0].icon;>
                                const icon =
                                    memberDef.icon || memberDef.type?.icon || referenceIcon;
                                return (
                                    <MenuItem
                                        key={i}
                                        text={memberDef.title || memberDef.type?.name}
                                        onClick={() => insertItem(memberDef)}
                                        icon={icon}
                                    />
                                );
                            })}
                        </Menu>
                    }
                />
            )}
            {children}
        </Grid>
    );
});

export default LimitArray;
You need to add this file to your
parts
in your
sanity.json
as follows:
{
    "implements": "part:@sanity/form-builder/input/array/functions",
    "path": "./schemas/parts/LimitArray.js"
}
And then you can use this by setting
validation
on an array field:
{
    title: "Content",
    name: "content",
    type: "array",
    of: [{ type: "img" }, { type: "vid" }, { type: "word" }],
    validation: (Rule) => Rule.max(2),
},
Wow, thanks for sharing šŸ‘
Just tried it and it works perfectly šŸ‘Œ Thanks again!
2 months later, still coming in clutch. Thanks
user A
and
user P
Sorry for bumping this, but have any of you tried implementing this with v3? Finding it quite nice to have a media type array where the user can add either image or video, but would be extra nice to have the button to be removed if the max length is reached. Now that i think of it, would be even nicer to have the text of the button change to ā€œremove mediaā€ or ā€œchange mediaā€.
I’ve actually been trying to make a plugin that does the same thing in V3! I haven’t had a chance to publish it yet, but here it is:

// Array form component

export const LimitArray = (props) => {

  const {
    schemaType: { validation = [] },
  } = props;

const validationRules = validation
    .flatMap((rule) => rule._rules)
    .find((rule) => rule.flag === "max");

  const arrayLimit = validationRules ? validationRules.constraint : undefined;

  const arrayHasMaxElements = members.length >= arrayLimit ? true : false;

 return arrayHasMaxElements ? props.renderDefault({ ...props, arrayFunctions: () => null }): props.renderDefault(props);
};
Then add this component to your sanity.config file via the form option:

export default defineConfig({
    ...
	form: {
      components: {
        input: (props) => {
          if (props.schemaType.name === "array") {
            return LimitArray(props)
          }
          return props.renderDefault(props);
        },
      },
    },
});
Niceee Simon! Gonna take it for a spin, thanks!

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?