Replacing Strapi's Default WYSIWYG Editor with TinyMCE Editor

Strapi - Sep 27 '22 - - Dev Community

Strapi is an open-source headless CMS 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),
  4. A Tiny account (Sign up here), and
  5. Node.js (Download and install here.)

Introduction

This tutorial is heavily based on this guide 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 and how to overcome them.

  • For TinyMCE to work, you will need to obtain an API Key by creating an account at Tinymce (the core editor is free).

  • If you are new to Strapi, make sure to take a look at this Quick Start Guide.

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.

    yarn create strapi-app my-new-strapi-app --quickstart
    #or
    npx create-strapi-app my-new-strapi-app --quickstart
Enter fullscreen mode Exit fullscreen mode

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 and fill in all the necessary information.

    yarn create strapi-app my-app --quickstart
    #or
    npx create-strapi-app@latest my-strapi-app --quickstart
Enter fullscreen mode Exit fullscreen mode

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 and fill in all the necessary information.

2. Generate a Plugin

Now, we want to generate a new Strapi plugin, 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

    cd my-new-strapi-app
Enter fullscreen mode Exit fullscreen mode

We will call our plugin wysiwyg so we should run:

    yarn strapi generate
Enter fullscreen mode Exit fullscreen mode

Choose "plugin" from the list, press Enter and name the plugin wysiwyg.

Enable the plugin by adding the code below to the plugins configurations file:

    // path: ./config/plugins.js

    module.exports = {
      wysiwyg: {
        enabled: true,
        resolve: "./src/plugins/wysiwyg", // path to plugin folder
      },
    };
Enter fullscreen mode Exit fullscreen mode

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:

    cd src/plugins/wysiwyg
Enter fullscreen mode Exit fullscreen mode

And then,

    yarn add @tinymce/tinymce-react
        #or
    npm install @tinymce/tinymce-react
Enter fullscreen mode Exit fullscreen mode

Start the application with the front-end development mode:

    # Go back to the application root folder
    cd ../../..
    yarn develop --watch-admin
Enter fullscreen mode Exit fullscreen mode

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.

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):

    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
Enter fullscreen mode Exit fullscreen mode

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:

    // ./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;
Enter fullscreen mode Exit fullscreen mode

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.

    // ./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;
Enter fullscreen mode Exit fullscreen mode

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.

        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;
Enter fullscreen mode Exit fullscreen mode

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


`...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);
      },
    };
Enter fullscreen mode Exit fullscreen mode

6. Run Strapi

Let's get back to the project folder:

cd ../../../../

    # After running this command I will be at .../my-new-strapi-app
    # Make sure you are in .../<your-project-name>
Enter fullscreen mode Exit fullscreen mode

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

        yarn develop --watch-admin
        #or
        npm run develop -- --watch-admin
        #or
        strapi develop --watch-admin
Enter fullscreen mode Exit fullscreen mode

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.

Enter a display name like episode.

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

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 😍.

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, 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

    // /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;
Enter fullscreen mode Exit fullscreen mode

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 ../../..
Enter fullscreen mode Exit fullscreen mode

Now Let’s run it without --watch-admin, but after we build it clean:

        yarn build
        yarn develop
        #OR
        npm run build
        npm run develop
        #OR
        strapi build
        strapi develop
Enter fullscreen mode Exit fullscreen mode

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

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .