feat: passes client field config to server components (#8166)

## Description

### TL;DR:

It's currently not possible to render our field components from a server
component because their `field` prop is the original field config, not
the _client_ config which our components require. Currently, the `field`
prop passed into custom fields changes type depending on whether it's a
server or client component, leaving server components without any access
to the client field config or mechanism to acquire it.

This PR passes the client config to all server field components through
a new `clientField` prop. This allows the following in a server
component, which is very similar to how client field components
currently work:

Server component:

```tsx
import { TextField } from '@payloadcms/ui'
import type { TextFieldServerComponent } from 'payload'

export const MyCustomServerField: TextFieldServerComponent = ({ clientField }) => {
  return <TextField field={clientField} />
}
```

Client component:

```tsx
'use client'
import { TextField } from '@payloadcms/ui'
import type { TextFieldClientComponent } from 'payload'

export const MyCustomClientField: TextFieldClientComponent = ({ field }) => {
  return <TextField field={field} />
}
```

### Full Background

If you have a custom field component, and it's a server component, there
is currently no way to pass the field prop into Payload's client-side
field components.

Here's an example of the problem:

```tsx
import { TextField } from '@payloadcms/ui'
import type { TextFieldServerComponent } from 'payload'

import React from 'react'

export const MyServerComponent: TextFieldServerComponent = (props) => {
  const { field } = props

  return (
    <TextField field={field} /> // This is not possible
  )
}
```

The config needs to be transformed into a client config, however,
because of the sheer number of hard-to-find arguments that the
`createClientField` requires, we cannot use it in its raw form.

Here is another example of the problem:

```tsx
import { TextField } from '@payloadcms/ui'
import { createClientField } from '@payloadcms/ui/utilities/createClientField'
import type { TextFieldServerComponent } from 'payload'

import React from 'react'

export const MyServerComponent: TextFieldServerComponent = ({ createClientField }) => {
  const clientField = createClientField({...}) // Not a good option bc it requires many hard-to-find args

  return (
    <TextField field={clientField} />
  )
}
```

Theoretically, we could preformat a `createFieldConfig` function so it
can simply be called without arguments:

```tsx
import { TextField } from '@payloadcms/ui'
import type { TextFieldServerComponent } from 'payload'

import React from 'react'

export const MyServerComponent: TextFieldServerComponent = ({ createClientField }) => {
  return <TextField field={createClientField()} />
}
```

But this means the field config would be evaluated twice unnecessarily,
including label functions, etc.

The right way to fix this is to simply pass the client config to server
components through a new `clientField` prop:

```tsx
import { TextField } from '@payloadcms/ui'
import type { TextFieldServerComponent } from 'payload'

import React from 'react'

export const MyServerComponent: TextFieldServerComponent = ({ clientField }) => {
  return <TextField field={clientField} />
}
```

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
This commit is contained in:
Jacob Fletcher
2024-09-11 15:47:56 -04:00
committed by GitHub
parent 9561aa3f79
commit 8b307012f3
38 changed files with 477 additions and 194 deletions

View File

@@ -1,9 +0,0 @@
'use client'
import type { TextFieldLabelClientComponent } from 'payload'
import React from 'react'
export const MyClientComponent: TextFieldLabelClientComponent = (props) => {
const { field } = props
return <p>{`The name of this field is: ${field.name}`}</p>
}

View File

@@ -0,0 +1,9 @@
'use client'
import type { TextFieldClientComponent } from 'payload'
import { TextField } from '@payloadcms/ui'
import React from 'react'
export const MyClientFieldComponent: TextFieldClientComponent = ({ field }) => {
return <TextField field={field} />
}

View File

@@ -1,13 +0,0 @@
import type { TextFieldLabelServerComponent } from 'payload'
import React from 'react'
export const MyServerComponent: TextFieldLabelServerComponent = (props) => {
const { field } = props
return (
<div>
<p>{`The name of this field is: ${field.name}`}</p>
</div>
)
}

View File

@@ -0,0 +1,8 @@
import type { TextFieldServerComponent } from 'payload'
import { TextField } from '@payloadcms/ui'
import React from 'react'
export const MyServerFieldComponent: TextFieldServerComponent = ({ clientField }) => {
return <TextField field={clientField} />
}

View File

@@ -11,7 +11,7 @@ export const PostsCollection: CollectionConfig = {
{
admin: {
components: {
Label: '/collections/Posts/MyClientComponent.js#MyClientComponent',
Field: '/collections/Posts/MyClientField.js#MyClientFieldComponent',
},
},
name: 'text',
@@ -21,7 +21,7 @@ export const PostsCollection: CollectionConfig = {
{
admin: {
components: {
Label: '/collections/Posts/MyServerComponent.js#MyServerComponent',
Field: '/collections/Posts/MyServerField.js#MyServerFieldComponent',
},
},
name: 'serverTextField',