# Introduction

Welcome to Easyblocks - an open-source visual builder framework.

Easyblocks is an open-source React toolkit (white-label editor + framework) for building **completely customised** visual page builders.

It can help you build intuitive visual editors like those in [Shopify](https://shopify.dev/docs/themes/tools/online-editor) (for e-commerce), [Mailchimp](https://mailchimp.com/features/landing-pages/) (landing pages), [Splash](https://splashthat.com/platform/design) (event pages) or [Carrd](https://carrd.co/) (one pagers). It can handle any visual building experience that outputs HTML/CSS or a React component tree - from landing pages to dashboards.

Easyblocks can handle such a wide range of seemingly different visual experiences thanks to a very clear separation between what's common for all visual builders and what's custom and project-specific. The Easyblocks editor knows how to handle common visual builder logic (drag\&drop, nested selections, inline rich text, responsive styling fields, etc), but at the same time doesn't know anything about project-specific things like your [components](/essentials/no-code-components), [data sources](/essentials/external-data) or [templates](/essentials/templates). Project-specific stuff can be defined with code using Easyblocks framework, which is based on a novel concept called [No-Code Components.](/essentials/no-code-components)

### Live demo

Visit <https://easyblocks-demo.vercel.app/> to try the editor demo.

### Video explainer

Easyblocks explained in less than 10 minutes:

{% embed url="<https://www.youtube.com/watch?v=iNVVb_snEiI>" %}

### Main Features

* **Out-of-the-box visual building logic**: drag\&drop, nested selections, inline rich text, multi-selection, styling fields (responsive), design tokens, history management, localisation, templates, dynamic data.
* **Simple for end-users.** Not based on HTML/CSS but on [No-Code Components](#no-code-components).
* **Bring your own components and templates**. You decide what [components](#no-code-components) are available, their variants, styling options, simplicity levels, children components, constraints, etc.
* [**Bring your own data**](#external-and-dynamic-data)**.** Connect any data source, fully control data fetching and data picker widget. The data can be dynamic. For example, you can connect texts or images from data sources in the editor.
* **Server-side rendering.** Fully compatible with modern frameworks like next.js or Remix, but can also render to pure HTML/CSS. All the heavy lifting happens on the server - no browser rendering and layout shifts.

### Why?

If you need a custom text editor there are so many solutions available: Slate, Lexical, TinyMCE, CKEditor, etc. But if you need a custom page builder there's a huge chance you must build one from scratch. And it’s an awfully expensive and tedious process.

The goal behind Easyblocks is to make it possible to create truly state-of-the-art visual page building experiences in weeks instead of years, without compromising flexibility.

{% hint style="info" %}
Off-the-shelf OSS builders like [Grape](https://grapesjs.com/) or [Webstudio](https://webstudio.is/) are based on HTML/CSS (they have a panel with HTML nodes on the left and CSS properties on the right). HTML/CSS is very powerful indeed but for many use cases it's too unconstrained and too hard to use for non-technical users. At Easyblocks we dropped HTML/CSS in favour of [No-Code Components.](#no-code-components)
{% endhint %}

## Main concepts

### No-Code Components

Each selectable element added to an Easyblocks editor canvas must be a No-Code Component. No-Code Component is a standard React component but extended with a so called "No-Code Component Definition", a special object that makes this component visually editable. In a No-Code Component Definition you can set data properties, styling properties or children components slots available in the visual editor when the component is selected. Such architecture allows developers to build visually editable components while keeping full control over what should and should not be customisable for end-users.

Below we're showing a code of a very simple No-Code Component: `SimpleBanner`:

```tsx
// No-Code Component Definition

import { NoCodeComponentDefinition } from "@easyblocks/core";

export const simpleBannerDefinition: NoCodeComponentDefinition = {
  id: "SimpleBanner",
  label: "SimpleBanner",
  type: "section",
  schema: [
    {
      prop: "backgroundColor",
      label: "Background Color",
      type: "color",
    },
    {
      prop: "hasBorder",
      label: "Has Border?",
      type: "boolean",
      responsive: true,
    },
    {
      prop: "padding",
      label: "Pading",
      type: "space",
    },
    {
      prop: "gap",
      label: "Gap",
      type: "space",
    },
    {
      prop: "buttonsGap",
      label: "Buttons gap",
      type: "space",
    },
    {
      prop: "Title",
      type: "component",
      required: true,
      accepts: ["@easyblocks/text"],
    },
    {
      prop: "Buttons",
      type: "component-collection",
      accepts: ["Button"],
      placeholderAppearance: {
        height: 36,
        width: 100,
        label: "Add button",
      },
    },
  ],
  styles: ({ values }) => {
    return {
      styled: {
        Root: {
          backgroundColor: values.backgroundColor,
          border: values.hasBorder ? "2px solid black" : "none",
          padding: values.padding,
        },
        Wrapper: {
          maxWidth: 600,
          display: "flex",
          flexDirection: "column",
          gap: values.gap,
        },
        ButtonsWrapper: {
          display: "flex",
          flexDirection: "row",
          flexWrap: "wrap",
          gap: values.buttonsGap,
        },
      },
    };
  },
  editing: ({ values, editingInfo }) => {
    return {
      components: {
        Buttons: values.Buttons.map(() => ({
          direction: "horizontal",
        })),
        Title: {
          fields: [
            {
              ...editingInfo.fields.find((field) => field.path === "gap")!,
              label: "Bottom gap",
            },
          ],
        },
      },
    };
  },
};

// Component code

import { ReactElement } from "react";

type SimpleBannerProps = {
  Root: ReactElement;
  Title: ReactElement;
  Wrapper: ReactElement;
  Buttons: ReactElement[];
  ButtonsWrapper: ReactElement;
};

export function SimpleBanner(props: SimpleBannerProps) {
  const { Root, Title, Wrapper, Buttons, ButtonsWrapper } = props;

  return (
    <Root.type {...Root.props}>
      <Wrapper.type {...Wrapper.props}>
        <Title.type {...Title.props} />
        <ButtonsWrapper.type {...ButtonsWrapper.props}>
          {Buttons.map((Button, index) => (
            <Button.type {...Button.props} key={index} />
          ))}
        </ButtonsWrapper.type>
      </Wrapper.type>
    </Root.type>
  );
}
```

To learn more, continue with [No-Code Components](/essentials/no-code-components) guide.

### External and dynamic data

When you build a custom visual builder you usually want to connect it to the data that is specific to your product. Easyblocks allows for a full control over external data:

* connect any external data sources, create custom widgets and fetching functions
* connect dynamic data to text fields, images, videos, etc

Please read [External Data section](https://github.com/easyblockshq/easyblocks/blob/main/docs/broken-reference/README.md) to learn more.

## Contact

We'd love to hear your questions, issues or feedback! You can contact us by [email](mailto:andrzej@easyblocks.io), on [X/Twitter](https://twitter.com/ardabrowski), or on [Github](https://github.com/easyblockshq/easyblocks).

#### Custom license & services

In case AGPL3.0 license is too strict we can offer you a custom license. We can also help you with custom services. Let us know via [email](mailto:andrzej@easyblocks.io).

## Project history

Under the hood it's a spin-off from [Shopstory](https://shopstory.app) - a visual builder for headless CMSes that drives millions of page views for e-commerce brands like [Ace\&Tate](https://aceandtate.com) or [Tekla Fabrics](https://teklafabrics.com/). Here's a quick video of Shopstory working inside of Sanity CMS:

{% embed url="<https://vimeo.com/821580462>" %}
Shopstory (Easyblocks-based) + Sanity CMS
{% endembed %}


# Getting started

## Quick start

Install the packages:

```bash
npm install @easyblocks/editor @easyblocks/core
```

Create the Editor page:

```typescript
import { EasyblocksEditor } from "@easyblocks/editor";
import { Config, EasyblocksBackend } from "@easyblocks/core";
import { ReactElement } from "react";

const easyblocksConfig: Config = {
  backend: new EasyblocksBackend({
    accessToken: "<<< your access token >>>", // read below how to aquire access token
  }),
  locales: [
    {
      code: "en-US",
      isDefault: true,
    },
    {
      code: "de-DE",
      fallback: "en-US",
    },
  ],
  components: [
    {
      id: "DummyBanner",
      label: "DummyBanner",
      schema: [
        {
          prop: "backgroundColor",
          label: "Background Color",
          type: "color",
        },
        {
          prop: "padding",
          label: "Pading",
          type: "space",
        },
        {
          prop: "Title",
          type: "component",
          required: true,
          accepts: ["@easyblocks/rich-text"],
        },
      ],
      styles: ({ values }) => {
        return {
          styled: {
            Root: {
              backgroundColor: values.backgroundColor,
              padding: values.padding,
            },
          },
        };
      },
    },
  ],
  tokens: {
    colors: [
      {
        id: "black",
        label: "Black",
        value: "#000000",
        isDefault: true,
      },
      {
        id: "white",
        label: "White",
        value: "#ffffff",
      },
      {
        id: "coral",
        label: "Coral",
        value: "#ff7f50",
      },
    ],
    fonts: [
      {
        id: "body",
        label: "Body",
        value: {
          fontSize: 18,
          lineHeight: 1.8,
          fontFamily: "sans-serif",
        },
        isDefault: true,
      },
      {
        id: "heading",
        label: "Heading",
        value: {
          fontSize: 24,
          fontFamily: "sans-serif",
          lineHeight: 1.2,
          fontWeight: 700,
        },
      },
    ],
    space: [
      {
        id: "0",
        label: "0",
        value: "0px",
        isDefault: true,
      },
      {
        id: "1",
        label: "1",
        value: "1px",
      },
      {
        id: "2",
        label: "2",
        value: "2px",
      },
      {
        id: "4",
        label: "4",
        value: "4px",
      },
      {
        id: "6",
        label: "6",
        value: "6px",
      },
      {
        id: "8",
        label: "8",
        value: "8px",
      },
      {
        id: "12",
        label: "12",
        value: "12px",
      },
      {
        id: "16",
        label: "16",
        value: "16px",
      },
      {
        id: "24",
        label: "24",
        value: "24px",
      },
      {
        id: "32",
        label: "32",
        value: "32px",
      },
      {
        id: "48",
        label: "48",
        value: "48px",
      },
      {
        id: "64",
        label: "64",
        value: "64px",
      },
      {
        id: "96",
        label: "96",
        value: "96px",
      },
      {
        id: "128",
        label: "128",
        value: "128px",
      },
      {
        id: "160",
        label: "160",
        value: "160px",
      },
    ],
  },
  hideCloseButton: true,
};

export function DummyBanner(props: {
  Root: ReactElement;
  Title: ReactElement;
}) {
  const { Root, Title } = props;

  return (
    <Root.type {...Root.props}>
      <Title.type {...Title.props} />
    </Root.type>
  );
}

export default function EasyblocksEditorPage() {
  return (
    <EasyblocksEditor config={easyblocksConfig} components={{ DummyBanner }} />
  );
}
```

### Get access token

Easyblocks configuration object requires a `backend` property. Backend is responsible for handling documents and templates saving, updating, versioning etc. The easiest way to start is to use our simple and free cloud service. Just go to <https://app.easyblocks.io>, create the account, go to the "Playground project", copy your access token:

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FZFbj8kZPShIjXFDtmUeB%2FScreenshot%202024-01-31%20at%2011.07.07.png?alt=media&amp;token=eb6355d8-c2e3-4622-8e37-de2edb889f67" alt=""><figcaption></figcaption></figure>

Place your token in a constructor of `EasyblocksBackend` class and you're ready to go.

**We're not forcing you to use our cloud.** Easyblocks is open-source and you can easily create your own backend later to control where and how your data is stored. You can learn more about this in [Backend](/essentials/backend) guide.

### Open the editor

Now you can visit the editor page: `https//localhost:3000/easyblocks-editor?rootComponent=DummyBanner`

Congratulations!

## Real-world example

The example above is very rudimentary. In order to see Easyblocks at a full power you should have at least a few No-Code Components, custom types, etc.

We highly recommend cloning our [Page Builder Example](https://github.com/easyblockshq/page-builder-demo) to play around with a real-world configuration.

You can use `EasyblocksBackend` with your access token in each of those examples easily.


# Editor page

In order to use Easyblocks your project must have a dedicated page that contains the Easyblocks Editor, called the editor page.

Creating the editor page is simple:

```tsx
import { EasyblocksEditor } from "@easyblocks/editor";
import { easyblocksConfig } from "../easyblocks.config.ts";

function EditorPage() {
  return (
    <EasyblocksEditor
      config={easyblocksConfig}
      components={yourNoCodeComponentInstances}
    />
  );
}
```

The `config` property takes an Easyblocks configuration object described in a [Configuration](/essentials/configuration) guide. The `components` property takes the instances of your No-Code Components (described in [No-Code Components](/essentials/no-code-components) section).

Please keep in mind that the editor page shouldn't render any extra headers, footers, popups etc. It must be blank canvas with `EasyblocksEditor` being a single component rendered.

In order to embed the Easyblocks editor in your product, you should use an `<iframe>` element that points to your editor page. However, in order to play around, you can simply open the editor page directly.

### Query parameters

The editor page takes a few important query parameters:

* `readOnly <boolean> = true` - if set to `true`, no permanent modifications will be done to existing documents or templates. Very good way to play around, debug components, documents, etc. When you open the editor page directly it's by default set to `true` in order to prevent unexpected modifications.
* `document <string>` - the id of the document you want to open. Leaving it undefined means you're creating a new document.
* `rootComponent <string>` - the id of the root component. Mandatory when creating a new document (`document` param is not set).
* `rootTemplate <string>` - the id of the template to copy when creating a new document. Can't be specified together with `rootComponent`.
* `locale <string>` - a locale id. The locale must exist in [`Config.locales`](https://docs.easyblocks.io/essentials/pages/tlUJwgoyv9lmK5xGOLi3#config.locales).

Examples:

* `https://youdomain.com/easyblocks-editor?rootComponent=RootSectionsStack` - open new document with a root component `RootSectionsStack`. `readOnly` is not set (defaults to `true`) so the document is just temporary, will never be saved to [backend](/essentials/backend). It's the best way to start playing around with the editor.
* `https://youdomain.com/easyblocks-editor?document=abcd123` - open a document with id `abcd123` in read-only mode, a good way to debug the document while being sure it won't get modified in the backend.
* `https://youdomain.com/easyblocks-editor?document=abcd123&readOnly=false&locale=de` - edit the document `abcd123`, all the changes will be saved to the backend. The editor is opened in `de` locale (German).

### Events

The editor page sends [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) events to the parent window.

To listen to any of these events, you can create a callback ref to `<iframe>` and add event listener to `message` event in `useEffect` hook:

```tsx
// We use state instead of ref on purpose to exactly know when the node is attached
// so we could react to it.
const [iframeNode, setIframeNode] = useState<HTMLIFrameElement | null>(null);

useEffect(() => {
  function handleMessage(event: MessageEvent) {
    if (event.data.type === "@easyblocks/closed") {
      // handle closed event
    }

    if (event.data.type === "@easyblocks/content-saved") {
      // handle content saved event
    }
  }

  iframeNode?.contentWindow?.addEventListener("message", handleMessage);

  return () => {
    iframeNode?.contentWindow?.removeEventListener("message", handleMessage);
  };
}, [iframeNode]);

return (
  <iframe
    src={`/your-editor-page?rootContainer=myContainer&mode=app`}
    ref={setIframeNode}
  ></iframe>
);
```

Below is the list of events that editor can publish:

#### `@easyblocks/closed`

Published when editor should be closed because user has clicked the close button in the top bar. The format:

```typescript
{
  type: "@easyblocks/closed";
}
```

#### `@easyblocks/content-saved`

Published each time the editor is performing the save of current content. It can be done by autosave mechanism or when closing the editor and the data the current data hasn't been saved yet. The format:

```typescript
{
    type: "@easyblocks/content-saved"
    document: {
        id: string,
        version: number,
        entry: NoCodeEntry
    }
}
```


# Configuration

The `Config` object in Easyblocks is a central object that holds all the essential configuration settings. It's a parameter required by `EasyblocksEditor` ([Editor Page](/essentials/editor-page)) and `buildDocument` ([Rendering Content](/essentials/rendering-content)).

```typescript
import type { Config } from "@easyblocks/core";

export const easyblocksConfig: Config = {
  /* config properties */
};
```

### Properties

#### `Config.backend`

Sets the backend service responsible for saving, updating and versioning documents and templates. Please read [Backend](/essentials/backend) guide to learn more.

```typescript
import type { Config, EasyblocksBackend } from "@easyblocks/core";

const config: Config = {
  backend: new EasyblocksBackend({ accessToken: MY_ACCESS_TOKEN }),
  // ...
};
```

#### `Config.components`

All the [No-Code Component Definitions](/essentials/no-code-components) available in your setup must be provided in `components` property:

```typescript
import type { Config } from '@easyblocks/core';

const config: Config = {
  ...,
  components: [
    {
      id: 'MyNoCodeComponent',
      schema: [
        {
          prop: 'title',
          type: 'string',
          label: 'Title'
        }
      ]
    }
  ]
};
```

#### `Config.devices`

Devices object allows you reconfigure default devices provided by Easyblocks. Easyblocks comes with an opinionated list of devices:

1. Mobile `xs` - `max-width: 568px`
2. Mobile Horizontal `sm` - `max-width: 768px`
3. Tablet `md` - `max-width: 992px`
4. Tablet Horizontal `lg` - `max-width: 1280px`
5. Desktop `xl` - `max-width: 1600px`
6. Large desktop `2xl`

You can switch between different devices using the device switch from the top bar of editor.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FyGfHxfWfS6fAhpsclWXL%2Fimage.png?alt=media&amp;token=b1275f60-f8bc-43d6-ad64-59269f293846" alt=""><figcaption><p>Device switch in the editor</p></figcaption></figure>

By default, Mobile Horizontal and Tablet Horizontal are hidden as we find them unnecessary to be visible out of the box. If you would like to have more control over your breakpoints you can make them visible by setting `hidden` property:

```typescript
import type { Config } from '@easyblocks/core';

const config: Config = {
  ...,
  devices: {
    sm: { hidden: false },
    lg: { hidden: false }
  }
};
```

#### `Config.locales`

List of available locales.

<pre class="language-typescript"><code class="lang-typescript">import type { Config } from '@easyblocks/core';

<strong>const config: Config = {
</strong><strong>  ...,
</strong>  locales: [
    {
      code: 'en-US',
      isDefault: true
    },
    {
      code: 'de-DE',
      fallback: 'en-US'
    }
  ]
};
</code></pre>

**One of the locales must be alwyas set as default.** This locale is going to be used as a fallback in cases where locale is missing or a translation for selected locale (different than default) is missing.

#### `Config.types`

Beside using built-in types provided by Eeasyblocks you can also define your own custom types for referencing external data. Learn more about custom types [here](/essentials/external-data).

```typescript
import type { Config } from '@easyblocks/core';

const config: Config = {
  ...,
  types: {
    "shopify.product": {
      widgets: [ /* widgets */ ]
    }
  }
};
```

#### `Config.tokens`

Each type in Easyblocks can be tokenised which means that a predefined list of variables (tokens) is always available in the field widgets. The tokens are defined via `Config.tokens` property.

The built-in types like `color`, `font`, `space`, `aspectRatio`, `boxShadow`, `icon` are tokenized. When you use any of those types please remember of defining its tokens (example below).

Your custom types can also be tokenised and you can create your own custom token scales.

```typescript
export const config: Config = {
  // ...,
  tokens: {
    colors: [
      {
        id: "grey_05",
        label: "Dark",
        value: "#252525",
      },
      {
        id: "grey_01",
        label: "Light",
        value: "#f9f8f3",
      },
      {
        id: "beige_01",
        label: "Beige",
        value: "#f1f0ea",
      },
      {
        id: "yellow",
        label: "Lemonade Yellow",
        value: "#FCF0C5",
      },
      {
        id: "golden-yellow",
        label: "Golden Yellow",
        value: "#FCF0C5",
      },
      {
        id: "lavender",
        label: "Lavender",
        value: "#E1E2ED",
      },
      {
        id: "olive",
        label: "Olive",
        value: "#A9A886",
      },
    ],
    space: [
      {
        id: "0",
        label: "0",
        value: "0px",
      },
      {
        id: "1",
        label: "1",
        value: "1px",
      },
      {
        id: "2",
        label: "2",
        value: "2px",
      },
      {
        id: "4",
        label: "4",
        value: "4px",
      },
      {
        id: "6",
        label: "6",
        value: "6px",
      },
      {
        id: "8",
        label: "8",
        value: "8px",
      },
      {
        id: "12",
        label: "12",
        value: "12px",
      },
      {
        id: "16",
        label: "16",
        value: "16px",
      },
      {
        id: "24",
        label: "24",
        value: "24px",
      },
      {
        id: "32",
        label: "32",
        value: "32px",
      },
      {
        id: "48",
        label: "48",
        value: "48px",
      },
      {
        id: "64",
        label: "64",
        value: "64px",
      },
      {
        id: "96",
        label: "96",
        value: "96px",
      },
      {
        id: "128",
        label: "128",
        value: "128px",
      },
      {
        id: "160",
        label: "160",
        value: "160px",
      },
      {
        id: "containerMargin.standard",
        label: "Standard",
        value: {
          // responsive value
          $res: true,
          md: "5vw", // vw units are allowed for "space" type
          lg: "8vw",
        },
      },
      {
        id: "containerMargin.large",
        label: "Large",
        value: {
          // repsonsive value
          $res: true,
          xs: "5vw",
          md: "8vw",
          lg: "12vw",
        },
      },
    ],
    fonts: [
      {
        id: "body",
        label: "Body",
        value: {
          fontSize: 20,
          lineHeight: 1.8,
          fontFamily: "test-soehne-mono",
        },
      },
      {
        id: "body2",
        label: "Body small",
        value: {
          fontSize: 13,
          lineHeight: 1.8,
          fontFamily: "test-soehne-mono",
        },
      },
      {
        id: "heading1",
        label: "Heading 1",
        value: {
          $res: true,
          sm: {
            fontSize: 36,
            fontFamily: "test-national-2",
            lineHeight: 1.2,
            fontWeight: 700,
          },
          md: {
            fontSize: 48,
            fontFamily: "test-national-2",
            lineHeight: 1.2,
            fontWeight: 700,
          },
        },
      },
      {
        id: "heading2",
        label: "Heading 2",
        value: {
          $res: true,
          sm: {
            fontFamily: "test-national-2",
            fontSize: 24,
            lineHeight: 1.2,
            fontWeight: 700,
          },
          md: {
            fontFamily: "test-national-2",
            fontSize: 36,
            lineHeight: 1.2,
            fontWeight: 700,
          },
        },
      },
    ],
    icons: [
      {
        id: "arrowLeft",
        label: "Arrow left",
        value: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="red" width="100px" height="100px"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>`,
      },
      {
        id: "arrowRight",
        label: "Arrow right",
        value: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg>`,
      },
      {
        id: "play",
        label: "Play",
        value: `<svg viewBox="0 0 24 24"><path fill="currentColor" d="M8,5.14V19.14L19,12.14L8,5.14Z" /></svg>`,
      },
      {
        id: "pause",
        label: "Pause",
        value: `<svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M14,19H18V5H14M6,19H10V5H6V19Z" /></svg>`,
      },
    ],
    aspectRatios: [
      {
        id: "panoramic",
        label: "Panoramic (2:1)",
        value: "2:1",
      },
      {
        id: "landscape",
        label: "Landscape (16:9)",
        value: "16:9",
      },
      {
        id: "portrait",
        label: "Portrait (4:5)",
        value: "4:5",
      },
      {
        id: "square",
        label: "Square (1:1)",
        value: "1:1",
      },
    ],
    boxShadows: [
      {
        id: "none",
        label: "None",
        value: "none",
      },
      {
        id: "sm",
        label: "sm",
        value: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
      },
      {
        id: "md",
        label: "md",
        value:
          "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
      },
      {
        id: "lg",
        label: "lg",
        value:
          "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
      },
      {
        id: "xl",
        label: "xl",
        value:
          "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)",
      },
      {
        id: "2xl",
        label: "2xl",
        value: "0 25px 50px -12px rgb(0 0 0 / 0.25)",
      },
    ],
  },
};
```

#### `config.templates`

This property allows for setting templates. Read the [Templates](/essentials/templates) section to learn more.

```typescript
import type { Config } from '@easyblocks/core';
import bannerTemplate1 from "bannerTemplate1.json"

const config: Config = {
  ...,
  templates: [
    {
      id: "BannerSection",
      entry: bannerTemplate1
    }
  ]
};
```


# Rendering content

## Building the document

In order to render the Easyblocks document it must first go through a so-called "build phase", which is done by `buildDocument` function from our SDK. This function prepares the document for rendering:

```typescript
import { buildDocument } from "@easyblocks/core";

const { renderableDocument } = await buildDocument({
  documentId: "<your_document_id>",
  config: easyblockConfig,
  locale: "<your_desired_locale>",
});
```

#### `buildDocument` parameters

* `documentId` - identifier of the document you want to build
* `config` - your Easyblocks config
* `locale` - locale's code for which you want to build content

The result of `buildDocument` is an object with a property `renderableDocument` which is of type `RenderableDocument` and it represents content optimised and prepared for rendering.

## Rendering content

Rendering content is dona via `Easyblocks` component:

```tsx
import { Easyblocks } from "@easyblocks/core";

<Easyblocks
  renderableDocument={renderableDocument}
  components={yourNoCodeComponentInstancesObject}
/>;
```

## External data

The document you build and render might be dependent on external data. If this is the case, you must use `externalData` property returned from `buildDocument` function, fetch the requested data and then pass it to `Easyblocks` via `externalData` property. Like this:

```tsx
const { renderableDocument, externalData } = await buildDocument({
  documentId: "<your_document_id>",
  config: easyblockConfig,
  locale: "<your_desired_locale>",
});

// custom fetching external data
const externalDataValues = await customFetch(externalData);

<Easyblocks
  renderableDocument={renderableDocument}
  components={yourNoCodeComponentInstancesObject}
  externalData={externalDataValues} // passing external data for render
/>;
```

Please read [External Data](/essentials/external-data) guide to understand this process better.

### Can I render in non-React environment? Like Vue.js or pure HTML?

If adding React to your bundle is problematic, there are two available options:

1. **Use Preact**. Easyblocks relies heavily on React, but there's nothing that prevents you from using Preact in the runtime (when real page is rendered). It will heavily decrease the bundle size.
2. **Pure HTML/CSS.** If the code of your No-Code Components doesn't use any `useEffect`, `useState`, etc, then you can pre-render content on the server-side and just sent the generated HTML+CSS to the browser. The rendered document is static so you don't need a rehydration phase or virtual DOM tree living in the front-end (it's kind of similar to what Astro or server components do).


# No-Code Components

No-Code Component is the main building block of Easyblocks. Each selectable element added to the editor canvas is an instance of a No-Code Component.

Each No-Code Component consists of 2 parts:

* React Component - a standard React component
* No-Code Component Definition - an object that defines visual editing capabilities of the component

Here's an example of a simple `SimpleBanner` No-Code Component (you can find it in our [example page builder demo](https://github.com/easyblockshq/page-builder-demo/blob/main/src/app/easyblocks/components/SimpleBanner/SimpleBanner.definition.ts)):

<pre class="language-tsx"><code class="lang-tsx">// No-Code Component Definition

import { NoCodeComponentDefinition } from "@easyblocks/core";

<strong>export const simpleBannerDefinition: NoCodeComponentDefinition = {
</strong>  id: "SimpleBanner",
  label: "SimpleBanner",
  type: "section",
  schema: [
    {
      prop: "backgroundColor",
      label: "Background Color",
      type: "color"
    },
    {
      prop: "hasBorder",
      label: "Has Border?",
      type: "boolean",
      responsive: true
    },
    {
      prop: "padding",
      label: "Pading",
      type: "space"
    },
    {
      prop: "gap",
      label: "Gap",
      type: "space"
    },
    {
      prop: "buttonsGap",
      label: "Buttons gap",
      type: "space"
    },
    {
      prop: "Title",
      type: "component",
      required: true,
      accepts: ["@easyblocks/text"]
    },
    {
      prop: "Buttons",
      type: "component-collection",
      accepts: ["Button"],
      placeholderAppearance: {
        height: 36,
        width: 100,
        label: "Add button" 
      }
    }
  ],
  styles: ({ values }) => {
    return {
      styled: {
        Root: {
          backgroundColor: values.backgroundColor,
          border: values.hasBorder ? "2px solid black" : "none",
          padding: values.padding
        },
        Wrapper: {
          maxWidth: 600,
          display: "flex",
          flexDirection: "column",
          gap: values.gap
        },
        ButtonsWrapper: {
          display: "flex",
          flexDirection: "row",
          flexWrap: "wrap",
          gap: values.buttonsGap
        }
      }
    }
  },
  editing: ({ values, editingInfo}) => {
    return {
      components: {
        Buttons: values.Buttons.map(() => ({
          direction: "horizontal",
        })),
        Title: {
          fields: [
            {
              ...editingInfo.fields.find(field => field.path === "gap")!,
              label: "Bottom gap"
            }
          ]
        }
      },
    };
  },
};

// Component code

import { ReactElement } from "react";

type SimpleBannerProps = {
  Root: ReactElement;
  Title: ReactElement;
  Wrapper: ReactElement;
  Buttons: ReactElement[];
  ButtonsWrapper: ReactElement
}

export function SimpleBanner(props: SimpleBannerProps) {
  const { Root, Title, Wrapper, Buttons, ButtonsWrapper } = props;

  return (
    &#x3C;Root.type {...Root.props}>
      &#x3C;Wrapper.type {...Wrapper.props}>
        &#x3C;Title.type {...Title.props} />
        &#x3C;ButtonsWrapper.type {...ButtonsWrapper.props}>
          {Buttons.map((Button, index) => &#x3C;Button.type {...Button.props} key={index} />)}
        &#x3C;/ButtonsWrapper.type>
      &#x3C;/Wrapper.type>
    &#x3C;/Root.type>
  )
}
</code></pre>

In order to use this component two things must happen. First, you must add the definition to the [Config.components](/essentials/configuration#components) property:

```typescript
// easyblocks.config.ts

export const easyblocksConfig = {
  components: {
    // ...other component definitions,
    simpleSectionDefinition,
  },
};
```

The component instance must be passed to the `components` property of the [`EasyblocksEditor`](/essentials/editor-page):

```tsx
<EasyblocksEditor
  config={easyblocksConfig}
  components={{ ...otherComponents, SimpleSection }}
/>
```

Now the component can be used.

Each No-Code Component must have a unique `id`. You can optionally add a `label` and `thumbnail` to display the component nicely in the UI.

Apart from those basic properties, the most important ones are `schema`, `styles` and `editing`. We'll explain them in the next sections.


# Schema

Schema is the most important property of a No-Code Component definition. It defines visually editable properties and subcomponents of a No-Code Component.

In our `SimpleBanner` example the schema looks like this:

```typescript
export const simpleBannerDefinition: NoCodeComponentDefinition = {
  // ...,
  schema: [
    {
      prop: "backgroundColor",
      label: "Background Color",
      type: "color",
    },
    {
      prop: "hasBorder",
      label: "Has Border?",
      type: "boolean",
      responsive: true,
    },
    {
      prop: "padding",
      label: "Pading",
      type: "space",
    },
    {
      prop: "gap",
      label: "Gap",
      type: "space",
    },
    {
      prop: "buttonsGap",
      label: "Buttons gap",
      type: "space",
    },
    {
      prop: "Title",
      type: "component",
      required: true,
      accepts: ["@easyblocks/text"],
    },
    {
      prop: "Buttons",
      type: "component-collection",
      accepts: ["Button"],
      placeholderAppearance: {
        height: 36,
        width: 100,
        label: "Add button",
      },
    },
  ],
  // ...
};
```

The first 5 properties are "basic properties" (`backgroundColor`, `hasBorder`, `padding`, `gap` and `buttonsGap`) whereas the `Title` and `Buttons` are subcomponents.

#### Basic properties

Basic properties are displayed in the sidebar when the component is selected (it's a default behaviour but it can be overridden via [editing function](/essentials/no-code-components/editing-function)).

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FO51f1vtdSdPaZ3s6yrjr%2FScreenshot%202024-01-29%20at%2016.05.21.png?alt=media&amp;token=138975dd-df5b-4bda-a68e-dc19e31a9032" alt=""><figcaption></figcaption></figure>

In this example we use built-in `space`, `color` and `boolean` properties. The full list of properties can be found in the [Reference](#schema-properties-reference) section below.

#### Responsiveness

The great thing about Easyblocks is **built-in responsiveness**. The `space` and `color` properties are responsive by default whereas for `boolean` property (`hasBorder`) we enabled it with `responsive: true` flag. Thanks to this each property can be overridden on mobile:

{% embed url="<https://vimeo.com/907487374>" %}

#### Subcomponents (children components)

The `Title` and `Buttons` schema properties are respectively of `component` and `component-collection` types. The subcomponents allow for nested selection in the editor. Important notes:

1. `accepts` property defines what kind of components are allowed as children for a specific subcomponent schema property. It's a very powerful property that allows for adding constraints to your visual builder and making sure non-technical users will always produce a correct output.
2. `Title` has a flag `required: true` which means that it's always defined.
3. `Buttons` is a `component-collection` which means an array of components of type `Button`.
4. `placeholderAppearance` determines the appearance of the placeholder shown in the layout when the list is empty.

In the video below you can see children components in action with nested selection and drag\&drop:

{% embed url="<https://vimeo.com/907493675>" %}

## Passing to React component

Each schema property will be passed as a prop to your React component. You can override this behaviour by setting `buildOnly: true` for your schema property:

```typescript
{
  prop: "isDisabled",
  type: "boolean",
  label: "Disabled?",
  buildOnly: true
}
```

When `buildOnly` is set to `true` the property will be passed as a parameter to `styles` and `editing` functions but it won't be passed to the React component. It's often good to set this flag for properties used only in `styles` function (read [this section](/essentials/no-code-components/styles-function) to learn more).

## No-Code Entry

Under the hood each No-Code Component instance added to the canvas is represented by a JSON called `No-Code Entry`. In this case our component JSON representation for selected element looks like this:

```json
{
  "_id": "ded5c983-42f7-4e05-98b3-8282fc85a358",
  "_component": "SimpleBanner",
  "backgroundColor": {
    "$res": true,
    "xl": {
      "tokenId": "sky-blue",
      "value": "#7DABDA",
      "widgetId": "@easyblocks/color"
    }
  },
  "hasBorder": {
    "$res": true,
    "xl": true
  },
  "padding": {
    "$res": true,
    "xl": {
      "tokenId": "48",
      "value": "48px",
      "widgetId": "@easyblocks/space"
    }
  },
  "gap": {
    "$res": true,
    "xl": {
      "tokenId": "16",
      "value": "16px",
      "widgetId": "@easyblocks/space"
    }
  },
  "buttonsGap": {
    "$res": true,
    "xl": {
      "tokenId": "12",
      "value": "12px",
      "widgetId": "@easyblocks/space"
    }
  },
  "Title": [
    {
      "_id": "eb6c8c9a-63b7-4b58-8ab3-9f0d310c68d2",
      "_component": "@easyblocks/text",
      "value": {
        "id": "local.9d550249-cdfa-4e01-8fd1-edeb69a1f7ba",
        "value": {
          "en-US": "Lorem ipsum dolor."
        },
        "widgetId": "@easyblocks/local-text"
      },
      "color": {
        "$res": true,
        "xl": {
          "tokenId": "black",
          "value": "#000000",
          "widgetId": "@easyblocks/color"
        }
      },
      "font": {
        "$res": true,
        "xl": {
          "tokenId": "heading2",
          "value": {
            "$res": true,
            "sm": {
              "fontFamily": "test-national-2",
              "fontSize": 24,
              "lineHeight": 1.2,
              "fontWeight": 700
            },
            "md": {
              "fontFamily": "test-national-2",
              "fontSize": 36,
              "lineHeight": 1.2,
              "fontWeight": 700
            }
          }
        }
      },
      "accessibilityRole": "p"
    }
  ],
  "Buttons": [
    {
      "_id": "047289c6-96ed-4b9b-9df3-204dff1315d0",
      "_component": "Button",
      "_itemProps": {
        "SimpleBanner": {
          "Buttons": {}
        }
      },
      "Action": [],
      "variant": "label",
      "label": {
        "id": "local.458b2e47-e5b9-4871-9125-3d37d2db3651",
        "value": {
          "en-US": "Primary button"
        },
        "widgetId": "@easyblocks/local-text"
      },
      "icon": {
        "value": "<svg viewBox=\"0 -960 960 960\"><path fill=\"currentColor\" d=\"m480-120-58-52q-101-91-167-157T150-447.5Q111-500 95.5-544T80-634q0-94 63-157t157-63q52 0 99 22t81 62q34-40 81-62t99-22q94 0 157 63t63 157q0 46-15.5 90T810-447.5Q771-395 705-329T538-172l-58 52Zm0-108q96-86 158-147.5t98-107q36-45.5 50-81t14-70.5q0-60-40-100t-100-40q-47 0-87 26.5T518-680h-76q-15-41-55-67.5T300-774q-60 0-100 40t-40 100q0 35 14 70.5t50 81q36 45.5 98 107T480-228Zm0-273Z\"/></svg>",
        "widgetId": "@easyblocks/icon"
      },
      "color": {
        "$res": true,
        "xl": {
          "tokenId": "grey_01",
          "value": "#f9f8f3",
          "widgetId": "@easyblocks/color"
        }
      },
      "minHeight": {
        "$res": true,
        "xl": "42"
      },
      "minWidth": {
        "$res": true,
        "xl": "100"
      },
      "horizontalPadding": {
        "$res": true,
        "xl": {
          "value": "16px",
          "tokenId": "16",
          "widgetId": "@easyblocks/space"
        }
      },
      "gap": {
        "$res": true,
        "xl": {
          "value": "6px",
          "tokenId": "6",
          "widgetId": "@easyblocks/space"
        }
      },
      "cornerMode": {
        "$res": true,
        "xl": "custom"
      },
      "cornerRadius": {
        "$res": true,
        "xl": "12"
      },
      "font": {
        "$res": true,
        "xl": {
          "value": {
            "fontSize": 13,
            "lineHeight": 1.8,
            "fontFamily": "test-soehne-mono"
          },
          "tokenId": "body2"
        }
      },
      "underline": "off",
      "underlineOffset": {
        "$res": true,
        "xl": "1"
      },
      "iconSize": {
        "$res": true,
        "xl": "24"
      },
      "hasBackground": true,
      "backgroundColor": {
        "$res": true,
        "xl": {
          "tokenId": "grey_05",
          "value": "#252525",
          "widgetId": "@easyblocks/color"
        }
      },
      "hasBorder": false,
      "borderWidth": {
        "$res": true,
        "xl": "1"
      },
      "borderColor": {
        "$res": true,
        "xl": {
          "value": "#000000",
          "widgetId": "@easyblocks/color"
        }
      },
      "boxShadow": {
        "$res": true,
        "xl": {
          "value": "none",
          "tokenId": "none"
        }
      }
    },
    {
      "_id": "ebebf572-9353-48a0-a0bb-eb5d0512b63c",
      "_component": "Button",
      "_itemProps": {
        "SimpleBanner": {
          "Buttons": {}
        }
      },
      "Action": [],
      "variant": "label",
      "label": {
        "id": "local.4e69bfb6-45f2-4595-9360-351445cdfbc8",
        "value": {
          "en-US": "Click me"
        },
        "widgetId": "@easyblocks/local-text"
      },
      "icon": {
        "value": "<svg viewBox=\"0 -960 960 960\"><path fill=\"currentColor\" d=\"m480-120-58-52q-101-91-167-157T150-447.5Q111-500 95.5-544T80-634q0-94 63-157t157-63q52 0 99 22t81 62q34-40 81-62t99-22q94 0 157 63t63 157q0 46-15.5 90T810-447.5Q771-395 705-329T538-172l-58 52Zm0-108q96-86 158-147.5t98-107q36-45.5 50-81t14-70.5q0-60-40-100t-100-40q-47 0-87 26.5T518-680h-76q-15-41-55-67.5T300-774q-60 0-100 40t-40 100q0 35 14 70.5t50 81q36 45.5 98 107T480-228Zm0-273Z\"/></svg>",
        "widgetId": "@easyblocks/icon"
      },
      "color": {
        "$res": true,
        "xl": {
          "value": "#252525",
          "tokenId": "grey_05",
          "widgetId": "@easyblocks/color"
        }
      },
      "minHeight": {
        "$res": true,
        "xl": "42"
      },
      "minWidth": {
        "$res": true,
        "xl": "100"
      },
      "horizontalPadding": {
        "$res": true,
        "xl": {
          "value": "16px",
          "tokenId": "16",
          "widgetId": "@easyblocks/space"
        }
      },
      "gap": {
        "$res": true,
        "xl": {
          "value": "6px",
          "tokenId": "6",
          "widgetId": "@easyblocks/space"
        }
      },
      "cornerMode": {
        "$res": true,
        "xl": "custom"
      },
      "cornerRadius": {
        "$res": true,
        "xl": "12"
      },
      "font": {
        "$res": true,
        "xl": {
          "value": {
            "fontSize": 13,
            "lineHeight": 1.8,
            "fontFamily": "test-soehne-mono"
          },
          "tokenId": "body2"
        }
      },
      "underline": "off",
      "underlineOffset": {
        "$res": true,
        "xl": "1"
      },
      "iconSize": {
        "$res": true,
        "xl": "24"
      },
      "hasBackground": true,
      "backgroundColor": {
        "$res": true,
        "xl": {
          "value": "#f9f8f3",
          "tokenId": "grey_01",
          "widgetId": "@easyblocks/color"
        }
      },
      "hasBorder": false,
      "borderWidth": {
        "$res": true,
        "xl": "1"
      },
      "borderColor": {
        "$res": true,
        "xl": {
          "value": "#000000",
          "widgetId": "@easyblocks/color"
        }
      },
      "boxShadow": {
        "$res": true,
        "xl": {
          "value": "none",
          "tokenId": "none"
        }
      }
    }
  ]
}
```

Each time makes changes to the fields, the underlying No-Code Entry changes.

**No-Code Entry is the most important data format used in Easyblocks**. It's a full JSON representation of what is visually built in the Easyblocks Editor. It's a tree structure so if a No-Code Component has children components (`component` or `component-collection` fields) then the children are represented by nested No-Code Entries (look at `Title` field in example above).

## Schema properties reference

### Basic types

#### Boolean

Boolean type can be either `true` or `false`.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FJJ9o53gZIw3wx6qRtbTn%2Fboolean_ui.gif?alt=media&amp;token=103773fb-9266-4b01-b31b-cfcdf6693d86" alt=""><figcaption><p>UI representation of boolean type in the sidebar</p></figcaption></figure>

```javascript
{
  prop: "isDisabled",
  type: "boolean",
  label: "Disabled?"
}
```

This type can optionally be [responsive](#responsiveness).

#### Select

Select type allows to choose a single value from predefined list of options. It holds a `string` value.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FcJ8NHNa2Ra5IIpXP21hc%2Fselect_ui.gif?alt=media&amp;token=ca39419f-6e03-4260-b157-9c8d43379017" alt=""><figcaption><p>UI representation of select type in the sidebar</p></figcaption></figure>

```javascript
{
  prop: "position",
  type: "select",
  params: {
    options: ["top", "bottom", "left"]
  }
}
```

Select type requires `options` parameter to be set to know what options are available. Options can be simple array of strings or each option can be an object of type `{ value: string; label: string }` which allows for better control how options are shown on the list.

This type can optionally be [responsive](#responsiveness).

#### String

String type holds a `string` value that's not localizable.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2F54Emh9PP5C2XKMP6xht7%2Fstring_ui.gif?alt=media&amp;token=1c87342d-f29d-472f-8945-de8539b48fbf" alt="UI representation of string type in the sidebar"><figcaption><p>UI representation of string type in the sidebar</p></figcaption></figure>

```javascript
{
  prop: "slug",
  type: "string",
  label: "Slug"
}
```

This type can optionally be [responsive](#responsiveness).

#### Text

Text type is similiar to type String, but the value can vary between locales - it's localizable.

```javascript
{
  prop: "title",
  type: "text",
  label: "Title"
}
```

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FTeAyR5Cjjq1IUGZPC0Mb%2Ftext_ui.gif?alt=media&amp;token=9a4e0798-b20b-48b8-8906-9d3dee41195e" alt=""><figcaption><p>UI representation of text type in the sidebar</p></figcaption></figure>

Value of field of type `text` is stored in No-Code Entry in the following way:

```json
{
  ...,
  "title": {
    "en-US": "Hello world"
  }
}
```

Easyblocks offers a built-in component `@easyblocks/text` utilising `text` type.

### Responsiveness

Most of the types can be responsive. It means that the user in the editor will be able to set different values for different breakpoints. `boolean`, `select` and `string` types can be optionally responsive. You can enable responsiveness with `responsive` flag.

The underlying data format of responsive fields in the No-Code Entry is different from non-reponsive fields. Here's an example for `boolean`:

```typescript
// NON-RESPONSIVE
{
  prop: "isDisabled",
  type: "boolean",
  label: "Disabled?"
}

// value in No-Code Entry
{
  // ...
  isDisabled: true
}

// RESPONSIVE FIELD
{
  prop: "isDisabled",
  type: "boolean",
  label: "Disabled?"
  responsive: true
}

// value in No-Code Entry
{
  isDisabled: {
    $res: true
    xs: false
    xl: true
  }
}
```

### Token types

Easyblocks comes with handy built-in types that are based on design tokens you can specify in your config ([`Config.tokens`](https://docs.easyblocks.io/essentials/no-code-components/pages/tlUJwgoyv9lmK5xGOLi3#config.tokens))

**All built-in token fields are responsive by default and it can't be disabled.**

#### Color

Color type allows to select single value from the list of values coming from `Config.tokens.colors`. You can also set a custom HEX value.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2Fq9X5XyrO61Ce3opQSaFG%2Fcolor_ui.gif?alt=media&amp;token=4ab7c368-efcb-4659-9ee5-6591b0c24823" alt=""><figcaption><p>UI representation of color type in the sidebar</p></figcaption></figure>

```javascript
{
  prop: "borderColor",
  type: "color",
  label: "Border color"
}
```

Here's the representation in No-Code Entry:

```json
{
  "borderColor": {
    "$res": true,
    "xl": {
      "tokenId": "sky-blue",
      "value": "#7DABDA"
    }
  }
}
```

As you can see token value it stored as an object that consists of `tokenId` and `value`:

1. `tokenId` property which is the token identifier of selected color
2. `value` property which is the actual string representation of selected color.

`value` might seem redundant but it exists in the No-Code Entry only for the case when the token is removed from the configuration.

When the token value goes to the component as props, you don't get a full object, you only get the `value`.

#### Font

Font type allows to select single value from the list of values based on the `Config.tokens.fonts`.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FadJ18BNu6zNA6Rf4u4Am%2Ffont_ui.gif?alt=media&amp;token=0c876c51-131e-4760-a328-317899b11102" alt=""><figcaption><p>UI representation of font type in the sidebar</p></figcaption></figure>

```javascript
{
  prop: "style",
  type: "font",
  label: "Style"
}
```

In the No-Code Entry:

```json
{
  "style": {
    "$res": true,
    "xl": {
      "tokenId": "heading2",
      "value": {
        "fontFamily": "'Open Sans', sans-serif",
        "fontSize": 30,
        "lineHeight": 1.2
      }
    }
  }
}
```

#### Space

Color type allows to select single value from the list of values based on the `Config.tokens.space`. Easyblocks also supplies for each `space` field type a set of predefined space values.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2Fa6dcNEMyigxvy3I5hVZv%2Fspace_ui.gif?alt=media&amp;token=ee739d7d-56af-44a4-be39-e02a21777f77" alt=""><figcaption><p>UI representation of space type in the sidebar</p></figcaption></figure>

```javascript
{
  prop: "marginBottom",
  type: "space",
  label: "Margin bottom"
}
```

In the No-Code Entry:

```json
{
  "bottomMargin": {
    "$res": true,
    "xl": {
      "tokenId": "32",
      "value": "32px"
    }
  }
}
```

### Component types

Component based types are special kind of fields. Instead of extending the sidebar experience, they allow you to extend capabilities of your component by defining nested components that can also be selected and can have their own set of fields. Component fields can't be responsive.

### Component

Component field creates a slot which can be filled with single component.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FMTNTfXEbd4Qseg11ypNV%2Fcomponent_ui.gif?alt=media&amp;token=46f40913-1ffe-42c3-a1d9-9a4f42c26a89" alt=""><figcaption><p>UI representation of component type in the canvas</p></figcaption></figure>

```javascript
{
  prop: "Component",
  type: "component",
  accepts: ["@easyblocks/rich-text"]
}
```

`component` field, as other field types, is passed within props to your React component. The passed component prop is a **React element**, not a React component. It's super important to remember it. Below is the example how you can use passed `Component` field:

```tsx
import type { ReactElement } from "react";

type MyNoCodeComponentProps = {
  Component: ReactElement;
};

function MyNoCodeComponent({ Component }: MyNoCodeComponentProps) {
  return <Component.type {...Component.props} />;
}
```

#### `accepts`

In the code example above, we tell editor to only allow to add built-in Rich Text component to our `Component` field by setting `accepts` property. You can limit number of accepted components also to your own No-Code Components by specifying their `id` property.

```javascript
// "MyComponent" No-Code Component definiton
{
  id: "MyComponent",
  schema: [...]
}

// Component field of type "MyComponent"
{
  prop: "Component",
  type: "component",
  accepts: ["MyComponent"]
}
```

When defining No-Code component you can set its `type`. You can think of this value as a tag or interface. It lets you gather multiple components under one name and then tell your `component` field to accept all components of given type ex.:

```javascript
// Section1 definition
{
  id: "Section1",
  type: "section",
  schema: [...]
}

// Section2 definition
{
  id: "Section2",
  type: "section",
  schema: [...]
}

// Component field of "section" type
{
  prop: "Section",
  type: "component",
  accepts: ["section"]
}
```

#### Non-empty components

By default, each `component` field is optional and can be left empty and it won't render anything (or a placeholder when in the editor). By setting `required: true`, we mark the field as non removable.

```javascript
{
  prop: "Component",
  type: "component",
  accepts: ["MyComponent"],
  // This field can't be empty
  required: true
}
```

It's great when your component should have a fixed element. Imagine a card component that always has to have heading, but subheading is optional. We could implement it like this:

```javascript
{
  id: "Card",
  schema: [
    {
      prop: "Heading",
      type: "component",
      accepts: ["@easyblocks/rich-text"],
      required: true
    },
    {
      prop: "Subheading",
      type: "component",
      accepts: ["@easyblocks/rich-text"],
    }
  ]
}
```

#### Built-in components

Easyblocks is shipped with two built-in components that can be handy:

* `@easyblocks/rich-text` - rich text component for text editing values that's also localizable
* `@easyblocks/text` - a simpler version component for text editing component that's also localizable, but without being rich (single font and color, no links).

### Component Collection

Component collection field creates a slot which can be filled with any number of components.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FQoLGcn3VOIk8ATSf0mfS%2Fcomponent_collection_ui.gif?alt=media&amp;token=32f631d4-a0a2-41fc-bea7-3cf2b2ef0cdd" alt=""><figcaption><p>UI representation of component-collection type in the canvas</p></figcaption></figure>

```javascript
{
  prop: "Components",
  type: "component-collection",
  accepts: ["@easyblocks/rich-text"]
}
```

Since the field represents a collection, it means that when the field is passed to your React component it's going to be an array of **React elements**. You can render it similarly to `component` field:

```tsx
import type { ReactElement } from "react";

type MyNoCodeComponentProps = {
  Components: Array<ReactElement>;
};

function MyNoCodeComponent({ Component }: MyNoCodeComponentProps) {
  return Components.map((Component, index) => {
    return <Component.type key={index} {...Component.props} />;
  });
}
```

#### Items adding direction

By default, when you use `component-collection` field and start adding items on the canvas the buttons for adding items at the beginning or at the end of each item are rendered vertically. If your collection is oriented horizontally, you cen edit this behaviour by using `editing` method of component definition and specifying `direction` property for each item.

```javascript
{
  prop: "Components",
  type: "component-collection",
  accepts: ["@easyblocks/rich-text"],
  editing({ values }) {
    return {
      components: {
        Components: values.Components.map(() => {
          return {
            // Override how to render add buttons when selecting collection item
            direction: "horizontal"
          }
        })
      }
    }
  }
}
```

#### Item fields

It's a common practise for parent component to determine how it should render based on its children. Let's imagine you're building a Grid component for displaying your Card components. In the simplest case we could just render each item using CSS Grid using fixed number of rows and columns. Each card would occupy one row and column.

```javascript
{
  prop: "Cards",
  type: "component-collection",
  accepts: ["MyNoCodeCardComponent"],
  styles() {
    return {
      styled: {
        CardsGrid: {
          display: "grid";
          gridTemplateColumns: repeat(4, 1fr);
        }
      }
    }
  }
}
```

To put more emphasise on some cards we would like to make them occupy 2 columns and 1 row or even 2 columns and 2 rows. This behaviour would affect how the parent component should render the grid. This would require to have information about each size of card within the parent, but allow to configure it on each grid item separately. To make it possible you can use `itemFields` property for your `component-collection` field. This property allows you to declare additional fields that are stored within the component defining the collection field, but these fields are configurable from the fields of collection items.

```javascript
{
  prop: "Cards",
  type: "component-collection",
  accepts: ["MyNoCodeCardComponent"],
  itemFields: [
    {
      prop: "size",
      type: "select",
      label: "Size",
      params: {
        options: ["1x1", "2x1", "2x2"]
      }
    }
  ],
  styles({ values: { Cards } }) {
    // Now we have full access to each size of card
    const cardSizes = Cards.map(Card => Card.size);

    return {
      styled: {
        CardsGrid: {
          display: "grid";
          // Calculate the final layout based on card sizes
        }
      }
    }
  }
}
```

## Custom types

Easyblocks allows to add custom types. Read the [Custom types](/essentials/custom-types) guide to learn more.


# styles function

The `styles` function has 3 goals:

1. Calculate CSS for your No-Code Component.
2. Creating "computed props" that will be later passed to your React Component instance.
3. Passing parameters to subcomponents.

## Calculate CSS

Let's analyse the `styles` function of our `SimpleBanner` component:

<pre class="language-typescript"><code class="lang-typescript">// No-Code Component Definition

import { NoCodeComponentDefinition } from "@easyblocks/core";

<strong>export const simpleBannerDefinition: NoCodeComponentDefinition = {
</strong>  // ...
  styles: ({ values }) => {
    return {
      styled: {
        Root: {
          backgroundColor: values.backgroundColor,
          border: values.hasBorder ? "2px solid black" : "none",
          padding: values.padding
        },
        Wrapper: {
          maxWidth: 600,
          display: "flex",
          flexDirection: "column",
          gap: values.gap
        },
        ButtonsWrapper: {
          display: "flex",
          flexDirection: "row",
          flexWrap: "wrap",
          gap: values.buttonsGap
        }
      }
    }
  },
  // ...
};
</code></pre>

The `styled` property of the output object contains "styled components" (we use [Stitches](https://stitches.dev/) under the hood).

{% hint style="info" %}
We're aware that Stitches seems to be outdated and not properly maintained. We'll planning to change it to a different engine as soon as possible.
{% endhint %}

The styled components you produce will show up as properties of your React component. In the `SimpleBanner.tsx` file shown below `Root`, `Wrapper` and `ButtonsWrapper` are styled components created by `styles` function:

```typescript
import { ReactElement } from "react";

type SimpleBannerProps = {
  Root: ReactElement;
  Title: ReactElement;
  Wrapper: ReactElement;
  Buttons: ReactElement[];
  ButtonsWrapper: ReactElement;
};

export function SimpleBanner(props: SimpleBannerProps) {
  const { Root, Title, Wrapper, Buttons, ButtonsWrapper } = props;

  return (
    <Root.type {...Root.props}>
      <Wrapper.type {...Wrapper.props}>
        <Title.type {...Title.props} />
        <ButtonsWrapper.type {...ButtonsWrapper.props}>
          {Buttons.map((Button, index) => (
            <Button.type {...Button.props} key={index} />
          ))}
        </ButtonsWrapper.type>
      </Wrapper.type>
    </Root.type>
  );
}
```

### Responsiveness

**The most important feature of `styles` function is that you don't need to worry about responsiveness at all.** Just write your CSS for a single breakpoint and Easyblocks will handle the rest. Under the hood Easyblocks will run `styles` function once per each breakpoint, combine the outputs and produce a single stylesheet with all the media queries properly applied.

Why so? A lot of schema properties of your No-Code Components will be responsive. Writing a function for calculating styles that takes into account all the possible combinations of responsive fields is simply a hell. When we started building Easyblocks it quickly became obvious that in order to allow for easy responsiveness and simple code we must create an abstraction layer for that - say hello to `styles` function!

Think of our `SimpleBanner` example. In the [video from the previous section](/essentials/no-code-components/schema#responsiveness) we show that users can set different values for other breakpoints. And if you look at the `styles` function code there's nothing at all about responsiveness.

## Computed props

If you want to compute some props that should be later passed to React component you can do it via `props` property of the output object of `styles` function. In the example below a `computedProp` will show up as a property of a `SimpleBanner` React component.

<pre class="language-typescript"><code class="lang-typescript">// No-Code Component Definition

import { NoCodeComponentDefinition } from "@easyblocks/core";

<strong>export const simpleBannerDefinition: NoCodeComponentDefinition = {
</strong>  // ...
  styles: ({ values }) => {
    return {
      styled: {
        // ...
      },
      props: {
        computedProp: 10
      }
    }
  },
  // ...
};
</code></pre>

## Passing parameters to subcomponents

`styles` function allows to pass parameters to subcomponents:

```typescript
// Parent component

const parentComponentDefinition: NoCodeComponentDefinition = {
  // ...
  styles: ({ values }) => {
    return {
      styled: {
        // ...
      },
      components: {
        ChildComponent: {
          passedParameter: 10,
        },
      },
    };
  },
  // ...
};

// Child component

const childComponentDefinition: NoCodeComponentDefinition = {
  // ...
  styles: ({ values, params }) => {
    console.log(params.passedParameter); // 10
    return {};
  },
};
```


# editing function

Each definition could define additional `editing` method. This method is responsible for a few things:

* showing/hiding fields displayed in the sidebar
* disabling selection of its own subcomponents (`component` or `component-collection`)
* changing direction in which blue plus buttons are shown when adding items to `component-collection` field
* fields passing - certain fields from parent component can be displayed when child component is selected

Let's look at the `SimpleBanner` `editing` function:

```typescript
import { NoCodeComponentDefinition } from "@easyblocks/core";

export const simpleBannerDefinition: NoCodeComponentDefinition = {
  // ...
  editing: ({ values, editingInfo }) => {
    return {
      components: {
        Buttons: values.Buttons.map(() => ({
          direction: "horizontal",
        })),
        Title: {
          fields: [
            {
              ...editingInfo.fields.find((field) => field.path === "gap")!,
              label: "Bottom gap",
            },
          ],
        },
      },
    };
  },
};
```

The most important input argument is editing info (`editingInfo`). It has the information about fields visibility, labels, grouping and subcomponents editing behaviour (is nested selection allowed, plus button direction, passed fields, etc). Here's the input `editingInfo` for our `SimpleBanner`:

```typescript
{
    "fields": [
        {
            "path": "backgroundColor",
            "type": "field",
            "visible": true,
            "group": "Properties",
            "label": "Background Color"
        },
        {
            "path": "hasBorder",
            "type": "field",
            "visible": true,
            "group": "Properties",
            "label": "Has Border?"
        },
        {
            "path": "padding",
            "type": "field",
            "visible": true,
            "group": "Properties",
            "label": "Pading"
        },
        {
            "path": "gap",
            "type": "field",
            "visible": true,
            "group": "Properties",
            "label": "Gap"
        },
        {
            "path": "buttonsGap",
            "type": "field",
            "visible": true,
            "group": "Properties",
            "label": "Buttons gap"
        },
        {
            "path": "Title",
            "type": "field",
            "visible": false,
            "group": "Properties",
            "label": "Title"
        }
    ],
    "components": {
        "Title": {
            "fields": []
        },
        "Buttons": []
    }
}
```

Whenever you need to make changes in a default editing info, just use `editing` function and return your modified editing info from it.

The `editing` function of the `SimpleBanner` does 2 things:

1. Sets `direction: "horizontal"` for each child Button instance. The default value is `vertical` (arrows would display at the top and bottom edge of selection frame).
2. Passes the `gap` property to the `Title` while changing the `label` to `Bottom margin`. It means that the same property `(gap)` is editable by a parent's `Gap` field and by `Bottom margin` field when title is selected.

#### Changing fields visibility

Also you can modify `visible` flag to show and hide fields. You can do it based on input `values` and `params` which unlocks a lot of customisation.

#### Disabling selection of child components

Sometimes you might want to disable nested selection of child components. You can do it by setting `selectable: false` for a child component:

```typescript
import { NoCodeComponentDefinition } from "@easyblocks/core";

export const simpleBannerDefinition: NoCodeComponentDefinition = {
  // ...
  editing: ({ values, editingInfo }) => {
    return {
      components: {
        Title: {
          selectable: false,
        },
      },
    };
  },
};
```

Of course disabling selection makes it impossible to change the properties of the child component. However, you still might allow for it from the sidebar by setting `Title` field to `visible: true` (by default all the subcomponents are not visible in the sidebar).


# Custom types

If a built-in set of types (`boolean`, `select`, etc) is not enough you can always create custom ones.

Custom types fall into 3 categories:

* `inline` - a standard type, the value will be stored in a NoCode Entry.
* `token` - similar to `inline` but allows for adding tokens (predefined values like in a built-in `color` or `font`).
* `external` - a special type that allows for connecting dynamic data (not stored in a NoCode Entry, but fetched each time the document is rendered)

To create a custom type you have to define it within [types](https://docs.easyblocks.io/essentials/pages/tlUJwgoyv9lmK5xGOLi3#config.types) property of your configuration.

### Inline type

Below we're defining a custom type `url`:

```javascript
const easyblocksConfig: Config = {
  ...,
  types: {
    url: {
      type: "inline",
      widget: {
        id: "url",
        label: "URL"
      },
      defaultValue: "https://easyblocks.io"
    }
  }
}
```

`defaultValue` and `widget` properties are mandatory.

Now let's define the widget UI component responsible for displaying the value for `url` field type and updating it:

```tsx
import { InlineTypeWidgetComponentProps } from "@easyblocks/core";
import { Input } from "@easyblocks/design-system";
import { useEffect, useState } from "react";

function UrlWidget(props: InlineTypeWidgetComponentProps<string>) {
  const [active, setActive] = useState(false);
  const [value, setValue] = useState(props.value);

  useEffect(() => {
    if (!active) {
      setValue(props.value);
    }
  });

  return (
    <Input
      value={value}
      onChange={(event) => {
        setActive(true);
        setValue(event.target.value);
      }}
      onBlur={() => {
        setActive(false);
        props.onChange(value);
      }}
      align={"right"}
    />
  );
}

export { UrlWidget };
```

For `url` type we render a text input component from our design system package that displays value of state variable `value` that's based on the prop `value`. When we blur the input, we call `onChange` callback passed in the props to update the property value in a NoCode Entry.

We must also tell editor to render `UrlWidget` component when showing `url` field. To do this, we pass the component to `widgets` prop of `EasyblocksEditor`:

```tsx
import { UrlWidget } from "./path-to-url-widget";

<EasyblocksEditor
  ...,
  widgets={{
    // Property key MUST match the id specified witin the type's declaration
    url: UrlWidget
  }}
/>
```

It's a good practice to make sure that a data saved in a NoCode Entry has a correct format. For example, we can make sure that the value for `url` type will always start with `https://` or `http://`. In order to achieve that, let's add a `validate` function to our new type:

```javascript
const easyblocksConfig: Config = {
  ...,
  types: {
    url: {
      type: "inline",
      widget: {
        id: "url",
        label: "URL"
      },
      defaultValue: "https://easyblocks.io",
      validate(value) {
        return (
          typeof value === "string" &&
          (value.startsWith("http://") || value.startsWith("https://"))
        );
      }
    }
  }
}
```

`validate` function is going to be called each time we invoke `onChange` callback passed to our widget component. If value is not a correct URL it won't get updated.

### Token type

Easyblocks comes with predefined types like `color`, `font` or `space`. These types are token types and they only allow you to pick a value based on what you've specified within the [`tokens`](https://docs.easyblocks.io/essentials/pages/tlUJwgoyv9lmK5xGOLi3#config.tokens) section for given type ex. `color` only allows to select a value defined within `Config.tokens.colors` property.

Every project is different and requires different tokens. Easyblocks allows you to define your own tokens and token types based on that.

```typescript
const easyblocksConfig: Config = {
  ...,
  tokens: {
    urls: [
      {
        id: "google",
        label: "Google",
        value: "https://google.com"
      },
      {
        id: "bing",
        label: "Bing",
        value: "https://bing.com"
      },
      {
        id: "brave",
        label: "Brave",
        value: "https://search.brave.com"
      }
    ]
  },
  types: {
    url: {
      type: "token",
      token: "urls",
      defaultValue: { tokenId: "google" }
    }
  }
}
```

Above snippet of code defines a custom token type `url` that only allows you to choose from three defined values.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2Fn0Vo1Ggg7EJ83hGswWen%2Fimage.png?alt=media&amp;token=6ba30e67-82c8-4425-9947-0c9829c0e49d" alt=""><figcaption><p>UI representation for token type url in sidebar</p></figcaption></figure>

By default token types don't require specifying a widget because you'll never use one, only the select with tokens is displayed. However, there are cases where you would like to allow user to enter a custom value that's not available in your tokens. To make it possible we have to tweak `url` type definition a bit:

```typescript
const easyblocksConfig: Config = {
  ...,
  types: {
    url: {
      type: "token",
      token: "urls",
      defaultValue: { tokenId: "google" },
      // Tell Easyblocks editor that your type accepts custom values
      allowCustom: true,
      // and define a custom widget for custom value input
      widget: {
        id: "url_custom",
        label: "URL Custom"
      }
    }
  }
}
```

Now we need to also define a UI responsible for rendering a custom input for `url` type:

```tsx
import type { TokenTypeWidgetComponentProps } from "@easyblocks/core";
import { Input } from "@easyblocks/design-system";
import { useState } from "react";

function validateURL(value: string) {
  return value.startsWith("http://") || value.startsWith("https://");
}

function UrlTokenWidget(props: TokenTypeWidgetComponentProps<string>) {
  const [inputValue, setInputValue] = useState(props.value);

  return (
    <Input
      value={inputValue}
      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
        setInputValue(e.target.value);
      }}
      onBlur={() => {
        if (!validateURL(inputValue)) {
          return;
        }

        props.onChange(inputValue);
      }}
      align={"right"}
    />
  );
}

export { UrlTokenWidget };
```

And finally, connect the dots and let know editor about `UrlTokenWidget`:

```tsx
import { UrlTokenWidget } from "./path-to-url-token-widget";

<EasyblocksEditor
  ...,
  widgets={{
    // Property key MUST match the id specified witin the type's declaration
    url_custom: UrlTokenWidget
  }}
/>
```

Allowing custom values adds a new option to already available options and when this new value is selected, it renders the supplied custom input widget component.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FNxhdLFykLyHSVBc8ZD7g%2Fcustom_input.gif?alt=media&amp;token=761c3bde-9caf-4fbd-8534-4a5cdd466f63" alt=""><figcaption><p>Setting custom value for token type url</p></figcaption></figure>

### External types

Both `inline` and `token` types store the data within NoCode Entry. But what if you wanted to have a type that connects to a data from external system like Shopify? In that case you obviously don't want to store this data directly in a NoCode entry as it would create an unnecessary cache layer that is hard to keep up-to-date. In order to solve this problem Easyblocks provides a special type called `external`. External types store in the NoCode Entry only the identifier of the external data. The real data payload must be provided dynamically via props.

In order to learn about this concept please read [External Data guide](/essentials/external-data).


# External data

When you build a custom visual builder you usually want to connect it to the data that is specific to your software. Easyblocks allows for a full control over external data:

1. **Connect any external data source**. You can define your own widget displayed in the editor to pick data entry of any type (for example a product data from e-commerce platform). You define how the data is fetched and from where.
2. **All the data is fully dynamic**. Easyblocks doesn’t store any copies of data inside of its no-code entries. All the data is fetched in your code and provided via `externalData` prop to `EasyblocksEditor` or `Easyblocks` components (for editing and rendering respectively).
3. **Compound data types**. Custom data sources can provide compound objects consisting of multiple "basic" data types like text, image or video. Those basic types can be later connected to text fields, images etc. For example you can create a `product` data source (a product data from e-commerce platform) and then connect `product.title` to a text box or `product.mainImage` to the image component.
4. **Document-level parameters.** Your documents can be rendered with different data thanks to the [root parameters](#root-parameters-and-template-system) feature. Imagine you're building an e-commerce platform. Your users want to build a product page template and reuse this template for all the product pages. You can do it by adding a `product` as a root parameter of your document's root component. While editing, users will be able to pick any product data just for preview. When the content is rendered, the product data must be passed dynamically depending on the product page that is being rendered. You can render hundreds of different products pages based on a single template, similar to Shopify Theme Editor. Root parameters allow for building advanced template systems.

### Connecting external data sources

Let's imagine we have the following list of products:

```json
[
  {
    "id": "1",
    "title": "Product 1",
    "price": 25.0
  },
  {
    "id": "2",
    "title": "Product 2",
    "price": 10.0
  },
  {
    "id": "3",
    "title": "Product 3",
    "price": 50.0
  },
  {
    "id": "4",
    "title": "Product 4",
    "price": 150.0
  }
]
```

Let's define a new No-Code Component `Product` to display data of single product:

```typescript
{
  id: "Product",
  schema: [
    {
      prop: "product",
      type: "product",
    },
  ]
}
```

As you can see, we've used a new type called `product`. Right now, Easyblocks doesn't know anything about our custom type and will throw an error that it can't find it. To define a custom type you have to add its definition to [types](/essentials/configuration#types) property.

```typescript
const easyblocksConfig: Config = {
  ...,

  // Declarations of custom types
  types: {
    product: {
      type: "external",
      widgets: [],
    }
  }
}
```

Right now, definition of our new `product` type consists only of empty `widgets` array, but we will get back to it soon. If we try to add our `Product` component to the canvas, the error about missing type is gone, but the sidebar won't display our new field because it doesn't know how to do it. Each type can have one or more widgets to tell the editor how to render input/picker for that particular type. Built-in types have their own widgets that aren't overridable right now.

Let's define a new custom widget for our `product` type. Widget's definition is split into two parts:

* defining widget within the definition of type
* supplying React component for rendering within the editor

```tsx
// Widgets definition
const easyblocksConfig: Config = {
  ...,
  types: {
    product: {
      type: "external",
      widgets: [
        {
          id: "product",
          label: "Product"
        }
      ],
    }
  }
}

// Widget's component
import { WidgetComponentProps } from "@easyblocks/core";

function ProductPickerWidget(props: WidgetComponentProps<string>) {
  return null;
}

<EasyblocksEditor
  config={easyblocksConfig}
  widgets={{ product: ProductPickerWidget }}
/>
```

After these changes, our `ProductPickerWidget` is rendered instead of the error message. The problem is that it doesn't do anything. Let's first examine what our component receives in the props object:

* `id` (`string | null`) - a unique identifier of the currently selected resource, in our case it would be product id. This `id` is always stored in a No-Code Entry and will be later used for fetching the data. If `id` equals `null` it means that no resource is picked.
* `onChange ((newId: string | null) => void)` - this callback should be called by widget code to notify Easyblocks that selected resource `id` changed.

Let's implement a simple widget component using `select` element:

```tsx
import { WidgetComponentProps } from "@easyblocks/core";
import { SimplePicker } from "@easyblocks/design-system";

const products = [
  {
    id: "1",
    title: "Product 1",
    price: 25.0,
  },
  {
    id: "2",
    title: "Product 2",
    price: 10.0,
  },
  {
    id: "3",
    title: "Product 3",
    price: 50.0,
  },
  {
    id: "4",
    title: "Product 4",
    price: 150.0,
  },
];

function ProductPickerWidget(props: WidgetComponentProps<string>) {
  return (
    <select
      value={props.id ?? ""}
      onChange={(event) => {
        props.onChange(event.target.value === "" ? null : event.target.value);
      }}
    >
      <option value={""}>Select a product</option>
      {products.map((p) => (
        <option value={p.id} key={p.id}>
          {p.title}
        </option>
      ))}
    </select>
  );
}
```

Out widget component simply renders all products as available options and an additional option for empty value. Selecting `Product` component on the canvas gives us now the access to our custom widget picker from the sidebar.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FNHHE5HpIJ22uGw59XMZh%2Fproduct_widget_picker_simple.gif?alt=media&amp;token=e302e1c3-a40a-402b-ab84-eba75c65d8ab" alt=""><figcaption></figcaption></figure>

The only things stored in No-Code Entry are unique `id` and `widgetId`:

```json
{
  "product": {
    "id": "3",
    "widgetId": "product"
  }
}
```

As you can see we don't store a full product object in a No-Code Entry. The full data will be provided dynamically.

The missing piece right now is displaying the selected product. Let's implement React component for our `Product` component now:

<pre class="language-tsx"><code class="lang-tsx"><strong>function Product({ product }: ProductProps) {
</strong>  return (
    &#x3C;div>
      &#x3C;h1>{product.title}&#x3C;/h1>
      &#x3C;p>{product.price}&#x3C;/p>
    &#x3C;/div>
  );
}

// Editor component
&#x3C;EasyblocksEditor
  config={easyblocksConfig}
  components={{ Product }}
  widgets={{ product: ProductPickerWidget }}
/>
</code></pre>

If we try to render `Product` component with empty `product` field we will see the following message instead of our component:

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FPAL6uiZwYZHjfoC3F3jE%2Fimage.png?alt=media&amp;token=10ddefff-2932-485c-b472-aa084e2a86f6" alt=""><figcaption></figcaption></figure>

This happens because by default each custom type field is **required**, which means that Easyblocks doesn't render the component until some value is selected (this can be changed with `optional` property).

However, even if you select a value for `product` field, the component still won't render. It happens because Easyblocks doesn't yet know how to fetch the actual data. To resolve this issue we need to define a custom fetcher. Fetcher is a function that receives external references stored within No-Code Entry and resolves them to their external data. To define a fetcher for editor you need to specify `onExternalDataChange` prop.

<pre class="language-tsx"><code class="lang-tsx"><strong>import { ExternalData } from '@easyblocks/core';
</strong>
<strong>// State variable for storing fetched external data during editing
</strong><strong>const [externalDataValues, setExternalDataValues] = useState&#x3C;ExternalData>(
</strong>  {}
);

<strong>&#x3C;EasyblocksEditor
</strong>  config={easyblocksConfig}
  externalData={externalDataValues}
  onExternalDataChange={async externals => {
    // fetch external data
  }} 
  components={{ Product }}
  widgets={{ product: ProductPickerWidget }}
/>
</code></pre>

That callback is invoked each time you update value for your custom type field. The `changedExternalData` parameter has the following structure:

```javascript
{
  "606106b5-86e3-4234-aa23-11d8191e6ab8.product": {
    id: "3",
    type: "product",
    widgetId: "product"
  }
}
```

Each property represents a field of your defined custom type that was recently updated and its value represents the external reference to some external data. To correctly resolve`product` type references we need to do the following:

1. from `externals` select only types of `product`
2. map each external reference to its related external data value
3. return resolved external data in the valid format

```tsx
const products = [
  {
    id: "1",
    title: "Product 1",
    price: 25.0,
  },
  {
    id: "2",
    title: "Product 2",
    price: 10.0,
  },
  {
    id: "3",
    title: "Product 3",
    price: 50.0,
  },
  {
    id: "4",
    title: "Product 4",
    price: 150.0,
  },
];

const [externalDataValues, setExternalDataValues] = useState<ExternalData>(
  {}
);

<EasyblocksEditor
  ...,
  onExternalDataChange={async externals => {
    // (1)
    const productReferences = Object.entries(externals)
      .filter(([, reference]) => {
        // Why do we check `widgetId` instead of `type`?
        // We could rely on `type` field, but we could have multiple widgets for
        // the same type and they could be resolved differently.
        // Why do we check for empty `id`?
        // Changing from non empty value to empty is also a change
        return reference.widgetId === 'product' && reference.id !== null;
      });

    const resolvedProducts = Object.fromEntries(
      productReferences.map(([id, reference]) => {
        // (2)
        const product = products.find(p => p.id === reference.id);

        // (3)
        if (!product) {
          // If product wasn't resolved/found, we report an error
          return [id, { error: new Error(
            `Product with id ${reference.id} was not found`
          )}]
        }

        // (3) If found, return it as resolved external data
        return [id, { type: 'product', value: product }];
    }));

    // We inform editor about change by updating `externalDataValues` state variable
    // with new data
    setExternalDataValues({
      ...externalDataValues,
      ...resolvedProducts
    });
  }}
/>
```

And here is the result component finally rendered with the external data :tada:

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FTfO8ZJpDYRcU8suFw23f%2Fimage.png?alt=media&amp;token=13b23172-78c4-48b6-b2c6-d494905ddf51" alt=""><figcaption></figcaption></figure>

### Built-in picker

To build a simple usable widget component, Easyblocks comes with `SimplePicker` component that can be imported from `@easyblocks/design-system` package. It comes with built-in support for:

* displaying available options from async source
* searching through available options based on given search criteria
* clearing selected value
* displaying preview image of each option

Let's use it now instead:

```tsx
import { WidgetComponentProps } from "@easyblocks/core";
import { SimplePicker } from "@easyblocks/design-system";

const products = [
  {
    id: "1",
    title: "Product 1",
    price: 25.0,
  },
  {
    id: "2",
    title: "Product 2",
    price: 10.0,
  },
  {
    id: "3",
    title: "Product 3",
    price: 50.0,
  },
  {
    id: "4",
    title: "Product 4",
    price: 150.0,
  },
];

function ProductPickerWidget(props: WidgetComponentProps) {
  return (
    <SimplePicker
      value={props.id}
      onChange={props.onChange}
      getItems={async (query) => {
        const filteredItems = products.filter((p) => p.title.includes(query));

        return filteredItems.map((i) => {
          return {
            id: i.id,
            name: i.title,
          };
        });
      }}
      getItemById={async (id) => {
        const item = products.find((p) => p.id === id);

        return {
          id: item.id,
          name: item.title,
        };
      }}
    />
  );
}
```

To use `SimplePicker` we need to do the following things:

* forward received `props` as `value` and `onChange`
* implement `getItems` method that's responsible for showing the list of available items to choose from based on entered search text
* implement `getItemById` method that's responsible for fetching data about currently selected item

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FZbvOyMghjjNJCE5sI0UE%2Fproduct_picker_demo_1.gif?alt=media&amp;token=ed156ebf-b28e-4ed0-89ec-dfd6f3d70fa5" alt=""><figcaption></figcaption></figure>

### Compound types

Easyblocks allows you to return compound data, which is basically a data that consists of multiple "basic" data types. It means that instead of returning:

```typescript
{
   type: "product",
   value: product
}
```

we can return:

```typescript
{
  type: "object",
  value: {
    productTitle: {
      type: "text",
      value: product.title
    },
    productPrimaryImage: {
      // `image` type isn't a part of Easyblocks and it would require to define
      // a custom type
      type: "image",
      value: product.images[0]
    },
    self: {
      type: "product",
      value: product
    }
  ]
}
```

Compound types are useful when working with root parameters.

### Root parameters and template system

So far we've built a content that's static. Static images, static products, static text. Our content isn't reusable. What if we would like to build a product landing page, but feed it with different product each time? That's possible in Easyblocks thanks to root parameters. Root parameters allows you to define fields whose value can be supplied during the rendering of your content.

Let's use above `product` type as our example and define a new component `ProductPage`:

```typescript
import type { Config } from "@easyblocks/core";

const easyblocksConfig: Config = {
  ...,
  components: [
    {
      id: "ProductPage",
      schema: [
        {
          prop: "data",
          type: "component-collection",
          accepts: ["item"]
        }
      ],
      rootParams: [
        {
          prop: "product",
          label: "Product",
          widgets: [{
            id: "product",
            label: "Product"
          }]
        },
      ]
    }
  ]
};
```

If we open the editor with search param `rootComponent=ProductPage` we're going to see a `product` field at the root level in the sidebar. Value for this field can be selected using one of the widgets specified in `widgets` field.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FTOKyfcj6gnC4eGS2Nw3v%2Fimage.png?alt=media&amp;token=f3320ea7-825b-4f84-a3fa-14059e0a66ec" alt=""><figcaption><p>Root param product shown in the sidebar</p></figcaption></figure>

If we add a `Simple Text` component to the canvas, we will see a new option to change selected widget of the `Text` value property

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FLpAF0lQG7lspCUYa710L%2Fimage.png?alt=media&amp;token=7de8697e-62db-4a8c-ad63-543c9289f7c2" alt=""><figcaption></figcaption></figure>

After selecting `Document data` option, `Text` property now would allow us to be connected to `text` external data returned for the `product` field.

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FV0gkxpFFYBFmHeDUIiPM%2Fimage.png?alt=media&amp;token=78b48bfe-f2f8-4fbe-bda5-c2c953dd5484" alt=""><figcaption></figcaption></figure>

<figure><img src="https://830721423-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fb8I1ungzsCMExEnILO8v%2Fuploads%2FHXxyYEUauIJl2lrYLZLm%2Fimage.png?alt=media&amp;token=1fcd665f-0410-4cc0-b9c3-fb2ac20279d4" alt=""><figcaption></figcaption></figure>

To dynamically render the content with different products we have to overwrite the property `$.product` of `externalData`

{% hint style="info" %}
To dynamically set a value for root parameter we reference it by joining dollar sign and a name of root parameter ex. `$.myRootParameter`
{% endhint %}

```tsx
import {
  Easyblocks,
  buildDocument,
  RequestedExternalData,
} from "@easyblocks/core";
import { easyblocksConfig } from "./path-to-your-easyblocks-config";

async function myFetcher(externalData: RequestedExternalData) {
  // Your custom fetcher logic
}

function ProductPage({ params }: Record<string, string>) {
  const { renderableDocument, externalData } = await buildDocument({
    documentId: "<DOCUMENT_ID_OF_PRODUCT_PAGE_CONTENT>",
    config: easyblocksConfig,
    locale: "en-US",
  });

  const fetchedExternalData = await myFetcher({
    ...externalData,
    "$.product": {
      ...externalData["$.product"],
      id: params.id,
    },
  });

  return (
    <Easyblocks
      renderableDocument={renderableDocument}
      externalData={fetchedExternalData}
    />
  );
}
```

For each product page, the content build using the editor is going to have different product data based od `params.id`.


# Templates

Whenever you add a new No-Code Component to `Config.components` the end-users can start adding its instances to a canvas. It's done via a template picker that is displayed after user clicks on a plus button or a blue placeholder.

The template picker shows a list of templates allowed in this particular slot (defined by a schema property of type `component` or `component-collection`). Under the hood each template is represented by a very simple object:

```typescript
{
    id: "template_1", // unique id
    label: "My Super Template", // label displayed in UI (optional)
    thumbnail: thumbnail_url // URL to the thumbnail image (optional)
    entry: { ... } // no-code entry
}
```

All the available templates are stored in a `Config.templates` array.

When you add a new component to `Config.components` and don't specify any templates for this component, Easyblocks will create a single default template and set all the values to the default ones. Whenever you specify at least a one template in `Config.templates` for your component, the default template will be gone.

{% hint style="info" %}
The template picker always displays only the templates for components that allowed by `accepts` field of the parent component (in parent component's `component` or `component-collection` schema property).
{% endhint %}

### Working with templates

The best way to work with templates is as follows:

1. Add a default template of your component to the canvas. Beware, it usually looks very bad.
2. Use Easyblocks Editor to make it look good.
3. Click 5 times on a header bar (it unlocks "developer mode").
4. Select your component instance, click "Copy entry" at the bottom of the sidebar. You just copied the No-Code Entry of your new component.
5. Add a new template to the `Config.templates` and paste the entry to the `entry` property of your template.

The best way to understand how it all works is too see the video:

{% embed url="<https://vimeo.com/908194117?share=copy>" %}

### Template picker types

There are 3 built-in template picker widgets available in Easyblocks:

* `large` - 2 cards in a row, good for sections
* `large-3` - 3 cards in a row, good for cards
* `compact` (default) - good for smaller items like stack elements, buttons, etc.

The picker widget can be set as a part of `component` or `component-collection` schema property:

```typescript
const componentDefinition = {
  // ...
  schema: [
    // ...
    {
      prop: "Card1",
      type: "component",
      required: false,
      picker: "large-3", // picker property allows for changing picker widget
    },
  ],
};
```


# Backend

The architecture of Easyblocks decouples the editor from the underlying backend service. The backend is responsible for:

1. Fetching, creating, updating and versioning of documents.
2. Fetching, creating and updating templates.

Easyblocks comes with a simple cloud service ([app.easyblocks.io](https://app.easyblocks.io)) that can be used in a following way:

```typescript
import { Config, EasyblocksBackend } from "@easyblocks/core";

export const easyblocksConfig: Config = {
  backend: new EasyblocksBackend({
    accessToken: "<<< your access token >>>",
  }),
  // ...
};
```

The access token can be acquired by creating the account described in [Getting Started](/getting-started#get-access-token).

### Building custom `Backend`

You can build your own backend. Each Backend must conform to the following TS interface:

```typescript
export type Backend = {
  documents: {
    get: (payload: { id: string; locale?: string }) => Promise<Document>;
    create: (payload: Omit<Document, "id" | "version">) => Promise<Document>;
    update: (payload: Omit<Document, "type">) => Promise<Document>;
  };
  templates: {
    get(payload: { id: string }): Promise<UserDefinedTemplate>;
    getAll: () => Promise<UserDefinedTemplate[]>;
    create: (payload: {
      label: string;
      entry: NoCodeComponentEntry;
      width?: number;
      widthAuto?: boolean;
    }) => Promise<UserDefinedTemplate>;
    update: (payload: {
      id: string;
      label: string;
    }) => Promise<Omit<UserDefinedTemplate, "entry">>;
    delete: (payload: { id: string }) => Promise<void>;
  };
};

export type Document = {
  id: string;
  version: number;
  entry: NoCodeComponentEntry;
};

export type UserDefinedTemplate = {
  id: string;
  label: string;
  thumbnail?: string;
  thumbnailLabel?: string;
  entry: NoCodeComponentEntry;
  isUserDefined: true;
  width?: number;
  widthAuto?: boolean;
};
```


