How to Add Dynamic Editing Regions to Custom Components

Need to create a custom component with an area where users can drag in additional blocks? This post will guide you through creating one, building off of the <DynamicColumns> component in our react-design-system starter and using <BuilderBlocks> for the editing region.

Read more about the React design system example here

Open the react-design-system example in your code editor and navigate to src/components/. Let’s call our new component CustomColumns and create a folder for it. Add a file called CustomColumns.jsx then copy and paste the code from DynamicColumns.jsx . To make the <CardContent> area editable, replace the text component with <BuilderBlocks/> :

import { Image, BuilderBlocks } from '@builder.io/react';

...

    <CardContent>
        <BuilderBlocks
            key={index}
            child
            parentElementId={props.builderBlock && props.builderBlock.id}
            blocks={col.blocks}
            dataPath={`component.options.columns.${index}.blocks`}
        />
    </CardContent>

...

This is similar to how we implement fully customizable columns in Builder’s built-in Columns component.

Next we need to register the component. Create another file in CustomColumns/ called CustomColumns.builder.js and copy and paste the code from DynamicColumns.builder.js . Notice the list input type here which is what allows users to add more columns identical to the default column. Replace the default text inputs with blocks to make the area under the image dynamically editable.

import { Builder } from '@builder.io/react';
import { CustomColumns } from './CustomColumns';

Builder.registerComponent(CustomColumns, {
  name: 'Custom Columns',
  description: 'Example of a custom column with editing regions',
  inputs: [
    {
      name: 'columns',
      type: 'array',
      defaultValue: [
        {
          image: 'https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d',
          blocks: [],
        },
        {
          image: 'https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d',
          blocks: [],
        },
      ],
      subFields: [
        {
          name: 'image',
          type: 'file',
          allowedFileTypes: ['jpeg', 'jpg', 'png', 'svg'],
          required: true,
          defaultValue:
            'https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d',
        },
				{
          name: 'blocks',
          type: 'blocks',
          hideFromUI: true,
          helperText: 'This is an editable region where you can drag and drop blocks.',
        },
      ],
    },
  ],
});

Now we can test our new Custom Columns component in the visual editor and see that the area under the image is editable!

Additionally, we can add a default component to the blocks as a placeholder:

...
{
    image:
        'https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d',
    blocks: [
        {
            '@type': '@builder.io/sdk:Element',
            component: {
                name: 'Text',
                options: {
                    text: 'Enter some text...',
                },
            },
        },
    ],
},
...

custom-cols (1)

View the full code for the Custom Columns component here:

https://github.com/BuilderIO/builder/tree/main/examples/react-design-system/src/components/CustomColumns

2 Likes

Just to add on - a simple example that just needs one set of children:

import { BuilderBlocks, Builder } from '@builder.io/react';

function CustomSection(props) {
  return (
    <div>
      <BuilderBlocks blocks={props.children} parentElementId={props.builderBlock.id} dataPath="children" />
    </div>
  );
}

Builder.registerComponent(CustomSection, {
  name: 'My Section',
  inputs: [/* optional, any other inputs */],
  // Optional, if you want default children. If none are provided or if children are empty the
  // "+ add block" button shows
  defaultChildren: [
    {
      '@type': '@builder.io/sdk:Element',
      component: {
        name: 'Text',
        options: {
          text: 'I am a default child!',
        },
      },
    }
  ]
});

Hi Steve, when I use your simple example and try to drop or add a new block using the " + Add block" button these blocks are being added after the Section Block and not inside.

Hey @luigiveoworld - can you share a reproducible code example? Will help us be able to point out any possible issues or replicate on our end to debug

I directly copy and pasted this code block, after that I used it in Builder visual editor and using the “Add block” button is adding the new block after this block and not inside of it.:

import { BuilderBlocks, Builder } from '@builder.io/react';

function CustomSection(props) {
  return (
    <div>
      <BuilderBlocks blocks={props.children} parentElementId={props.builderBlock.id} dataPath="children" />
    </div>
  );
}

Builder.registerComponent(CustomSection, {
  name: 'My Section',
  inputs: [],
  defaultChildren: []
});

hey @luigiveoworld - thanks for sending. On second look I may have gotten a couple important nuances wrong on my first suggestion, I think what we want is this - can you give it a try?

import { BuilderBlocks, Builder } from '@builder.io/react';

function CustomSection(props) {
  return (
    <div>
      <BuilderBlocks blocks={props.builderBlock.children} parentElementId={props.builderBlock.id} dataPath="this.children" />
    </div>
  );
}

Builder.registerComponent(CustomSection, {
  name: 'My Section',
  canHaveChildren: true
});

It’s based on this example builder/Mutation.tsx at main · BuilderIO/builder · GitHub

1 Like

Thank you @steve I can confirm that it works as expected now.

I have a component with two columns and each column need to receive a block, this code is working in parts because when I add child component in a column in the visual editor, it’s adding the child component in the two columns automaticaly, but I need to add separated, how to do it?

example code:

import React, { useState } from ‘react’
import { Container, Column } from ‘./styles’;
import { BuilderBlocks } from ‘@builder.io/react’;

type Props = {
left?: boolean;
right?: boolean;
builderBlock?: any;
};

function TestComponent({ left, right, builderBlock }: Props) {

return (


{ }

  <Column right={right}>
    { <BuilderBlocks blocks={builderBlock.children} parentElementId={builderBlock.id} dataPath="this.children" /> }
  </Column>

</Container>

)
}

export default TestComponent

I can’t use left or right props because these props are used to define which column will be largest.

Hi @Luiz you can name the props whatever you like, here is an example with two builder components that will take separate content, try it out and adjust the styling, inputs, etc to meet your needs !

import { Builder, withChildren, BuilderBlocks  } from '@builder.io/react';

export const ExampleWithChildren = (props) => {
  return (
    <div className="flex">
        <BuilderBlocks
            child
            parentElementId={props.builderBlock && props.builderBlock.id}
            blocks={props.leftSection}
            dataPath={`component.options.firstSectionWhatever`} />
        <BuilderBlocks
            child
            parentElementId={props.builderBlock && props.builderBlock.id}
            blocks={props.rightSection}
            dataPath={`component.options.thisOtherThing`} />
    </div>
  )
 };

 Builder.registerComponent(withChildren(ExampleWithChildren), {
    name: "exampleWithChildren",
    inputs: [
      {
        name: "leftSection",
        type: "blocks",
        hideFromUI: true,
        defaultValue: [],
      },
      {
        name: "rightSection",
        type: "blocks",
        hideFromUI: true,
        defaultValue: [],
      },
    ],
 })

Hey @TimG ,

I tried using your example on the Builder playground. I’m experiencing an issue where child components are added outside the ExampleWithChildren component. I created a short screencast to demonstrate the issue Screen Recording 2024-02-16 at 1.03.18 PM.mov - Droplr

Any ideas how to resolve this? We have the same issue on any custom component with children we create.

Thanks!

this issue happens when you do not correctly set the default data + <BuilderBlocs /> component parameters.

  • first make sure your input responsible for the blocks contain an empty array, like so:
{
      name: 'includedChildren',
      type: 'array',
      hideFromUI: true,
      subFields: [
        {
          name: 'item',
          type: 'array',
        },
      ],
      defaultValue: [
        {
          item: [],
        },
      ],
    },
  • make sure your builder blocks component have the proper setup (all of the parameters that I used here are necessary):
{includedChildren?.map((child, index) => (
  <BuilderBlocks
     key={index}
     parentElementId={builderBlock?.id}
     dataPath={`component.options.includedChildren.${index}.item`}
     blocks={child.item}d
   />
}

config:-

import type { Component } from "@builder.io/sdk";

const SeoSectionConfig: Component = {
	name: "SeoSection",
	inputs: [
		{
			name: "heading",
			type: "string",
			required: true,
			defaultValue: "Start your weight loss journey",
			friendlyName: "Heading",
			helperText: "The main heading of AssessmentCTA",
		},
		{
			name: "subheading",
			type: "string",
			required: true,
			defaultValue: "Assessments typically take between 5-10 minutes.",
			friendlyName: "Subheading",
			helperText: "The subheading displayed below the main heading",
		},
		{
			name: "links",
			friendlyName: "Anchor Links for Navigation Sidebar",
			type: "list",
			required: true,
			subFields: [
				{
					name: "anchorlinktext",
					friendlyName: "Text Displayed on the Link",
					type: "string",
					required: true,
				},
				{
					name: "sectionid",
					friendlyName: "Target Section ID (Scroll to Section)",
					type: "string",
					required: true,
				},
				{
					name: "description",
					friendlyName: "Description of the Section",
					type: "string",
				},
			],
		},
	],
	canHaveChildren: true,
	defaultChildren: [
		{
			"@type": "@builder.io/sdk:Element",
			component: {
				name: "Text",
				options: {
					text: "This is a default child block.",
				},
			},
		},
	],
};

export default SeoSectionConfig;

code :-

import { withChildren } from "@builder.io/react";
import React from "react";
import type { PropsWithChildren, ReactNode } from "react";

import BlogNavigationSidebar from "../BlogNavigationSidebar";
import StickyAnchorWrapper from "../StickyAnchorWrapper";

export interface SEOSectionProps extends PropsWithChildren {
	heading: string;
	subheading: string;
	links?: {
		anchorlinktext: string;
		sectionid: string;
		description: string;
		additionalContent?: ReactNode;
	}[];
}

const SeoSection = ({
	heading,
	subheading,
	links,
	...props
}: SEOSectionProps) => {
	return (
		<div className='flex min-h-screen w-full flex-col gap-[40px] bg-transparent px-4 py-16 sm:gap-[32px] sm:px-[32px] sm:py-[80px] lg:flex-row lg:gap-[20px] lg:px-[40px]'>
			<aside className='flex w-full flex-col gap-6 bg-transparent sm:gap-[32px] lg:w-[47%] lg:gap-[40px]'>
				<div className='flex flex-col gap-2 sm:gap-3'>
					{heading && (
						<h2 className='text-3xl font-medium leading-lg tracking-[-0.02em] text-grey-900'>
							{heading}
						</h2>
					)}

					{subheading && (
						<p className='text-sm leading-sm text-grey-600'>{subheading}</p>
					)}
				</div>
				<div className='flex flex-col gap-3 sm:gap-4'>
					<p className='text-sm leading-sm text-grey-500'>Table of contents</p>
					{links && <BlogNavigationSidebar links={links} />}
					{links && (
						<StickyAnchorWrapper links={links} className='flex lg:hidden' />
					)}
				</div>
			</aside>

			<div className='flex w-full flex-col gap-[28px] bg-transparent sm:gap-[32px] lg:gap-[40px]'>
				{links?.map(
					(link, index) =>
						link && (
							<>
								<section
									key={index}
									id={link.sectionid}
									className='flex flex-col gap-3'
								>
									{link?.anchorlinktext && (
										<h5 className='text-2xl font-medium tracking-[-0.01em] text-grey-900'>
											{link.anchorlinktext}
										</h5>
									)}
									{link?.description && (
										<p className='text-base font-normal leading-base text-grey-700'>
											{link.description}
										</p>
									)}

									{/* Render additional content dynamically */}
									{link.additionalContent && (
										<div className='mt-4'>{link.additionalContent}</div>
									)}
								</section>
							</>
						),
				)}
				{props.children}
			</div>
		</div>
	);
};

// export default SeoSection;

// IMPORTANT: withChildren is required to enable child block functionality
export const HeroWithBuilderChildren = withChildren(SeoSection);

so i want to add multiple blokcs below the links mapped like if one link added in builder then i can drop somthign below on that and same for other links too…

Hi @Umang_001 I would suggest checking out this doc: Adding Children to Custom Components - Builder.io

Based on your example I think you need to add some inputs of type uiBlocks that map to the areas on your component where you want to have editable regions and be able to drag and drop components

Let me know if the examples in these docs are helpful to address your exact requirements!

that way it does’nt working.

code :-

import type { BuilderElement } from "@builder.io/react";
import { BuilderBlocks } from "@builder.io/react";
import React from "react";

import BlogNavigationSidebar from "../BlogNavigationSidebar";
import StickyAnchorWrapper from "../StickyAnchorWrapper";

export interface SEOSectionProps {
	heading: string;
	subheading: string;
	links: {
		anchorlinktext: string;
		sectionid: string;
		description: string;
		blocks: BuilderElement[];
	}[];
}

const SeoSection = ({ heading, subheading, links }: SEOSectionProps) => {
	return (
		<div className='flex min-h-screen w-full flex-col gap-[40px] bg-transparent px-4 py-16 sm:gap-[32px] sm:px-[32px] sm:py-[80px] lg:flex-row lg:gap-[20px] lg:px-[40px]'>
			<aside className='flex w-full flex-col gap-6 bg-transparent sm:gap-[32px] lg:w-[47%] lg:gap-[40px]'>
				<div className='flex flex-col gap-2 sm:gap-3'>
					{heading && (
						<h2 className='text-3xl font-medium leading-lg tracking-[-0.02em] text-grey-900'>
							{heading}
						</h2>
					)}
					{subheading && (
						<p className='text-sm leading-sm text-grey-600'>{subheading}</p>
					)}
				</div>
				<div className='flex flex-col gap-3 sm:gap-4'>
					<p className='text-sm leading-sm text-grey-500'>Table of contents</p>
					{links && (
						<BlogNavigationSidebar links={links} className='border-none p-0' />
					)}
					{links && (
						<StickyAnchorWrapper
							links={links}
							className='flex border-none lg:hidden'
						/>
					)}
				</div>
			</aside>

			<div
				id='abc'
				className='flex w-full flex-col gap-[28px] bg-transparent sm:gap-[32px] lg:gap-[40px]'
			>
				{links?.map((link, index) => (
					<section
						key={index}
						id={link.sectionid}
						className='flex flex-col gap-3'
					>
						{link?.anchorlinktext && (
							<h5 className='text-2xl font-medium tracking-[-0.01em] text-grey-900'>
								{link.anchorlinktext}
							</h5>
						)}
						{link?.description && (
							<p className='text-base font-normal leading-base text-grey-700'>
								{link.description}
							</p>
						)}

						<div className='specific-block-container'>
							<BuilderBlocks
								child
								parentElementId={link.sectionid}
								blocks={link.blocks}
								dataPath={`links.${index}.blocks`}
							/>
						</div>
					</section>
				))}
			</div>
		</div>
	);
};

export default SeoSection;

config.file :-
import type { Component } from “@builder.io/sdk”;

const SeoSectionConfig: Component = {
name: “SeoSection”,
noWrap: true,
inputs: [
{
name: “heading”,
type: “string”,
required: true,
defaultValue: “Start your weight loss journey”,
friendlyName: “Heading”,
helperText: “The main heading displayed at the top of the SEO Section.”,
},
{
name: “subheading”,
type: “string”,
required: true,
defaultValue: “Assessments typically take between 5-10 minutes.”,
friendlyName: “Subheading”,
helperText:
“The subheading displayed below the main heading in the SEO Section.”,
},
{
name: “tableOfContentsHeading”,
type: “string”,
required: false,
defaultValue: “Table of contents”,
friendlyName: “Table of Contents Heading”,
helperText: “The heading displayed above the table of contents section.”,
},
{
name: “links”,
friendlyName: “Anchor Links for Navigation Sidebar”,
type: “array”,
required: true,
subFields: [
{
name: “anchorlinktext”,
friendlyName: “Text Displayed on the Link”,
type: “string”,
required: true,
},
{
name: “sectionid”,
friendlyName: “Target Section ID (Scroll to Section)”,
type: “string”,
required: true,
},
{
name: “description”,
friendlyName: “Description of the Section”,
type: “string”,
},
{
name: “blocks”,
type: “blocks”,
defaultValue: ,
helperText:
“This is an editable region where you can drag and drop blocks.”,
},
],
},
],
};

export default SeoSectionConfig;

this way previsuosly its working but now it does’nt working

@Umang_001 it looks like you have input set to blocks where it should be uiBlocks, if you update it to that does that work for you?