In this guide, you will learn how to replace the default WYSIWYG editor (Draftjs) in Strapi with the TinyMCE editor.
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.
Now that we are ready, let's get our hands dirty.
Setup
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 to avoid any complications.
yarn create strapi-app my-app --quickstart
#or
npx create-strapi-app my-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 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-app
We will call our plugin wysiwyg
so we should run:
yarn strapi generate:plugin wysiwyg
#or
npm run strapi generate:plugin wysiwyg
#or
strapi generate:plugin wysiwyg
3. Install the needed dependencies:
To be able 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 plugins/wysiwyg
And then,
yarn add @tinymce/tinymce-react
#or
npm install @tinymce/tinymce-react
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 (.../<your app name>/plugins/wysiwyg
):
cd admin/src/
#The following will create .../MediaLib/index.js
mkdir -p components/MediaLib/; touch components/MediaLib/index.js
#The following will create .../Wysiwyg/index.js
mkdir -p components/Wysiwyg/; touch components/Wysiwyg/index.js
#The following will create .../Tinymce/index.js
mkdir -p components/Tinymce/; touch components/Tinymce/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 nano
), open the file:
nano ./components/MediaLib/index.js
And paste the following code then save:
import React, { useEffect, useState } from "react";
import { useStrapi, prefixFileUrlWithBackendUrl } from "strapi-helper-plugin";
import PropTypes from "prop-types";
const MediaLib = ({ isOpen, onChange, onToggle }) => {
const {
strapi: {
componentApi: { getComponent },
},
} = useStrapi();
const [data, setData] = useState(null);
const [isDisplayed, setIsDisplayed] = useState(false);
const Component = getComponent("media-library").Component;
const handleInputChange = (data) => {
if (data) {
const { url } = data;
setData({ ...data, url: prefixFileUrlWithBackendUrl(url) });
}
};
const handleClosed = () => {
if (data) {
onChange(data);
}
setData(null);
setIsDisplayed(false);
};
useEffect(() => {
if (isOpen) {
setIsDisplayed(true);
}
}, [isOpen]);
if (Component && isDisplayed) {
return (
<Component
allowedTypes={["images", "videos", "files"]}
isOpen={isOpen}
multiple={false}
noNavigation
onClosed={handleClosed}
onInputMediaChange={handleInputChange}
onToggle={onToggle}
/>
);
}
return null;
};
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:
nano ./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.
import React, { useState } from "react";
import PropTypes from "prop-types";
import { isEmpty } from "lodash";
import { Button } from "@buffetjs/core";
import { Label, InputDescription, InputErrors } from "strapi-helper-plugin";
import MediaLib from "../MediaLib";
import TinyEditor from "../Tinymce";
const Wysiwyg = ({
inputDescription,
errors,
label,
name,
noErrorsDescription,
onChange,
value,
}) => {
const [isOpen, setIsOpen] = useState(false);
let spacer = !isEmpty(inputDescription) ? (
<div style={{ height: ".4rem" }} />
) : (
<div />
);
if (!noErrorsDescription && !isEmpty(errors)) {
spacer = <div />;
}
const handleChange = (data) => {
if (data.mime.includes("image")) {
const imgTag = `<p><img src="${data.url}" caption="${data.caption}" alt="${data.alternativeText}"></img></p>`;
const newValue = value ? `${value}${imgTag}` : imgTag;
onChange({ target: { name, value: newValue } });
}
// Handle videos and other type of files by adding some code
};
const handleToggle = () => setIsOpen((prev) => !prev);
return (
<div
style={{
marginBottom: "1.6rem",
fontSize: "1.3rem",
fontFamily: "Lato",
}}
>
<Label htmlFor={name} message={label} style={{ marginBottom: 10 }} />
<div style={{ position: "absolute", right: "15px", top: "-10px" }}>
<Button color="primary" onClick={handleToggle}>
MediaLib
</Button>
</div>
<TinyEditor name={name} onChange={onChange} value={value} />
<InputDescription
message={inputDescription}
style={!isEmpty(inputDescription) ? { marginTop: "1.4rem" } : {}}
/>
<InputErrors
errors={(!noErrorsDescription && errors) || []}
name={name}
/>
{spacer}
<MediaLib
onToggle={handleToggle}
isOpen={isOpen}
onChange={handleChange}
/>
</div>
);
};
Wysiwyg.defaultProps = {
errors: [],
inputDescription: null,
label: "",
noErrorsDescription: false,
value: "",
};
Wysiwyg.propTypes = {
errors: PropTypes.array,
inputDescription: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.shape({
id: PropTypes.string,
params: PropTypes.object,
}),
]),
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.shape({
id: PropTypes.string,
params: PropTypes.object,
}),
]),
name: PropTypes.string.isRequired,
noErrorsDescription: PropTypes.bool,
onChange: PropTypes.func.isRequired,
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 visit it again 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 that 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;
5. Register the field and the plugin:
Our plugin is ready and waiting, but Strapi doesn't know about it yet! So we need to register it with Strapi and give it 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).
Last time, using your favorite editor, open the file:
Note: Make sure you are still inside the plugin folder .../<your app name>/plugins/wysiwyg
nano index.js
Delete the existing code and add the following:
import pluginPkg from "../../package.json";
import pluginId from "./pluginId";
import Wysiwyg from "./components/Wysiwyg";
export default (strapi) => {
const pluginDescription =
pluginPkg.strapi.description || pluginPkg.description;
const icon = pluginPkg.strapi.icon;
const name = pluginPkg.strapi.name;
const plugin = {
blockerComponent: null,
blockerComponentProps: {},
description: pluginDescription,
icon,
id: pluginId,
injectedComponents: [],
isReady: true,
isRequired: pluginPkg.strapi.required || false,
mainComponent: null,
name,
preventComponentRendering: false,
trads: {},
};
strapi.registerField({ type: "wysiwyg", Component: Wysiwyg });
return strapi.registerPlugin(plugin);
};
6. Run Strapi:
That was boring, wasn't it? Now let's have fun and see some results! Let's run Strapi đ
- First, let's get back to the project folder:
cd ../../../../
# After running this command I will be at .../my-app
# Make sure you are in .../<your-project-name>
- Re-build Strapi from scratch:
yarn build --clean
#or
npm run build --clean
#or
strapi build --clean
- 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
Â
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 display name something like Tinymce Test
.
Â
Chose Rich Text.
Â
Give it a name like Test
and hit Finish
.
Â
From the top right corner hit Save
, and wait for the server to restart
Â
OK, the moment of truth. In the left menu, you will find the newly created content Tinymce Test
, 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 do as we are doing them (Cool ha? đ).
OK, Let's see what we can do about this.
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:
nano 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-project-name>/plugins/wysiwyg/admin/src/components/Tinymce/index.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='html'
init={{
selector: 'textarea',
plugins: 'fullscreen insertdatetime .... MORE PLUGINS',
toolbar: 'code numlist bullist .... MORE PLUGINS',
}}
/>
);
};
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.
Now Letâs run it without --watch-admin
, but after we build it clean:
yarn build --clean
yarn develop
#OR
npm run build --clean
npm run develop
#OR
strapi build --clean
strapi develop
After running the commands, a new browser tab should open. If it didn't, head to localhost:1337/admin.
Now go back to our Tinymce Test
and give it another try, everything should be working fine đ.
8. Final Words:
You did it! Congratulations đ„łđ„ł
Now we have a special Strapi field that is using TinyMCE as its editor. This will open the creativity doors for your users đ, but remember âWith Great Power Comes Greater Responsibilityâ.
We have talked about this earlier, but letâs emphasize it even more. 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)â). That means even if you think your users are trustworthy, their innocence might open the door for nightmares, XSS to name one.
But hey! that shouldnât stop you from using TinyMCE. In fact, they claim to be âThe world's #1 JavaScript library for rich text editingâ, and indeed they have millions of users. So go ahead! Enjoy the Intelligence and Power of Strapi combined with the Flexibility of TinyMCE ⊠just be careful, OK? đ