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.