How to make a custom field for an icon library?

default discord avatar
snailedlt
2 months ago
39

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
    notchr
    last month

    @snailedlt 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
    snailedlt
    last month

    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
    notchr
    last month

    @snailedlt 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
    snailedlt
    last month

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



    I'll give it a try

  • default discord avatar
    notchr
    last month

    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
    snailedlt
    last month

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

  • default discord avatar
    notchr
    last month

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



    @snailedlt 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
    snailedlt
    last month

    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
    notchr
    last month

    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
    snailedlt
    last month

    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
    notchr
    last month

    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
    snailedlt
    last month

    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
    notchr
    last month

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



    Maybe their custom select dos



    does*

  • default discord avatar
    snailedlt
    last month

    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
    notchr
    last month

    Well im saying, the built in one



    This one looks nice:

    https://react-select.com/home
  • default discord avatar
    snailedlt
    last month

    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
    notchr
    last month

    hmmm

  • default discord avatar
    snailedlt
    last month

    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
    notchr
    last month

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

  • default discord avatar
    snailedlt
    last month

    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
    notchr
    last month

    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
    snailedlt
    last month

    Folder structure in attached image for reference

    image.png
  • default discord avatar
    notchr
    last month

    And what value type does it expect?

  • default discord avatar
    snailedlt
    last month

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

  • default discord avatar
    notchr
    last month

    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
    snailedlt
    last month

    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_not_jared This is what I have working^



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

  • discord user avatar
    jarrod_not_jared
    Payload Team
    last month

    Like this

    CleanShot_2023-07-31_at_16.15.16.png
  • default discord avatar
    snailedlt
    last month

    Oooo, nice! Gonna try it out ASAP

  • discord user avatar
    jarrod_not_jared
    Payload Team
    last month

    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
    snailedlt
    last month

    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
  • discord user avatar
    jarrod_not_jared
    Payload Team
    last month

    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
    snailedlt
    last month

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

  • discord user avatar
    jarrod_not_jared
    Payload Team
    last month

    what do you mean? What props?

  • default discord avatar
    snailedlt
    last month

    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

  • discord user avatar
    jarrod_not_jared
    Payload Team
    last month

    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
    snailedlt
    last month

    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!

  • discord user avatar
    jarrod_not_jared
    Payload Team
    last month

    Sounds great! No problem

  • default discord avatar
    snailedlt
    last month

    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_not_jared ^



    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
  • discord user avatar
    jarrod_not_jared
    Payload Team
    last month

    Nice! A little css and they could be right next to each other 😁

Open the post
Continue the discussion in Discord
Like what we're doing?
Star us on GitHub!

Star

Connect with the Payload Community on Discord

Discord

online

Can't find what you're looking for?

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