editor.editTab plugin: How do you add block(s)?

Hello!
I’m trying to create new editTab plugin and are having trouble inserting new blocks onto the page. In digging through the context object, I see that there is context.designerState.editingContentModel?.allBlocks?.forEach which allows you to iterate over existing blocks and mutate them. However, I can’t figure out how to insert new blocks to the page. How would I do that?

Code stack you are integrating Builder with
NextJS/React

Hi Mike,

Thank you for reaching out!

Would you be able to share how you have your code implemented with us?

Hi Mike, you’d want to modify the content snapshot and make your modification then apply it back:

// mobx-state-tree is an external lib builder provide when initializing plugin
// make sure to mark it as external when building your plugin
import { applySnapshot} from 'mobx-state-tree'

const snapshot = context.designerState.editingContentModel;
// modify what you need on the snapshot, then
applySnapshot(context.designerState.editingContentModel, modifiedSnapshot)

@nicke920 @aziz Thanks for getting back to me so quickly :slight_smile:

You’ll have to forgive me that I’m not super familiar with mobx or the inner workings of the Builder.io context API.

How do I create the modified snapshot with the element(s) added?

Let’s imagine that my plugin just wants to add a new Text component every time the every time I click on a button. How would I fill in the middle of it?

(I know this is a contrived example, but just trying to use it to wrap my head around things)

import React from 'react';
import { applySnapshot} from 'mobx-state-tree';
import { ApplicationContext } from '@builder.io/app-context';
import { Builder } from '@builder.io/react';


function AddTextPlugin({context}: {context: ApplicationContext}) {

  const handleClick = () => {

     const snapshot = context.designerState.editingContentModel;

     // I WANT TO ADD A BLOCK HERE. HOW DO I DO THAT?

     applySnapshot(context.designerState.editingContentModel, snapshot);
  }
  return <button onClick={handleClick}></button>
}

Builder.register('editor.editTab', { name: 'AddText', component: AddTextPlugin } );

Adding a text block at the end of the content would be something like this:

// Blocks are of type `BuilderElement`
snapshot.data.blocks.push( {
  "@type": "@builder.io/sdk:Element",
  component: {
    name: "Text",
    options: {
      text: "Hello world",
    },
  },
  responsiveStyles: {
    large: {
      display: "flex",
      flexDirection: "column",
      position: "relative",
      flexShrink: "0",
      boxSizing: "border-box",
      marginTop: "20px",
      lineHeight: "normal",
      height: "auto",
    },
  },
})

For more details on the BuilderElement type and all the different types from Builder, check this schema package builder/packages/json-schema at main · BuilderIO/builder · GitHub

In the editor, pressing cmd + e on any block you select, will show you a JSON representation of it

@aziz I’m still having some trouble.

The below is what my code looks like. I’ve added comments about what the TS errors are saying.

import React from 'react';
import { Builder } from '@builder.io/react';
import { ApplicationContext } from '@builder.io/app-context';
import { applySnapshot } from 'mobx-state-tree';

import { Button } from '@material-ui/core';

function Plugin(props: { context: ApplicationContext }) {
  const handleClick = () => {
    const snapshot = props.context.designerState.editingContentModel;
    // TS error says "Property 'blocks' does not exist on type 'Map<string, any>'.ts("
    snapshot?.data?.blocks?.push({
      '@type': '@builder.io/sdk:Element',
      component: {
        name: 'Text',
        options: {
          text: 'Hello world',
        },
      },
      responsiveStyles: {
        large: {
          display: 'flex',
          flexDirection: 'column',
          position: 'relative',
          flexShrink: '0',
          boxSizing: 'border-box',
          marginTop: '20px',
          lineHeight: 'normal',
          height: 'auto',
        },
      },
    });
    // TS error says Argument of type 'ContentModel | null' is not assignable to parameter of type 'IStateTreeNode<IType<ContentModel | null, any, any>>'. Type 'null' is not assignable to type 'IStateTreeNode<IType<ContentModel | null, any, any>>'
    applySnapshot(props.context.designerState.editingContentModel, snapshot);
  };

  return <Button onClick={handleClick}>Submit</Button>;
}

Builder.register('editor.editTab', {
  component: Plugin,
  name: 'Plugin',
});

Ok, I think that we figured it out. I was using context that’s passed into the plugin as a prop. That doesn’t have data as a mobx observable.

Using the context import from @builder.io/application-context appears to work :slight_smile:

import React from 'react';
import { Builder } from '@builder.io/react';
import { applySnapshot } from 'mobx-state-tree';
import { ApplicationContext } from '@builder.io/app-context';
import appContext from '@builder.io/app-context';

const context = appContext as ApplicationContext;

function Plugin() {
  const handleClick = () => {
    const blocks =
      context.designerState.editingContentModel?.data?.get('blocks');
    blocks.push({
      '@type': '@builder.io/sdk:Element',
      component: {
        name: 'Text',
        options: {
          text: 'Hello world',
        },
      },
      responsiveStyles: {
        large: {
          display: 'flex',
          flexDirection: 'column',
          position: 'relative',
          flexShrink: '0',
          boxSizing: 'border-box',
          marginTop: '20px',
          lineHeight: 'normal',
          height: 'auto',
        },
      },
    });
    applySnapshot(
      context.designerState.editingContentModel?.data?.get('blocks'),
      blocks,
    );
  };

@aziz Another question for you:

Let’s say that I want to add a block that can take a child and add the Text block inside of it. How would I do that?

Hi Mike,
We have a great doc for how to create custom components with child elements inside: Adding Children to Custom Components - Builder.io

@nicke920 Thanks for the documentation!

We’re actually working in an editTab plugin and adding children to the page entirely through interaction with @builder.io/application-context. The components are already registered with Builder.

I may be missing it, but I don’t think that the documentation has anything about how to use context to programmatically add blocks with children to the page from a plugin.

We ended up figuring it out though! :smiley: If we want to add a Section with Text inside, it would look like this.

import React from 'react';
import { Builder } from '@builder.io/react';
import { applySnapshot } from 'mobx-state-tree';
import { ApplicationContext } from '@builder.io/app-context';
import appContext from '@builder.io/app-context';

const context = appContext as ApplicationContext;

function Plugin() {
const blocks = context.designerState.editingContentModel?.data?.get('blocks');
  blocks.push({
    '@type': '@builder.io/sdk:Element',
    component: {
      name: 'Core:Section',
      options: {},
    },
    children: [
      {
        '@type': '@builder.io/sdk:Element',
        component: {
          name: 'Text',
          options: {
            text: 'Hello world',
          },
        },
      },
    ],
  });
  applySnapshot(
    context.designerState.editingContentModel?.data?.get('blocks'),
    blocks,
  );
};
};

I was getting tripped up figuring out where the children property goes for the component and had been trying to set it inside of component property. It turns out that it needs to be a sibling property of component.

In general we found that there isn’t much documentation about how to work with @builder.io/application-context, which made plugin development a little tough. In the end though, we got it to work, which we’re pretty excited about! :smiley: