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
- Import and use Draft.js library to create an editor component.
- Create and style buttons to use the inline styles and block types native to Draft.js.
- Use a markdown dependency to create a window that will show the markdown equivalent of our stylized text.
- 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:
- Import the stateToMarkdown module from the draft-js-export-markdown dependency.
- Create a functional component that receives the current state of editorState from the MyEditor component.
- Transcribe that content through the stateToMarkdown function, returning a textarea that will display the result of that action.
- 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)
Could u please share source code..! I have some problems to display content in the page ..and thnx for article 🙏
github.com/leandrohrmonteiro/portf...