Is it possible to populate field with the title of a relation?

default discord avatar
snailedlt8 months ago
12

I want to automatically populate the name field with the name of another collection which is related.



Take a look at the screenshot.


I want to populate the Name based on the name of the selected Range.



Here's my code:


import type { CollectionConfig } from 'payload/types'

export const Zones: CollectionConfig = {
  slug: 'zones',
  labels: {
    singular: 'Zone',
    plural: 'Zones',
  },
  fields: [
    {
      name: 'name',
      type: 'text',
      required: true,
    },
    {
      name: 'range',
      type: 'relationship',
      relationTo: 'ranges',
      required: true,
    },
    {
      name: 'priceType',
      type: 'radio',
      defaultValue: 'flat',
      options: [
        {
          label: 'Flat Price',
          value: 'flat',
        },
        {
          label: 'Price per km over range start',
          value: 'perkm',
        },
      ],
      admin: {
        layout: 'horizontal',
        width: '100%',
      },
    },
    {
      name: 'price',
      label: 'Price (in NOK)',
      type: 'text',
      required: true,
      defaultValue: '0',
      validate: value => {
        if (isNaN(value)) {
          return 'Must be a number'
        }
      },
    },
  ],
  admin: {
    useAsTitle: 'name',
    group: 'Delivery',
    defaultColumns: ['name', 'range', 'price', 'priceType', 'threshold'],
  },
  timestamps: true,
}
  • default discord avatar
    clement63968 months ago
  • default discord avatar
    dr_mint8 months ago

    Is it that you want to give

    name

    a default value which is the name of the relationship, and then still be able to change it? Because if not, is it necessary to have a name field if the information can already be deduced from the title of the relationship. If you want to keep it to give your Zone a title, it could be done with a hidden field and a beforeChange hook to set this field value as the title of the relationship

  • default discord avatar
    clement63968 months ago


    Sorry im on m’y phone , you can se e I can access the field title of my document to fill the slug

    image0.jpg
  • default discord avatar
    snailedlt8 months ago

    I want the name field to be auto-populated with information from both the Range and price fields so that it is shown in the collection overview on the admin panel.



    Example values for the name field:


    -

    0-21km - 200nok

    -

    21-50km - 50nok per km


    Do I need a custom field though?


    I'd like to just set the

    name

    text field's value to

    range.name + price.value + 'nok'

    whenever either the range or the price fields change value



    I very much appreciate both of you helping me btw, so thank you!

  • default discord avatar
    clement63968 months ago

    Yep, you have to create this custom field, it's pretty easy if you already do some React

  • default discord avatar
    snailedlt8 months ago

    Alright 👍


    I don't have much experience with React unfortunately.


    Do you have a complete example by any chance?

  • default discord avatar
    dr_mint8 months ago

    You can try using a beforeChange hook on the collection (

    https://payloadcms.com/docs/hooks/collections#beforechange

    ). Whenever an entry is changed/created, it will modify the changed data to update the

    name

    value.


    It would look a bit like that


    import { CollectionBeforeChangeHook } from 'payload/types'
    
    const beforeChangeHook: CollectionBeforeChangeHook = async ({
      data // incoming data to update or create with
    }) => {
      console.debug("beforeChangeHook data:", data); // Just for debugging, can be removed once you've confirmed it is working
      return { ...data, name: data.range + data.price + 'nok' }; // Return data to update the document with
    }

    Because

    range

    is a relationship data.range will be the id of the selected Range. If you want to use its name instead, you will need to use the local api to fetch that info. It will probably be something like:


    const range = payload.findByID({collection: "ranges", id: data.range})


    You can then add the hook to your collection by doing:


    export const Zones: CollectionConfig = {
      slug: 'zones',
      labels: {
        singular: 'Zone',
        plural: 'Zones',
      },
      hooks: {                               // 
        beforeChange: [ beforeChangeHook ]   // These lines have been added
      },                                     // 
      fields: [ ... ],
      ...
    }
  • default discord avatar
    snailedlt8 months ago

    Wow, thank you very much!


    You basically did the whole implementation for me too!



    I'm gonna test it right away!



    That worked perfectly!


    Thank you for both the implementation and explanation



    For those interested, here's the solution:



    import payload from 'payload'
    import type { CollectionConfig } from 'payload/types'
    import { BeforeChangeHook } from 'payload/types'
    
    const BeforeChangeHook: BeforeChangeHook = async ({
      data, // incoming data to update or create with
    }) => {
      const range = await payload.findByID({ collection: 'ranges', id: data.range })
      return { ...data, name: range.name + ' - ' + data.price + 'nok' }
    }
    
    export const Zones: CollectionConfig = {
      slug: 'zones',
      labels: {
        singular: 'Zone',
        plural: 'Zones',
      },
      hooks: {
        beforeChange: [BeforeChangeHook],
      },
      fields: [
        {
          name: 'name',
          type: 'text',
          required: true,
        },
        {
          name: 'range',
          type: 'relationship',
          relationTo: 'ranges',
          required: true,
        },
        {
          name: 'priceType',
          type: 'radio',
          defaultValue: 'flat',
          options: [
            {
              label: 'Flat Price',
              value: 'flat',
            },
            {
              label: 'Price per km over range start',
              value: 'perkm',
            },
          ],
          admin: {
            layout: 'horizontal',
            width: '100%',
          },
        },
        {
          name: 'price',
          label: 'Price (in NOK)',
          type: 'text',
          required: true,
          defaultValue: '0',
          validate: value => {
            if (isNaN(value)) {
              return 'Must be a number'
            }
          },
        },
      ],
      admin: {
        useAsTitle: 'name',
        group: 'Delivery',
        defaultColumns: ['name', 'range', 'price', 'priceType', 'threshold'],
      },
      timestamps: true,
    }
  • default discord avatar
    dr_mint8 months ago

    Awesome, glad to hear 👍 btw you can probably set the name field to be hidden to avoid anyone changing it manually. If you still want the value to appear when you fetch it through the API, instead you can set condition to return false

  • default discord avatar
    eustachi08 months ago

    Great! Hooks are very powerful. Thanks for sharing.


    I did a custom field component as I wanted the data to change in real-time for a system I'm working on.



    Here is the code if anybody is interested


    import React, { useEffect, useState } from 'react';
    import { Label, useFormFields, TextInput } from 'payload/components/forms';
    import { Props } from 'payload/components/fields/Text';
    
    export const TestField: React.FC<Props> = (props) => {
        const { label, name } = props;
        const [value, setValue] = useState<string>(); // you can use the useField hook instead
    
        // get the values of the fields you want to use
        const timeValue: any = useFormFields(([fields, dispatch]) => fields.time.value);
        const distanceValue: any = useFormFields(([fields, dispatch]) => fields.distance.value);
    
        const concatFunction = () => {
            // you can do more stuff here
            let message = `You haven't walked yet!`;
            if (timeValue !== undefined && distanceValue !== undefined && timeValue > 0 && distanceValue > 0) {
                message = `You walked ${distanceValue}m in ${timeValue}h`;
            }
            setValue(message);
        }
    
        useEffect(() => {
            concatFunction()
        }, [timeValue, distanceValue])
    
    
        return (
            <div>
                <Label
                    label={label}
                />
                <TextInput value={value !== undefined ? value : ''} name='uiField' path='uiField' onChange={concatFunction} />
            </div>
        )
    }


    You can plug it as a field of UI field type in your collection

    ui-field.gif
    image.png
  • default discord avatar
    clement63968 months ago

    Good job guys :payloadlove:

  • default discord avatar
    snailedlt8 months ago

    I tried doing that, but then the field didn't get set for some reason.


    Seems like it might be a bug?



    I tried to implement a hook the same way here:

    https://github.com/Snailedlt/next-payload-gv/blob/90a8eb104882e1c8137f475083216ec3179773d0/payload/collections/PriceZones/hooks/beforeChange.ts#L11

    But I keep getting this error:


    TypeError: hook is not a function
        at eval (webpack-internal:///(api)/./node_modules/payload/dist/collections/operations/create.js:111:22)
        at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
        at async create (webpack-internal:///(api)/./node_modules/payload/dist/collections/operations/create.js:109:5)
        at async handler (webpack-internal:///(api)/./node_modules/@payloadcms/next-payload/dist/handlers/[collection]/index.js:83:33)

    Anyone know why I'm getting that error, and how to fix it?

Star on GitHub

Star

Chat on Discord

Discord

online

Can't find what you're looking for?

Get help straight from the Payload team with an Enterprise License.