Change the default WYSIWYG to Toast UI editor
Tags: webdev, headlesscms, wysiwyg
Published: 2021-01-18 15:23:24

Another new WYSIWYG editor that allows you to replace the default one on Strapi! 

Well this time, I would not need to explain in detail how to replace it by yourself because it turns out that [Fagbokforlaget](https://github.com/fagbokforlaget), which is a Norwegian publishing company that publishes nonfiction works and teaching aids for instruction at various levels, already made an [npm package](https://www.npmjs.com/package/@forlagshuset/strapi-plugin-wysiwyg-toastui) you can directly add to your Strapi project in order to have a wonderful Toast UI editor in your admin.

This editor is great as it implements real-time previsualization of what you are writing.
This package is listed on our [awesome Strapi](https://github.com/strapi/awesome-strapi) repository but I find it important to write a blog post because many of you want to replace the default editor but do not know the existence of this Github repository.

However, I will explain the code of this package. This is exactly like we did for manually installing [Quill editor](https://strapi.io/blog/how-to-change-the-default-wysiwy-to-quill-editor).

It's very simple to get started. Head into an existing Strapi project and add the package via npm or yarn:

```bash
npm i --save strapi-plugin-wysiwyg-toastui
# or
yarn add strapi-plugin-wysiwyg-toastui
```

Perfect, all you have to do is build and launch your Strapi application:

```
yarn build
yarn develop
# or
npm run build
npm run develop
```

You can now edit your Rich Text content using the Toast UI editor!

Let's take a closer look at how they made it...

You can observe the package code by looking at the following path:

`. /node_modules/strapi-plugin-wysiwyg-toastui/admin/src/components`

We can see that 3 components have been created:

## MediaLib component

```js
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);

  useEffect (() => {
    if (isOpen) {
      setIsDisplayed (true);
    }
  }, [isOpen]);

  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);
  };

  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;
```

This component allows you to use the Media Library and will be called in the editor component in order to directly use images from the Media Library.
Nothing much! Let's see the second component.

## WYSIWYG component

```js
/**
 *
 * Wysiwyg
 *
 */

import React from 'react';
import PropTypes from 'prop-types';
import { isEmpty, isFunction } from 'lodash';
import cn from 'classnames';

import { Description, ErrorMessage, Label } from '@buffetjs/styles';
import { Error } from '@buffetjs/core';

import Editor from '../TOASTUI';
import Wrapper from './Wrapper';

class Wysiwyg extends React.Component {

  render() {
    const {
      autoFocus,
      className,
      deactivateErrorHighlight,
      disabled,
      error: inputError,
      inputClassName,
      inputDescription,
      inputStyle,
      label,
      name,
      onBlur: handleBlur,
      onChange,
      placeholder,
      resetProps,
      style,
      tabIndex,
      validations,
      value,
      ...rest
    } = this.props;

    return (
      <Error
        inputError={inputError}
        name={name}
        type="text"
        validations={validations}
      >
        {({ canCheck, onBlur, error, dispatch }) => {
          const hasError = error && error !== null;

          return (
            <Wrapper
              className={`${cn(!isEmpty(className) && className)} ${
                hasError ? 'bordered' : ''
              }`}
              style={style}
            >
              <Label htmlFor={name}>{label}</Label>
              <Editor
                {...rest}
                autoFocus={autoFocus}
                className={inputClassName}
                disabled={disabled}
                deactivateErrorHighlight={deactivateErrorHighlight}
                error={hasError}
                name={name}
                onBlur={isFunction(handleBlur) ? handleBlur : onBlur}
                onChange={e => {
                  if (!canCheck) {
                    dispatch({
                      type: 'SET_CHECK',
                    });
                  }

                  dispatch({
                    type: 'SET_ERROR',
                    error: null,
                  });
                  onChange(e);
                }}
                placeholder={placeholder}
                resetProps={resetProps}
                style={inputStyle}
                tabIndex={tabIndex}
                value={value}
              />
              {!hasError && inputDescription && (
                <Description>{inputDescription}</Description>
              )}
              {hasError && <ErrorMessage>{error}</ErrorMessage>}
            </Wrapper>
          );
        }}
      </Error>
    );
  }
}

Wysiwyg.defaultProps = {
  autoFocus: false,
  className: '',
  deactivateErrorHighlight: false,
  didCheckErrors: false,
  disabled: false,
  error: null,
  inputClassName: '',
  inputDescription: '',
  inputStyle: {},
  label: '',
  onBlur: false,
  placeholder: '',
  resetProps: false,
  style: {},
  tabIndex: '0',
  validations: {},
  value: null,
};

Wysiwyg.propTypes = {
  autoFocus: PropTypes.bool,
  className: PropTypes.string,
  deactivateErrorHighlight: PropTypes.bool,
  didCheckErrors: PropTypes.bool,
  disabled: PropTypes.bool,
  error: PropTypes.string,
  inputClassName: PropTypes.string,
  inputDescription: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.func,
    PropTypes.shape({
      id: PropTypes.string,
      params: PropTypes.object,
    }),
  ]),
  inputStyle: PropTypes.object,
  label: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.func,
    PropTypes.shape({
      id: PropTypes.string,
      params: PropTypes.object,
    }),
  ]),
  name: PropTypes.string.isRequired,
  onBlur: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
  onChange: PropTypes.func.isRequired,
  placeholder: PropTypes.string,
  resetProps: PropTypes.bool,
  style: PropTypes.object,
  tabIndex: PropTypes.string,
  validations: PropTypes.object,
  value: PropTypes.string,
};

export default Wysiwyg;
```

This component will wrap the Toast UI editor with a label and the errors.
You can see that the index.js file is not alone. There is also a Wrapper.js file containing some style using styled-components.

```js
import styled from 'styled-components';

const Wrapper = styled.div`
  padding-bottom: 2.8rem;
  font-size: 1.3rem;
  font-family: 'Lato';
  label {
    display: block;
    margin-bottom: 1rem;
  }
  &.bordered {
    .editorWrapper {
      border-color: red;
    }
  }
  > div + p {
    width: 100%;
    padding-top: 12px;
    font-size: 1.2rem;
    line-height: normal;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-bottom: -9px;
  }
`;

export default Wrapper;
```

Now, the final component which is the one for the Toast UI editor itself!

## ToastUI component

```js
import React from 'react';
import PropTypes from 'prop-types';

import '@toast-ui/editor/dist/toastui-editor.css';
import 'codemirror/lib/codemirror.css';
import { Editor } from '@toast-ui/react-editor';
import { Button } from '@buffetjs/core';

import MediaLib from '../MediaLib';

class TOIEditor extends React.Component {
  editorRef = React.createRef();

  constructor(props) {
    super(props);
    this.height = "400px";
    this.initialEditType = "markdown";
    this.previewStyle = "vertical";
    this.state = { isOpen : false };
    this.handleToggle = this.handleToggle.bind(this);
  }

  componentDidMount() {
    const editor = this.editorRef.current.getInstance();
    const toolbar = editor.getUI().getToolbar();

    editor.eventManager.addEventType('insertMediaButton');
    editor.eventManager.listen('insertMediaButton', () => {
      this.handleToggle();
    } );

    toolbar.insertItem(0, {
      type: 'button',
      options: {
        className: 'first tui-image',
        event: 'insertMediaButton',
        tooltip: 'Insert Media',
        text: '@',
      }
    });
  }

  componentDidUpdate() {
    // Bug fix, where switch button become submit type - editor bug
    const elements = document.getElementsByClassName('te-switch-button');
    if ( elements.length ) {
      elements[0].setAttribute('type','button');
      elements[1].setAttribute('type','button');
    }
  }

  handleChange = data => {
    let value = this.props.value;
    let editor_instance = this.editorRef.current.getInstance();
    if (data.mime.includes('image')) {
      editor_instance.exec('AddImage', { 'altText': data.caption, 'imageUrl': data.url } );
    }
    else {
      editor_instance.exec('AddLink', { 'linkText': data.name, 'url': data.url } );
    }
  };

  handleToggle = () => this.setState({ isOpen : !this.state.isOpen });

  render() {
    return (
      <>
        <Editor
          previewStyle={this.previewStyle}
          height={this.height}
          initialEditType={this.initialEditType}
          initialValue={this.props.value}
          ref={this.editorRef}
          usageStatistics={false}
          onChange={(event) => {
            this.props.onChange({
              target: {
                value: this.editorRef.current.getInstance().getMarkdown(),
                name: this.props.name,
                type: 'textarea',
              },
            });
          }}
          toolbarItems={[
            'heading',
            'bold',
            'italic',
            'strike',
            'divider',
            'hr',
            'quote',
            'divider',
            'ul',
            'ol',
            'task',
            'indent',
            'outdent',
            'divider',
            'table',
            'link',
            'divider',
            'code',
            'codeblock',
            'divider',
          ]}
        />

        <MediaLib onToggle={this.handleToggle} isOpen={this.state.isOpen} onChange={this.handleChange}/>
      </>
    );
  }

}

export default TOIEditor;
```

As you understood, this component is the implementation of the new WYSIWYG which is simply using the Media Library.

That's it for this article which, I hope, will have introduced you to a very useful and especially great package.

![strapi-toast-ui](https://dev-to-uploads.s3.amazonaws.com/i/i8bpvxyr1v3auf4pjygw.png)

## To learn more about other WYSIWYG 

  - [Replace the default WYSIWYG by CKEditor](https://strapi.io/blog/how-to-change-the-wysiwyg-in-strapi).
  - [Replace the default WYSIWYG by Quill Editor](https://strapi.io/blog/how-to-change-the-default-wysiwy-to-quill-editor).