Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Dynamic field types custom component issues (3)

default discord avatar
kaspartr2 years ago
15

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/1077554638229884998

I can now access the data type of the relationship field.



This brought to light three problems.

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)

  • default discord avatar
    markatomniux2 years ago

    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>
        );
    };
  • default discord avatar
    kaspartr2 years ago
    @191776538205618177

    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:


    Component
    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
        }
      }
    }
  • default discord avatar
    markatomniux2 years ago

    The second approach I mentioned may be better for your use case

  • default discord avatar
    kaspartr2 years ago

    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.

  • default discord avatar
    markatomniux2 years ago

    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 🙂

  • default discord avatar
    yersn2 years ago

    Hi

    @191776538205618177

    , can you please give a link to github, or an example where it is implemented

  • default discord avatar
    markatomniux2 years ago
    https://github.com/payloadcms/payload/blob/02d2c517176a775c6eeb4b164d9e6b45d8f5e4c1/packages/payload/src/admin/components/views/CreateFirstUser/index.tsx#L8

    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

  • default discord avatar
    yersn2 years ago

    Thank you

    @191776538205618177

    If you have time can you please take a look at this post?



    https://discord.com/channels/967097582721572934/1222529839504818207

    I am trying to make a custom datepicker, if you have any advice, would be very glad to here it

  • default discord avatar
    notchr2 years ago

    I think this is a duplicate post



    @191776538205618177

    right?

  • default discord avatar
    markatomniux2 years ago

    How do you mean?

  • default discord avatar
    notchr2 years ago

    Oh I see



    Sorry, I saw Yersn post a similar thing



    In that datepicker thread

  • default discord avatar
    markatomniux2 years ago

    no worries 😄

  • default discord avatar
    kaspartr2 years ago
    @191776538205618177

    I started from something very simple - using custom component inside

    array

    field. 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, 
                  },
                }
             }
              }
        ]
         }
    ],
    ...
  • default discord avatar
    markatomniux2 years ago

    The UI field is not intended for saving values to the DB



    I think you want a text or JSON field

  • default discord avatar
    kaspartr2 years ago

    Thank You

    @191776538205618177

    for 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 on GitHub

Star

Chat on Discord

Discord

online

Can't find what you're looking for?

Get dedicated engineering support directly from the Payload team.