Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic function schema "builders" and conditionally required #686

Open
heggemsnes opened this issue Feb 10, 2024 · 0 comments
Open

Generic function schema "builders" and conditionally required #686

heggemsnes opened this issue Feb 10, 2024 · 0 comments

Comments

@heggemsnes
Copy link

heggemsnes commented Feb 10, 2024

Hi!

In our Sanity schemas we often create custom functions instead of reusable fields to give more customisation and control over the schema building. An example of this is a re-usable externalLink function that takes the required as a prop, see the example here:

// Define a generic FieldDef type that captures additional properties via TOtherProps
export type FieldDef<
  T,
  TName extends string,
  TRequired extends boolean = boolean,
  TOtherProps = object,
> = Omit<T, "type"> & {
  name: TName
  required: TRequired
  group?: string
} & TOtherProps


export function externalLinkField<
  TName extends string,
  TRequired extends boolean,
  TOtherProps = object,
>(props: FieldDef<UrlDefinition<TRequired>, TName, TRequired, TOtherProps>) {
  if (props.required === true) {
    return defineField({
      ...props,
      type: "url",
      options: {
        ...props.options,
        required: props.required,
      },
      validation: (Rule) => {
        const rules = [
          Rule.uri({
            scheme: ["https", "http", "mailto", "tel"],
          }).error(
            'Invalid URL. The URL must start with "https://", "http://", "mailto:" or "tel:',
          ),
          Rule.required().error("This field is required."),
        ]
        return rules
      },
    })
  }
  return defineField({
    ...props,
    type: "url",
    options: {
      ...props.options,
      required: props.required,
    },
    validation: (Rule) => {
      const rules = [
        Rule.uri({
          scheme: ["https", "http", "mailto", "tel"],
        }).error(
          'Invalid URL. The URL must start with "https://", "http://", "mailto:" or "tel:',
        ),
      ]
      return rules
    },
  })
}

This works fine but is a bit verbose. It does not work with a conditional validation on the rule.

A reason why we want to do this is to be more aware about validation when setting up schemas, as well as render a simple custom input component when fields are required (by checking schemaType.options.required).

My first question is then – is there any better way to do this pattern?

We also use these kinds of functions for fields with different options. A good example is this "figure" schema:

const altText = defineField({
  name: "alt",
  title: "Alt text",
  type: "string",
  description:
    "Describe the content of the image. Important for SEO and accessiblity.",

  hidden: ({
    parent,
  }: {
    parent: {
      decorative: boolean
    }
  }) => {
    return parent?.decorative
  },
  validation: (Rule) =>
    Rule.custom((field, context) => {
      const parent = context.parent as ImageFieldsType

      if (!parent) return true
      if (parent?.decorative || !parent?.asset || (field && field.length > 0)) {
        return true
      }

      return "Alt text is required"
    }),
})

export const figureField = <
  TName extends string,
  TRequired extends boolean,
  TOtherProps extends {
    showAlt?: boolean
    showDecorative?: boolean
    showCaption?: boolean
  },
>(
  props: FieldDef<
    ImageDefinition<
      true,
      {
        name: TName
      },
      TRequired
    >,
    TName,
    TRequired,
    TOtherProps
  >,
) => {
  const {
    name,
    title,
    group,
    hidden,
    showAlt = true,
    showCaption = true,
    showDecorative = true,
  } = props
  //const excludeAllFields = excludeCaption && excludeAlt

  return defineField({
    name,
    title: title,
    type: "image",
    group,
    hidden,
    options: {
      hotspot: true,
    },
    fields: [
      ...(showAlt ? [altText] : []),
      ...(showDecorative
        ? [
            defineField({
              name: "decorative",
              title: "Is the image purely decorative?",
              type: "boolean",
              initialValue: false,
            }),
          ]
        : []),
      ...(showCaption
        ? [
            defineField({
              name: "caption",
              title: "Caption",
              type: "string",
            }),
          ]
        : []),
    ],
    validation: (Rule) => {
      return props.required ? Rule.required().assetRequired() : Rule.optional()
    },
    preview: {
      select: {
        imageUrl: "asset.url",
        title: "caption",
      },
      prepare({ title, imageUrl }) {
        return {
          title: title ?? " ",
          imageUrl,
        }
      },
    },
  })
}

Since neither of these add on fields are always required, they are defined as possibly undefined in the schema which is fine, but if we add a required to i.e. the caption our type expects it to always be a string regardless.

figureField({
      name: "figure",
      title: "Figure",
      showCaption: false,
      showAlt: false,
      required: false,
    }),
    
 // Inferred type:
 figure: {
        _type: "image";
        asset: {
            _ref: string;
            _type: "reference";
            [referenced]: "sanity.imageAsset";
        };
        alt?: string | undefined;
        decorative?: boolean | undefined;
        caption: string;
        crop: {
            _type?: "sanity.imageCrop" | undefined;
            left: number;
            bottom: number;
            right: number;
            top: number;
        };
        hotspot: {
            _type?: "sanity.imageHotspot" | undefined;
            width: number;
            height: number;
            x: number;
            y: number;
        };
    };   

Is there any way to make this work? I guess I can solve this case by making the required conditional based on the showCaption

Also for some reason the "hack" with duplicate defineField does not work for this figureField.

Hopefully this is not out of bounds of this package, because it is a very powerful tool to create schemas. Any pointers or tips would be greatly appreciated!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant