Custom object preview in Portable Text block not working

11 replies
Last updated: Sep 28, 2021
Hi, I'm having trouble figuring out how to properly preview a custom object in a portable text block. Here is the custom block that I insert into the portable text object:

// objects/metric.js
import {IoTrendingUpOutline} from 'react-icons/io5'

export default {
  title: 'Metric',
  name: 'metric',
  type: 'object',
  icon: IoTrendingUpOutline,
  fields: [
    {
      title: 'Label',
      name: 'label',
      type: 'string'
    },
    {
      title: 'Key Number',
      name: 'keyNumber',
      type: 'string'
    },
    {
      title: 'Metric Type',
      name: 'metricType',
      type: 'string',
      options: {
        list: [
          {title: 'Number', value: 'number'},
          {title: 'Increase', value: 'increase'},
          {title: 'Decrease', value: 'Decrease'}
        ]
      }
    }
  ],
  preview: {
    select: {
      label: 'metric.label',
      keyNumber: 'metric.keyNumber',
      metricType: 'metric.metricType'
    },
    prepare (data) {
      let subtitlePrepend = ''
      switch (data.metricType) {
        case 'increase':
          subtitlePrepend = 'Increase in'
          break
        case 'decrease':
          subtitlePrepend = 'Decrease in'
          break
        default:
          subtitlePrepend = ''
          break
      }
      return {
        ...data,
        title: data.keyNumber,
        subtitle: subtitlePrepend && data.label.join(' ')
      }
    }
  }
}
is my switch or the use of
prepare (data)
messing it up in some way? Should I try and move the switch logic into a
previewComponent
(like is shown in this videoEmbedPreview example )?
AI Update

Looking at your code, I can spot the issue! In your preview.select, you're using paths like metric.label, metric.keyNumber, etc., but since this schema IS the metric object itself, those fields are at the root level, not nested under metric.

Here's the corrected version:

preview: {
  select: {
    label: 'label',        // Not 'metric.label'
    keyNumber: 'keyNumber', // Not 'metric.keyNumber'
    metricType: 'metricType' // Not 'metric.metricType'
  },
  prepare(data) {
    let subtitlePrepend = ''
    switch (data.metricType) {
      case 'increase':
        subtitlePrepend = 'Increase in'
        break
      case 'decrease':
        subtitlePrepend = 'Decrease in'
        break
      default:
        subtitlePrepend = ''
        break
    }
    return {
      title: data.keyNumber,
      subtitle: subtitlePrepend ? `${subtitlePrepend} ${data.label}` : data.label
    }
  }
}

A couple of other things I noticed:

  1. The metric. prefix: You'd only use dot notation like metric.label if you were accessing a referenced document or nested object. Since these fields are directly on this object, reference them directly.

  2. The data.label.join(' '): Your label field is a string, not an array, so calling .join() on it would cause an error. I changed it to just use data.label directly.

  3. The subtitle logic: I simplified it to use a template literal for cleaner string concatenation.

Your switch logic and prepare function approach are totally fine! You don't need to move to a custom previewComponent for this - the standard prepare function is perfect for transforming data before display. Custom preview components are mainly useful when you need more complex rendering beyond title/subtitle/media.

Show original thread
11 replies
Using what you wrote
user M
its an improvement... but not quite there:the Subtitle still isn't rendering
Disregard my last response. It works perfectly, I missed something when copying & 🍝 πŸ€¦β€β™‚οΈ TYSM!!! πŸ™ πŸ™Œ
βœ…
So, only problem with your solution compared to the switch case method, is i'd like to not show anything in the beginning if Number is selected (as you can see in the attached screenshot). Maybe we can do this with a ternary statement?
This is what I'm trying so far:
...
  preview: {
    select: {
      title: 'keyNumber',
      label: 'label',
      metricType: 'metricType'
    },
    prepare ({title, label, metricType}) {
      return {
        title,
        subtitle: (metricType[0] !== 'number' ? `${metricType.charAt(0).toUpperCase() + metricType.slice(1)} in ` : ``) + label
      }
    }
  }
...
but, I think theres something wrong with the
metricType[0]
, because its still giving me the Number in Survey Respondents
but, I think theres something wrong with the selection, because its still giving me the Number in Survey Respondents
try:
subtitle: `${metricType != 'number' ? `${metricType.charAt(0).toUpperCase() + metricType.slice(1)} in ` : '' }${label}`
works perfectly! (searches for chef's kiss emoji) πŸ‘©β€πŸ³
thanks again you're my MVP of the day
user M
Someone could argue that a ternary is less readable than a good old
if
or
switch
statement, but if that happens feel free to blame me!
haha important thing is it's working and takes up less space ✨

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?