I remember when I was doing freeCodeCamp, I was about to make a Markdown editor in one of the projects. So I decided to go with the Markdown editor this time, combined with React.js and TypeScript.
What you'll learn
- Setting up React.js project with TypeScript
- Creating a markdown editor by compiling down it to html
- Using React hooks to create theming for the application
- Continuous deployments through Github Actions
I am a lazy person, I think most of you are, too. So here's the code and demo link, if you directly want to see them.
Project Source Code:
ashwamegh / react-typescript-markdown-editor
Markdown editor with the use of React.js and TypeScript combined with continuous deployments using Github actions workflow
Project Demo: ashwamegh/react-typescript-markdown-editor
Lets start with setting up our project
1. Setting up our project with React.js & TypeScript
We all know the capabilities of TypeScript, how it can save the day for your silly mistakes. And if combined with react, they both become a great combination to power any application.
I will be using create-react-app
since, it gives the TypeScript support out of the box. Go to your root directory where you want to create the project and run this command:
npx create-react-app markdown-editor --template typescript
this --template typescript
flag will do all the hard work for you, setting up React.js project with TypeScript.
Later, you'll need to remove some of bootstrapped code to start creating your application.
For reference you can check this initial commit to see what has been removed:
https://github.com/ashwamegh/react-typescript-markdown-editor/commit/7cc379ec0d01f3f1a07396ff2ac6c170785df57b
After you've completed initial steps, finally we'll be moving on to creating our Markdown Editor.
2. Creating Markdown Editor
Before diving into the code, let's see the folder structure for our project, which we will be developing.
├── README.md
├── package.json
├── public
| ├── favicon.ico
| ├── index.html
| ├── logo192.png
| ├── logo512.png
| ├── manifest.json
| └── robots.txt
├── src
| ├── App.test.tsx
| ├── App.tsx
| ├── components
| | ├── Editor.tsx
| | ├── Footer.tsx
| | ├── Header.tsx
| | ├── Main.tsx
| | ├── Preview.tsx
| | └── shared
| | └── index.tsx
| ├── index.css
| ├── index.tsx
| ├── react-app-env.d.ts
| ├── serviceWorker.ts
| ├── setupTests.ts
| └── userDarkMode.js
├── tsconfig.json
└── yarn.lock
I will be using emotion
for creating styles for my components and react-icons
for icons used in the project. So you'll be needed to install emotion
and react-icons
by running this command:
npm i -S @emotion/core @emotion/styled react-icons
or if you're using yarn
like me, you can run
yarn add @emotion/core @emotion/styled react-icons
Moving forward, first of we will create a shared
components folder to create components we will be reusing.
/* src/components/shared/index.tsx */
import React from 'react'
import styled from '@emotion/styled'
export const ColumnFlex = styled.div`
display: flex;
flex-direction: column;
`
export const RowFlex = styled.div`
display: flex;
flex-direction: row;
`
In this file, we have declared two styled components for
flex-column
andflex-row
styleddivs
which we'll be using later.
To know more aboutstyled-components
withemotion
library, head on to this link.
3 Using React hooks to create a custom theme hook
We'll use react hooks to create our custom hook to implement basic theming capabilities, using which we can toggle our theme from light to dark colors.
/* useDarMode.js */
import { useEffect, useState } from 'react'
export default () => {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
if (theme === 'dark') {
setTheme('light')
} else {
setTheme('dark')
}
}
useEffect(() => {
const localTheme = localStorage.getItem('theme')
if (localTheme) {
setTheme(localTheme)
}
}, [])
return {
theme,
toggleTheme,
}
}
In our hooks file, we are setting the initial state of the theme to be
light
usinguseState
hook. And usinguseEffect
to check whether any theme item exists in our browser's local storage, and if there is one, pick the theme from there and set it for our application.
Since, we have defined our shared components and custom react hook for theming, let's dive into our app components.
So, I have divided our app structure into 5 components and those are: Header, Main (contains main section of the app with Editor & Preview component) and Footer component.
- Header // contains normal header code and a switch to toggle theme
- Main // container for Editor and Preview components i. Editor // contains code for Editor ii. Preview // contains code for previewing markdown code into HTML
- Footer // contains normal footer code
/* src/components/Header.tsx */
import React from 'react'
import { FiSun } from 'react-icons/fi'
import { FaMoon } from 'react-icons/fa'
// this comment tells babel to convert jsx to calls to a function called jsx instead of React.createElement
/** @jsx jsx */
import { css, jsx } from '@emotion/core'
// Prop check in typescript
interface Props {
toggleTheme: () => void,
theme: string
}
const Header: React.FC<Props> = ({ theme, toggleTheme }) => {
return (
<header
css={theme === 'dark' ?
css`
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: #f89541;
padding: 24px 32px;
font-size: 16px;
`:css`
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: #f8f541;
padding: 24px 32px;
box-shadow: 0px -2px 8px #000;
font-size: 16px;
`}>
<div className="header-title">
Markdown Editor
</div>
<div css={
css`
cursor: pointer;
`}
onClick={toggleTheme}
>
{
theme === 'dark'?
<FaMoon />:
<FiSun />
}
</div>
</header>
)
}
export default Header;
In this component, we are using TypeScript for prop checks and you may wonder, why we're mentioning
React.FC
here. Its just that, by typing our component as an FC (FunctionComponent), the React TypeScripts types allow us to handle children and defaultProps correctly.
For styling our components we're using css
prop with string styles from emotion
library, you can learn more about this by following the docs here
After creating the Header component, we'll create our Footer component and then we'll move on to Main component.
Let's see the code for Footer component
import React from 'react'
// this comment tells babel to convert jsx to calls to a function called jsx instead of React.createElement
/** @jsx jsx */
import { css, jsx } from '@emotion/core'
const Footer: React.FC = () => {
return (
<footer>
<div
className="footer-description"
css={
css`
padding: 16px 0px;
overflow: hidden;
position: absolute;
width: 100%;
text-align: center;
bottom: 0px;
color: #f89541;
background: #000;
`
}>
<span>{`</>`}</span><span> with <a href="https://reactjs.org" target="_blank">React.js</a> & <a href="https://www.typescriptlang.org/" target="_blank">TypeScript</a></span>
</div>
</footer>
)
}
export default Footer;
Footer component contains simple code to render usual credit stuff.
/* src/components/Main.tsx */
import React, { useState } from 'react'
// this comment tells babel to convert jsx to calls to a function called jsx instead of React.createElement
/** @jsx jsx */
import { css, jsx } from '@emotion/core'
import { RowFlex } from './shared'
import Editor from './Editor';
import Preview from './Preview';
interface Props {
theme: string
}
const Main: React.FC<Props> = ({ theme }) => {
const [markdownContent, setMarkdownContent] = useState<string>(`
# H1
## H2
### H3
#### H4
##### H5
__bold__
**bold**
_italic_
`);
return (
<RowFlex
css={css`
padding: 32px;
padding-top: 0px;
height: calc(100vh - 170px);
`}>
<Editor theme={theme} markdownContent={markdownContent} setMarkdownContent={setMarkdownContent}/>
<Preview theme={theme} markdownContent={markdownContent}/>
</RowFlex>
)
}
export default Main;
Since, some of the code will look familiar to you from the previous components which you can now understand yourself. Other than that, we have used useState
hook to create a state to hold our markdown content and a handler to set it, called setMarkdownContent
in the code.
We need to pass these down to our
Editor
andPreview
components, so that, they can provide the user the way to edit and preview their markdown content. We have also set an initial state for content with some basic markdown text.
Let's see the code for Editor component:
/* src/components/Editor.tsx */
import React, { ChangeEvent } from 'react'
import PropTypes from 'prop-types';
// this comment tells babel to convert jsx to calls to a function called jsx instead of React.createElement
/** @jsx jsx */
import { css, jsx } from '@emotion/core'
import { ColumnFlex } from './shared'
interface Props {
markdownContent: string;
setMarkdownContent: (value: string) => void,
theme: string
}
const Editor: React.FC<Props> = ({ markdownContent, setMarkdownContent, theme }) => {
return (
<ColumnFlex
id="editor"
css={css`
flex: 1;
padding: 16px;
`}>
<h2>
Editor
</h2>
<textarea
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => setMarkdownContent(e.target.value)}
css={theme === 'dark'?
css`
height: 100%;
border-radius: 4px;
border: none;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 1);
background: #000;
color: #fff;
font-size: 100%;
line-height: inherit;
padding: 8px 16px;
resize: none;
overflow: auto;
&:focus {
outline: none;
}
`
: css`
height: 100%;
border-radius: 4px;
border: none;
box-shadow: 2px 2px 10px #999;
font-size: 100%;
line-height: inherit;
padding: 8px 16px;
resize: none;
overflow: auto;
&:focus {
outline: none;
}
`}
rows={9}
value={markdownContent}
/>
</ColumnFlex>
)
}
Editor.propTypes = {
markdownContent: PropTypes.string.isRequired,
setMarkdownContent: PropTypes.func.isRequired,
}
export default Editor;
This is a straight forward component which uses
<textarea/>
to provide the user a way to enter their inputs, which have to be further compiled down to render it as HTML content in the Preview component.
Now, we have created almost all the components to hold our code except the Preview component.
We'll need something to compile down the user's markdown content to simple HTML, and we don't want to write all the compiler code, because we have plenty of options to choose from.
In this application, we'll be using marked
library to compile down our markdown content to HTML. So you will be need to install that, by running this command:
npm i -S marked
or with yarn
yarn add marked
If you want to know more about this library, you can see it here
Let's see the code for our Preview component
/* src/components/Preview.tsx */
import React from 'react'
import PropTypes from 'prop-types'
import marked from 'marked'
// this comment tells babel to convert jsx to calls to a function called jsx instead of React.createElement
/** @jsx jsx */
import { css, jsx } from '@emotion/core'
import { ColumnFlex } from './shared'
interface Props {
markdownContent: string,
theme: string
}
const Preview: React.FC<Props> = ({ markdownContent, theme }) => {
const mardownFormattedContent = ( marked(markdownContent));
return (
<ColumnFlex
id="preview"
css={css`
flex: 1;
padding: 16px;
`}
>
<h2>Preview</h2>
<div
css={theme === 'dark'
? css`
height: 100%;
border-radius: 4px;
border: none;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 1);
font-size: 100%;
line-height: inherit;
overflow: auto;
background: #000;
padding: 8px 16px;
color: #fff;
`
: css`
height: 100%;
border-radius: 4px;
border: none;
box-shadow: 2px 2px 10px #999;
font-size: 100%;
line-height: inherit;
overflow: auto;
background: #fff;
padding: 8px 16px;
color: #000;
`}
dangerouslySetInnerHTML={{__html: mardownFormattedContent}}
>
</div>
</ColumnFlex>
)
}
Preview.propTypes = {
markdownContent: PropTypes.string.isRequired
}
export default Preview;
In this component, we are compiling the markdown content and storing it in mardownFormattedContent variable. And to show a preview of the content in HTML, we will have to use
dangerouslySetInnerHTML
prop to display the HTML content directly into our DOM, which we are doing by adding thisdangerouslySetInnerHTML={{__html: mardownFormattedContent}}
prop for the div element.
Finally we'are ready with all the component which will be need to create our Markdown editor application. Let's bring all of them in our App.tsx
file.
/* src/App.tsx */
import React from 'react'
import { css, jsx } from '@emotion/core'
// Components
import Header from './components/Header'
import Main from './components/Main'
import Footer from './components/Footer';
import useDarkMode from './userDarkMode';
function App() {
const { theme, toggleTheme } = useDarkMode();
const themeStyles = theme === 'light'? {
backgroundColor: '#eee',
color: '#000'
}: {
backgroundColor: '#171616',
color: '#fff'
}
return (
<div
className="App"
style={themeStyles}
>
<Header theme={theme} toggleTheme={toggleTheme}/>
<Main theme={theme}/>
<Footer />
</div>
);
}
export default App;
In our App component, we are importing the child components and passing down the theme props.
Now, if you have followed all steps above, you'll have a running markdown editor application, for styles I have used, you can see my source code using then link I mentioned.
Now, its time to create Github actions for our project to create continuous deployment workflow on every push to master.
4 Setting up continuous deployments through Github Actions
We’ll be using Github actions workflow to build and deploy our web application on every push to master.
Since this is not a enterprise application that holds the branches for production and development, I will setup my workflow for master branch, but if in any time in future, you require to setup the Github action workflow for your enterprise application, Just be careful with the branches.
To do so, we’ll follow some steps:
- Create a folder in our project root directory
.github/workflows/
, this will hold all the workflows config. - We’ll be using
JamesIves/github-pages-deploy-action
action to deploy our application. - Next we’ll create our
.yml
file here, which will be responsible for the action to build and deploy our application to GitHub pages. Let’s name itbuild-and-deploy-to-gh-pages.yml
Let's see what goes inside this build-and-deploy-to-gh-pages.yml
# build-and-deploy-to-gh-pages.yml
name: Build & deploy to GitHub Pages
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Set email
run: git config --global user.email "${{ secrets.adminemail }}"
- name: Set username
run: git config --global user.name "${{ secrets.adminname }}"
- name: npm install command
run: npm install
- name: Run build command
run: npm run build
- name: Deploy
uses: JamesIves/github-pages-deploy-action@releases/v3
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_BRANCH: master
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: build # The folder the action should deploy.
This workflow will run every time, we push something into master and will deploy the application through gh-pages
branch.
Let's Breakdown the Workflow file
name: Build & deploy to GitHub Pages
on:
push:
branches:
- master
This defines our workflow name and trigger for running the jobs inside it. Here we are setting the trigger to listen to any Push
events in master
branch.
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Set email
run: git config --global user.email "${{ secrets.adminemail }}"
- name: Set username
run: git config --global user.name "${{ secrets.adminname }}"
- name: npm install command
run: npm install
- name: Run build command
run: npm run build
- name: Deploy
uses: JamesIves/github-pages-deploy-action@releases/v3
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_BRANCH: master
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: build # The folder the action should deploy.
This is the most important part in our workflow, which declares the jobs
to be done. Some of the lines in the config are self-explanatory runs-on: ubuntu-latest
it defines the system, it will be running on.
- name: Checkout
uses: actions/checkout@v1
This is an action for checking out a repo, and in later jobs we are setting our development environment by installing node and setting our git profile config. Then we are running npm install
to pull out all the dependencies and finally running the build
command.
- name: Deploy
uses: JamesIves/github-pages-deploy-action@releases/v3
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_BRANCH: master
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: build # The folder the action should deploy.
After the build command has been completed, we are using
JamesIves/github-pages-deploy-action@releases/v3
action to deploy our build folder togh-pages
.
Whenever, you'll push something in your master branch, this workflow will run and will deploy your static build folder to gh-pages
branch.
Now, when the deployment is completed, you'all have your app running at you github link https://yourusername.github.io/markdown-editor/.
Don't forget to add
"homepage" : "https://yourusername.github.io/markdown-editor/"
inpackage.json
file, otherwise serving of static contents may cause problem.
If you liked my article, you can follow me on Twitter for my daily paper The JavaSc®ipt Showcase
, also you can follow my personal projects over Github. Please post in comments, how do you like this article. Thanks!!
Liquid error: internal
Top comments (0)