Create a “coupon generator” string field input
Take the guesswork out of creating fields with correct values and automate content creation for authors.
This developer guide was contributed by Simeon Griggs (Principal Educator).
Take the guesswork out of creating fields with correct values and automate content creation for authors.
What you need to know
This guide assumes that you know how to set up and configure Sanity Studio and have basic knowledge about defining a schema with document and field types. Basic knowledge of React and TypeScript is also useful, although you should be able to copy-paste the example code to get a runnable result.
The code examples import Code from @sanity/ui/code, which requires @sanity/ui 4.0.0 or later. Sanity Studio v6.9.2 and later include a compatible version.
Custom form components by example
One of Sanity Studio’s most powerful features is custom drop-in replacements for form fields. This guide is one in a series of code examples.
You can get more familiar with the Form Components API in the documentation.
- Create a “coupon generator” string field input
- Create a visual string selector field input
- Create a survey rating number field input
- Create a time duration object field
- Create an array input field with selectable templates
- Create interactive array items for featured elements
- Create richer array item previews
- Create a document form progress component
What you’ll be making
A string field that can generate its own valid strings:
Create the schema types
In this example, you’ll build a custom coupon generator. In this instance, a coupon is a string field that is four characters long, containing only uppercase letters and numbers.
The simplest way to add this sort of input to a document in Sanity would be a string field with a validation rule. This is functional but not pleasant to your authors. With a custom input they can generate these codes in one click while still having full control over the editing input.
Create a new field in your studio, and register it to your schema in sanity.config.ts:
// ./schema/coupon/couponType.ts
import {defineType} from 'sanity'
export const couponType = defineType({
name: 'coupon',
title: 'Coupon',
description: 'A unique, all uppercase, four-character alphanumeric code',
type: 'string',
validation: (rule) =>
rule
.min(4)
.max(4)
.regex(/^[A-Z0-9]+$/),
})Creating a new schema type for this string lets you reuse it throughout your studio. For example, multiple document types can use this coupon field type with its custom input, but with unique options. After you import this schema type into your studio’s schema, you can refer to this type by its name value: type: 'coupon'.
Add the coupon field type to a document type’s fields:
// ./schema/storeType.ts
import {defineField, defineType} from 'sanity'
export const storeType = defineType({
name: 'store',
title: 'Store',
type: 'document',
fields: [
defineField({
name: 'title',
type: 'string',
}),
defineField({
name: 'coupon',
type: 'coupon',
}),
],
})Register both schema types in the schema.types array of your sanity.config.ts file:
// ./sanity.config.ts
import {defineConfig} from 'sanity'
import {couponType} from './schema/coupon/couponType'
import {storeType} from './schema/storeType'
export default defineConfig({
// ...all other settings
schema: {
types: [couponType, storeType],
},
})In your studio, create a new store document. The title and coupon fields both render as default string inputs:
Create an input component
Create a new component in your studio with the following code:
// ./schema/coupon/CouponInput.tsx
import {Box, Button, Flex} from '@sanity/ui'
import {StringInputProps} from 'sanity'
export function CouponInput(props: StringInputProps) {
return (
<Flex gap={3} align="center">
<Box flex={1}>{props.renderDefault(props)}</Box>
<Button mode="ghost" text="Generate coupon" />
</Flex>
)
}Notice the following:
- On line 3, the imports from Sanity UI help you create custom components that use the same UI and design language as the rest of the studio. For more information, see Get started with Sanity UI.
- On line 9,
props.renderDefault(props)renders the original string input, so you don’t need to handle complex APIs like validation and presence yourself.
To use this component, you’ll need to load it into the correct slot back on the coupon schema. You’ll use input here because you don’t want to replace the field’s title and description.
// ./schema/coupon/couponType.ts
import {defineType} from 'sanity'
import {CouponInput} from './CouponInput'
export const couponType = defineType({
name: 'coupon',
// ...all other settings
components: {input: CouponInput},
})Open your store document again. The coupon field now shows your custom input and a Generate coupon button, but the button doesn’t do anything yet.
To make the button write a value to the field, use the onChange function from the input’s props.
Handle changes and patch data
Custom inputs contain helpful functions and details in their props. This input only needs one: onChange.
This function wraps any patch (such as setting or unsetting the value of a field) and keeps the rest of the studio up to date with changes.
When working with forms in React, a common recommendation is to store values in a component’s state. In Sanity Studio input components, this is an anti-pattern: content written to state is only reflected in the browser of the person using the input. Sanity’s real-time APIs let content creators collaborate and avoid overwriting each other’s changes by always syncing directly to the Content Lake.
Update your CouponInput component to use the following code:
// ./schema/coupon/CouponInput.tsx
import {Box, Button, Flex} from '@sanity/ui'
import {Code} from '@sanity/ui/code'
import {set, StringInputProps} from 'sanity'
import {useCallback} from 'react'
export function CouponInput(props: StringInputProps) {
// onChange handles patches to the document
const {onChange} = props
const generateCoupon = useCallback(() => {
const coupon = Math.random().toString(36).substring(2, 6).toUpperCase()
// "set()" will write a value to this field
onChange(set(coupon))
}, [onChange])
return (
<Flex gap={3} align="center">
<Box flex={1}>{props.renderDefault(props)}</Box>
{/* Display the value in a monospaced font */}
{props.value ? <Code size={4}>{props.value}</Code> : null}
<Button mode="ghost" onClick={generateCoupon} text="Generate coupon" />
</Flex>
)
}- Notice how
onChangeis destructured from the component’sprops. - The
onChangefunction is then used insidegenerateCoupon, with theset()function, to update the field’s value. The new value is validated immediately and updated in the browser of any other authors viewing the same document. - The
generateCouponfunction is registered with auseCallbackhook to cache it between re-renders. - Sometimes these codes can contain easily confused characters (such as
0andO), so rendering the current field’s value in the<Code>component can make them clearer.
Now you have a fully functional, automated, and editable coupon generator field with a handy visual preview!
Next steps
Some ideas to extend this custom input include:
- Extend the
couponfield’s validation rule to userule.custom()and check that no other document in the dataset contains the same coupon code. - Add configurability to the
couponfield schema, such as a setting inoptionsto determine the length of the generated coupon string. - Change the
generateCouponfunction and the field validation so that they don’t create or allow potentially confusing characters such as0andO. - Import
unsetfromsanityand add an extra button to remove the coupon from the field.
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?