How to nest block into same block

default discord avatar
Marťafiixek
3 months ago
20

Hi, suppose I have block named "Container". I want to have an option to have "Container" block as a child of "Container" block (nest block into same block).



Is there an recommended approach on how to do that? Thanks



Because we can't do that:



export const Container: Block = {
  slug: "container",
  labels: {
    singular: "Kontejner",
    plural: "Kontejnery",
  },
  fields: [
    {
      label: "Komponenty",
      labels: {
        singular: "Komponenta",
        plural: "Komponenty",
      },
      name: "layout",
      type: "blocks",
      required: true,
      blocks: [
        Container, // we can't use it before it's declared
      ],
    },
  ],
};


I understand, we can do something like this to reuse fields and keep it a bit DRY and it would work, but isn't there really some better way?



const fields: Field[] = [
  {
    name: "width",
    label: "Šířka kontejneru",
    type: "radio",
    options: [
      {
        label: "Výchozí šířka",
        value: "default",
      },
      {
        label: "Rozšířená šířka",
        value: "extended",
      },
      {
        label: "Plná šířka",
        value: "full",
      },
    ],
    defaultValue: "default",
    admin: {
      layout: "horizontal",
    },
  },
  {
    name: "paddingY",
    label: "Horizontální odsazení",
    type: "radio",
    options: [
      { label: "Malé", value: "sm" },
      { label: "Střední", value: "md" },
      { label: "Velké", value: "lg" },
      { label: "Velmi velké", value: "xl" },
    ],
    defaultValue: "default",
    admin: {
      layout: "horizontal",
    },
  },
  colorField,
];

export const Container: Block = {
  slug: "container",
  labels: {
    singular: "Kontejner",
    plural: "Kontejnery",
  },
  fields: [
    ...fields,
    {
      label: "Komponenty",
      labels: {
        singular: "Komponenta",
        plural: "Komponenty",
      },
      name: "layout",
      type: "blocks",
      required: true,
      blocks: [
        Articles,
        Button,
        Columns,
        ContactForm,
        ContactSection,
        Divider,
        Faq,
        Features,
        Gallery,
        Heading,
        NumberedList,
        OwlCarousel,
        Content,
        SectionImage,
        Timeline,
        {
          slug: "container",
          labels: {
            singular: "Kontejner",
            plural: "Kontejnery",
          },
          fields: [...fields],
        },
      ],
    },
  ],
};
  • discord user avatar
    jesschow
    Payload Team
    2 months ago

    Hi @Marťafiixek - apologies for the delay, is this the method you've settled on? Reusing your fields as you have done above is a nice solution honestly although it may feel bulky

  • default discord avatar
    Marťafiixek
    2 months ago

    Hi, yes - I settled on this method. Nesting containers works nicely with the Payload UI/UX and I can render the nested containers on the website with no problems. One minor thing I encountered today is this:



    When you want to use types for blocks in your frontend code, you can do this:


    type ButtonBlock = Extract<Page['layout'][0], { blockType: 'button' }>


    The above works. However, how can I use this approach to get type of a block with

    blockType === "articles"

    that is nested one level deeper?


    export interface Page {
      id: string;
      title: string;
      layout: (
        | {
            blockType: 'container';
            layout: (
              | {
                  blockType: 'articles';
                }
              | {
                  blockType: 'button';
                }
    ...


    Eg. block "articles" nested in "container" block? I mean, I tried every possible solution but every time I got "never" as a type 😦

  • discord user avatar
    jmikrut
    Payload Team
    2 months ago

    hmm, it's a good question



    we have a roadmap item that will allow you to declare top-level interfaces for fields / blocks / etc - and then have those interfaces be re-used



    which is likely what will solve this in a permanent way for you as it is



    but in the interim, i feel like there should be a way to do this



    especially if you can first get the type of your

    ButtonBlock

    , and then use

    Extract

    against

    that

    type in the same way you did to get the

    ButtonBlock

    itself... in theory, it's no different, right?

  • default discord avatar
    Marťafiixek
    2 months ago

    Thought the same thing, it shouldn't be different, however, somehow I can't make it work. When I try to do it like you suggest, I get

    never

    type, meaning the Extract utility doesn't "see" the

    blockType === "articles"


    So, I had to skip this and use an

    any

    type on all blocks that are deeply nested



    And I will try to make it work after I deliver the project to client



    I figured it out, gonna post a solution soon



    So, let's imagine this tree:



    - Container block


    - Columns block


    - Column block



    The correct way to get the type of a column block is like this:


    type ColumnBlock = Extract<
      Extract<
        Extract<Page["layout"][number], { blockType: "container" }>["layout"][number],
        { blockType: "columns" }
      >["layout"][number],
      { blockType: "column" }
    >;


    Usage inside TSX below:


    import { WIDTH } from "./style";
    import { Page } from "payload-types";
    
    import { joinClassNames } from "utils/helper";
    
    import { RenderBlocks } from "components/RenderBlocks";
    
    type ColumnBlock = Extract<
      Extract<
        Extract<Page["layout"][number], { blockType: "container" }>["layout"][number],
        { blockType: "columns" }
      >["layout"][number],
      { blockType: "column" }
    >;
    
    const Column = ({ width, layout }: ColumnBlock) => {
      return (
        <div className={joinClassNames(WIDTH[width])}>
          <RenderBlocks blocks={layout} />
        </div>
      );
    };
    
    export default Column;


    Is it ugly? A bit. Does it work? Hell YEAH!



    Also, I sometimes tend to omit properties like

    blockType

    that are needed by Payload but not needed by the FE component itself.



    An example from a

    Button

    component:


    type ButtonBlockType = Partial<
      Omit<Extract<Page["layout"][0], { blockType: "button" }>, "blockType" | "link">
    > & {
      link?: any;
    };


    In the example above I also substituted the link property as I didn't want to think a lot about

    link.reference.value

    being string vs. the populated page or post (hope you understand what I mean) so I just set it as any (I know, demonic thing to do)



    But yeah, top level interface or type for a block would be nice thing to have ❤️

  • discord user avatar
    jmikrut
    Payload Team
    2 months ago

    amazing



    this is a great interim solution for sure



    but yes, shortly, we will have the ability to define to-level interfaces for sure

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.