React Elements Custom Tool

In this example, we will create a custom tool that uses Unlayer Elements under the hood.

React Elements lets you write reusable content with React components. Here, the same component renders in the editor preview and in the exported custom tool HTML.

Read the full Elements documentation at docs.unlayer.com/elements.

Register a React Elements custom tool

/** @jsx React.createElement */
import {
  Button,
  Column,
  ColumnLayouts,
  Heading,
  Image,
  Paragraph,
  Row,
  renderToHtmlParts,
} from '@unlayer/react-elements';

const React = window.unlayer.React;

function getReactElementsCardProps(props = {}) {
  const isViewer = props.isViewer === true;

  return {
    bodyValues: isViewer
      ? { ...props.bodyValues, contentWidth: '100%' }
      : props.bodyValues || {},
    displayMode: isViewer ? 'web' : props.displayMode || 'web',
  };
}

function ReactElementsCard(props = {}) {
  const { bodyValues, displayMode } = getReactElementsCardProps(props);

  return (
    <Row
      layout={ColumnLayouts.OneColumn}
      backgroundColor="#ffffff"
      bodyValues={bodyValues}
      mode={displayMode}
    >
      <Column>
        <Image
          src="https://placehold.co/600x150?text=React+Elements"
          alt="React Elements"
        />
        <Heading level="h1">Build custom tools with React Elements</Heading>
        <Paragraph>
          Use React Elements components inside a custom tool, then render the
          exported HTML with the same component tree.
        </Paragraph>
        <Button
          href="https://docs.unlayer.com/elements"
          backgroundColor="#0f766e"
        >
          Read the Elements docs
        </Button>
      </Column>
    </Row>
  );
}

unlayer.registerTool({
  name: 'react_elements',
  label: 'React Elements',
  icon: 'fa-code',
  supportedDisplayModes: ['web', 'email'],
  options: {},
  values: {},
  renderer: {
    Viewer: function (props) {
      return <ReactElementsCard {...props} isViewer />;
    },
    // renderToHtml() returns a complete HTML document. renderToHtmlParts()
    // separates the body from head assets so custom tool exporters can return
    // only the body markup.
    exporters: {
      web: function (values, index, colIndex, cells, bodyValues) {
        return renderToHtmlParts(
          <ReactElementsCard
            values={values}
            bodyValues={bodyValues}
            displayMode="web"
          />,
          { mode: 'web' }
        ).body;
      },
      email: function (values, index, colIndex, cells, bodyValues) {
        return renderToHtmlParts(
          <ReactElementsCard
            values={values}
            bodyValues={bodyValues}
            displayMode="email"
          />,
          { mode: 'email' }
        ).body;
      },
    },
  },
});

Preview

Add a React property editor

To add a property editor without repeating the whole custom tool, register a React widget and wire its value into the existing component and exporter.

const defaultAccentColor = '#0f766e';

const accentColors = [
  { label: 'Teal', value: '#0f766e' },
  { label: 'Blue', value: '#2563eb' },
  { label: 'Purple', value: '#7c3aed' },
];

function getAccentColor(values = {}) {
  return values.accentColor || defaultAccentColor;
}

function ReactElementsAccentPicker(props) {
  const { value, updateValue } = props;
  const currentValue = value || defaultAccentColor;
  const [selectedColor, setSelectedColor] = React.useState(currentValue);

  React.useEffect(() => {
    setSelectedColor(currentValue);
  }, [currentValue]);

  function selectColor(nextColor) {
    setSelectedColor(nextColor);
    updateValue(nextColor);
  }

  return (
    <div>
      {accentColors.map((color) => (
        <button
          key={color.value}
          type="button"
          onClick={() => selectColor(color.value)}
          style={{ background: color.value }}
        >
          {color.label}
        </button>
      ))}
      <input
        type="color"
        value={selectedColor}
        onChange={(event) => selectColor(event.target.value)}
      />
    </div>
  );
}

unlayer.registerPropertyEditor({
  name: 'react_elements_accent_picker',
  Widget: ReactElementsAccentPicker,
});

Use the new value in the existing component:

function ReactElementsCard(props = {}) {
  const accentColor = getAccentColor(props.values);
  const { bodyValues, displayMode } = getReactElementsCardProps(props);

  return (
    <Row bodyValues={bodyValues} mode={displayMode}>
      <Column>
        <Button
          href="https://docs.unlayer.com/elements"
          backgroundColor={accentColor}
        >
          Read the Elements docs
        </Button>
      </Column>
    </Row>
  );
}

Then expose the property editor in the tool options and pass the updated values through your exporters:

unlayer.registerTool({
  name: 'react_elements',
  options: {
    default: {
      title: null,
    },
    style: {
      title: 'Style',
      position: 1,
      options: {
        accentColor: {
          label: 'Accent Color',
          defaultValue: defaultAccentColor,
          widget: 'react_elements_accent_picker',
        },
      },
    },
  },
  renderer: {
    Viewer: function (props) {
      return <ReactElementsCard {...props} isViewer />;
    },
    // renderToHtml() returns a complete HTML document. renderToHtmlParts()
    // separates the body from head assets so custom tool exporters can return
    // only the body markup.
    exporters: {
      web: function (values, index, colIndex, cells, bodyValues) {
        return renderToHtmlParts(
          <ReactElementsCard
            values={values}
            bodyValues={bodyValues}
            displayMode="web"
          />,
          { mode: 'web' }
        ).body;
      },
      email: function (values, index, colIndex, cells, bodyValues) {
        return renderToHtmlParts(
          <ReactElementsCard
            values={values}
            bodyValues={bodyValues}
            displayMode="email"
          />,
          { mode: 'email' }
        ).body;
      },
    },
  },
});

Property editor preview

Bundle without importing React

Keep React out of the custom tool bundle so it uses the same React runtime as the editor. This example uses Parcel aliases that point React imports to Unlayer's global React:

{
  "alias": {
    "react": "./unlayer-react-shim.js",
    "react/jsx-runtime": "./unlayer-jsx-runtime-shim.js"
  }
}

unlayer-react-shim.js

module.exports = window.unlayer.React;

unlayer-jsx-runtime-shim.js

const React = window.unlayer.React;

export const Fragment = React.Fragment;

export function jsx(type, props, key) {
  const nextProps = { ...(props || {}) };
  const children = nextProps.children;

  delete nextProps.children;

  if (key !== undefined) {
    nextProps.key = key;
  }

  return React.createElement(type, nextProps, children);
}

export const jsxs = jsx;

export default {
  Fragment,
  jsx,
  jsxs,
};

Load the custom tool

unlayer.init({
  id: 'editor',
  displayMode: 'email',
  customJS: ['https://examples.unlayer.com/examples/react-elements/custom.js'],
});

For the property editor version, load custom-property-editor.js instead.