🎤 Builder Talk: The Story Behind Lady Gaga’s Digital Experience – Register now

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?
May 6, 2022, 3:32 PM
Like radio inputs somewhat?
May 6, 2022, 3:47 PM
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 😄
May 6, 2022, 3:56 PM
Ah! I see.
May 6, 2022, 3:57 PM
We have exactly the same thing at work and we solved it with a
.max(1)
validation rule.
May 6, 2022, 3:57 PM
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.
May 6, 2022, 3:58 PM
And you ensure that a production document can’t have more than 1 with the validation rule. Win-win. 🙂
May 6, 2022, 3:58 PM
Yeh good points! Thanks for the quick reply 🙂. It would be cool to see how others have dealt with it
May 6, 2022, 4:02 PM
May 6, 2022, 5:11 PM
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:
May 6, 2022, 5:47 PM
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),
},
May 6, 2022, 5:48 PM
Wow, thanks for sharing 👍
May 6, 2022, 6:45 PM
Just tried it and it works perfectly 👌 Thanks again!
May 6, 2022, 7:37 PM
2 months later, still coming in clutch. Thanks
user A
and
user P
Jun 29, 2022, 8:02 PM
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”.
May 4, 2024, 10:22 PM
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);
        },
      },
    },
});
May 9, 2024, 3:26 PM
Niceee Simon! Gonna take it for a spin, thanks!
May 9, 2024, 3:43 PM

Sanity– build remarkable experiences at scale

Sanity is a modern headless CMS that treats content as data to power your digital business. Free to get started, and pay-as-you-go on all plans.

Was this answer helpful?