Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

More control over array items' labels #1843

grundiss started this conversation in Feature Requests & Ideas
Jan 10, 2023 · 12 comments · 11 replies
Discussion options

It'd be great to have more control over the labels of an array field's items.

Consider the following screenshot, please.

image

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?

You must be logged in to vote

Replies: 12 comments 11 replies

Comment options

👍🏻 If you don't plan to implement this, getting pointers on how to implement it would be helpful as well.

You must be logged in to vote
0 replies
Comment options

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

You must be logged in to vote
0 replies
Comment options

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}`;
 },
 },
 }
 }
You must be logged in to vote
1 reply
Comment options

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,
 },
 }
}
Comment options

How should this be done with Payload 3 beta?

You must be logged in to vote
3 replies
Comment options

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,
};
Comment options

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
}
Comment options

@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.

Comment options

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')}`
 },
 },
 },
 },
 ],
}
You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
2 replies
Comment options

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

Comment options

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
Comment options

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()],
		},
You must be logged in to vote
0 replies
Comment options

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?

You must be logged in to vote
0 replies
Comment options

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 ... */
 ]
}
You must be logged in to vote
0 replies
Comment options

@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.

You must be logged in to vote
0 replies
Comment options

We really need an approach that doesn't require custom components.

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
5 replies
Comment options

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.

Comment options

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.

Comment options

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.

Comment options

The only reason we do that for collections is so when a relationship field renders the values(s), they are not just ID's.

Comment options

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /