Thanks to the answers in the following post, I created a Custom component that allows to return a dynamic field types based on the selected refence (relationship field) of another field in the collection.
Original post:
https://discord.com/channels/967097582721572934/1074901824785625149/1077554638229884998I can now access the data type of the relationship field.
1. How to access all native Payload components (e.g. Number, TextArea etc)?
2. When using the available components, I can see it in UI, but when I save, I get the below error.
3. When I manage to get pass the error (presumably) then after reload, the values are not loaded into the components even when they reach DB as its not explicilty set in code. How would one do this?
TypeError: Cannot create property 'id' on string 'lkjl'
at /home/.../node_modules/payload/src/fields/hooks/beforeChange/promise.ts:97:34
at processTicksAndRejections (node:internal/process/task_queues:95:5)Here is the custom component code:
import React, { useEffect, useState } from 'react';
import { useAllFormFields, useField, getSiblingData} from 'payload/components/forms'
import { Text, Checkbox, Select, Date } from 'payload/components/forms';
type Props = { path: string }
const FeatureValue: React.FC<Props> = ({ path }) => {
const { value, setValue } = useField<string>({ path });
const [fields] = useAllFormFields();
const [inputType, setInputType] = useState('text'); // Default input type
const pathParts = path.split(".")
const relevantPath = pathParts[0] + "." + pathParts[1] + ".feature"
const siblingData = getSiblingData(fields, relevantPath);
useEffect(() => {
if (!siblingData?.feature?.value) return;
const getRelation = async () => {
try {
const relationFetch = await fetch(
`/api/features/${siblingData.feature.value}`
);
const relation = await relationFetch.json();
const featureDataType = relation.datatype
setInputType(featureDataType);
} catch (error) {
console.log(error);
}
};
getRelation();
}, [siblingData]);
// Render the appropriate input type based on the feature's dataType. Following pseudocode as I don't know how to access the native components.
switch (inputType) {
case 'text':
return <Text path={path} name="featureValue" />;
// return <TextInput path={path} value={value} onChange={(e) => setValue(e.target.value)} name="featureValue" />;
case 'number':
return <Text path={path} name="featureValue"/>; // todo: check if number
case 'select':
return <Select path={path} options={selectOptions} name="featureValue" hasMany={false} />;
case 'selectMany':
return <Select path={path} options={selectOptions} name="featureValue" hasMany={true} />;
case 'date':
return <Date path={path} name="featureValue" />;
case 'checkbox':
return <Checkbox path={path} name="featureValue" />;
default:
return <div><p>Select feature</p></div>;
}
};
export default FeatureValue;Here is how I use the custom component inside the collection
...
{
name: "featureValue",
type: "ui",
admin: {
components: {
Field: FeatureValue
},
}
}
...Dynamic field types custom component issues (3)
There are a couple ways to access the underlying Payload Form Fields.
One way is to directly import the Form Field Component you would like to use and apply a useField hook to control the field
Another way is to use the RenderFields component from 'payload/components/forms' and provide a fieldSchema which uses the CollectionConfig['fields'] type.
Here is an example of the first approach;
const SelectFont = ({ path, name }) => {
const { value, setValue } = useField<string>({ path });
const [font, setFont] = React.useState();
const [options, setOptions] = React.useState(
top100.map((font) => ({
label: font.family,
value: font.family,
}))
);
return (
<>
<Label htmlFor={path} label='Font family' />
<SelectInput
onChange={(e) => setValue(e.value)}
value={value}
name={name}
path={path}
options={options}
/>
</>
);
};This might not be best for your use case as it requires a lot of conditional logic
Alternatively, the second approach is closer to the way Payload generates fields on the fly and might be better for your use case;
export const AddEditPhaseModal = () => {
function submit() { ... }
return (
<Form
initialData={{ title: 'Hello World }}
onSubmit={submit}>
<RenderFields
fieldTypes={fieldTypes}
fieldSchema={[{
name: 'title',
label: 'Title',
type: 'text',
required: true,
}]}
/>
<FormSubmit>Submit!</FormSubmit>
</Form>
);
};thanks for the tips. I tried this but I'm still getting the following error when I save the collection item after defining the TextInput value:
03:49:02] ERROR (payload): TypeError: Cannot create property 'id' on string 'My feature value'
Here the "My feature value" was what I typed into the dynamically returned TextInput field.
Here is my component and its usage:
import React, { useEffect, useState } from 'react';
import { useAllFormFields, useField, getSiblingData, Label, SelectInput } from 'payload/components/forms'
import { Text, TextInput, Checkbox, Select, Date } from 'payload/components/forms';
let previousDataType = ""
const FeatureValue = ({ path, name }) => {
const { value, setValue } = useField<string>({ path });
const [fields] = useAllFormFields();
const [inputType, setInputType] = useState('text'); // Default input type as 'text'
// my custom code no having effect on the below code (tested by commeting out)
switch (inputType) {
case 'text':
return (
<>
<TextInput
onChange={(e) => setValue(e.target.value)}
value={value}
name={name}
path={path}
/>
</>
)
}
};
export default FeatureValue;Component usage:
{
name: "featureValue",
type: "ui",
label: "Value",
admin: {
components: {
Field: FeatureValue
}
}
}The second approach I mentioned may be better for your use case
I see. Is there more documentation on that approach? I don't seem to get that working but I'm also quite blind to how it should be implemented.
I'm afraid there isn't any documentation on that approach as it is an internal component of Payload. But share what you are doing so far and I can try to help you correct it 🙂
Hi
@191776538205618177, can you please give a link to github, or an example where it is implemented
Here's a good example
as you can see, you just pass in an array of Payload Field types and then the form gets rendered
Thank you
@191776538205618177If you have time can you please take a look at this post?
I am trying to make a custom datepicker, if you have any advice, would be very glad to here it
I think this is a duplicate post
right?
How do you mean?
Oh I see
Sorry, I saw Yersn post a similar thing
In that datepicker thread
no worries 😄
I started from something very simple - using custom component inside
arrayfield. When defining the custom component field and saving the collection item, I get the following error:
[05:33:16] ERROR (payload): TypeError: Cannot create property 'id' on string 'hello'When I don't use the CustomComponent inside Array field but as "stand-alone" field (at root level in the collection), it doesn't throw any errors but the value doesn't reach DB.
CustomComponent.tsx
import { SelectInput, useField } from "payload/components/forms";
import React from "react";
const CustomSelectInput = ({ path, name }) => {
const { value, setValue } = useField<string>({ path });
const [options, setOptions] = React.useState([{label: "Hello", value:"hello"}, {label: "World", value:"world"}]);
return (
<>
<SelectInput
onChange={(e) => setValue(e.value)}
value={value}
name={name}
path={path}
options={options}
/>
</>
);
};
export default CustomSelectInput;Usage of this component:
...
fields: [
// { // <-- NO ERRORS, BUT VALUE DOESN'T REACH DB
// name: "featureValue",
// type: "ui",
// admin: {
// components: {
// Field: CustomSelectInput,
// },
// }
// },
{
name: 'featuresArray',
type: 'array',
minRows: 0,
maxRows: 100,
fields: [
{
type: 'row',
fields: [
{
name: 'feature',
type: 'relationship',
required: false,
hasMany: false,
relationTo: ['features'],
maxDepth: 10,
},
{ // <-- THIS THROWS ERROR !
name: "featureValue",
type: "ui",
admin: {
components: {
Field: CustomSelectInput,
},
}
}
}
]
}
],
...The UI field is not intended for saving values to the DB
I think you want a text or JSON field
Thank You
@191776538205618177for the tips. I got it working finally with some quirks.
That last small change fixed the key issu. Here is the final solution if someone wishes to have dynamic fields.
Collection.ts
...
{
name: 'featuresArray',
type: 'array',
fields: [
{
type: 'row',
fields: [
{
name: 'feature',
type: 'relationship',
required: false,
hasMany: false,
relationTo: ['features'],
maxDepth: 10
},
{
name: "featureValue",
type: "text",
admin: {
components: {
Field: FeatureValue,
},
}
},
]
}
],
},
...FeatureValue.ts
imports...
const FeatureValue = ({ path, name }) => {
const { value, setValue } = useField<string>({ path });
const [fields] = useAllFormFields();
const [inputType, setInputType] = useState('text'); // Default input type is text, todo: change to undefined
const [prevRelationValue, setPrevRelationValue] = useState('unk'); // Default input type
let options = [{value: ""}]
const [selectOptions, setSelectOptions] = React.useState(
options.map((option) => ({
label: option.value,
value: option.value,
}))
);
const pathParts = path.split(".")
const relevantPath = pathParts[0] + "." + pathParts[1] + ".feature"
const siblingData = getSiblingData(fields, relevantPath);
useEffect(() => {
if (!siblingData?.feature?.value) return;
const getRelation = async () => {
try {
setSelectOptions([])
const relationFetch = await fetch(
`/api/features/${siblingData.feature.value}`
);
const relation = await relationFetch.json();
const featureDataType = relation.datatype
let selectOptions = []
if (relation.valueLimits){
const options = relation.valueLimits.split(",")
for (let i = 0; i < options.length; i++) {
selectOptions.push({label: options[i], value: options[i]})
}
setSelectOptions(selectOptions)
}
setInputType(featureDataType);
} catch (error) {
console.log(error);
}
};
// this is necessary to stop the getRelation() being called indefinately resulting in Too Many Request error.
// only getRelation when relation value actually changes.
if(prevRelationValue != siblingData.feature.value) {
setPrevRelationValue(siblingData.feature.value);
getRelation();
}
}, [siblingData, prevRelationValue]);
if(value && inputType == "date" && !isValidDate(value)){
return; // hide featureValue field from user otherwise this crashes the app with invalid date error. TODO: find an elegant solution.
}
// Render the appropriate input type based on the feature's dataType
switch (inputType) {
case 'text':
return (
<>
<TextInput
label={"Value"}
onChange={(e) => setValue(e.target.value)}
value={value}
name={name}
path={path}
/>
</>
)
case 'number':
return (...)
case 'select':
return (
<>
<SelectInput
label={"Value"}
onChange={(e) => setValue(e.value)}
value={value}
name={name}
path={path}
options={selectOptions}
/>
</>
)
case 'selectMany':
return (...)
case 'date':
return <Date path={path} name="featureValue" label={"Value"}/>;
case 'checkbox':
return <Checkbox path={path} name="featureValue" label={"Value"} />;
default:
return <div><p>Select feature first</p></div>;
}
};
export default FeatureValue;Star
Discord
online
Get dedicated engineering support directly from the Payload team.