46
loading...
This website collects cookies to deliver better user experience
text
field typeuseFieldType
hookPreferences
featureCell
component, showing the selected color in the List
view of the admin panelnpx create-payload-app
in your terminal.text
field type does, we'll set our field's type
equal to text
. That will tell Payload how to handle storing the data. We'll also write a simple validation function to tell the backend and frontend what to allow to be saved.import { Field } from 'payload/types';
export const validateHexColor = (value: string): boolean | string => {
return value.match(/^#(?:[0-9a-fA-F]{3}){1,2}$/).length === 1 || `${value} is not a valid hex color`;
}
const colorField: Field = {
name: 'color',
type: 'text',
validate: validateHexColor,
required: true,
};
export default colorField;
Note that though code snippets are TypeScript, it can be done the same way in regular JavaScript by omitting the extra
type declarations.
/src/collections/ToDoLists.ts
:import { CollectionConfig } from 'payload/types';
import colorField from '../color-picker/config';
const Todo: CollectionConfig = {
fields: [
colorField,
]
}
colorField
instead do { ...colorField, required: false }
, or any other properties as needed.Text
component is still rendering in the admin panel. Let's swap that out with a custom component, and modify the field's config to include it.Field
component:/src/color-picker/InputField.tsx
:import React from 'react'
// this is how we'll interface with Payload itself
import { useFieldType } from 'payload/components/forms';
// we'll re-use the built in Label component directly from Payload
import { Label } from 'payload/components/forms';
// we can use existing Payload types easily
import { Props } from 'payload/components/fields/Text';
// we'll import and reuse our existing validator function on the frontend, too
import { validateHexColor } from './config';
// Import the SCSS stylesheet
import './styles.scss';
// keep a list of default colors to choose from
const defaultColors = [
'#333333',
'#9A9A9A',
'#F3F3F3',
'#FF6F76',
'#FDFFA4',
'#B2FFD6',
'#F3DDF3',
];
const baseClass = 'custom-color-picker';
const InputField: React.FC<Props> = (props) => {
const {
path,
label,
required
} = props;
const {
value = '',
setValue,
} = useFieldType({
path,
validate: validateHexColor,
});
return (
<div className={baseClass}>
<Label
htmlFor={path}
label={label}
required={required}
/>
<ul className={`${baseClass}__colors`}>
{defaultColors.map((color, i) => (
<li key={i}>
<button
type="button"
key={color}
className={`chip ${color === value ? 'chip--selected' : ''} chip--clickable`}
style={{ backgroundColor: color }}
aria-label={color}
onClick={() => setValue(color)}
/>
</li>
)
)}
</ul>
</div>
)
};
export default InputField;
props
that it needs. The most important prop
is the path
, which we pass on to the useFieldType
hook. This hook allows us to set the field's value and have it work with the rest of the Payload form.import './styles.scss';
. Create that file and paste in the following SCSS:/src/color-picker/styles.scss
:@import '~payload/scss';
.custom-color-picker {
&__colors {
display: flex;
flex-wrap: wrap;
list-style: none;
padding: 0;
margin: 0;
}
}
.chip {
border-radius: 50%;
border: $style-stroke-width-m solid #fff;
height: base(1.25);
width: base(1.25);
margin-right: base(.5);
box-shadow: none;
&--selected {
box-shadow: 0 0 0 $style-stroke-width-m $color-dark-gray;
}
&--clickable {
cursor: pointer;
}
}
List
. There, we can create the following:/src/color-picker/Cell.tsx
:import React from 'react';
import { Props } from 'payload/components/views/Cell';
import './styles.scss';
const Cell: React.FC<Props> = (props) => {
const { cellData } = props;
if (!cellData) return null;
return (
<div
className="chip"
style={{ backgroundColor: cellData as string }}
/>
)
}
export default Cell;
color-picker/config.ts
with a new admin
property:import { Field } from 'payload/types';
import InputField from './InputField';
import Cell from './Cell';
const colorField: Field = {
// ...
admin: {
components: {
Field: InputField,
Cell,
},
},
};
List
view, you should also be able to see the color that was chosen right in the table. If you don't see the color column, expand the column list to include it.useEffect
hooks. We also need to import and use the validation logic from the config, and set the value in a new Input which we can import styles directly from Payload to make it look right.usePreferences()
hook, we can get and set user specific data relevant to the color picker all persisted in the database without needing to write new endpoints. You will see we call setPreference()
and getPreference()
to get and set the array of color options specific to the authenticated user./src/color-picker/InputField.tsx
:import React, { useEffect, useState, useCallback, Fragment } from 'react'
// this is how we'll interface with Payload itself
import { useFieldType } from 'payload/components/forms';
// retrieve and store the last used colors of your users
import { usePreferences } from 'payload/components/preferences';
// re-use Payload's built-in button component
import { Button } from 'payload/components';
// we'll re-use the built in Label component directly from Payload
import { Label } from 'payload/components/forms';
// we can use existing Payload types easily
import { Props } from 'payload/components/fields/Text';
// we'll import and reuse our existing validator function on the frontend, too
import { validateHexColor } from './config';
// Import the SCSS stylesheet
import './styles.scss';
// keep a list of default colors to choose from
const defaultColors = [
'#333333',
'#9A9A9A',
'#F3F3F3',
'#FF6F76',
'#FDFFA4',
'#B2FFD6',
'#F3DDF3',
];
const baseClass = 'custom-color-picker';
const preferenceKey = 'color-picker-colors';
const InputField: React.FC<Props> = (props) => {
const {
path,
label,
required
} = props;
const {
value = '',
setValue,
} = useFieldType({
path,
validate: validateHexColor,
});
const { getPreference, setPreference } = usePreferences();
const [colorOptions, setColorOptions] = useState(defaultColors);
const [isAdding, setIsAdding] = useState(false);
const [colorToAdd, setColorToAdd] = useState('');
useEffect(() => {
const mergeColorsFromPreferences = async () => {
const colorPreferences = await getPreference<string[]>(preferenceKey);
if (colorPreferences) {
setColorOptions(colorPreferences);
}
};
mergeColorsFromPreferences();
}, [getPreference, setColorOptions]);
const handleAddColor = useCallback(() => {
setIsAdding(false);
setValue(colorToAdd);
// prevent adding duplicates
if (colorOptions.indexOf(colorToAdd) > -1) return;
let newOptions = colorOptions;
newOptions.unshift(colorToAdd);
// update state with new colors
setColorOptions(newOptions);
// store the user color preferences for future use
setPreference(preferenceKey, newOptions);
}, [colorOptions, setPreference, colorToAdd, setIsAdding, setValue]);
return (
<div className={baseClass}>
<Label
htmlFor={path}
label={label}
required={required}
/>
{isAdding && (
<div>
<input
className={`${baseClass}__input`}
type="text"
placeholder="#000000"
onChange={(e) => setColorToAdd(e.target.value)}
value={colorToAdd}
/>
<Button
className={`${baseClass}__btn`}
buttonStyle="primary"
iconPosition="left"
iconStyle="with-border"
size="small"
onClick={handleAddColor}
disabled={validateHexColor(colorToAdd) !== true}
>
Add
</Button>
<Button
className={`${baseClass}__btn`}
buttonStyle="secondary"
iconPosition="left"
iconStyle="with-border"
size="small"
onClick={() => setIsAdding(false)}
>
Cancel
</Button>
</div>
)}
{!isAdding && (
<Fragment>
<ul className={`${baseClass}__colors`}>
{colorOptions.map((color, i) => (
<li key={i}>
<button
type="button"
key={color}
className={`chip ${color === value ? 'chip--selected' : ''} chip--clickable`}
style={{ backgroundColor: color }}
aria-label={color}
onClick={() => setValue(color)}
/>
</li>
)
)}
</ul>
<Button
className="add-color"
icon="plus"
buttonStyle="icon-label"
iconPosition="left"
iconStyle="with-border"
onClick={() => {
setIsAdding(true);
setValue('');
}}
/>
</Fragment>
)}
</div>
)
};
export default InputField;
styles.scss
with the following:/src/color-picker/styles.scss
:@import '~payload/scss';
.add-color.btn {
margin: 0;
padding: 0;
border: $style-stroke-width-m solid #fff;
}
.custom-color-picker {
&__btn.btn {
margin: base(.25);
&:first-of-type {
margin-left: unset;
}
}
&__input {
// Payload exports a mixin from the vars file for quickly applying formInput rules to the class for our input
@include formInput
}
&__colors {
display: flex;
flex-wrap: wrap;
list-style: none;
padding: 0;
margin: 0;
}
}
.chip {
border-radius: 50%;
border: $style-stroke-width-m solid #fff;
height: base(1.25);
width: base(1.25);
margin-right: base(.5);
box-shadow: none;
&--selected {
box-shadow: 0 0 0 $style-stroke-width-m $color-dark-gray;
}
&--clickable {
cursor: pointer;
}
}