Discussing methods to easily remove nested objects in Sanity schemas

9 replies
Last updated: May 22, 2024
Hey all! πŸ™‚Is there any way to allow easy deleting an object field inside another object? I have a "IntroText" component that has a "CTA" Button component which is optional. When I want to remove an existing button I have to 1. delete the title and 2. remove the page reference. Is there a way to show a "Remove" button on the "CTA" button that just removes everything belonging to the object?
AI Update

Yes, you can definitely add a "Remove" button to clear out your entire CTA object field at once! The best approach is to create a custom input component that adds a remove button alongside the default object input.

Here's how to implement it using the correct custom input component pattern:

import {ObjectInputProps, set, unset} from 'sanity'
import {Button, Stack} from '@sanity/ui'
import {TrashIcon} from '@sanity/icons'

function CTAInput(props: ObjectInputProps) {
  const {renderDefault, onChange, value} = props
  
  const handleRemove = () => {
    // Use unset() to remove the entire object field
    onChange(unset())
  }
  
  return (
    <Stack space={3}>
      {renderDefault(props)}
      {value && ( // Only show button if CTA exists
        <Button
          text="Remove CTA"
          icon={TrashIcon}
          tone="critical"
          mode="ghost"
          onClick={handleRemove}
        />
      )}
    </Stack>
  )
}

// In your schema definition:
defineField({
  name: 'cta',
  type: 'object',
  title: 'CTA Button',
  fields: [
    {name: 'title', type: 'string'},
    {name: 'page', type: 'reference', to: [{type: 'page'}]}
  ],
  components: {
    input: CTAInput
  }
})

The key here is using onChange(unset()) directly. According to the PatchEvent documentation, when you call onChange with unset(), it creates the appropriate patch to remove the entire field. This is the proper way to handle patches in custom input components.

The renderDefault(props) function renders the standard object input with all your fields (title and page reference), and the button appears below it. When clicked, it removes the entire CTA object in one go, which is exactly what you're looking for!

The conditional {value && ...} ensures the remove button only appears when there's actually a CTA to remove, keeping the UI clean when the field is empty.

This approach works perfectly with Sanity's real-time collaboration system and gives you full control over the button's appearance and placement.

Schema:

export const introTextComponentType = defineType({
  name: 'introTextComponent',
  title: 'Intro Text',
  type: 'object',
  preview: {
    select: {
      title: 'title',
    },
  },
  icon: TextIcon,
  fields: [
    defineField({
      name: 'title',
      type: 'headline',
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'subTitle',
      type: 'string',
    }),
    defineField({
      name: 'cta',
      title: 'CTA',
      type: 'button',

      options: {
        collapsible: true,
      },
    }),
  ],
})
export const buttonType = defineType({
  name: 'button',
  type: 'object',
  fields: [
    defineField({
      name: 'buttonText',
      title: 'Button Text',
      type: 'string',
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'link',
      type: 'link',
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'variation',
      type: 'string',
      initialValue: 'contained',
      options: {
        list: [
          {title: 'Contained Style', value: 'contained'},
          {title: 'Text Style', value: 'text'},
        ],
      },
      validation: (Rule) => Rule.required(),
    }),
  ],
})
so basically I just want to show an "Delete" or "Remove" button next to the "CTA" title. Only workaround I can think of is an array with a max length of 1
You could also add a boolean field to enable/disable the button even if there is text or a reference already there.
When the boolean field is false, you can set to hide the text reference field. When the boolean field is true, you can set some validation rules to make sure the user to filling out the fields.
thanks for your quick answer
user R
!So I guess there is no (easy) way of just allowing the user to remove the existing object? i.e. an
optional
parameter or similar?
I want to avoid adding logic on the parent component
Well you wont have to add the logic to the parent component but to the button component itself and then when you fetch the data for the CTA, you only fetch when the button is enabled.
If you do want to go down the route of adding a reset or remove button, that would have to be a custom component which in this case shouldn't be too difficult to create
I guess I will go with an array for now, as I also have references to pages in the button, which will not be removed when I set the boolean to
false
. or do you see any issues with the array approach?
(except that it does not look so nice) πŸ™‚
I dont see an issue going with the array approach, I also use it for my reusable modules where I have set it to only allow a max of 1 module. Sounds like the best option for your case since you want to remove the text and reference in one go which having it in a array will allow you to do without having to go into creating a custom component

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?