Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/demo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Demo and documentation

on:
push:
branches: [master]
paths:
- 'demo/**'
- 'docs/**'
- 'src/**'
- 'README.md'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'tsup.config.ts'
- '.nvmrc'
- '.github/workflows/ci.yml'
- '.github/workflows/demo.yml'
pull_request:
paths:
- 'demo/**'
- 'docs/**'
- 'src/**'
- 'README.md'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'tsup.config.ts'
- '.nvmrc'
- '.github/workflows/ci.yml'
- '.github/workflows/demo.yml'

jobs:
demo:
name: Demo and docs (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [22, 25]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: npm
cache-dependency-path: |
package-lock.json
demo/package-lock.json
# The root prepare script builds the wrapper during installation.
- run: npm ci
- run: npm --prefix demo ci
- run: npm --prefix demo run build
- run: npm --prefix demo test
- run: npm --prefix demo run test:docs
24 changes: 20 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
## Prerequisites

[Node.js](http://nodejs.org/) >= v10 must be installed.
[Node.js](https://nodejs.org/) 22, matching `.nvmrc`, must be installed.

## Installation

- Running `npm install` in the components's root directory will install everything you need for development.
- Running `npm install` in the `demo` directory will install everything you need to run a demo app locally.
From the repository root:

```bash
npm install
npm --prefix demo install
```

The root install builds the wrapper through its `prepare` script.

## Demo Development Server

- `npm start` from `demo` directory will run the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading.
The demo consumes the built wrapper through `file:..`. Run these commands in two terminals, both from the repository root:

```bash
npm start
```

```bash
npm --prefix demo run dev
```

`npm start` runs `tsup --watch` to rebuild wrapper changes. The demo server reloads those builds at [http://localhost:3000](http://localhost:3000); running only the demo server will not pick up wrapper source edits.

## Running Tests

Expand Down
51 changes: 28 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,48 +53,53 @@ See the [CHANGELOG](CHANGELOG.md) for the full list.

Require the EmailEditor component and render it with JSX:

<!-- check-docs -->

```tsx
import React, { useRef } from 'react';
import { useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import EmailEditor, {
type EditorRef,
type EmailEditorProps,
} from 'react-email-editor';

import EmailEditor, { EditorRef, EmailEditorProps } from 'react-email-editor';

const App = (props) => {
const App = () => {
const emailEditorRef = useRef<EditorRef>(null);
const [ready, setReady] = useState(false);
const [html, setHtml] = useState('');

const exportHtml = () => {
const unlayer = emailEditorRef.current?.editor;

unlayer?.exportHtml((data) => {
const { design, html } = data;
console.log('exportHtml', html);
if (!ready) return;
emailEditorRef.current?.editor?.exportHtml((data) => {
setHtml(data.html);
// Persist data.design to reopen it later with editor.loadDesign(data.design).
});
};

const onReady: EmailEditorProps['onReady'] = (unlayer) => {
// editor is ready
// you can load your template here;
// the design json can be obtained by calling
// unlayer.loadDesign(callback) or unlayer.exportHtml(callback)
// or generated from React code with @unlayer/react-elements (renderToJson)
// const templateJson = { DESIGN JSON GOES HERE };
// unlayer.loadDesign(templateJson);
const onReady: EmailEditorProps['onReady'] = (editor) => {
setReady(true);
// editor is now ready for calls such as saveDesign and loadDesign.
// Load an existing design here: editor.loadDesign(savedDesign).
// Obtain design JSON from editor.saveDesign(callback), editor.exportHtml(callback),
// or @unlayer/react-elements' renderToJson(). loadDesign accepts JSON, not a callback.
};

return (
<div>
<div>
<button onClick={exportHtml}>Export HTML</button>
</div>

<button onClick={exportHtml} disabled={!ready}>
Export HTML
</button>
<EmailEditor ref={emailEditorRef} onReady={onReady} />
{html && <textarea aria-label="Exported HTML" readOnly value={html} />}
</div>
);
};

createRoot(document.getElementById('app')!).render(<App />);
```

This example requires React 18+ for `createRoot` and uses the automatic JSX transform. With React 16.8/17, use `ReactDOM.render`; also add `import React` when using the classic JSX transform. Use an element with `id="app"` in your HTML.

See the [example source](https://github.com/unlayer/react-email-editor/blob/master/demo/src/example/index.tsx) for a reference implementation.

### Next.js / React Server Components
Expand Down Expand Up @@ -145,9 +150,9 @@ Custom tools can help you add your own content blocks to the editor. Every appli

You can submit new language translations by creating a PR on this GitHub repo: https://github.com/unlayer/translations. Translations managed by [PhraseApp](https://phraseapp.com)

## Related packages
## Guides and related workflows

- [`@unlayer/react-elements`](https://github.com/unlayer/elements): write templates in code as React components (`Email`, `Row`, `Column`, `Heading`, `Paragraph`, `Button`, …) and render them to email HTML, responsive web pages or print-ready HTML for PDFs. `renderToJson()` synchronously returns design JSON you can pass to `loadDesign`, so templates written in code (by developers, coding agents, or an LLM at runtime) stay editable in this editor.
For React-authored email templates, including templates created by coding agents, use `@unlayer/react-elements`. Generate design JSON with `renderToJson()`, then pass it to React Email Editor’s `loadDesign()` for visual editing, saving, and HTML export. See the [complete example](https://github.com/unlayer/react-email-editor/blob/master/docs/react-elements.md).

### License

Expand Down
3 changes: 2 additions & 1 deletion context7.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"Call editor methods through the ref (emailEditorRef.current?.editor), e.g. loadDesign, saveDesign and exportHtml, and only after onReady has fired.",
"Store the design JSON from saveDesign() or exportHtml() to reopen a design later with loadDesign(); send the exported html.",
"Pass Unlayer configuration through the options prop (for example projectId, displayMode, appearance, mergeTags).",
"For templates written in code (by developers, coding agents, or an LLM), generate design JSON with renderToJson() from @unlayer/react-elements and pass it to loadDesign()."
"Use renderToJson() from @unlayer/react-elements; loadDesign() in onReady; wait for design:loaded; exportHtml(). Guide: https://github.com/unlayer/react-email-editor/blob/master/docs/react-elements.md",
"The combined Elements/editor example requires React 18+. Persist edited design JSON to reopen it; regenerating the original React template replaces edits. JSON does not regenerate JSX."
]
}
40 changes: 40 additions & 0 deletions demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# React Email Editor demo

Run from the repository root with Node.js 22.12+:

```bash
npm ci
npm --prefix demo ci
npm --prefix demo run dev
```

Open <http://localhost:3000> for the basic editor. The editor is hosted by Unlayer and requires internet access. The basic demo loads its sample after `onReady`; its save/export actions log results to the console.

## Elements to visual editor

Open <http://localhost:3000/elements>, or follow **Try Elements** from the basic demo.

The welcome email is generated with `@unlayer/react-elements` and loaded into `react-email-editor`. Edit a block, click **Save and export**, inspect/copy/download the HTML or JSON, and reload to reopen your saved design. Expand **View the React template** to see the source. Only this browser's local storage is used for saving; no backend or email delivery is provided.

| File | Purpose |
| -------------------------- | ---------------------------------------------------------------------- |
| `src/elements/welcome.tsx` | React template and synchronous `renderToJson()` call |
| `src/elements/index.tsx` | Editor readiness, saved-design loading, export UI, and template source |
| `src/elements/storage.ts` | Versioned demo storage and basic saved-data checks |
| `test/elements.test.tsx` | Readiness, export, reopening edits, and storage failures |

Elements is a demo dependency, not a dependency of the editor wrapper. This route imports the built local `react-email-editor` package through `file:..`; run the root `npm start` watcher alongside the demo server when editing wrapper code (see [contributing](../CONTRIBUTING.md#demo-development-server)). All demo routes use the same local package.

Read the [complete integration guide](../docs/react-elements.md) for installing the two packages in your own React app and handling persistence in production.

## Checks

From the repository root, after installation and the root build:

```bash
npm --prefix demo run build
npm --prefix demo test
npm --prefix demo run test:docs
```

The demo tests use the real Elements renderer and a controlled hosted-editor boundary. They do not prove hosted-editor compatibility. The documentation check compiles complete TSX examples marked with `<!-- check-docs -->` in the README and integration guide. It uses strict TypeScript against the built wrapper and reports errors at their Markdown lines; unmarked explanatory fragments are ignored. A live smoke check should additionally edit a block, export it, reload, and verify the saved edit in the real editor.
10 changes: 10 additions & 0 deletions demo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { lazy, Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import { createGlobalStyle } from 'styled-components';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

import Example from './src/example';
import Dashboard from './src/dashboard';
const ElementsExample = lazy(() => import('./src/elements'));

const GlobalStyle = createGlobalStyle`
html, body {
Expand All @@ -25,6 +27,14 @@ const App = () => {
<Routes>
<Route path="/" element={<Example />} />
<Route path="/dashboard/*" element={<Dashboard />} />
<Route
path="/elements"
element={
<Suspense fallback={<p>Loading the Elements example…</p>}>
<ElementsExample />
</Suspense>
}
/>
</Routes>
</Router>
);
Expand Down
Loading
Loading