It is a common pattern to dynamically show and validate a select field's
options based on various criteria such as the current user or underlying
document.
Some examples of this might include:
- Restricting options based on a user's role, e.g. admin-only options
- Displaying different options based on the value of another field, e.g.
a city/state selector
While this is already possible to do with a custom `validate` function,
the user can still view and select the forbidden option...unless you
_also_ wired up a custom component.
Now, you can define `filterOptions` on select fields.
This behaves similarly to the existing `filterOptions` property on
relationship and upload fields, except the return value of this function
is simply an array of options, not a query constraint. The result of
this function will determine what is shown to the user and what is
validated on the server.
Here's an example:
```ts
{
name: 'select',
type: 'select',
options: [
{
label: 'One',
value: 'one',
},
{
label: 'Two',
value: 'two',
},
{
label: 'Three',
value: 'three',
},
],
filterOptions: ({ options, data }) =>
data.disallowOption1
? options.filter(
(option) => (typeof option === 'string' ? options : option.value) !== 'one',
)
: options,
}
```
146 lines
4.3 KiB
TypeScript
146 lines
4.3 KiB
TypeScript
'use client'
|
|
import type { LabelFunction, OptionObject, StaticDescription, StaticLabel } from 'payload'
|
|
|
|
import { getTranslation } from '@payloadcms/translations'
|
|
import React from 'react'
|
|
|
|
import type { ReactSelectAdapterProps } from '../../elements/ReactSelect/types.js'
|
|
|
|
import { ReactSelect } from '../../elements/ReactSelect/index.js'
|
|
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
|
|
import { FieldDescription } from '../../fields/FieldDescription/index.js'
|
|
import { FieldError } from '../../fields/FieldError/index.js'
|
|
import { FieldLabel } from '../../fields/FieldLabel/index.js'
|
|
import { useTranslation } from '../../providers/Translation/index.js'
|
|
import { fieldBaseClass } from '../shared/index.js'
|
|
import './index.scss'
|
|
|
|
export type SelectInputProps = {
|
|
readonly AfterInput?: React.ReactNode
|
|
readonly BeforeInput?: React.ReactNode
|
|
readonly className?: string
|
|
readonly Description?: React.ReactNode
|
|
readonly description?: StaticDescription
|
|
readonly Error?: React.ReactNode
|
|
readonly filterOption?: ReactSelectAdapterProps['filterOption']
|
|
readonly hasMany?: boolean
|
|
readonly isClearable?: boolean
|
|
readonly isSortable?: boolean
|
|
readonly Label?: React.ReactNode
|
|
readonly label?: StaticLabel
|
|
readonly localized?: boolean
|
|
readonly name: string
|
|
readonly onChange?: ReactSelectAdapterProps['onChange']
|
|
readonly onInputChange?: ReactSelectAdapterProps['onInputChange']
|
|
readonly options?: OptionObject[]
|
|
readonly path: string
|
|
readonly placeholder?: LabelFunction | string
|
|
readonly readOnly?: boolean
|
|
readonly required?: boolean
|
|
readonly showError?: boolean
|
|
readonly style?: React.CSSProperties
|
|
readonly value?: string | string[]
|
|
}
|
|
|
|
export const SelectInput: React.FC<SelectInputProps> = (props) => {
|
|
const {
|
|
AfterInput,
|
|
BeforeInput,
|
|
className,
|
|
Description,
|
|
description,
|
|
Error,
|
|
filterOption,
|
|
hasMany = false,
|
|
isClearable = true,
|
|
isSortable = true,
|
|
label,
|
|
Label,
|
|
localized,
|
|
onChange,
|
|
onInputChange,
|
|
options,
|
|
path,
|
|
placeholder,
|
|
readOnly,
|
|
required,
|
|
showError,
|
|
style,
|
|
value,
|
|
} = props
|
|
|
|
const { i18n } = useTranslation()
|
|
|
|
let valueToRender
|
|
|
|
if (hasMany && Array.isArray(value)) {
|
|
valueToRender = value.map((val) => {
|
|
const matchingOption = options.find((option) => option.value === val)
|
|
return {
|
|
label: matchingOption ? getTranslation(matchingOption.label, i18n) : val,
|
|
value: matchingOption?.value ?? val,
|
|
}
|
|
})
|
|
} else if (value) {
|
|
const matchingOption = options.find((option) => option.value === value)
|
|
valueToRender = {
|
|
label: matchingOption ? getTranslation(matchingOption.label, i18n) : value,
|
|
value: matchingOption?.value ?? value,
|
|
}
|
|
} else {
|
|
// If value is not present then render nothing, allowing select fields to reset to their initial 'Select an option' state
|
|
valueToRender = null
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={[
|
|
fieldBaseClass,
|
|
'select',
|
|
className,
|
|
showError && 'error',
|
|
readOnly && 'read-only',
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')}
|
|
id={`field-${path.replace(/\./g, '__')}`}
|
|
style={style}
|
|
>
|
|
<RenderCustomComponent
|
|
CustomComponent={Label}
|
|
Fallback={
|
|
<FieldLabel label={label} localized={localized} path={path} required={required} />
|
|
}
|
|
/>
|
|
<div className={`${fieldBaseClass}__wrap`}>
|
|
<RenderCustomComponent
|
|
CustomComponent={Error}
|
|
Fallback={<FieldError path={path} showError={showError} />}
|
|
/>
|
|
{BeforeInput}
|
|
<ReactSelect
|
|
disabled={readOnly}
|
|
filterOption={filterOption}
|
|
isClearable={isClearable}
|
|
isMulti={hasMany}
|
|
isSortable={isSortable}
|
|
onChange={onChange}
|
|
onInputChange={onInputChange}
|
|
options={options.map((option) => ({
|
|
...option,
|
|
label: getTranslation(option.label, i18n),
|
|
}))}
|
|
placeholder={placeholder}
|
|
showError={showError}
|
|
value={valueToRender as OptionObject}
|
|
/>
|
|
{AfterInput}
|
|
</div>
|
|
<RenderCustomComponent
|
|
CustomComponent={Description}
|
|
Fallback={<FieldDescription description={description} path={path} />}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|