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?