├── .env.example ├── .gitignore ├── color-picker.gif ├── nodemon.json ├── package.json ├── readme.md ├── src ├── .env.example ├── collections │ ├── TodoLists.ts │ └── Users.ts ├── color-picker │ ├── Cell.tsx │ ├── InputField.tsx │ ├── config.ts │ └── styles.scss ├── payload.config.ts └── server.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URI=mongodb://localhost/color-picker-guide 2 | PAYLOAD_SECRET=mysecret 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | .env 4 | .idea 5 | yarn-error.log 6 | -------------------------------------------------------------------------------- /color-picker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/payloadcms/custom-field-guide/3cb4465bf5c6e22770cb51cf990eabfdd96c05a6/color-picker.gif -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts", 3 | "exec": "ts-node src/server.ts" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payload-starter-typescript", 3 | "description": "Simple to-do list example", 4 | "version": "1.0.0", 5 | "main": "dist/server.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon", 9 | "build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build", 10 | "build:server": "tsc", 11 | "build": "yarn build:payload && yarn build:server", 12 | "serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js" 13 | }, 14 | "dependencies": { 15 | "dotenv": "^8.2.0", 16 | "express": "^4.17.1", 17 | "payload": "^0.7.4" 18 | }, 19 | "devDependencies": { 20 | "@types/express": "^4.17.9", 21 | "cross-env": "^7.0.3", 22 | "nodemon": "^2.0.6", 23 | "ts-node": "^9.1.1", 24 | "typescript": "^4.1.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Custom Field Sample Project - Color Picker 2 | 3 | This is an example Payload CMS project that has a custom color picker field and was created for the blog post [Building a Custom Field for Payload](https://payloadcms.com/blog/how-to-create-a-custom-select-field-in-payload-a-step-by-step-guide) 4 | 5 | ### Color Picker Field Features 6 | - Payload compatible field configuration 7 | - Custom input for admin panel 8 | - Custom Cell component 9 | - Input validation for frontend and backend 10 | - User preferences that remember individual user's color choices 11 | 12 | Component Demo 13 | 14 | ## Prerequisites: 15 | - MongoDB 16 | - Node 17 | 18 | ## Steps to run this project: 19 | - git clone git@github.com:payloadcms/custom-field-guide.git 20 | - `cp .env.example .env` 21 | - edit `.env` file to have valid MongoDB connection 22 | - `yarn` or `npm install` 23 | - `yarn dev` or `npm run dev` 24 | - after startup open web browser to localhost:3000/admin 25 | -------------------------------------------------------------------------------- /src/.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URI=mongodb://localhost/payload-custom-field-guide 2 | PAYLOAD_SECRET= 3 | -------------------------------------------------------------------------------- /src/collections/TodoLists.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | import colorField from "../color-picker/config"; 3 | 4 | const Todo: CollectionConfig = { 5 | slug: 'todos', 6 | admin: { 7 | defaultColumns: ['listName', 'tasks', 'updatedAt'], 8 | useAsTitle: 'listName', 9 | }, 10 | access: { 11 | create: () => true, 12 | read: () => true, 13 | update: () => true, 14 | delete: () => true, 15 | }, 16 | fields: [ 17 | { 18 | name: 'listName', 19 | type: 'text', 20 | }, 21 | colorField, 22 | { 23 | name: 'tasks', 24 | type: 'array', 25 | fields: [ 26 | { 27 | name: 'name', 28 | type: 'text', 29 | }, 30 | { 31 | name: 'complete', 32 | type: 'checkbox', 33 | defaultValue: false, 34 | }, 35 | ] 36 | }, 37 | ], 38 | } 39 | 40 | export default Todo; 41 | -------------------------------------------------------------------------------- /src/collections/Users.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | 3 | const Users: CollectionConfig = { 4 | slug: 'users', 5 | auth: true, 6 | admin: { 7 | useAsTitle: 'email', 8 | }, 9 | access: { 10 | read: () => true, 11 | }, 12 | fields: [ 13 | // Email added by default 14 | // Add more fields as needed 15 | ], 16 | }; 17 | 18 | export default Users; -------------------------------------------------------------------------------- /src/color-picker/Cell.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Props } from 'payload/components/views/Cell'; 3 | import './styles.scss'; 4 | 5 | const Cell: React.FC = (props) => { 6 | const { cellData } = props; 7 | 8 | if (!cellData) return null; 9 | 10 | return ( 11 |
15 |
16 | ) 17 | } 18 | 19 | export default Cell; 20 | -------------------------------------------------------------------------------- /src/color-picker/InputField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback, Fragment } from 'react' 2 | 3 | // this is how we'll interface with Payload itself 4 | import { useFieldType } from 'payload/components/forms'; 5 | 6 | // retrieve and store the last used colors of your users 7 | import { usePreferences } from 'payload/components/preferences'; 8 | 9 | // re-use Payload's built-in button component 10 | import { Button } from 'payload/components'; 11 | 12 | // we'll re-use the built in Label component directly from Payload 13 | import { Label } from 'payload/components/forms'; 14 | 15 | import Error from 'payload/dist/admin/components/forms/Error/index'; 16 | 17 | // we can use existing Payload types easily 18 | import { Props } from 'payload/components/fields/Text'; 19 | 20 | // we'll import and reuse our existing validator function on the frontend, too 21 | import { validateHexColor } from './config'; 22 | 23 | // Import the SCSS stylesheet 24 | import './styles.scss'; 25 | 26 | // keep a list of default colors to choose from 27 | const defaultColors = [ 28 | '#333333', 29 | '#9A9A9A', 30 | '#F3F3F3', 31 | '#FF6F76', 32 | '#FDFFA4', 33 | '#B2FFD6', 34 | '#F3DDF3', 35 | ]; 36 | const baseClass = 'custom-color-picker'; 37 | 38 | const preferenceKey = 'color-picker-colors'; 39 | 40 | const InputField: React.FC = (props) => { 41 | const { 42 | path, 43 | label, 44 | required, 45 | validate, 46 | } = props; 47 | 48 | const { 49 | value = "", 50 | setValue, 51 | errorMessage, 52 | showError, 53 | } = useFieldType({ 54 | path, 55 | validate, 56 | }); 57 | const classes = ["field-type", "text", baseClass, showError && "error"] 58 | .filter(Boolean) 59 | .join(" "); 60 | 61 | 62 | const { getPreference, setPreference } = usePreferences(); 63 | const [colorOptions, setColorOptions] = useState(defaultColors); 64 | const [isAdding, setIsAdding] = useState(false); 65 | const [colorToAdd, setColorToAdd] = useState(''); 66 | 67 | useEffect(() => { 68 | const mergeColorsFromPreferences = async () => { 69 | const colorPreferences = await getPreference(preferenceKey); 70 | if (colorPreferences) { 71 | setColorOptions(colorPreferences); 72 | } 73 | }; 74 | mergeColorsFromPreferences(); 75 | }, [getPreference, setColorOptions]); 76 | 77 | const handleAddColor = useCallback(() => { 78 | setIsAdding(false); 79 | setValue(colorToAdd); 80 | 81 | // prevent adding duplicates 82 | if (colorOptions.indexOf(colorToAdd) > -1) return; 83 | 84 | let newOptions = colorOptions; 85 | newOptions.unshift(colorToAdd); 86 | 87 | // update state with new colors 88 | setColorOptions(newOptions); 89 | // store the user color preferences for future use 90 | setPreference(preferenceKey, newOptions); 91 | }, [colorOptions, setPreference, colorToAdd, setIsAdding, setValue]); 92 | 93 | return ( 94 |
95 |
164 | ) 165 | }; 166 | export default InputField; 167 | -------------------------------------------------------------------------------- /src/color-picker/config.ts: -------------------------------------------------------------------------------- 1 | import { Field } from 'payload/types'; 2 | import InputField from './InputField'; 3 | import Cell from './Cell'; 4 | 5 | export const validateHexColor = (value: string = ''): true | string => { 6 | return value.match(/^#(?:[0-9a-fA-F]{3}){1,2}$/) !== null || `Please give a valid hex color`; 7 | } 8 | 9 | const colorField: Field = { 10 | name: 'color', 11 | type: 'text', 12 | validate: validateHexColor, 13 | required: true, 14 | admin: { 15 | components: { 16 | Field: InputField, 17 | Cell, 18 | } 19 | } 20 | }; 21 | 22 | export default colorField; 23 | -------------------------------------------------------------------------------- /src/color-picker/styles.scss: -------------------------------------------------------------------------------- 1 | @import '~payload/scss'; 2 | 3 | .add-color.btn { 4 | margin: 0; 5 | padding: 0; 6 | border: $style-stroke-width-m solid #fff; 7 | } 8 | 9 | .custom-color-picker { 10 | &__btn.btn { 11 | margin: base(.25); 12 | 13 | &:first-of-type { 14 | margin-left: unset; 15 | } 16 | } 17 | 18 | &__input { 19 | // Payload exports a mixin from the vars file for quickly applying formInput rules to the class for our input 20 | @include formInput 21 | } 22 | 23 | &__colors { 24 | display: flex; 25 | flex-wrap: wrap; 26 | list-style: none; 27 | padding: 0; 28 | margin: 0; 29 | } 30 | 31 | .error { 32 | background-color: var(--theme-error-100); 33 | border: 1px solid var(--theme-error-400); 34 | } 35 | } 36 | 37 | .chip { 38 | border-radius: 50%; 39 | border: $style-stroke-width-m solid #fff; 40 | height: base(1.25); 41 | width: base(1.25); 42 | margin-right: base(.5); 43 | box-shadow: none; 44 | 45 | &--selected { 46 | box-shadow: 0 0 0 $style-stroke-width-m $color-dark-gray; 47 | } 48 | 49 | &--clickable { 50 | cursor: pointer; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/payload.config.ts: -------------------------------------------------------------------------------- 1 | import { buildConfig } from 'payload/config'; 2 | import TodoLists from './collections/TodoLists'; 3 | import Users from './collections/Users'; 4 | 5 | export default buildConfig({ 6 | serverURL: 'http://localhost:3000', 7 | admin: { 8 | user: Users.slug, 9 | }, 10 | collections: [ 11 | TodoLists, 12 | Users, 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import payload from 'payload'; 3 | 4 | require('dotenv').config(); 5 | const app = express(); 6 | 7 | // Initialize Payload 8 | payload.init({ 9 | secret: process.env.PAYLOAD_SECRET, 10 | mongoURL: process.env.MONGODB_URI, 11 | express: app, 12 | onInit: () => { 13 | payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`); 14 | }, 15 | }); 16 | 17 | // Add your own express routes here 18 | 19 | app.listen(3000); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "strict": false, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "outDir": "./dist", 13 | "rootDir": "./src", 14 | "jsx": "react", 15 | }, 16 | "ts-node": { 17 | "transpileOnly": true 18 | } 19 | } --------------------------------------------------------------------------------