Warning
You're browsing the documentation for an old version of Webiny. Consider upgrading your project to Webiny 5.35.x.
What you'll learn
  • how to create a new Page Builder element
  • how to register plugins in Webiny applications

Overview
anchor

Although the Page Builder application comes with a plethora of ready-made page elements, at one point in time, you might need to create your own to satisfy your specific requirements. To achieve that, we can utilize a couple of simple plugins, which is what we’ll cover in this short tutorial.

What We'll Build
anchor

We’ll create a new page element that allows content creators to embed content using an iframe. Here’s what the final result will look like:

Iframe page elementIframe page element
(click to enlarge)
The IFrame element is now available out of the box from the 5.26.0 release. You can still follow this tutorial to create custom IFrame elements and get insights on how to create a new Page Element element.

The full code example used in this tutorial is available in our webiny-examples external link GitHub repository.

Organization of Files
anchor

Although you can place and organize the plugins in any way you prefer for your project, we recommend adopting the following organization.

When creating new page elements, it’s recommended to have all of the needed plugins in a single shared folder. This is because some of the plugins will need to be imported both within the Admin Area (apps/admin) and Website (apps/website) applications.

With that in mind, for this tutorial, we will create the new pb-element-iframe external link folder in the apps/extensions folder.

By default, the extensions folder does not exist in the apps directory, so you’ll need to create it.

Anatomy of Element Plugins
anchor

To create a new page element, we need to register two sets of plugins:

  1. For the Admin Area application:
    • a plugin that defines how it’s rendered in the page editor
    • a plugin that defines all the available settings and options for it
  2. For the Website application:
    • a plugin that defines how the page element is rendered on the actual page

To better organize files, we’ll write all the plugins needed for the Admin Area application and Website application in apps/extensions/pb-element-iframe/admin external link and apps/extensions/pb-element-iframe/render external link, respectively

Let’s get started with scaffolding initial folders and files for our page element plugins.

Scaffold Folder and Files for Plugins
anchor

  • create a folder named pb-element-iframe inside apps/extensions
  • create a package.json external link file inside apps/extensions/pb-element-iframe with the following contents:
apps/extensions/pb-element-iframe/package.json
{
  "name": "@extensions/pb-element-iframe",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "@emotion/styled": "10.0.27",
    "@webiny/app-page-builder": "5.26.0",
    "@webiny/ui": "5.26.0",
    "@webiny/validation": "5.26.0",
    "classnames": "^2.2.6",
    "emotion": "10.0.27",
    "react": "^16.14.0"
  }
}

Update the @webiny/* dependencies versions above to match the version of your Webiny project. You can check the Webiny version by running the yarn webiny --version command from the project root.

apps/extensions/pb-element-iframe/tsconfig.json
{
  "extends": "../../../tsconfig",
  "include": ["."],
  "compilerOptions": {
    "composite": false
  }
}
  • add "apps/extensions/pb-element-iframe" to workspaces packages inside package.json external link in project root
{project-root}/package.json
(...)"workspaces": {"packages": [  "apps/admin/code",  "apps/website/code"  // (...)   "apps/extensions/pb-element-iframe"]// (...)}
  • create admin and render folders inside apps/extensions/pb-element-iframe

  • link workspaces by running yarn install command from project root. After completion there will be a symbolic link external link of @extensions/pb-element-iframe package in node_modules

  • add @extensions/pb-element-iframe package as a dependency to both the Admin Area application and Website application, by editing the apps/admin/package.json and apps/website/package.json files respectively:

"@extensions/pb-element-iframe": "^1.0.0",

After successfully performing the above-mentioned steps, the folder structure of our project will look similar to this:

// Config files are omitted for sake of brevity.

├── apps
│   ├── api
│   ├── admin
│   ├── extensions
│   │   └── pb-element-iFrame
│   │       ├── admin
│   │       ├── render
│   │       ├── package.json
│   │       └── tsconfig.json
│   ├── theme
│   └── website
├── package.json
├── tsconfig.build.json
├── tsconfig.json
├── webiny.project.ts
└── yarn.lock

Create Plugins for the Admin Area Application
anchor

Click the plus icon located on the left sidebar of the editor to access all the available page elements:

Iframe page elementIframe page element
(click to enlarge)

As mentioned, this list of page elements can be expanded, and custom page elements can be created via plugins. Now let’s create all the necessary plugins for the Admin Area application.

Editor Element Plugin
anchor

We will start with the PbEditorPageElementPlugin plugin.
Create index.tsx external link file in apps/extensions/pb-element-iframe/admin directory.
apps/extensions/pb-element-iframe/admin/index.tsx
import React from "react";
import styled from "@emotion/styled";
import {
  PbEditorPageElementPlugin,
  DisplayMode,
  PbEditorPageElementAdvancedSettingsPlugin
} from "@webiny/app-page-builder/types";
import { createInitialPerDeviceSettingValue } from "@webiny/app-page-builder/editor/plugins/elementSettings/elementSettingsUtils";

import { ReactComponent as IFrameIcon } from "./assets/iframe-icon.svg";
import IframeEditor from "./components/iframeEditor";
import IFrameSettings from "./iframeSettings";

const PreviewBox = styled("div")({
  textAlign: "center",
  height: 50,
  svg: {
    height: 50,
    width: 50,
    color: "var(--mdc-theme-text-secondary-on-background)"
  }
});

export default () => {
  return [
    {
      name: "pb-editor-page-element-iframe-custom",
      type: "pb-editor-page-element",

      elementType: "iframe-custom",
      toolbar: {
        // We use `pb-editor-element-group-basic` to put our plugin into the Media group.
        title: "Custom iFrame",
        group: "pb-editor-element-group-basic",
        preview() {
          return (
            <PreviewBox>
              <IFrameIcon />
            </PreviewBox>
          );
        }
      },
      settings: [
        "pb-editor-page-element-settings-delete",
        "pb-editor-page-element-style-settings-height",
        "pb-editor-page-element-style-settings-width"
      ],
      target: ["cell", "block"],

      onCreate: "open-settings",
      create(options) {
        /*
                    Create function is here to create the initial data
                    for the page element, which then is utilized in the
                    IFrameEditor component and in the element settings.
                */
        return {
          type: "iframe-custom",
          elements: [],
          data: {
            iframe: {
              // The URL property will be populated when user enters the URL in the element settings.
              url: "",
              height: 370
            },
            settings: {
              height: createInitialPerDeviceSettingValue({ value: "380px" }, DisplayMode.DESKTOP)
            }
          },
          ...options
        };
      },
      render(props) {
        /*
                    Every render function receives the page element's
                    data assigned to the "element.data" property in
                    the received props. In here we will store the
                    "iframe.url" which will be provided via the page
                    element's settings.
                */
        return <IframeEditor {...props} />;
      },
      renderElementPreview({ width, height }) {
        return <img style={{ width, height }} alt={"iFrame"} />;
      }
    } as PbEditorPageElementPlugin,

    {
      name: "pb-editor-page-element-advanced-settings-iframe-custom",
      type: "pb-editor-page-element-advanced-settings",
      elementType: "iframe-custom",
      render(props) {
        return <IFrameSettings {...props} />;
      }
    } as PbEditorPageElementAdvancedSettingsPlugin
  ];
};

Create an assets folder in apps/extensions/pb-element-iframe/admin then download and keep this iframe-icon.svg external link icon file in the assets folder.

The key properties of the plugin are the create and render. They define the initial page element’s settings and how it will be rendered in the editor, respectively.

The data property holds the initial state of the page element which can contain any data you might need.

The toolbar property helps us put our plugin into the toolbar.

Finally, it’s up to the render function to define how the page element will be rendered once the user drops it on the page.

Notice the props that were passed to the render function. This object contains all the relevant page element’s data and settings.

Now that we’ve covered the overall structure of the plugin. Let’s create an IFrameEditor external link component.
It will be a simple React component that will render an iframe HTML element.

Create components directory in apps/extensions/pb-element-iframe/admin and then create iframeEditor.tsx external link file in components directory.

apps/extensions/pb-element-iframe/admin/components/iframeEditor.tsx
import React from "react";
import { css } from "emotion";
import styled from "@emotion/styled";
import { ElementRoot } from "@webiny/app-page-builder/render/components/ElementRoot";
import { ReactComponent as IFrameIcon } from "../assets/iframe-icon.svg";
import { PbEditorElement } from "@webiny/app-page-builder/types";

const outerWrapper = css({
  boxSizing: "border-box"
});

const PreviewBox = styled("div")({
  textAlign: "center",
  height: 50,
  svg: {
    height: 50,
    width: 50,
    color: "var(--mdc-theme-text-secondary-on-background)"
  }
});

interface IFrameProps {
  element: PbEditorElement;
}

const IframeEditor: React.FC<IFrameProps> = ({ element }) => {
  if (!element?.data?.iframe?.url) {
    return (
      <PreviewBox>
        <IFrameIcon />
      </PreviewBox>
    );
  }

  return (
    <ElementRoot
      className={"webiny-pb-base-page-element-style webiny-pb-page-element-iframe " + outerWrapper}
      element={element}
    >
      <iframe src={element.data.iframe.url} width="100%" height="100%" />
    </ElementRoot>
  );
};

export default IframeEditor;

Every element needs to have the ElementRoot external link component as root because it applies styles, classes and attributes to the element assigned via right sidebar style tab.

Notice we’ve added webiny-pb-base-page-element-style external link CSS class. Every element needs to have it as it contains all base styles applied by the Page Builder application.

Notice we’ve added webiny-pb-page-element-iframe external link CSS class, which enables us to add custom CSS styling if needed.

Element Settings Plugin
anchor

The next plugin we’ll need to define is the PbEditorPageElementAdvancedSettingsPlugin, which we will use to render the required UI for settings, so the user can provide an iframe URL. The settings UI will be shown automatically in the right sidebar when the user drag-and-drop the page element on the page.

Create iframeSettings.tsx external link file in the apps/extensions/pb-element-iframe/admin directory.

apps/extensions/pb-element-iframe/admin/iframeSettings.tsx
import React from "react";
import { validation } from "@webiny/validation";
import { Input } from "@webiny/ui/Input";
import { Cell, Grid } from "@webiny/ui/Grid";
import {
  ButtonContainer,
  classes,
  SimpleButton
} from "@webiny/app-page-builder/editor/plugins/elementSettings/components/StyledComponents";
import Accordion from "@webiny/app-page-builder/editor/plugins/elementSettings/components/Accordion";
import { BindComponent } from "@webiny/form";

interface iFrameImagesSettingsProps {
  Bind: BindComponent;
  submit: () => void;
}

const IFrameSettings: React.FC<iFrameImagesSettingsProps> = props => {
  const { Bind, submit } = props;
  return (
    <Accordion title={"IFrame Source"} defaultValue={true}>
      <React.Fragment>
        <Bind name={"iframe.url"} validators={validation.create("required,url")}>
          <Input label={"URL"} description={"Enter an iFrame URL"} />
        </Bind>
        <Grid className={classes.simpleGrid}>
          <Cell span={12}>
            {/*  @ts-ignore */}
            <ButtonContainer>
              {/*  @ts-ignore */}
              <SimpleButton onClick={submit}>Save</SimpleButton>
            </ButtonContainer>
          </Cell>
        </Grid>
      </React.Fragment>
    </Accordion>
  );
};

export default IFrameSettings;

As mentioned, this code will show the settings UI in sidebar and ask for the URL of the iframe, as shown below:

Iframe page element settingsIframe page element settings
(click to enlarge)

Render Element Plugin
anchor

As mentioned, every page element consists of two sets of plugins. We’ve already covered the editor side of the logic, now, let’s write the plugin that will render it to the page preview and the actual page.

In order to render the element on the actual page, we will use the PbEditorPageElementPlugin plugin.
Create index.tsx external link file in the apps/extensions/pb-element-iframe/render directory.
apps/extensions/pb-element-iframe/render/index.tsx
import React from "react";
import { PbRenderElementPlugin } from "@webiny/app-page-builder/types";

import IFrame from "./components/iFrame";

export default () =>
  ({
    name: "pb-render-page-element-iframe",
    type: "pb-render-page-element",
    elementType: "iframe-custom",
    render({ element }) {
      return <IFrame element={element} />;
    }
  } as PbRenderElementPlugin);

Now let’s create the IFrame external link component used in the index.tsx above. Create iFrame.tsx file in the apps/extensions/pb-element-iframe/render/components directory.

apps/extensions/pb-element-iframe/render/components/iFrame.tsx
import React from "react";
import { css } from "emotion";
import { ElementRoot } from "@webiny/app-page-builder/render/components/ElementRoot";
import { PbEditorElement } from "@webiny/app-page-builder/types";

const outerWrapper = css({
  boxSizing: "border-box"
});

interface IFrameProps {
  element: PbEditorElement;
}

const IFrame: React.FC<IFrameProps> = ({ element }) => {
  const { data } = element;
  // If the user didn't enter a URL, let's show a simple message.
  if (!data?.iframe?.url) {
    return <div>IFrame URL is missing.</div>;
  }

  // Otherwise, let's render the iframe.
  return (
    <ElementRoot
      className={
        "webiny-pb-base-page-element-style webiny-pb-page-element-embed-iframe " + outerWrapper
      }
      element={element}
    >
      <iframe src={data.iframe.url} width="100%" height="100%" />
    </ElementRoot>
  );
};

export default IFrame;

Along with the actual page, this plugin will also responsible for rendering the element in the page preview, as shown in the image below:

Page preview inside editorPage preview inside editor
(click to enlarge)

Register Plugins in the Admin Area Application
anchor

Now that we’ve created plugins for the iframe element, we need to register them in the Admin Area application to work. Let’s make the following changes in apps/admin/src/plugins/pageBuilder/editorPlugins.ts file:

apps/admin/src/plugins/pageBuilder/editorPlugins.ts
(...)
// Import the `iframe` element and it's settingsimport iframeElement from "@extensions/pb-element-iframe/admin";
export default [   iframeElement(),   // Rest of the plugins   (...)];

Also in apps/admin/src/plugins/pageBuilder/renderPlugins.ts file:

apps/admin/src/plugins/pageBuilder/renderPlugins.ts
(...)
// Import the `iframe` elementimport iframeElement from "@extensions/pb-element-iframe/render";
export default [  // Elements  iframeElement(),  // Rest of the plugins  (...)];

After registering the plugins successfully, we should be able to see the iframe element in the Admin Area application, as shown below:

Editor Iframe page elementEditor Iframe page element
(click to enlarge)
With the webiny watch command up and running, the performed application code changes should be automatically rebuilt and redeployed into the cloud.

Add Plugins in the Website Application
anchor

In the Website application, we will reuse the Render Element Plugin to render the iframe page element. To reuse the Render Element Plugin, simply register it by making the following changes in the apps/website/src/plugins/pageBuilder.ts file:

apps/website/src/plugins/pageBuilder.ts
// Some code is removed for the sake of brevity.(...)
// Import `iframe` elementimport iframeElement from "@extensions/pb-element-iframe/render";
export default [  (...)  // Page elements  iframeElement(),  (...)];

After registering the plugins successfully, the page element should be visible in your Website application:

Page preview inside editorPage preview inside editor
(click to enlarge)

Conclusion
anchor

In this tutorial, we’ve learned how we can create a new iframe page element and make it configurable within the Page Builder application’s page editor. The same approach can be used to create any page element, so make sure to bookmark and revisit this tutorial, in case you need a refresher on any of the steps.

Once again, a full code example can be found in our webiny-examples external link GitHub repository. And, if you have further questions, or you simply got stuck, feel free to get in touch with us via Slack external link.

FAQ
anchor

How to Add Dependencies in Workspace Package?
anchor

You can add dependencies to your workspace package using the yarn workspace <YOUR_WORKSPACE_NAME> add <DEPENDENCY_NAME> command.

For example, running the following command, will add the @webiny/form as dependency in the @extensions/pb-element-iframe workspace package:

yarn workspace @extensions/pb-element-iframe add @webiny/form