DEV Community

Cover image for Rich text editor with markdown viewer and save file support
LeandroHRMonteiro
LeandroHRMonteiro

Posted on

Rich text editor with markdown viewer and save file support

The why of this application

When writing documentation many sites ask the user to apply styles to the text through markdown, so an application that allowed me to both train my understanding of React hooks and could later still be of use for future projects seemed like a good idea.

If you are interested in doing a similar project, but using class components, there is a great tutorial by https://dev.to/rose that touches on these and other topics here.


Steps in this project

  1. Import and use Draft.js library to create an editor component.
  2. Create and style buttons to use the inline styles and block types native to Draft.js.
  3. Use a markdown dependency to create a window that will show the markdown equivalent of our stylized text.
  4. Use a save dependency to save the markdown content into a .txt file.

1.1
Let's start by creating a react app in a folder where we wish our app to be created using a terminal.
As we are going to use Visual Studio Code for this project, we will use its terminal from the get-go by opening the folder we wish the app to be installed in and then accessing the terminal window by clicking: terminal -> new terminal to create a new terminal.
In that terminal we will insert the following:

npx create-react-app my-editor
cd my-editor
npm start

With this, we created a basic react app inside the directory my-editor,(as long as you have npm already installed) and started a localhost server for that app. If at any time you want to shut down that server just click ctrl+c on windows, command+c on mac.


1.2
Now when we open the my-editor directory with a code editor, Visual Studio Code in my case, we see the file App.js that holds the information that will be rendered on the react app page.
Our final goal will be to have App.js rendering our editor component. To that end lets start by importing Draft.js, the editor we will be using, as a dependency.
Note: we can't install dependencies if the server is active so remember to shut down the server with ctrl+c on windows, command+c on mac beforehand.
In the terminal:

npm install draft-js

1.3
We now have access to Draft.js enabling us to work on our main Editor component, but we should start thinking about the size and complexity of the project we are going to create. In this tutorial, that complexity will be fairly simple but as projects progress so does the amount of code per file, which might cause confusion and slow down future debugs. To minimize that, we will separate different responsibilities into multiple components in our application. These components will be housed in different files and then imported into the main editor component, which in turn will be imported into the App component to be rendered.

With those thoughts in mind let's start by creating a components folder inside the source directory of our react app.

Inside the components folder, we will create our MyEditor.jsx component. If you are wondering why .jsx and not .js let me leave you with this link that explains why jsx is an interesting choice when working with react.


1.4
Time to create the MyEditor component:
We start by importing React and the Draft.js framework into the component.

import React from 'react'

import { Editor, EditorState} from 'draft-js'
import 'draft-js/dist/Draft.css'

Editor is the contentEditable component we will use in our MyEditor functional component, and EditorState is the state object for Editor that holds all of the information of what to render and how to render it.
EditorState is immutable, which means that on every change we are deleting the existing instance of EditorState and creating a new one.
You might have noticed we also imported a Draft.css file that

should be included when rendering the editor, as these styles set defaults for text alignment, spacing, and other important features. Without it, you may encounter issues with block positioning, alignment, and cursor behavior.


1.5
Let's create the MyEditor functional component so we can start using the elements previously imported.

function MyEditor() {

  const [editorState, setEditorState] = React.useState(() => EditorState.createEmpty());

  return(
    <div>
      <Editor 
      editorState={editorState}
      onChange={setEditorState}
      placeholder='Click here to activate input field'
      />
    </div>
  )
};

export default MyEditor

We started by declaring a hooks state variable where we can define what will be the state and the method allowed to change such state, in our case: editorState and setEditorState. More information on React hooks useState here.

EditorState has a number of methods, and we used the createEmpty() one to set the initial state of our editor. As the name implies, it will be empty. :)

On the return of our MyEditor function, we get to define the Editor component, telling it what it should consider as its state, what method should be used to change such state, and a placeholder text that will let the user see where the Editor input field is at the start of the session.

At the end of the file and because we are exporting just one main function, we can use the ES6 export module as a default export instead of a named one. More on that subject here.

All we need to finish step 1 of our project is to import the MyEditor component to App.js and insert it in the App function return statement to see it appear in the localhost browser.

import React from "react";
import "./styles.css";

import MyEditor from './components/MyEditor.jsx'

export default function App() {
  return (
    <div className="App">
      <MyEditor />
    </div>
  );
}

2.1
Right now, all we can see is a placeholder text that shows us where to write. So let's get a bit of visual style by creating a MyEditor.sass file inside the Components folder that we are going to import to MyEditor.jsx.
(sass is an extension to CSS, more information here.

Note: you might have to install node-sass to work with the following file:

npm install node-sass

In MyEditor.sass:

.editor-general-appearance
  background-color: #9b9b9b
  width: 500px
  margin: 50px auto
  padding: 20px
  border: solid
  border-radius: 5px
  border-width: 1px

.draft-editor-wrapper
  background-color: #303030
  color: white
  margin-top: 20px

  .public-DraftEditorPlaceholder-root
    color: #9b9b9b

2.2
By now we should be seeing a grey box with a dark grey input field and grey placeholder text.
It is indeed an editor and we can write on it, but not much else. Let's build on that by adding styling functionalities to the editor through buttons.
What will allow us to do that is a Draft.js module called RichUtils.
In this tutorial, we will strive to separate concerns as best I know right now, so the buttons and styles will be created in different files. We will then import the styles to the button components, and the button component into the editor component,
but let's start with a simpler example of a button that has its own style hardcoded.

Note: As this part might become a bit confusing, at the end of each complete styling process we will look at the complete code of the files involved so we can regain our bearings.

We will start by creating a buttons folder inside the components folder.
In the buttons folder, we then create an InlineStylesButtons.jsx file and import the RichUtils module so we can address the styling issue.

import React from 'react'
import { RichUtils } from 'draft-js'

Out of the box RichUtils supports a number of inline styles as seen here, and block styles here.


2.3
Notice that this InlineStylesButtons has access to the RichUtils module, but to apply any changes to the editorState of MyEditor it will need to access the properties of that component. We can achieve this if:

A. We export the InlineStylesButtons component into the MyEditor component

function InlineStyleButtons(props) {
}

export default InlineStyleButtons

B. And then with the imported InlineStyleButtons component in MyEditor...

import InlineStylesButtons from './buttons/InlineStylesButtons.jsx'

...in the return statement of the MyEditor we will pass the editorState and setEditorState of MyEditor as properties of the InlineStyleButtons component, so that we can change the MyEditor state from the InlineStyleComponent:

  return(
    <div className='editor-general-appearance'>
    <div>
    <InlineStylesButtons editorState={editorState} setEditorState={setEditorState}
 />    
    </div>      
     <div className="draft-editor-wrapper">
      <Editor 
        editorState={editorState}
        onChange={setEditorState}
        placeholder='Click here to activate input'
      />
      </div>
    </div>
  )
}

export default MyEditor

2.4
Now back in the InlineStylesButton file, we used a props parameter in our function. This is how we can access the information that was passed into the InlineStylesButton component as it was returned by the MyEditor component.
Now we can write a function that makes use of the editorState and setEditorState that resides in MyEditor, like so:

function InlineStyleButtons(props) {

const inlineStyleButton = (event) => { 
  let style = event.currentTarget.getAttribute('use-style')
props.setEditorState(RichUtils.toggleInlineStyle(props.editorState, style))
}
  return(

    <input 
      type='button'
      value='Bold'
      use-style='BOLD'
      onClick= {inlineStyleButton}
      /> 
  )
}

export default InlineStyleButtons

Having styled our first hardcoded style button, let's pause for a second and see how our code should look like at this point.

MyEditor component:

import React from 'react'

import {Editor, EditorState} from 'draft-js';
import 'draft-js/dist/Draft.css'
import './MyEditor.sass'


import InlineStylesButtons from './buttons/InlineStylesButtons.jsx'


function MyEditor() {

const [editorState, setEditorState] = React.useState( ()=> EditorState.createEmpty())

  return(
    <div className='editor-general-appearance'>

    <div>
    <InlineStylesButtons editorState={editorState} setEditorState={setEditorState}
 />    
    </div>      
     <div className="draft-editor-wrapper">
      <Editor 
        editorState={editorState}
        onChange={setEditorState}
        placeholder='Click here to activate input'
      />
      </div>
    </div>
  )
}

export default MyEditor

and the InlineStylesButtons component:

import React from 'react'
import {RichUtils} from 'draft-js';

function InlineStyleButtons(props) {

const inlineStyleButton = (event) => { 
  let style = event.currentTarget.getAttribute('use-style')
props.setEditorState(RichUtils.toggleInlineStyle(props.editorState, style))
}
  return(

    <input 
      type='button'
      value='Bold'
      use-style='BOLD'
      onClick= {inlineStyleButton}
      /> 
  )
}

export default InlineStyleButtons

2.5
We can continue creating individual buttons and assign them a style, but that would mean that we would be repeating code that is mostly the same, with the only change being the style and value of the buttons.
So we are going to place all the values and styles in an InlineStyles.js file and import them into the InlineStyleButtons component passing them through a .map function that will use a secondary function to create buttons for each style in the InlineStyles.js.
This might seem overwhelming but we will do it in small steps, and at the end of this process, we will look at the entire code for each file again.

So let's start by creating an InlineStyles.js file inside the buttons folder. Since this is just a variable that holds an array of objects that we will export, we are going for a .js file instead of .jsx:

 const InlineStyles = [
  {
    value: 'Bold',
    style: 'BOLD'
  },

  {
    value: 'Italic',
    style: 'ITALIC'
  },

  {
    value: 'Underline',
    style: 'UNDERLINE'
  },

  {
    value: 'Strikethrough',
    style: 'STRIKETHROUGH'
  },

  {
    value: 'Code',
    style: 'CODE'
  }
];

export default InlineStyles;

Now we import it in InlineStylesButtons.jsx:

import InlineStyles from './InlineStyles'

And we create the secondary function that will take these values and styles when we use the .map() on it later on:

const renderInlineStyleButton = (value, style) => {
  const currentInlineStyle = props.editorState.getCurrentInlineStyle();
  let styleActive = '';
  if (currentInlineStyle.has(style)) {
    styleActive = 'active';
  }
  return(

    <input 
    type='button'
    key={style}
    value={value}
    use-style={style}
    onMouseDown={setInlineStyle}   
    className={styleActive}
    /> 
  )
}

You might have noticed that the function renderInlineStyleButton does more than returning a button with the values and styles we feed it in the parameters. It also asks editorState if there is a style connected to the currentInline style and sets the className styleActive to active if such is true. We will associate some SASS styling later on for when styleActive is active.

Now on the return of InlineStylesButtons, we will map the different styles and values that come from InlineStyles, and pass them to the renderInlineStyleButton function so we can create a button for each of the styles:

<div className='toolbar'>
    {InlineStyles.map((button) => {
      return renderInlineStyleButton(button.value, button.style);
      })}
    </div>

The styling for these buttons will occur inside a toolbar class so that when we apply a style, the corresponding button will change color to red and look pressed:
In MyEditor.sass:

.toolbar input[type="button"]
  background-color: #303030
  color: white
  border-radius: 5px
  cursor: pointer
  margin-right: 2px
  margin-bottom: 2px

  &:active
    transform: scale(0.95)

  &:not(.active):hover
    background-color: red
    transition: 0.3s

  &.active
    background-color: red
    border-color: transparent

Right now your files should look like this:

MyEditor.jsx:

import React from 'react'

import {Editor, EditorState} from 'draft-js';
import 'draft-js/dist/Draft.css'
import './MyEditor.sass'


import InlineStylesButtons from './buttons/InlineStylesButtons.jsx'


function MyEditor() {

const [editorState, setEditorState] = React.useState( ()=> EditorState.createEmpty())

  return(
    <div className='editor-general-appearance'>

    <div>
    <InlineStylesButtons editorState={editorState} setEditorState={setEditorState}
 />    
    </div>      
     <div className="draft-editor-wrapper">
      <Editor 
        editorState={editorState}
        onChange={setEditorState}
        placeholder='Click here to activate input'
      />
      </div>
    </div>
  )
}

export default MyEditor

InlineStylesButtons.jsx:

import React from 'react'
import {RichUtils} from 'draft-js'

import InlineStyles from './InlineStyles'

function InlineStyleButtons(props) {

const setInlineStyle = (event) => { 
  event.preventDefault();
  let style = event.currentTarget.getAttribute('use-style')
props.setEditorState(RichUtils.toggleInlineStyle(props.editorState, style))
}

const renderInlineStyleButton = (value, style) => {
  const currentInlineStyle = props.editorState.getCurrentInlineStyle();
  let styleActive = '';
  if (currentInlineStyle.has(style)) {
    styleActive = 'active';
  }
  return(

    <input 
    type='button'
    key={style}
    value={value}
    use-style={style}
    onMouseDown={setInlineStyle}   
    className={styleActive}
      /> 
  )
}

  return(
    <div className='toolbar'>
    {InlineStyles.map((button) => {
      return renderInlineStyleButton(button.value, button.style);
      })}
    </div>
  )
}

export default InlineStyleButtons

InlineStyles.js:

const InlineStyles = [
  {
    value: 'Bold',
    style: 'BOLD'
  },

  {
    value: 'Italic',
    style: 'ITALIC'
  },

  {
    value: 'Underline',
    style: 'UNDERLINE'
  },

  {
    value: 'Strikethrough',
    style: 'STRIKETHROUGH'
  },

  {
    value: 'Code',
    style: 'CODE'
  }
];

export default InlineStyles;

MyEditor.sass

.editor-general-appearance
  background-color: #9b9b9b
  width: 500px
  margin: 50px auto
  padding: 20px
  border: solid
  border-radius: 5px
  border-width: 1px

.toolbar input[type="button"]
  background-color: #303030
  color: white
  border-radius: 5px
  cursor: pointer
  margin-right: 2px
  margin-bottom: 2px

  &:active
    transform: scale(0.95)

  &:not(.active):hover
    background-color: red
    transition: 0.3s

  &.active
    background-color: red
    border-color: transparent

.draft-editor-wrapper
  background-color: #303030
  color: white
  margin-top: 20px

.public-DraftEditorPlaceholder-root
  color: #9b9b9b

2.6
The rendering of block type buttons will be similar, starting with creating a StyleBlockButtons.jsx and a BlockTypes.jsx files in the buttons folder:

StyleBlockButtons.jsx:

import React from 'react'
import {RichUtils} from 'draft-js'

import BlockTypes from './BlockTypes'

function StyleBlockButtons(props) {

  const setBlockType = (event) => {
    event.preventDefault()
    let type = event.currentTarget.getAttribute('use-type')
    props.setEditorState(RichUtils.toggleBlockType(props.editorState, type))
  }

  const renderBlockTypeButton = (value, block) => {

    const currentBlockType = RichUtils.getCurrentBlockType(props.editorState);
    let typeActive = '';
    if (currentBlockType === block) {
      typeActive = 'active'
    }

    return (
        <input
            type='button'
            key={block}
            value={value}
            use-type={block}
            onMouseDown={setBlockType} 
            className={typeActive}  
        />
      )
  }
  return(
    <div className='toolbar'>
    {BlockTypes.map((button) => {
      return renderBlockTypeButton(button.value, button.block);
      })}
    </div>
  )


}

export default StyleBlockButtons

BlockTypes.jsx:

const BlockTypes = [
  {
    value: 'H1',
    block: 'header-one'
  },

  {
    value: 'H2',
    block: 'header-two'
  },

  {
    value: 'H3',
    block: 'header-three'
  },

  {
    value: 'H4',
    block: 'header-four'
  },


  {
    value: 'Unordered List',
    block: 'unordered-list-item'
  },

  {
    value: 'Ordered List',
    block: 'ordered-list-item'
  },

  {
    value: 'Blockquote',
    block: 'blockquote'
  },
];


export default BlockTypes

Finally importing the StylesBlockButtons component into the MyEditor component file which should now look like this:

MyEditor.jsx:

import React from 'react'

import {Editor, EditorState} from 'draft-js';
import 'draft-js/dist/Draft.css'
import './MyEditor.sass'


import InlineStylesButtons from './buttons/InlineStylesButtons.jsx'
import StyleBlockButtons from './buttons/StyleBlockButtons.jsx'

function MyEditor() {

const [editorState, setEditorState] = React.useState( ()=> EditorState.createEmpty())

  return(
    <div className='editor-general-appearance'>

    <div>
    <InlineStylesButtons editorState={editorState} setEditorState={setEditorState} /> 
    <StyleBlockButtons editorState={editorState} setEditorState={setEditorState} /> 

    </div>      
     <div className="draft-editor-wrapper">
      <Editor 
        editorState={editorState}
        onChange={setEditorState}
        placeholder='Click here to activate input'
      />
      </div>
    </div>
  )
}

export default MyEditor

Note: There are ways to create your own styles, and although that doesn't fall under the scope of this tutorial we can learn about it by looking at Rose's tutorial for extra help on that subject here


3.1
By this point, we have an editor that supports different styles. Arguably the hardest part is done, but our project is not.
We will first add a new dependency to the project that transcribes our editorState into markdown language.
The dependency can be found here
and we will install it with npm in our terminal as follows:

npm install --save draft-js-export-markdown

3.2
Now we can create a MarkdownViewer.jsx functional component in the components folder, and perform the following actions:

  1. Import the stateToMarkdown module from the draft-js-export-markdown dependency.
  2. Create a functional component that receives the current state of editorState from the MyEditor component.
  3. Transcribe that content through the stateToMarkdown function, returning a textarea that will display the result of that action.
  4. We will add a div id of markdown-viewer to that return so we can style the component when we're done.

MarkdownViewer.jsx:

import React from "react";
import { stateToMarkdown } from "draft-js-export-markdown";

function MarkdownViewer(props) {
  const markdownViewer = () => {
    const markdown = stateToMarkdown(props.editorState.getCurrentContent());
    return <textarea readOnly value={markdown} />;
  };

  return <div id="markdown-viewer">{markdownViewer()}</div>;
}

export default MarkdownViewer;

Now in MyEditor we still have to import the MarkdownViewer component and feed it the editorState so that it has something to transcribe, so the MyEditor component will now look as follows...
MyEditor.jsx:

import React from "react";

import { Editor, EditorState } from "draft-js";
import "draft-js/dist/Draft.css";
import "./MyEditor.sass";

import InlineStylesButtons from "./buttons/InlineStylesButtons.jsx";
import StyleBlockButtons from "./buttons/StyleBlockButtons.jsx";

import MarkdownViewer from "./MarkdownViewer.jsx";

function MyEditor() {
  const [editorState, setEditorState] = React.useState(() =>
    EditorState.createEmpty()
  );

  return (
    <div className="editor-general-appearance">
      <div>
        <InlineStylesButtons
          editorState={editorState}
          setEditorState={setEditorState}
        />
        <StyleBlockButtons
          editorState={editorState}
          setEditorState={setEditorState}
        />
      </div>
      <div className="draft-editor-wrapper">
        <Editor
          editorState={editorState}
          onChange={setEditorState}
          placeholder="Click here to activate input"
        />
      </div>

      <div>
        <MarkdownViewer editorState={editorState} />
      </div>
    </div>
  );
}

export default MyEditor;

For the styling, we will add the following to our MyEditor.sass.

MyEditor.sass:

public-DraftEditor-content
  height: 150px
  overflow-y: auto

#markdown-viewer
  overflow-y: auto

textarea
  padding: 0
  resize: none
  width: 500px
  height: 150px
  background-color: #303030
  color: white
  border: none
  border-top: solid 1px #9b9b9b
  &:focus
    outline: none

Please notice that we have also overwritten a bit of the original draft-js CSS to give ourselves a bigger input window.

At this point we have an editor that can style text and display its equivalent in markdown, allowing us to select and copy/paste its contents.


4.1
We are only missing one thing to finish this project, the ability to save the markdown content of our sessions into a file. Exactly what we are going to deal with now.

Let's start by installing the dependency that will allow us to save, file-saver.js.
The dependency can be found here
and we will install it with npm in our terminal as follows:

npm install file-saver --save

4.2
Then we create the FileSaverComponent.jsx file inside the components folder as follows:

import React from "react";
import FileSaver from "file-saver";
import { stateToMarkdown } from "draft-js-export-markdown";

function FileSaverComponent(props) {
  const saveToContent = (event) => {
    event.preventDefault();
    const markdown = stateToMarkdown(props.editorState.getCurrentContent());
    let filename = document.getElementById("filename").value;
    let blob = new Blob([markdown], { type: "text/plain;charset=utf-8" });
    FileSaver.saveAs(blob, filename);
  };

  return (
    <div className="toolbar">
      <input type="text" id="filename" placeholder=" insert filename.txt" />

      <input type="button" value="SAVE" onMouseDown={saveToContent} />
    </div>
  );
}

export default FileSaverComponent;

4.3
Importing it into the MyEditor component:

import React from "react";

import { Editor, EditorState } from "draft-js";
import "draft-js/dist/Draft.css";
import "./MyEditor.sass";

import InlineStylesButtons from "./buttons/InlineStylesButtons.jsx";
import StyleBlockButtons from "./buttons/StyleBlockButtons.jsx";

import MarkdownViewer from "./MarkdownViewer.jsx";
import FileSaverComponent from './FileSaverComponent.jsx'

function MyEditor() {
  const [editorState, setEditorState] = React.useState(() =>
    EditorState.createEmpty()
  );

  return (
    <div className="editor-general-appearance">
      <div>
        <InlineStylesButtons
          editorState={editorState}
          setEditorState={setEditorState}
        />
        <StyleBlockButtons
          editorState={editorState}
          setEditorState={setEditorState}
        />
      </div>
      <div className="draft-editor-wrapper">
        <Editor
          editorState={editorState}
          onChange={setEditorState}
          placeholder="Click here to activate input"
        />
      </div>

      <div>
        <MarkdownViewer editorState={editorState} />
        <FileSaverComponent editorState={editorState}/>
      </div>
    </div>
  );
}

export default MyEditor;

4.4
And we style it:
MyEditor.sass:

.editor-general-appearance
  background-color: #9b9b9b
  width: 500px
  margin: 50px auto
  padding: 20px
  border: solid
  border-radius: 5px
  border-width: 1px

.toolbar input[type="button"]
  background-color: #303030
  color: white
  border-radius: 5px
  cursor: pointer
  margin-right: 2px
  margin-bottom: 2px

  &:active
    transform: scale(0.95)

  &:not(.active):hover
    background-color: red
    transition: 0.3s

  &.active
    background-color: red
    border-color: transparent

.draft-editor-wrapper
  background-color: #303030
  color: white
  margin-top: 20px

.public-DraftEditorPlaceholder-root
  color: #9b9b9b

.public-DraftEditor-content
  height: 150px
  overflow-y: auto

#markdown-viewer
  overflow-y: auto


textarea
  padding: 0
  resize: none
  width: 500px
  height: 150px
  background-color: #303030
  color: white
  border: none
  border-top: solid 1px #9b9b9b
  &:focus
    outline: none


#filename
  background-color: #303030
  color: white
  margin-top: 10px
  border: none

  &::placeholder
    color: #9b9b9b
    opacity: 1

  &:focus
    outline: none

Epilog:
There we have it, a rich text editor with a markdown viewer and saving support. There is much more one can do with Draft.js, but for now, this will be the end of our tutorial.

Top comments (2)

Collapse
 
yunusbenaggoune profile image
YunusBenaggoune

Could u please share source code..! I have some problems to display content in the page ..and thnx for article 🙏

Collapse
 
leandrohrmonteiro profile image
LeandroHRMonteiro