Creating a new Field in the administration panel

In this guide we will see how you can create a new Field for your administration panel.

Introduction

For this example, we will see how to change the WYSIWYG with CKEditorRegistering a new field in the admin panel - 图1 (opens new window) in the Content Manager plugin by creating a new plugin which will add a new Field in your application.

Setup

  1. Create a new project:
  1. # Create an application using SQLite and prevent the server from starting automatically as we will create a plugin
  2. # right after the project generation
  3. yarn create strapi-app my-app --quickstart --no-run
  1. # Create an application using SQLite and prevent the server from starting automatically as we will create a plugin
  2. # right after the project generation
  3. npx create-strapi-app my-app --quickstart --no-run
  1. Generate a plugin:
  1. cd my-app
  2. yarn strapi generate:plugin wysiwyg
  1. cd my-app
  2. npm run strapi generate:plugin wysiwyg
  1. cd my-app
  2. strapi generate:plugin wysiwyg
  1. Install the needed dependencies:
  1. cd plugins/wysiwyg
  2. yarn add @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic
  1. cd plugins/wysiwyg
  2. npm install @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic
  1. Start your application with the front-end development mode:
  1. # Go back to to strapi root folder
  2. cd ../..
  3. yarn develop --watch-admin
  1. # Go back to to strapi root folder
  2. cd ../..
  3. npm run develop -- --watch-admin
  1. # Go back to to strapi root folder
  2. cd ../..
  3. strapi develop --watch-admin

Once this step is over all we need to do is to create our new WYSIWYG which will replace the default one in the Content Manager plugin.

Creating the WYSIWYG

In this part we will create three components:

  • MediaLib which will be used to insert media in the editor
  • Wysiwyg which will wrap the CKEditor with a label and the errors
  • CKEditor which will be the implementation of the new WYSIWYG

Creating the MediaLib

Path — ./plugins/wysiwyg/admin/src/components/MediaLib/index.js

  1. import React, { useEffect, useState } from 'react';
  2. import { useStrapi, prefixFileUrlWithBackendUrl } from 'strapi-helper-plugin';
  3. import PropTypes from 'prop-types';
  4. const MediaLib = ({ isOpen, onChange, onToggle }) => {
  5. const {
  6. strapi: {
  7. componentApi: { getComponent },
  8. },
  9. } = useStrapi();
  10. const [data, setData] = useState(null);
  11. const [isDisplayed, setIsDisplayed] = useState(false);
  12. useEffect(() => {
  13. if (isOpen) {
  14. setIsDisplayed(true);
  15. }
  16. }, [isOpen]);
  17. const Component = getComponent('media-library').Component;
  18. const handleInputChange = data => {
  19. if (data) {
  20. const { url } = data;
  21. setData({ ...data, url: prefixFileUrlWithBackendUrl(url) });
  22. }
  23. };
  24. const handleClosed = () => {
  25. if (data) {
  26. onChange(data);
  27. }
  28. setData(null);
  29. setIsDisplayed(false);
  30. };
  31. if (Component && isDisplayed) {
  32. return (
  33. <Component
  34. allowedTypes={['images', 'videos', 'files']}
  35. isOpen={isOpen}
  36. multiple={false}
  37. noNavigation
  38. onClosed={handleClosed}
  39. onInputMediaChange={handleInputChange}
  40. onToggle={onToggle}
  41. />
  42. );
  43. }
  44. return null;
  45. };
  46. MediaLib.defaultProps = {
  47. isOpen: false,
  48. onChange: () => {},
  49. onToggle: () => {},
  50. };
  51. MediaLib.propTypes = {
  52. isOpen: PropTypes.bool,
  53. onChange: PropTypes.func,
  54. onToggle: PropTypes.func,
  55. };
  56. export default MediaLib;

Creating the WYSIWYG Wrapper

Path — ./plugins/wysiwyg/admin/src/components/Wysiwyg/index.js

  1. import React, { useState } from 'react';
  2. import PropTypes from 'prop-types';
  3. import { isEmpty } from 'lodash';
  4. import { Button } from '@buffetjs/core';
  5. import { Label, InputDescription, InputErrors } from 'strapi-helper-plugin';
  6. import Editor from '../CKEditor';
  7. import MediaLib from '../MediaLib';
  8. const Wysiwyg = ({
  9. inputDescription,
  10. errors,
  11. label,
  12. name,
  13. noErrorsDescription,
  14. onChange,
  15. value,
  16. }) => {
  17. const [isOpen, setIsOpen] = useState(false);
  18. let spacer = !isEmpty(inputDescription) ? <div style={{ height: '.4rem' }} /> : <div />;
  19. if (!noErrorsDescription && !isEmpty(errors)) {
  20. spacer = <div />;
  21. }
  22. const handleChange = data => {
  23. if (data.mime.includes('image')) {
  24. const imgTag = `<p><img src="${data.url}" caption="${data.caption}" alt="${data.alternativeText}"></img></p>`;
  25. const newValue = value ? `${value}${imgTag}` : imgTag;
  26. onChange({ target: { name, value: newValue } });
  27. }
  28. // Handle videos and other type of files by adding some code
  29. };
  30. const handleToggle = () => setIsOpen(prev => !prev);
  31. return (
  32. <div
  33. style={{
  34. marginBottom: '1.6rem',
  35. fontSize: '1.3rem',
  36. fontFamily: 'Lato',
  37. }}
  38. >
  39. <Label htmlFor={name} message={label} style={{ marginBottom: 10 }} />
  40. <div>
  41. <Button color="primary" onClick={handleToggle}>
  42. MediaLib
  43. </Button>
  44. </div>
  45. <Editor name={name} onChange={onChange} value={value} />
  46. <InputDescription
  47. message={inputDescription}
  48. style={!isEmpty(inputDescription) ? { marginTop: '1.4rem' } : {}}
  49. />
  50. <InputErrors errors={(!noErrorsDescription && errors) || []} name={name} />
  51. {spacer}
  52. <MediaLib onToggle={handleToggle} isOpen={isOpen} onChange={handleChange} />
  53. </div>
  54. );
  55. };
  56. Wysiwyg.defaultProps = {
  57. errors: [],
  58. inputDescription: null,
  59. label: '',
  60. noErrorsDescription: false,
  61. value: '',
  62. };
  63. Wysiwyg.propTypes = {
  64. errors: PropTypes.array,
  65. inputDescription: PropTypes.oneOfType([
  66. PropTypes.string,
  67. PropTypes.func,
  68. PropTypes.shape({
  69. id: PropTypes.string,
  70. params: PropTypes.object,
  71. }),
  72. ]),
  73. label: PropTypes.oneOfType([
  74. PropTypes.string,
  75. PropTypes.func,
  76. PropTypes.shape({
  77. id: PropTypes.string,
  78. params: PropTypes.object,
  79. }),
  80. ]),
  81. name: PropTypes.string.isRequired,
  82. noErrorsDescription: PropTypes.bool,
  83. onChange: PropTypes.func.isRequired,
  84. value: PropTypes.string,
  85. };
  86. export default Wysiwyg;

Implementing CKEditor

Path — ./plugins/wysiwyg/admin/src/components/CKEditor/index.js

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import { CKEditor } from '@ckeditor/ckeditor5-react';
  4. import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
  5. import styled from 'styled-components';
  6. const Wrapper = styled.div`
  7. .ck-editor__main {
  8. min-height: 200px;
  9. > div {
  10. min-height: 200px;
  11. }
  12. }
  13. `;
  14. const configuration = {
  15. toolbar: [
  16. 'heading',
  17. '|',
  18. 'bold',
  19. 'italic',
  20. 'link',
  21. 'bulletedList',
  22. 'numberedList',
  23. '|',
  24. 'indent',
  25. 'outdent',
  26. '|',
  27. 'blockQuote',
  28. 'insertTable',
  29. 'mediaEmbed',
  30. 'undo',
  31. 'redo',
  32. ],
  33. };
  34. const Editor = ({ onChange, name, value }) => {
  35. return (
  36. <Wrapper>
  37. <CKEditor
  38. editor={ClassicEditor}
  39. config={configuration}
  40. data={value}
  41. onChange={(event, editor) => {
  42. const data = editor.getData();
  43. onChange({ target: { name, value: data } });
  44. }}
  45. />
  46. </Wrapper>
  47. );
  48. };
  49. Editor.propTypes = {
  50. onChange: PropTypes.func.isRequired,
  51. name: PropTypes.string.isRequired,
  52. value: PropTypes.string,
  53. };
  54. export default Editor;

At this point we have simply created a new plugin which is mounted in our project but our custom Field has not been registered yet.

Registering a our new Field

Since the goal of our plugin is to override the current WYSIWYG we don’t want it to be displayed in the administration panel but we need it to register our new Field. In order to do so, we will simply modify the front-end entry point of our plugin.

This file is already present. Please replace the content of this file wit the following:

Path — ./plugins/wysiwyg/admin/src/index.js

  1. import pluginPkg from '../../package.json';
  2. import Wysiwyg from './components/Wysiwyg';
  3. import pluginId from './pluginId';
  4. export default strapi => {
  5. const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
  6. const plugin = {
  7. blockerComponent: null,
  8. blockerComponentProps: {},
  9. description: pluginDescription,
  10. icon: pluginPkg.strapi.icon,
  11. id: pluginId,
  12. initializer: () => null,
  13. injectedComponents: [],
  14. isReady: true,
  15. isRequired: pluginPkg.strapi.required || false,
  16. mainComponent: null,
  17. name: pluginPkg.strapi.name,
  18. preventComponentRendering: false,
  19. settings: null,
  20. trads: {},
  21. };
  22. strapi.registerField({ type: 'wysiwyg', Component: Wysiwyg });
  23. return strapi.registerPlugin(plugin);
  24. };

Finally you will have to rebuild strapi so the new plugin is loaded correctly

  1. yarn build
  1. npm run build
  1. strapi build

TIP

If the plugin still doesn’t show up, you should probably empty the .cache folder too.

And VOILA, if you create a new collectionType or a singleType with a richtext field you will see the implementation of CKEditorRegistering a new field in the admin panel - 图2 (opens new window) instead of the default WYSIWYG.