Replacing Strapi's Default WYSIWYG Editor with TinyMCE Editor
Tags: tinymce, wysiwyg, strapi, javascript
Published: 2022-09-27 07:54:35

[Strapi is an open-source headless CMS](https://strapi.io) that gives developers the freedom to choose their favorite tools and frameworks while also allowing editors to manage and distribute their content using their application's admin panel.

In this guide, you will learn how to replace the default WYSIWYG editor (Draftjs) in Strapi with the TinyMCE editor.

## Prerequisites
To follow through this article, you need to the following:
1. Basic knowledge of JavaScript,
2. Basic knowledge of ReactJS,
3. Basic understanding of Strapi [(get started here)](https://strapi.io/documentation/developer-docs/latest/getting-started/introduction.html),
4. A Tiny account  [(Sign up here)](https://www.tiny.cloud/), and
5. Node.js [(Download and install here.)](https://nodejs.org/en/)


## Introduction

This tutorial is heavily based on [this guide](https://strapi.io/documentation/developer-docs/latest/guides/registering-a-field-in-admin.html#setup) from the Strapi documentation. The idea here is to create a new field that will be modified to use TinyMCE as its editor, but before we start, there are a few things that we should know:

- TinyMCE is **NOT** a Markdown editor, it's an HTML editor.

    This means that the value taken from the field could contain HTML tags like `<p>Text</p>`, `<img src="..." />` and even `<table>...</table>`. Therefore you should be aware of the potential [security issues](https://www.tiny.cloud/docs-3x/extras/TinyMCE3x@Security/) and how to overcome them.

- For TinyMCE to work, you will need to obtain an API Key by creating an account at [Tinymce](https://www.tiny.cloud/) (the core editor is free).
- If you are new to Strapi, make sure to take a look at this [Quick Start Guide](https://strapi.io/documentation/developer-docs/latest/getting-started/quick-start.html).

## Project Setup
Now that we are ready, let's dive into our project.

### 1. Create a New Project

First, we will create a new project. I will call it `my-app` you can call it whatever you like. The `--quickstart` option will tell Strapi to create a basic project with default configurations and without templates. This is just to make the process easier and avoid any complications.

```bash
    yarn create strapi-app my-new-strapi-app --quickstart
    #or
    npx create-strapi-app my-new-strapi-app --quickstart
```
After running the command, a new browser tab will open for you to create a new administrator account. If it didn't, head to [http://localhost:1337/admin](http://localhost:1337/admin) and fill in all the necessary information.

```bash
    yarn create strapi-app my-app --quickstart
    #or
    npx create-strapi-app@latest my-strapi-app --quickstart
```
After running the command, a new browser tab will open for you to create a new administrator account. If it didn't, head to [http://localhost:1337/admin](http://localhost:1337/admin) and fill in all the necessary information.

![](https://paper-attachments.dropbox.com/s_9373F52E3548C5F2DAEA5949620C9D8EA1D5A2C70F5F43C0617CE96CFA3F185C_1659007048140_screencapture-localhost-1337-admin-auth-register-admin-2022-07-28-12_16_10.png)

### 2. Generate a Plugin
Now, we want to generate a new [Strapi plugin](https://strapi.io/documentation/developer-docs/latest/development/plugin-customization.html), but let's first stop Strapi by pressing `Ctrl+C` or `Command+C` and `cd` into the project directory. Make sure to replace "my-app" with your project name

```bash
    cd my-new-strapi-app
```
We will call our plugin `wysiwyg` so we should run:
```bash
    yarn strapi generate
```
Choose "plugin" from the list, press Enter and name the plugin `wysiwyg`.

Enable the plugin by adding the code below to the [plugins configurations](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/plugins.md) file:

```js
    // path: ./config/plugins.js
    
    module.exports = {
      wysiwyg: {
        enabled: true,
        resolve: "./src/plugins/wysiwyg", // path to plugin folder
      },
    };
```

### 3. Install the Necessary Dependencies
To use TinyMCE, we will need to install its library, and because Strapi is using React, we will install the TinyMCE library for React `@tinymce/tinymce-react`. But first, let's `cd` into the newly created plugin and only then install it there:
```bash
    cd src/plugins/wysiwyg
```
And then,

```bash
    yarn add @tinymce/tinymce-react
        #or
    npm install @tinymce/tinymce-react
```

Start the application with the front-end development mode:
```bash
    # Go back to the application root folder
    cd ../../..
    yarn develop --watch-admin
```
**Note**: Launching the Strapi server in watch mode without creating a user account first will open `localhost:1337` with a JSON format error. Creating a user on `localhost:8081` prevents this alert.

![](https://paper-attachments.dropbox.com/s_9373F52E3548C5F2DAEA5949620C9D8EA1D5A2C70F5F43C0617CE96CFA3F185C_1659008501193_Screenshot+2022-07-28+at+12.41.11.png)

We now need to create our new WYSIWYG, which will replace the default one in the Content Manager.


### 4. Create the Plugin
In step 2, we generated the necessary files for any plugin. Now, we need to make it ours by creating a few files to tell Strapi what to do with this plugin. So first, we will create the necessary directories and files (React Components), then we will write into them.

To create the directories and files (make sure you are inside the plugin directory (`.../src/plugins/wysiwyg`):

```bash
    cd admin/src/components
    
    #The following will create .../MediaLib/index.js
    mkdir -p MediaLib && cd MediaLib && touch index.js
    
    #The following will create .../Wysiwyg/index.js
    cd .. && mkdir -p Wysiwyg && cd Wysiwyg && touch index.js
    
    #The following will create .../Tinymce/index.js
    cd .. && mkdir -p Tinymce && cd Tinymce && touch index.js
```

#### MediaLib/index.js

This file will handle the insertion of media i.e. insert media (images, video...etc) to TinyMCE editor. It's important to notice here that we are using Strapi Media Library to handle the media instead of letting TinyMCE handle it, and that's perfect because we don't want to let the user (The person who is using the Editor) insert media from somewhere else, so make sure **NOT** to allow such insertion in TinyMCE settings (More on that later).

Now using your favorite editor (I am using VS Code), open the file `code ./components/MediaLib/index.js`, paste the following code and save:

```js
    // ./src/plugins/wysiwyg/admin/src/components/MediaLib/index.js
    
    import React from 'react';
    import { prefixFileUrlWithBackendUrl, useLibrary } from '@strapi/helper-plugin';
    import PropTypes from 'prop-types';
    const MediaLib = ({ isOpen, onChange, onToggle }) => {
        const { components } = useLibrary();
        const MediaLibraryDialog = components['media-library'];
        const handleSelectAssets = (files) => {
            const formattedFiles = files.map((f) => ({
                alt: f.alternativeText || f.name,
                url: prefixFileUrlWithBackendUrl(f.url),
                mime: f.mime,
            }));
            onChange(formattedFiles);
        };
        if (!isOpen) {
            return null;
        }
        return (
            <MediaLibraryDialog
                onClose={onToggle}
                onSelectAssets={handleSelectAssets}
            />
        );
    };
    MediaLib.defaultProps = {
        isOpen: false,
        onChange: () => {
        },
        onToggle: () => {
        },
    };
    MediaLib.propTypes = {
        isOpen: PropTypes.bool,
        onChange: PropTypes.func,
        onToggle: PropTypes.func,
    };
    export default MediaLib;
```

#### Wysiwyg/index.js

This file will be the wrapper of TinyMCE editor, it will display the labels and handle the error messages as well as inserting media. An important thing to notice here is that this code is only handling images. Further steps are required to handle videos and other media.

Again, using your favorite editor, open the file `code ./components/Wysiwyg/index.js` and paste the following code:

> **Note:** If you get `file not found` error around the `import TinyEditor...` Ignore it for now as we will create it in the next step.

```js
    // ./src/plugins/wysiwyg/admin/src/components/Wysiwyg/index.js
    
    import React, { useState } from 'react';
    import PropTypes from 'prop-types';
    import { Stack } from '@strapi/design-system/Stack';
    import { Box } from '@strapi/design-system/Box';
    import { Button } from '@strapi/design-system/Button';
    import { Typography } from '@strapi/design-system/Typography';
    import Landscape from '@strapi/icons/Landscape';
    import MediaLib from '../MediaLib';
    import Tinymce from '../Tinymce';
    import { useIntl } from 'react-intl';
    const Wysiwyg = ({
        name,
        onChange,
        value,
        intlLabel,
        disabled,
        error,
        description,
        required,
    }) => {
        const { formatMessage } = useIntl();
        const [mediaLibVisible, setMediaLibVisible] = useState(false);
        const handleToggleMediaLib = () => setMediaLibVisible((prev) => !prev);
        const handleChangeAssets = (assets) => {
            let newValue = value ? value : '';
            assets.map((asset) => {
                if (asset.mime.includes('image')) {
                    const imgTag = `<p><img src="${asset.url}" alt="${asset.alt}"></img></p>`;
                    newValue = `${newValue}${imgTag}`;
                }
                // Handle videos and other type of files by adding some code
            });
            onChange({ target: { name, value: newValue } });
            handleToggleMediaLib();
        };
        return (
            <>
                <Stack size={1}>
                    <Box>
                        <Typography variant="pi" fontWeight="bold">
                            {formatMessage(intlLabel)}
                        </Typography>
                        {required && (
                            <Typography variant="pi" fontWeight="bold" textColor="danger600">
                                *
                            </Typography>
                        )}
                    </Box>
                    <Button
                        startIcon={<Landscape />}
                        variant="secondary"
                        fullWidth
                        onClick={handleToggleMediaLib}
                    >
                        Media library
                    </Button>
                    <Tinymce
                        disabled={disabled}
                        name={name}
                        onChange={onChange}
                        value={value}
                    />
                    {error && (
                        <Typography variant="pi" textColor="danger600">
                            {formatMessage({ id: error, defaultMessage: error })}
                        </Typography>
                    )}
                    {description && (
                        <Typography variant="pi">{formatMessage(description)}</Typography>
                    )}
                </Stack>
                <MediaLib
                    isOpen={mediaLibVisible}
                    onChange={handleChangeAssets}
                    onToggle={handleToggleMediaLib}
                />
            </>
        );
    };
    Wysiwyg.defaultProps = {
        description: '',
        disabled: false,
        error: undefined,
        intlLabel: '',
        required: false,
        value: '',
    };
    Wysiwyg.propTypes = {
        description: PropTypes.shape({
            id: PropTypes.string,
            defaultMessage: PropTypes.string,
        }),
        disabled: PropTypes.bool,
        error: PropTypes.string,
        intlLabel: PropTypes.shape({
            id: PropTypes.string,
            defaultMessage: PropTypes.string,
        }),
        name: PropTypes.string.isRequired,
        onChange: PropTypes.func.isRequired,
        required: PropTypes.bool,
        value: PropTypes.string,
    };
    export default Wysiwyg;
```  

#### Tinymce/index.js
This is where all the work is done, it's the file that will implement the editor
**Note:** mark this file as we will revisit it to configure TinyMCE.
One more time, using your favorite editor, open the file `nano ./components/Tinymce/index.js` and paste the following code:

> **Note:** Make sure to replace `API_KEY` with the actual key you obtained from TinyMCE.

```js
        import React from "react";
        import PropTypes from "prop-types";
        import { Editor } from "@tinymce/tinymce-react";
        const TinyEditor = ({ onChange, name, value }) => {
          return (
            <Editor
              apiKey="API KEY"
              value={value}
              tagName={name}
              onEditorChange={(editorContent) => {
                onChange({ target: { name, value: editorContent } });
              }}
              outputFormat="text"
              init={{}}
            />
          );
        };
        TinyEditor.propTypes = {
          onChange: PropTypes.func.isRequired,
          name: PropTypes.string.isRequired,
          value: PropTypes.string,
        };
        export default TinyEditor;
```

### 5. Register the Field & Plugin
Our plugin is ready and waiting, but Strapi doesn't know about it yet. Hence, we need to register it with Strapi and give some information about it. To do so, we will edit one last file (The file is already there, we will just change the code inside it).

Again, using your favorite editor, open the file:
> **Note:** Make sure you are still inside the plugin folder 

```js

`...src/plugins/wysiwyg/admin/src/index.js`


    // src/plugins/wysiwyg/admin/src/index.js
    
    import { prefixPluginTranslations } from '@strapi/helper-plugin';
    import pluginPkg from '../../package.json';
    import pluginId from './pluginId';
    import Initializer from './components/Initializer';
    import PluginIcon from './components/PluginIcon';
    import Wysiwyg from "./components/Wysiwyg"
    const name = pluginPkg.strapi.name;
    export default {
      register(app) {
        app.addMenuLink({
          to: `/plugins/${pluginId}`,
          icon: PluginIcon,
          intlLabel: {
            id: `${pluginId}.plugin.name`,
            defaultMessage: name,
          },
          Component: async () => {
            const component = await import(/* webpackChunkName: "[request]" */ './pages/App');
            return component;
          },
        });
        app.addFields({ type: "wysiwyg", Component: Wysiwyg });
        app.registerPlugin({
          id: pluginId,
          initializer: Initializer,
          isReady: false,
          name,
        });
      },
      bootstrap(app) { },
      async registerTrads({ locales }) {
        const importedTrads = await Promise.all(
          locales.map(locale => {
            return import(
              /* webpackChunkName: "translation-[request]" */ `./translations/${locale}.json`
            )
              .then(({ default: data }) => {
                return {
                  data: prefixPluginTranslations(data, pluginId),
                  locale,
                };
              })
              .catch(() => {
                return {
                  data: {},
                  locale,
                };
              });
          })
        );
        return Promise.resolve(importedTrads);
      },
    };
```    

#### 6. Run Strapi

Let's get back to the project folder:
```bash
cd ../../../../
        
    # After running this command I will be at .../my-new-strapi-app
    # Make sure you are in .../<your-project-name>
```

Finally, Start Strapi with the front-end development mode `--watch-admin`:

```bash
        yarn develop --watch-admin
        #or
        npm run develop -- --watch-admin
        #or
        strapi develop --watch-admin
```

When you run the last command, it will open a new tab in the browser (if it didn't, head to localhost:8000/admin) and log in with the administrator account you created earlier.

From the menu on the left go to `Content-Types Builder` so we can create new content for testing. Choose `Create new single type`.

![](https://paper-attachments.dropbox.com/s_9373F52E3548C5F2DAEA5949620C9D8EA1D5A2C70F5F43C0617CE96CFA3F185C_1659015616514_Screenshot+2022-07-28+at+14.35.40.png)

Enter a display name like `episode`.

![](https://paper-attachments.dropbox.com/s_9373F52E3548C5F2DAEA5949620C9D8EA1D5A2C70F5F43C0617CE96CFA3F185C_1659015648478_Screenshot+2022-07-28+at+14.35.58.png)

Choose Rich Text. Give it a name like `Description` and hit `Finish`.

![](https://paper-attachments.dropbox.com/s_9373F52E3548C5F2DAEA5949620C9D8EA1D5A2C70F5F43C0617CE96CFA3F185C_1659015791434_Screenshot+2022-07-28+at+14.36.19.png)

From the top right corner, hit `Save`, and wait for the server to restart In the left menu, you will find the newly created content `episode`, press it to edit it. And hop!, there you go, TinyMCE is working! Yaaay 😍.

![](https://paper-attachments.dropbox.com/s_9373F52E3548C5F2DAEA5949620C9D8EA1D5A2C70F5F43C0617CE96CFA3F185C_1659015850355_Screenshot+2022-07-28+at+14.37.02.png)

Hmm 😕 , something isn't quite right yet! You are probably not able to insert a new line or do pretty much anything useful. Don’t stop Strapi just yet! Since we started Strapi with `--watch-admin` mode, we don’t need to stop it, and we will still be able to see the changes we will make as we are doing them (Cool ha? 😎).

### 7. Configure TinyMCE Editor
Remember the file we marked? In that file, we need to configure TinyMCE to work for us as we expect it to do. we need to tell TinyMCE **three** important things.

From the project directory, open the file using your favorite editor `code plugins/wysiwyg/admin/src/components/Tinymce/index.js`.

And do the following changes:

- outputFormat:

To make full use of TinyMCE, we will tell it to deal with the input as an HTML and give the output as an HTML too,

Change: `outputFormat='text'` To: `outputFormat='html'`

- selector:

inside `init={{}}` add: `selector: 'textarea',`
this is to tell Strapi that we are using `<textarea></textarea>` tags for input.

- plugins & toolbar:

This is where all the fun is. again, inside `init={{}}` and after the previously added `selector`, add two things:

- `plugins: '',` Here we will add all the features and functionalities that we want TinyMCE to have.
- `toolbar: '',` It's also for adding features, but those who are added here will appear directly in the top toolbar of TinyMCE, while the ones we added earlier will appear in a drop-down menu.

> **Note:** Add all the plugins you want between the single quotes `' HERE '` and separate them with single spaces, [A full list can be found here](https://www.tiny.cloud/docs/plugins/opensource/), Remember not to add any plugin that allows users to upload the media directly to the editor.

When you are done picking from the TinyMCE plugins, the final version of the file will look something like this:
`#PATH: <your-projectname>/src/plugins/wysiwyg/admin/src/components/Tinymce/index.js`

```js
    // /src/plugins/wysiwyg/admin/src/components/Tinymce/index.js
    
    import React, { useRef } from 'react';
    import { Editor } from '@tinymce/tinymce-react';
    import PropTypes from 'prop-types';
    const TinyEditor = ({ onChange, name, value, disabled }) => {
        const onChangeRef = useRef(onChange);
        function onBlur(ev, editor) {
            const content = editor.getContent();
            onChangeRef.current({ target: { name, value: content, type: 'wysiwyg' } });
        }
        return (
            <>
                <Editor
                    apiKey={process.env.TINY_API}
                    value={value}
                    tagName={name}
                    onEditorChange={(editorContent) => {
                        onChange({ target: { name, value: editorContent } });
                    }}
                    outputFormat='html'
                    init={{
                        selector: 'textarea',
                        plugins: 'fullscreen insertdatetime .... MORE PLUGINS',
                        toolbar: 'code numlist bullist .... MORE PLUGINS',
                    }}
                />
            </>
        );
    }
    TinyEditor.defaultProps = {
        value: '',
    };
    TinyEditor.propTypes = {
        onChange: PropTypes.func.isRequired,
        name: PropTypes.string.isRequired,
        value: PropTypes.string,
    };
    export default TinyEditor;
```

Because Strapi is still running, we can add some plugins and try it out, then add some more and so on… and when we are all set and ready to see it in action, we can now stop Strapi and start it fresh again. Press `Ctrl+C` or `Command+C` to stop Strapi.

Head back to the root directory

    cd ../../..

Now Let’s run it **without** `--watch-admin`, but after we build it clean:
```bash
        yarn build
        yarn develop
        #OR
        npm run build
        npm run develop
        #OR
        strapi build
        strapi develop
```

After running the commands, a new browser tab should open. If it didn't, head to [localhost:1337/admin](http://localhost:1337/admin). Now go back to our `episode` and give it another try, everything should be working fine 😆.

![](https://paper-attachments.dropbox.com/s_9373F52E3548C5F2DAEA5949620C9D8EA1D5A2C70F5F43C0617CE96CFA3F185C_1659017180590_screencapture-localhost-1337-admin-auth-register-admin-2022-07-28-12_16_10.png)

## Conclusion

Congratulations! 🥳🥳 Now, we have a special Strapi field that is using TinyMCE as its editor. This will open the creativity doors for your users. It’s important to spend some time making sure that you only get the plugins that you need from TinyMCE. You should also know that even if you disabled some plugins from the editor, users will still be able to copy-paste some “Formatted Text” from other places (Formatted text is a cooler name for “text with Style appended to it (CSS and possibly JavaScript in our case)”). Enjoy the Intelligence and Power of Strapi combined with the flexibility of TinyMCE.