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.

How to make a custom field for an icon library?

default discord avatar
snailedltlast year
41

I want to integrate Lucide-icons into payloadcms.



To do this I want a autofill or searchable select field which shows the name and a preview of the icon from the lucide-icons icon pack.



It seems like this should be possible, but I'm not sure how to implement it. Are there any ready made examples of icon fields that integrate with icon packs?

  • default discord avatar
    notchrlast year

    @Snailed lt This would be a super cool idea, I love the lucide icons



    I'm not a React guy, but since Payload uses React components, this would probably be a good fit

    https://lucide.dev/guide/packages/lucide-react


    And then built via custom component / field



    https://payloadcms.com/docs/admin/components#fields
  • default discord avatar
    snailedltlast year

    Looks like a good starting point!


    I'm just not sure how to build the custom component so that the select field autopopulates options with the lucide icon names.



    I hope someone ends up making a plugin for such a field... If not I might give it a try soon™

  • default discord avatar
    notchrlast year

    @Snailed lt Ah that shoiuld be pretty easy!



    for instance, say it was plain javascript



    https://jsfiddle.net/notchris/37t4pkz0/5/


    const iconSelect = document.createElement('select')
    Object.keys(window.lucide.icons).forEach((icon) => {
        const opt = document.createElement('option')
      opt.value = icon
      opt.innerHTML = icon
      iconSelect.appendChild(opt)
    })
    document.body.appendChild(iconSelect)


    That creates a select list of every icon name from the Lucide package



    (the main package exports

    icons

    as a keyed list



    So now you can create a custom select field in payload



    and BOOM it shall work



    If you still need help, I can help you write it



    But give it a go and show me what you come up with

  • default discord avatar
    snailedltlast year

    Wow, that was a fast demonstration! Thanks a bunch!



    I'll give it a try

  • default discord avatar
    notchrlast year

    Good luck! And yeah, if you get really stuck, let me know, I would start with.....



    Creating a basic Select field type (Before creating a custom field that displays the actual icon, etc)



    https://payloadcms.com/docs/fields/select


    Note: In the JSFIDDLE example I shared, the

    lucide.icons

    object is a keyed by the icon names, where each value is the SVG of that icon. I'm simply getting the key names, not the SVG, so youll want to loop over each key/value, maybe with Object.entries.

  • default discord avatar
    snailedltlast year

    how did you get lucide.icons added to the window variable btw?

  • default discord avatar
    notchrlast year

    @Snailed lt When you use the CDN version of the library, it adds lucide as a property to window, like most CDN libraries



    @Snailed lt The ESM version should have it available under the modules default export



    or one of the exports



    This page has some information about that

    https://lucide.dev/guide/packages/lucide


    You will likely install lucide via your package mananger instead



    And then import it right into your component



    import { createIcons, icons } from 'lucide'


    at which point, icons should be the same as

    lucide.icons

    in my example

  • default discord avatar
    snailedltlast year

    I managed to get the select up, now I just need to make the value a combination of a SVG and the name of the icon



    Implementation 👇


    import { Field } from 'payload/types';
    import { Option } from 'payload/dist/fields/config/types';
    import deepMerge from '../utilities/deepMerge';
    import { createIcons, icons } from 'lucide';
    
    export function generateLucideIconOptions(): Option[] {
      const lucideIconOptions: Option[] = [];
      Object.keys(icons).forEach((icon) => {
        lucideIconOptions.push({
          label: icon,
          value: icon,
        });
      });
      console.debug('lucideIconOptions', lucideIconOptions);
      return lucideIconOptions;
    }
    
    export function lucideIcon({ overrides = {} } = {}): Field {
      const lucideIcon: Field = {
        name: 'lucideIcon',
        type: 'select',
        options: generateLucideIconOptions(),
      };
      return deepMerge(lucideIcon, overrides);
    }


    Usage 👇


    import { CollectionConfig } from 'payload/types';
    import { lucideIcon } from '../fields/lucideIcon';
    
    export const Icons: CollectionConfig = {
      slug: 'icons',
      labels: {
        singular: 'Icon',
        plural: 'Icons',
      },
      fields: [lucideIcon()],
    };
    image.png
  • default discord avatar
    notchrlast year

    YOO



    Nicely done!



    😄



    Note: Rendering a ton of SVGs could cause render blocking



    You may only want to display the icon of the selected value



    and add maybe a link to the live icon list on lucide

  • default discord avatar
    snailedltlast year

    Good idea!


    Thanks for all the help



    Do you still have time to help with the ui field implementation?



    I haven't messed around with ui fields yet, so not sure where to start



    The documentation is a bit scarce:

    https://payloadcms.com/docs/fields/ui
  • default discord avatar
    notchrlast year

    I can help out in a little bit! I'm working on finishing a task at work but will be avail around Noon to check back 😄

  • default discord avatar
    snailedltlast year

    Great, thanks!



    So I've figured out that I need to create a Custom Component Field.



    My idea is that I can just copy the one used for

    type: 'select'

    , and allow the addition of this type as an option:


    export type OptionObjectWithIcon = {
      label: Record<string, string> | string;
      value: string;
      icon: string; // svg code as a string
    };


    Then if the type is

    OptionObjectWithIcon

    I'll just render the svg inside the select's option

  • default discord avatar
    notchrlast year

    Ah that would be cool! Though, does option support non-text?



    Maybe their custom select dos



    does*

  • default discord avatar
    snailedltlast year

    I have no idea how to do it in practice though, so I've been trying to get ChatGPT to do it for me to no avail:

    https://chat.openai.com/share/cd94d7b8-b639-432a-9acc-b2e9060f72ca


    They have a custom select? 😮

  • default discord avatar
    notchrlast year

    Well im saying, the built in one



    This one looks nice:

    https://react-select.com/home
  • default discord avatar
    snailedltlast year

    The built in select field is of type

    SelectField

    :


    export type OptionObject = {
        label: Record<string, string> | string;
        value: string;
    };
    export type Option = OptionObject | string;
    export type SelectField = FieldBase & {
        type: 'select';
        options: Option[];
        hasMany?: boolean;
        admin?: Admin & {
            isClearable?: boolean;
            isSortable?: boolean;
        };
    };
  • default discord avatar
    notchrlast year

    hmmm

  • default discord avatar
    snailedltlast year

    Agreed, but if we can just override the original one so that it accepts my type (

    OptionObjectWithIcon

    ) as an option, and renders the SVG if

    option.icon

    is not empty I think that would be better since we hopefully keep the same styling instead of having to re-style a new component

  • default discord avatar
    notchrlast year

    And what is the current issue with getting the icon to display?

  • default discord avatar
    snailedltlast year

    I get this error:


    [15:29:35] ERROR (payload): There were 1 errors validating your Payload config
    [15:29:35] ERROR (payload): 1: Collection "products" > Field "categories" > "value" does not match any of the allowed types   
    [nodemon] app crashed - waiting for file changes before starting...


    not sure why though



    I can share all the relevant files if you want

  • default discord avatar
    notchrlast year

    Hmm, if we get to the point where I need to check the files then OK, but lets try to debug some more



    So the error complains

    Collection "products" > Field "categories" > "value"


    When are you getting the value of the categories field



    setting*

  • default discord avatar
    snailedltlast year

    Folder structure in attached image for reference

    image.png
  • default discord avatar
    notchrlast year

    And what value type does it expect?

  • default discord avatar
    snailedltlast year

    I'm not even sure I'm trying to get the value anywhere

  • default discord avatar
    notchrlast year

    It's likely just through general validation



    Give me a little bit and I'll be back on after lunch to review this more in depth

  • default discord avatar
    snailedltlast year

    Don't feel the urge to respond right away, but I'll just post this while I still remember it 😛



    I think it's complaining about the value inside categoriesOptions here:


    // collections/Products.ts
    
    import { CollectionConfig } from 'payload/types';
    import CustomSelectFieldWithIcon, {
      OptionObjectWithIcon,
    } from '../fields/selectFieldWithIcon'; // Adjust the path if needed
    
    // Define the options for the Categories field as custom OptionObject[] type
    const categoriesOptions: OptionObjectWithIcon[] = [
      {
        label: {
          en: 'Electronics',
          fr: 'Électronique', // You can include labels for other languages as well
        },
        value: 'electronics',
        icon: '<svg>...</svg>', // Replace with your SVG icon as a string
      },
      {
        label: {
          en: 'Clothing',
          fr: 'Vêtements',
        },
        value: 'clothing',
        icon: '<svg>...</svg>',
      },
      // Add more options as needed
    ];
    
    export const Products: CollectionConfig = {
      slug: 'products',
      fields: [
        // Your other fields...
        {
          name: 'categories',
          label: 'Categories',
          type: 'select',
          options: categoriesOptions, // Use the custom OptionObject type for options
          admin: {
            // Optional admin configuration
            components: {
              Field: CustomSelectFieldWithIcon, // Use the custom field component
            },
          },
        },
        // Your other fields...
      ],
    };


    @Jarrod This is what I have working^



    I want to add an icon for each select option. How do I do that?

  • Like this

    CleanShot_2023-07-31_at_16.15.16.png
  • default discord avatar
    snailedltlast year

    Oooo, nice! Gonna try it out ASAP

  • Basically the underlying field is a text field, because the payload config is static and you will not know the icon keys beforehand. So you render a custom react component that

    renders

    a select field, but the actual value saved is not an enum.

  • default discord avatar
    snailedltlast year

    I see. That example was great since it taught me which "components" I need to make to create a custom component and make use of it



    I'm wondering how I can pass parameters to the index.tsx file though. So that I can choose the

    options

    content myself



    I'm not really experienced with React btw.


    So please let me know if I'm asking questions about React and not Payload 🙂



    This is what I get when I run the example code btw:

    image.png
  • Yes, that is what you should get. Now you can style it however you want, you will need to generate the options for lucid icons and use those instead of my placeholders.

  • default discord avatar
    snailedltlast year

    How do I pass in the props though? Is there any documentation on that?

  • what do you mean? What props?

  • default discord avatar
    snailedltlast year

    like this?


    export function CustomSelect({
      path,
      options = ['icon-1', 'icon-2'],
    }): React.FC<any> {
      return (
        <div>
          <Select path={path} name={path} options={options} />
          <RenderIcon path={path} />
        </div>
      );
    }


    The props to the CustomSelect function

  • in that component, you need to create the options object to pass into the select component



    sounds like you guys talked above about how to curate a list of icon keys



    that is what you would pass into the select component



    and you can run that code right within this component. You will likely want to create some state with React (setting the value inside a useEffect) and then pass the state value through to the Select

  • default discord avatar
    snailedltlast year

    Alright, I think I kinda understand. I'll try for a bit and get back to you if I'm completely stuck.



    Thanks a lot for the help!

  • Sounds great! No problem

  • default discord avatar
    snailedltlast year

    Shouldn't this work?


    import { Field } from 'payload/types';
    import { Option } from 'payload/dist/fields/config/types';
    import { createIcons, icons } from 'lucide';
    import { CustomSelect } from './CustomSelect';
    
    export function generateLucideIconOptions(): Option[] {
      const lucideIconOptions: Option[] = [];
      Object.keys(icons).forEach((icon) => {
        lucideIconOptions.push({
          label: icon,
          value: icon,
        });
      });
      return lucideIconOptions;
    }
    
    export const IconSelectorField: Field = {
      name: 'lucidIcon',
      type: 'text',
      admin: {
        components: {
          Field: CustomSelect({ options: generateLucideIconOptions() }),
        },
      },
    };


    @Jarrod ^



    It happens when my CustomSelect code is like this



    If I change it to this I get a different error





    I made it!



    Not as generic as I'd wanted, but I finally made it work



    My implementation:



    Thank you both chris and Jarrod for the help!



    Result:

    image.png
    image.png
    image.png
    image.png
    image.png
    image.png
    image.png
  • Nice! A little css and they could be right next to each other 😁

  • default discord avatar
    infitcon4 months ago

    Do you have a working gist for this? it would be nice

  • default discord avatar
    snailedlt4 months ago

    No, I sadly didn't make one. Maybe a image to text service can be used to get the code though?


    Feel free to make a gist if you'd like. Consider the code open-sourced 🙂

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..