Home App Docs Github

React custom component containing editable subcomponents

We are using custom React components and they are working well. However, I would like to implement a custom component that behaves like the built in builder component called “Tabs”.

When you drag the “Tabs” component in it is made up of other subcomponents, each of which is editable on its own. I also notice that the icon under “Layers” is a Folder icon, so it might be defined differently to a standard component.

How do I achieve this in a React component? Many Thanks.

5 Likes

Great Q @niall!

We have a section in our docs that should help you out with this here https://www.builder.io/c/docs/custom-react-components#text:children-in-custom-components

Easy mode

For most cases of needing children, you just need to use our withChildren function, e.g.

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

export const Hero = props =>
  <div className={heroStyles}>{div}</div>

const HeroWithBuilderChildren = withChildren(Hero)

Builder.registerElement(HeroWithBuilderChildren, {
  name: 'Hero',
  // Adding defaults is important for easy usability
  defaultChildren: [
    { 
      '@type': '@builder.io/sdk:Element',
      component: { name: 'Text', options: { text: 'I am child text block!' } }
    }
  ]
})

You can see some helpful examples in our design system example project, specifically with this component

Advanced mode

For your use case with tabs though, where you will likely want different children per tab, you will want a slightly more advanced option.

Instead of using withChildren for a tabs example, we’ll specify we’ll take child elements as an input to our component (type: 'uiBlocks') and render out multiple sets of children (e.g. in this case one per tab).

You can also see the source code of all of our built-in components in our Github repo, e.g. the full source of the tabs component is here.

You’ll see that it dynamically renders multiple sets of children, depending on which tab is selected.

You’ll also find that instead of using {props.children} it uses the <BuilderBlocks /> component. This adds one more nuanced behavior which is any time there are no children, it shows a big “+ new block” button in the editor to point the users attention to add a block inside (vs displaying nothing inside and possibly creating confusion how to add tab content)

E.g. see here

{this.activeTabSpec && (
  <BuilderBlocks
    parentElementId={this.props.builderBlock.id}
    dataPath={`component.options.tabs.${this.state.activeTab}.content`}
    blocks={this.activeTabSpec.content}
  />
)}

This tabs component input schema uses a list so you can have a + new tab option, and then for each tab renders it’s label and content. You can see the schema here, namely:

export const Tabs = withBuilder(TabsComponent, {
  name: 'Builder: Tabs',
  inputs: [
    {
      name: 'tabs',
      type: 'list',
      subFields: [
        {
          name: 'label',
          type: 'uiBlocks',
          hideFromUI: true,
          defaultValue: [defaultTab]
        },
        {
          name: 'content',
          type: 'uiBlocks',
          hideFromUI: true,
          defaultValue: [defaultElement]
        }
      ],
...

Block JSON format

You can then add default children using our JSON format that you can see here, simplest example would be something like

{
  '@type': '@builder.io/sdk:Element',
  component: {
    name: 'Text',
    options: {
      text: 'New tab'
    }
  }
}

To have a new text block inside each tab.

To get a sense of the JSON format for default values, you can select any block in the UI, and go to the “data” tab on the left and choose “toggle json view” at the very bottom left to see the JSON for every element, and you can copy + modify them for child element defaults

json view

Hope this helps and let us know if you have any followup Qs or we can explain anything better!

1 Like

Thank you @steve! This is such a great answer.

I appreciate you providing all the example code too.

I will give this a shot and post here if I get stuck.

1 Like

Thanks @niall, and sounds good!