In this article I will tell you about how to create Markdown Editor with React.js and Web Storage

Purpose of this article is not to teach you React.js from scratch, I’m only just going to show you how to create a specific project using React.js and explain the code

If you want to know more about React.js and explore this technology, I recommend that you go through the following courses on Codecademy

Introduction to React.js: Part I

Introduction to React.js: Part I

Introduction to React.js: Part II

Introduction to React.js: Part II

In this project, we will use the following libraries and technologies: React.js, Babel, jQuery, Bootstap, Web Storage, Marked.js and Highlight.js

1) React.js — JavaScript Library for Building User Interfaces

2) Babel — Babel is a JavaScript compiler. We will use it for Setting up React for ES6

3) Marked.js — A markdown parser and compiler

4) Highlight.js — Syntax highlighting for the Web

5 Min Quickstart

Get started by creating a new folder for your project, and name it anything you like. Then, inside that folder, create additional folders and files to match the following structure

.
├── css
|    ├── style.css
├── js
|    ├── index.babel
|    └── storage.js
└── index.html

Start by creating a index.html file with the connected necessary libraries

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>ReactJS Markdown Editor</title>
	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.5.0/styles/default.min.css">
	<link rel="stylesheet" href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css'>
	<link rel="stylesheet" href="css/style.css">
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.5.0/highlight.min.js"></script>
	<script type="text/babel" src="js/index.babel"></script>
</head>
<body>
	<div id="app"></div>
</body>
</html>

Open the js/index.babel in your favorite code editor (for example Sublime Text 3 or Atom) and create a new component that simply displays Hello World on page

var MarkdownEditor = React.createClass({
	render: function(){
		return (
			<h1>Hello, world!</h1>
		)
	}
})
ReactDOM.render(
	<MarkdownEditor />,
	document.getElementById('app')
)

Now, run your website on a web server. If you see Hello World on page, it means that you have done everything correctly

If you do not have installed MAMP or WAMP server, for this project you can use Node.js local-web-server package

Now we can get started

First we need to create a text box and markdown preview box where you can see the result of the work

To do this, replace the render function in your component with

render: function() {
	return (
		<div className="container-fluid">
			<div className="row">
				<h1 className="text-center">
					ReactJS Markdown Editor
				</h1>
				<div className="col-xs-12 col-sm-6">
					<h3>Markdown</h3>
					<textarea id="markdown" className="markdown"></textarea>
				</div>
				<div className="col-xs-12 col-sm-6">
					<h3>Preview</h3>
					<div id="preview"></div>
				</div>
			</div>
		</div>
	)
}

and add the following styles to your css/style.css file

.markdown {
	padding: 10px 5px;
	resize: vertical;
	overflow: auto;
	width: 100%;
	height: 564px;
	margin: 0 auto;
	display: block;
	margin-bottom: 15px;
}

Now when you run, your page will look like this

MARKDOWN_EDITOR

The next step we define a initial default value that the user will see in a textarea. To do this, add getInitialState function before render

getInitialState: function() {
	return {
		content: '### Type Markdown Here'
	}
}

and replace

<textarea id="markdown" className="markdown"></textarea>

with

<textarea id="markdown" className="markdown" defaultValue={this.state.content}></textarea>

Now, every time you run the page, you’ll see the following text in a textarea

### Type Markdown Here

INIT_MARKDOWN

But you’ve probably noticed that in the preview block nothing. Let’s fix it. Add rawMarkup function immediately after getInitialState that will parse the contents of the text box and output the result in preview box

rawMarkup: function() {
	marked.setOptions({
		renderer: new marked.Renderer(),
		gfm: true,
		tables: true,
		breaks: false,
		pedantic: false,
		sanitize: true,
		smartLists: true,
		smartypants: false,
		highlight: function (code) {
			return hljs.highlightAuto(code).value
		}
	})

	var rawMarkup = marked(this.state.content, {sanitize: true})
	return {
		__html: rawMarkup
	}
}

and replace

<div id="preview"></div>

with

<div id="preview" dangerouslySetInnerHTML={this.rawMarkup()}></div>

In this code snippet, I use the standard recommended settings specified in the project repository Marked.js here https://github.com/chjj/marked#usage

marked.setOptions({
	renderer: new marked.Renderer(),
	gfm: true,
	tables: true,
	breaks: false,
	pedantic: false,
	sanitize: true,
	smartLists: true,
	smartypants: false
})

and added support syntax highlighting using highlight.js library

highlight: function(code) {
	return hljs.highlightAuto(code).value
}

and the following code snippet

var rawMarkup = marked(this.state.content, {sanitize: true})
return {
	__html: rawMarkup
}

takes a this.state.content as input, in this case, the contents of the textarea as a result of the output we get the HTML that you will see in preview box

PREVIEW_MARKDOWN

If you try to change the contents of the textarea notice that your changes are not displayed in the preview box. To fix this, add the following handleChange function after getInitialState

handleChange: function(event) {
	this.setState({
		content: event.target.value
	})
}

and replace

<textarea className="markdown" defaultValue={this.state.content}></textarea>

with

<textarea className="markdown" defaultValue={this.state.content} onChange={this.handleChange}></textarea>

Function handleChange is attached to the textarea onChange event. The handleChange function updates the components state with the new value of textarea. This causes the component to re-render with the new value

So when you change the markdown in the textarea field you will see changes in the preview

At this point, your code in index.babel file should look like this

index.babel

var MarkdownEditor = React.createClass({
	getInitialState: function() {
		return {
			content: '### Type Markdown Here'
		}
	},
	handleChange: function(event) {
		this.setState({
			content: event.target.value
		})
	},
	rawMarkup: function() {
		marked.setOptions({
			renderer: new marked.Renderer(),
			gfm: true,
			tables: true,
			breaks: false,
			pedantic: false,
			sanitize: true,
			smartLists: true,
			smartypants: false,
			highlight: function (code) {
				return hljs.highlightAuto(code).value
			}
		})

		var rawMarkup = marked(this.state.content, {sanitize: true})
		return {
			__html: rawMarkup
		}
	},
	render: function() {
		return (
			<div className="container-fluid">
				<div className="row">
					<h1 className="text-center">
						ReactJS Markdown Editor
					</h1>
					<div className="col-xs-12 col-sm-6">
						<h3>Markdown</h3>
						<textarea className="markdown" defaultValue={this.state.content} onChange={this.handleChange}></textarea>
					</div>
					<div className="col-xs-12 col-sm-6">
						<h3>Preview</h3>
						<div dangerouslySetInnerHTML={this.rawMarkup()}></div>
					</div>
				</div>
			</div>
		)
	}
})

ReactDOM.render(
	<MarkdownEditor />,
	document.getElementById('app')
)

Using Web Storage

Suppose a user has written a large amount of text and reload the page by accident, reset computer or even any of the case for which the user did not have time or are not able to save the typed text. The user will probably be upset

What can we do in this case and how to prevent it?

We will keep all written user’s markdown to Local Storage and after each restart page, restarted browser or rebooting the PC, the user will see the text written by him

To do this, add the componentWillMount function immediately after rawMarkup

componentWillMount: function() {
	const script = document.createElement("script")

	script.src = "./js/storage.js"
	script.async = true

	document.body.appendChild(script)
}

This code snippet we use to load JavaScript, which is in the file ./js/storage.js on our page within the script tag and execute it every time our component is rendered

Note that the path to storage.js file must be specified relative to the file index.html

Now include the jQuery adding to render

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>

and add the following code in your js/storage.js file

$(document).ready(function() {
	if (window.localStorage) {
		$('textarea').on('input', function(e) {
			var markdown = $('textarea').val()
			localStorage.setItem('markdownStorage', markdown)
		})
	}
})

or without jQuery

var textarea = document.getElementById('markdown')
if (window.localStorage) {
	textarea.addEventListener('input', function() {
		localStorage.setItem('markdownStorage', textarea.value)
	})
}

In this code snippet, we check the user’s browser supports Local Storage (you can check the browser compatibility using the service Can I Use) and if Local Storage is support we add an event listener to the textarea. Every time the input event is fired on them, it means something has changed and we save textarea contents with all changes to Local Storage with markdownStorage key

LOCAL_STORAGE

Finally, replace the initial default value

getInitialState: function() {
	return {
		content: '### Type Markdown Here'
	}
}

with add text from Local Storage

getInitialState: function() {
	return {
		content: localStorage.getItem('markdownStorage') || '### Type Markdown Here'
	}
}

Now, if the user has in Local Storage markdownStorage key, to the textarea will be inserted its contents. If markdownStorage key not found or empty then user will see a default value

### Type Markdown Here

Full Code

index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>ReactJS Markdown Editor</title>
	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.5.0/styles/default.min.css">
	<link rel="stylesheet" href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css'>
	<link rel="stylesheet" href="css/style.css">
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.5.0/highlight.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
	<script type="text/babel" src="js/index.babel"></script>
</head>
<body>
	<div id="app"></div>
</body>
</html>

css/style.css

.markdown {
	padding: 10px 5px;
	resize: vertical;
	overflow: auto;
	width: 100%;
	height: 564px;
	margin: 0 auto;
	display: block;
	margin-bottom: 15px;
}

js/index.babel

var MarkdownEditor = React.createClass({
	getInitialState: function() {
		return {
			content: localStorage.getItem('markdownStorage') || '### Type Markdown Here'
		}
	},
	handleChange: function(event) {
		this.setState({
			content: event.target.value
		})
	},
	rawMarkup: function() {
		marked.setOptions({
			renderer: new marked.Renderer(),
			gfm: true,
			tables: true,
			breaks: false,
			pedantic: false,
			sanitize: true,
			smartLists: true,
			smartypants: false,
			highlight: function (code) {
				return hljs.highlightAuto(code).value
			}
		})

		var rawMarkup = marked(this.state.content, {sanitize: true})
		return {
			__html: rawMarkup
		}
	},
	componentWillMount: function() {
		const script = document.createElement("script")

		script.src = "./js/storage.js"
		script.async = true

		document.body.appendChild(script)
	},
	render: function() {
		return (
			<div className="container-fluid">
				<div className="row">
					<h1 className="text-center">
						ReactJS Markdown Editor
					</h1>
					<div className="col-xs-12 col-sm-6">
						<h3>Markdown</h3>
						<textarea className="markdown" defaultValue={this.state.content} onChange={this.handleChange}></textarea>
					</div>
					<div className="col-xs-12 col-sm-6">
						<h3>Preview</h3>
						<div dangerouslySetInnerHTML={this.rawMarkup()}></div>
					</div>
				</div>
			</div>
		)
	}
})

ReactDOM.render(
	<MarkdownEditor />, 
	document.getElementById('app')
)

js/storage.js

$(document).ready(function() {
	if (window.localStorage) {
		$('textarea').on('input', function(e) {
			var markdown = $('textarea').val()
			localStorage.setItem('markdownStorage', markdown)
		})
	}
})

Mikhail

I am Mikhail Evdokimov, a Hobbyist Self Taught Programmer