-
Notifications
You must be signed in to change notification settings - Fork 3k
More control over array items' labels #1843
-
It'd be great to have more control over the labels of an array field's items.
Consider the following screenshot, please.
UX would be so much better if there were a way to replace Question 01 with the actual question title. Or maybe such a mechanism already exists and I just failed to find it?
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 12 comments 11 replies
-
👍🏻 If you don't plan to implement this, getting pointers on how to implement it would be helpful as well.
Beta Was this translation helpful? Give feedback.
All reactions
-
There's a labels option there, but it doesn't really do the job I want. In theory, I might have put a React element there, but that would count as cheating the type check and therefore can easily turn broken on the next upgrade
Beta Was this translation helpful? Give feedback.
All reactions
-
There is the following option, but I don't see how you get the actual title in there because it just gives you the id:
{
type: 'array',
name: 'questions',
label: 'Questions',
minRows: 0,
maxRows: 20,
admin: {
components: {
RowLabel: ({ data, index, path }) => {
if (data.question) {
return data.question; // this will return the ID
}
return `Question ${index + 1}`;
},
},
}
}
Beta Was this translation helpful? Give feedback.
All reactions
-
Hey @Swahjak you can do exactly what you are looking for (and more) by creating a React component to render your row label. Here is an example derived from your code snippet above:
// QuestionRowLabel.tsx import React from 'react' import { useField } from 'payload/components/forms' export const QuestionRowLabel = ({ data, index, path: arrayFieldPath }) => { // arrayFieldPath example: "questions.0" const path = `${arrayFieldPath}.question` const { value } = useField({ path }) if (value) { // assuming question is a text field inside the array return data.question; } return `Question ${index + 1}`; }
And you would import that into your config file like so:
// example array field with custom component { type: 'array', name: 'questions', label: 'Questions', minRows: 0, maxRows: 20, admin: { components: { RowLabel: QuestionRowLabel, }, } }
Beta Was this translation helpful? Give feedback.
All reactions
-
🚀 1
-
How should this be done with Payload 3 beta?
Beta Was this translation helpful? Give feedback.
All reactions
-
hi @Qubica! i saw this pattern somewhere in the 3.0 code, can't seem to find it now, but this is how i have custom labels working:
src/
├── app/
└── payload/
├── components/
│ └── row-label/
│ ├── get-row-label.tsx
│ └── index.tsx
└── fields/
└── link.ts
// row-label/index.ts 'use client'; import { useRowLabel } from '@payloadcms/ui'; import { Data } from 'payload/types'; export function RowLabel({ path, fallback }: { path: string; fallback: string }) { const { data, rowNumber } = useRowLabel<Data>(); const fieldValue: any = path.split('.').reduce((value, part) => value?.[part], data); return <>{fieldValue || `${fallback} ${rowNumber}`}</>; }
// get-row-label.tsx import { RowLabel } from '@/payload/components/row-label'; export const getRowLabel = (path: string, fallback: string) => ( <RowLabel path={path} fallback={fallback} /> );
// link.ts import { Field } from 'payload/types'; import { getRowLabel } from '@/payload/components/row-label/get-row-label'; const fields: Field[] = [...]; export const linkArray: Field = { name: 'links', type: 'array', admin: { components: { RowLabel: () => getRowLabel('text', 'Link'), }, }, interfaceName: 'FieldLinkArray', fields, };
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
I don't think this is working anymore. I am on a new beta version (3beta55 to be precise) and all I get from the useRowLabel hook is:
{
"data": {},
"path": "",
"rowNumber": undefined
}
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 6
-
@tomekwlod did you manage to get some data from useRowLabel? I have the same issue.
Edit: Figured it out. I needed to install @payloadcms/ui explicitly, although I did have type resolution working without it.
Beta Was this translation helpful? Give feedback.
All reactions
-
The docs for the array field have a working example of how to change the label using another field.
import { CollectionConfig } from 'payload/types' import { RowLabelArgs } from 'payload/dist/admin/components/forms/RowLabel/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'slider', // required type: 'array', // required label: 'Image Slider', minRows: 2, maxRows: 10, interfaceName: 'CardSlider', // optional labels: { singular: 'Slide', plural: 'Slides', }, fields: [ // required { name: 'title', type: 'text', }, { name: 'image', type: 'upload', relationTo: 'media', required: true, }, { name: 'caption', type: 'text', }, ], admin: { components: { RowLabel: ({ data, index }: RowLabelArgs) => { return data?.title || `Slide ${String(index).padStart(2, '0')}` }, }, }, }, ], }
Beta Was this translation helpful? Give feedback.
All reactions
-
I have a very basic/simple working version for v3 beta.90:
ArrayFieldTitle.tsx
'use client' import type { Data } from 'payload' import { useRowLabel } from '@payloadcms/ui' function ArrayFieldTitle() { const { data, rowNumber } = useRowLabel<Data>(); return ( <> {`Component: ${ data?.componentTitle}` || `Component ${String(rowNumber).padStart(2, '0')}`} </> ) } export default ArrayFieldTitle
In this case my Array Field is declared as follows:
{ name: 'myComponents', type: 'array', label: 'Components', minRows: 1, admin: { components: { RowLabel: 'src/components/shared/ArrayFieldTitle.tsx', }, }, fields: [ { type: 'row', fields: [ { name: 'componentTitle', type: 'text', label: 'Component Title', }, ], }, { type: 'row', fields: [ { name: 'parts', type: 'textarea', label: 'Parts', }, ], }, { type: 'row', fields: [ { name: 'steps', type: 'textarea', label: 'Steps to Build', }, ], }, ],
Hope it helps.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 2
-
this is a good solution for v3 beta.90.
And for anyone wondering, you can add props to your function like so.
'use client' import type { Data } from 'payload' import { useRowLabel } from '@payloadcms/ui' function ArrayFieldTitle ({itemPlaceholder}: {itemPlaceholder?: string}) { const { data, rowNumber } = useRowLabel<Data>(); return ( <> {`${ data?.title}` || `${itemPlaceholder}: ${String(rowNumber).padStart(2, '0')}`} </> ) } export default ArrayFieldTitle
And add then add the clientProps property to the RowLabel
{ name: 'myComponents', type: 'array', label: 'Components', minRows: 1, admin: { components: { RowLabel: { path: "src/payload/components/shared/ArrayFieldTitle.tsx", clientProps: { itemPlaceholder: 'placeholder text' }, }, }, }, ... ... }
more info about this can be found here in the the beta docs
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 2
-
Figured out how to update the Select Options array in the formBuilderPlugin using this method as well (in case it helps anyone else):
formBuilderPlugin({ fields: { payment: false, }, formOverrides: { fields: ({ defaultFields }) => { return defaultFields.map((field) => { if ('name' in field && field.name === 'confirmationMessage') { return { ...field, editor: lexicalEditor({ features: ({ rootFeatures }) => { return [ ...rootFeatures, FixedToolbarFeature(), HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }), ] }, }), } } if ('type' in field && field.type === 'blocks') { return { ...field, blocks: field.blocks.map((block) => { if ('slug' in block && block.slug === 'select') { return { ...block, fields: block.fields.map((thisField) => { if ('type' in thisField && thisField.type==='array' && 'name' in thisField && thisField.name === 'options') { return { ...thisField, admin: { components: { RowLabel: { path: 'src/components/shared/ArrayFieldTitle.tsx', clientProps: { labelPrefix: 'Option: ', // Optional - Prefix text for your option label labelValueField: 'label', // Optional - Field to use for each option label - ensure it exactly matches field name you want to use exactly }, }, }, } }; } return thisField; }) }; } // Return the original block if not the "select" field return block; }), } } return field }) }, }, }),
Also, evolution of my original component solution, so you can choose the field that you want to use as the label (thanks to @adameli22 for the inspiration to not be lazy! :) ):
'use client' import type { Data } from 'payload' import { useRowLabel } from '@payloadcms/ui' function ArrayFieldTitle({ labelPrefix, labelValueField}: { labelPrefix?: string, labelValueField?: string }) { const { data, rowNumber } = useRowLabel<Data>(); const displayOptionValue = labelValueField && data ? data[labelValueField as keyof Data] : undefined; return ( <> {`${ labelPrefix ? labelPrefix : (displayOptionValue ? '' : 'Item: ')}${ displayOptionValue || String(rowNumber).padStart(2, '0') }`} </> ) } export default ArrayFieldTitle
Beta Was this translation helpful? Give feedback.
All reactions
-
❤️ 1
-
I've implemented the following that handles multilingual support and relational fields.
Here is the component:
"use client"; import { getTranslation } from "@payloadcms/translations"; import { useRowLabel, useTranslation } from "@payloadcms/ui"; import { CollectionSlug, LabelFunction, StaticLabel } from "payload"; import { useEffect, useState } from "react"; const RowLabel = ({ defaultLabel, path, relationTo }: { defaultLabel: LabelFunction | StaticLabel; path: string; relationTo?: CollectionSlug }) => { const [label, setLabel] = useState<string | null>(null); const { i18n } = useTranslation() const { data, rowNumber } = useRowLabel<any>(); useEffect(() => { if (relationTo) { const [field, property] = path.split("."); fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/${relationTo}/${data?.[field]}`).then((res) => res.json()).then((res) => { setLabel(res[property]); }); } }, []); if(label) return label; let generated: any = data; for (const segment of path.split(".")) { generated = generated?.[segment]; } if (generated) return generated; return `${getTranslation(defaultLabel, i18n)} ${String(rowNumber).padStart(2, "0")}`; }; export default RowLabel;
The function I call on the fields:
import type { CollectionSlug, LabelFunction, StaticLabel } from "payload"; export function createRowLabel({ defaultLabel, path, relationTo }: { defaultLabel: LabelFunction | StaticLabel; path: string; relationTo?: CollectionSlug }) { return { path: "@payload/components/row-label", clientProps: { defaultLabel: defaultLabel, path: path, relationTo: relationTo, }, }; }
And the way to use it:
{ type: "array", name: "menu", label: "Menu", labels: { singular: { fr: "Élément", en: "Item", }, plural: { fr: "Éléments", en: "Items", }, }, admin: { initCollapsed: true, components: { RowLabel: createRowLabel({ defaultLabel: { fr: "Élément", en: "Item", }, path: "link.label", }), }, }, fields: [linkField()], },
Beta Was this translation helpful? Give feedback.
All reactions
-
❤️ 1
-
I think this is really needed. Using blocks and arrays, these default labels are jarring. From a UX perspective, how can I arrange items if I don't see the specific name I gave that item?
Beta Was this translation helpful? Give feedback.
All reactions
-
I think providing an admin.useAsTitle option would provide a great solution to this problem, reflective of the same option used for collections/globals.
for example:
{ type: 'array', name: 'categories', admin: { useAsTitle: 'title' } fields: [ { type: 'text', name: 'title', required: true, }, /* ... other fields ... */ ] }
Beta Was this translation helpful? Give feedback.
All reactions
-
❤️ 4
-
@adameli22 solution seems to be working in payload version 3.35 as well.
However, it would be nice to have a cleaner solution over the custom component approach.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 3
-
We really need an approach that doesn't require custom components.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 2
-
Then we would have 2 ways to do the same thing. Custom component handles basic use cases and complex cases. You should use the custom component approach.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1 -
👎 1
-
I like having the custom component approach for complex cases, but it's incredibly cumbersome for the 95% most common use-case of just picking a text field to use as the label. The admin: { useAsTitle: 'field_name' } seems like an elegant solution for the vast majority of users and use-cases.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 2 -
❤️ 1
-
You could build a reusable custom component and use it in any project. I do not see the need to add another property. Feel free to use the code provided here in your own project
The React component
// file: src/payload/components/shared/BasicRowLabelComponent/index.ts 'use client' import type { Data, RowLabelComponent } from 'payload' import { useRowLabel } from '@payloadcms/ui' function BasicRowLabelComponent ({ itemPlaceholder, fieldName }: { itemPlaceholder?: string, fieldName: string }) { const { data, rowNumber } = useRowLabel<Data>() return ( {`${ data?.[fieldName]}` || `${itemPlaceholder}: ${String(rowNumber).padStart(2, '0')}`} ) }
The JS helper function
// file: src/payload/fields/utilities import type { RowLabelComponent } from 'payload' const basicRowLabel = ({ fieldName, itemPlaceholder, }: { fieldName: string itemPlaceholder?: string }): RowLabelComponent => { return { path: 'src/payload/components/shared/BasicRowLabelComponent/index.js#BasicRowLabelComponent' clientProps: { fieldName: fieldName, itemPlaceholder: itemPlaceholder, }, } }
The actual usage
// example array field using the function { name: 'array', type: 'array', admin: { components: { RowLabel: basicRowLabel({ fieldName: 'text', itemPlaceholder: 'Item' }), } }, fields: [{name: 'text', type: 'text'}], }
I think this is the approach I would recommend. It makes it easy, you set it up 1 time and use it wherever.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 2
-
I don't know, but I think this really is a basic use-case that should come out of the box. Setting up 2 extra files using this quite complex syntax feels overkill and it took me quite some time to find this solution when getting started with PayloadCMS and even with this solution it is still quite some stuff to remember compared to just writing useAsTitle.
I think the main argument for this is that consistency also demands a build in way since it is possible for collections to write:
admin: { useAsTitle: 'title', },
I would expect to be able to do the same with array fields. Not being able to do this here, but being able to do this at other places makes me very confused.
I think an accessible and consistent API is very important for the success of a software, even if there are already complex ways to do the same.
Because the argument of There are already complex ways of doing this is an argument that people could come up with in very many cases and I don't think it is always justified.
I will just give a very exaggerated example to bring the point across:
We could say: Why do we need react? We could just call document.getElementById and call it a day. Using react does not increase the functionality. It allows us to do something, that we can already do, but in a much more easy way that doesn't drive us crazy.
In my opinion react has its justification to exist and so does setting the title of an array field in PayloadCMS using useAsTitle without needing to use the very complex component syntax.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 2
-
The only reason we do that for collections is so when a relationship field renders the values(s), they are not just ID's.
Beta Was this translation helpful? Give feedback.
All reactions
-
But isn't that the same situation with arrays?
Right now the titles of array items are also basically empty counters with no information about what is inside
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 5