In this tutorial we will see how you can create a new field for the admin panel.
For this example, we will replace the default WYSIWYG with CKEditor in the Content Manager by creating a new plugin that will add a new field in your application.
yarn create strapi-app my-app --quickstart --no-run
# or
npx create-strapi-app@latest my-app --quickstart --no-run
The --no-run
flag was added as we will run additional commands to create a plugin right after the project generation.
cd my-app
yarn strapi generate
Choose "plugin" from the list, press Enter and name the plugin wysiwyg
.
1// path: ./config/plugins.js
2
3module.exports = {
4 // ...
5 wysiwyg: {
6 enabled: true,
7 resolve: "./src/plugins/wysiwyg", // path to plugin folder
8 },
9 // ...
10};
cd src/plugins/wysiwyg
yarn add @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic
# 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.
We now need to create our new WYSIWYG, which will replace the default one in the Content Manager.
In this part we will create 3 components:
MediaLib
component used to insert media in the editorEditor
component that uses CKEditor as the WYSIWYG editorWysiwyg
component to wrap the CKEditorThe following code examples can be used to implement the logic for the 3 components:
Example of a MediaLib component used to insert media in the editor:
1// path: ./src/plugins/wysiwyg/admin/src/components/MediaLib/index.js
2
3import React from "react";
4import { prefixFileUrlWithBackendUrl, useLibrary } from "@strapi/helper-plugin";
5import PropTypes from "prop-types";
6
7const MediaLib = ({ isOpen, onChange, onToggle }) => {
8 const { components } = useLibrary();
9 const MediaLibraryDialog = components["media-library"];
10
11 const handleSelectAssets = (files) => {
12 const formattedFiles = files.map((f) => ({
13 alt: f.alternativeText || f.name,
14 url: prefixFileUrlWithBackendUrl(f.url),
15 mime: f.mime,
16 }));
17
18 onChange(formattedFiles);
19 };
20
21 if (!isOpen) {
22 return null;
23 }
24
25 return (
26 <MediaLibraryDialog
27 onClose={onToggle}
28 onSelectAssets={handleSelectAssets}
29 />
30 );
31};
32
33MediaLib.defaultProps = {
34 isOpen: false,
35 onChange: () => {},
36 onToggle: () => {},
37};
38
39MediaLib.propTypes = {
40 isOpen: PropTypes.bool,
41 onChange: PropTypes.func,
42 onToggle: PropTypes.func,
43};
44
45export default MediaLib;
Example of an Editor component using CKEditor as the WYSIWYG editor:
1// path: ./src/plugins/wysiwyg/admin/src/components/Editor/index.js
2
3import React from "react";
4import PropTypes from "prop-types";
5import styled from "styled-components";
6import { CKEditor } from "@ckeditor/ckeditor5-react";
7import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
8import { Box } from "@strapi/design-system/Box";
9
10const Wrapper = styled(Box)`
11 .ck-editor__main {
12 min-height: ${200 / 16}em;
13 > div {
14 min-height: ${200 / 16}em;
15 }
16 // Since Strapi resets css styles, it can be configured here (h2, h3, strong, i, ...)
17 }
18`;
19
20const configuration = {
21 toolbar: [
22 "heading",
23 "|",
24 "bold",
25 "italic",
26 "link",
27 "bulletedList",
28 "numberedList",
29 "|",
30 "indent",
31 "outdent",
32 "|",
33 "blockQuote",
34 "insertTable",
35 "mediaEmbed",
36 "undo",
37 "redo",
38 ],
39};
40
41const Editor = ({ onChange, name, value, disabled }) => {
42 return (
43 <Wrapper>
44 <CKEditor
45 editor={ClassicEditor}
46 disabled={disabled}
47 config={configuration}
48 data={value || ""}
49 onReady={(editor) => editor.setData(value || "")}
50 onChange={(event, editor) => {
51 const data = editor.getData();
52 onChange({ target: { name, value: data } });
53 }}
54 />
55 </Wrapper>
56 );
57};
58
59Editor.defaultProps = {
60 value: "",
61 disabled: false,
62};
63
64Editor.propTypes = {
65 onChange: PropTypes.func.isRequired,
66 name: PropTypes.string.isRequired,
67 value: PropTypes.string,
68 disabled: PropTypes.bool,
69};
70
71export default Editor;
Example of a Wysiwyg component wrapping CKEditor:
1// path: ./src/plugins/wysiwyg/admin/src/components/Wysiwyg/index.js
2
3import React, { useState } from "react";
4import PropTypes from "prop-types";
5import { Stack } from "@strapi/design-system/Stack";
6import { Box } from "@strapi/design-system/Box";
7import { Button } from "@strapi/design-system/Button";
8import { Typography } from "@strapi/design-system/Typography";
9import Landscape from "@strapi/icons/Landscape";
10import MediaLib from "../MediaLib";
11import Editor from "../Editor";
12import { useIntl } from "react-intl";
13
14const Wysiwyg = ({
15 name,
16 onChange,
17 value,
18 intlLabel,
19 disabled,
20 error,
21 description,
22 required,
23}) => {
24 const { formatMessage } = useIntl();
25 const [mediaLibVisible, setMediaLibVisible] = useState(false);
26
27 const handleToggleMediaLib = () => setMediaLibVisible((prev) => !prev);
28
29 const handleChangeAssets = (assets) => {
30 let newValue = value ? value : "";
31
32 assets.map((asset) => {
33 if (asset.mime.includes("image")) {
34 const imgTag = `<p><img src="${asset.url}" alt="${asset.alt}"></img></p>`;
35
36 newValue = `${newValue}${imgTag}`;
37 }
38
39 // Handle videos and other type of files by adding some code
40 });
41
42 onChange({ target: { name, value: newValue } });
43 handleToggleMediaLib();
44 };
45
46 return (
47 <>
48 <Stack size={1}>
49 <Box>
50 <Typography variant="pi" fontWeight="bold">
51 {formatMessage(intlLabel)}
52 </Typography>
53 {required && (
54 <Typography variant="pi" fontWeight="bold" textColor="danger600">
55 *
56 </Typography>
57 )}
58 </Box>
59 <Button
60 startIcon={<Landscape />}
61 variant="secondary"
62 fullWidth
63 onClick={handleToggleMediaLib}
64 >
65 Media library
66 </Button>
67 <Editor
68 disabled={disabled}
69 name={name}
70 onChange={onChange}
71 value={value}
72 />
73 {error && (
74 <Typography variant="pi" textColor="danger600">
75 {formatMessage({ id: error, defaultMessage: error })}
76 </Typography>
77 )}
78 {description && (
79 <Typography variant="pi">{formatMessage(description)}</Typography>
80 )}
81 </Stack>
82 <MediaLib
83 isOpen={mediaLibVisible}
84 onChange={handleChangeAssets}
85 onToggle={handleToggleMediaLib}
86 />
87 </>
88 );
89};
90
91Wysiwyg.defaultProps = {
92 description: "",
93 disabled: false,
94 error: undefined,
95 intlLabel: "",
96 required: false,
97 value: "",
98};
99
100Wysiwyg.propTypes = {
101 description: PropTypes.shape({
102 id: PropTypes.string,
103 defaultMessage: PropTypes.string,
104 }),
105 disabled: PropTypes.bool,
106 error: PropTypes.string,
107 intlLabel: PropTypes.shape({
108 id: PropTypes.string,
109 defaultMessage: PropTypes.string,
110 }),
111 name: PropTypes.string.isRequired,
112 onChange: PropTypes.func.isRequired,
113 required: PropTypes.bool,
114 value: PropTypes.string,
115};
116
117export default Wysiwyg;
The last step is to register the wysiwyg
field with the Wysiwyg
component using addFields()
. Replace the content of the admin/src/index.js
field of the plugin with the following code:
1// path: ./src/plugins/wysiwyg/admin/src/index.js
2
3import pluginPkg from "../../package.json";
4import Wysiwyg from "./components/Wysiwyg";
5import pluginId from "./pluginId";
6
7const name = pluginPkg.strapi.name;
8
9export default {
10 register(app) {
11 app.addFields({ type: "wysiwyg", Component: Wysiwyg });
12
13 app.registerPlugin({
14 id: pluginId,
15 isReady: true,
16 name,
17 });
18 },
19 bootstrap() {},
20};
And voilà, if you create a new collection type or single type with a rich text field you will see the implementation of CKEditor instead of the default WYSIWYG: