React SimpleMDE (EasyMDE) Markdown Editor
!NPM versionnpm-badgenpmReact component wrapper for EasyMDE (the most fresh SimpleMDE fork).
Only two dependencies, React (peer) and EasyMDE (peer).
Built by @RIP21 👨💻
- Controlled usage - Options - Hotkeys - Custom preview rendering example - Events / Additional event listeners for events of CodeMirror - Autosaving - Retrieve
easymde
, codemirror
or cursor
info to be able to manipulate it.
- Basic testing
- Props
- All exports list
- New in v4
- New in v3
- New in v2Table of contents generated with markdown-toc
New in v5
- breaking Full rewrite to hooks. Means more reactive so, probably, less bugs related with updates. Minimum React version required
>=16.8.2
- breaking
easymde
now a peer dependency, please install it manually - breaking
label
prop has been removed - breaking SSR safe nets removed, please make sure to import it dynamically
- breaking
options
shall be memoized to prevent new instances from being created on each render and other related to that bugs (more on that below) - potentially-breaking Forwards
ref
, so you can easily get access todiv
wrapper by usingref
prop. - potentially-breaking Lots of bugs fixed, examples updated
- potentially-breaking
@babel/runtime
helpers are no longer inlined but imported.
Install
npm install --save react-simplemde-editor easymde
Note: Possibly you may need to install
@babel/runtime
, try without it, but if you don't have any issues, then you shouldn't.Demo
Hosted demoor to see it locally:
git clone https://github.com/RIP21/react-simplemde-editor.git
cd react-simplemde-editor
yarn install
yarn demo
open browser at localhost:3000
Usage
View the demo code for more examples.All examples below are in TypeScript
Uncontrolled usage
import React from "react";
import SimpleMDE from "react-simplemde-editor";
import "easymde/dist/easymde.min.css";
<SimpleMDE />;
Controlled usage
export const ControlledUsage = () => {
const [value, setValue] = useState("Initial value");
const onChange = useCallback((value: string) => {
setValue(value);
}, []);
return <SimpleMdeReact value={value} onChange={onChange} />;
};
Options
You can set API of SimpleMDE options which you pass down as aoptions
prop.
If you're using TypeScript it will be inferred by compiler.Note: if you don't specify a custom id it will automatically generate an id for you.
Note that you need to
useMemo
to memoize options
so they do not change on each rerender! It will affect behavior and performance
because then on each render of the parent that renders SimpleMdeReact
you'll get a new instance of the editor, which you definitely want to avoid!
Also, if you change options
on each value
change you will lose focus.
So, put options
as a const
outside of the component, or if options
shall be partially or fully set by props
make sure to useMemo
in
case of functional/hooks components, or class field for class
based components.
Slightly more on that here: #164export const UsingOptions = () => {
const [value, setValue] = useState("Initial");
const onChange = useCallback((value: string) => {
setValue(value);
}, []);
const autofocusNoSpellcheckerOptions = useMemo(() => {
return {
autofocus: true,
spellChecker: false,
} as SimpleMDE.Options;
}, []);
return (
<SimpleMdeReact
options={autofocusNoSpellcheckerOptions}
value={value}
onChange={onChange}
/>
);
};
Hotkeys
You can include key maps using theextraKeys
prop.
Read more at CodeMirror extra keysexport const UpdateableByHotKeys = () => {
const extraKeys = useMemo<KeyMap>(() => {
return {
Up: function (cm) {
cm.replaceSelection(" surprise. ");
},
Down: function (cm) {
cm.replaceSelection(" surprise again! ");
},
};
}, []);
const [value, setValue] = useState("initial");
const onChange = (value: string) => setValue(value);
return (
<SimpleMdeReact value={value} onChange={onChange} extraKeys={extraKeys} />
);
};
Custom preview rendering example
import ReactDOMServer from "react-dom/server";
export const CustomPreview = () => {
const customRendererOptions = useMemo(() => {
return {
previewRender() {
return ReactDOMServer.renderToString(
<ReactMarkdown
source={text}
renderers={{
CodeBlock: CodeRenderer,
Code: CodeRenderer,
}}
/>
);
},
} as SimpleMDE.Options;
}, []);
return (
<div>
<h4>Custom preview</h4>
<SimpleMdeReact options={customRendererOptions} />
</div>
);
};
Events / Additional event listeners for events of CodeMirror
See full list of events hereimport { SimpleMdeReact } from "react-simplemde-editor";
import type { SimpleMdeToCodemirrorEvents } from "react-simplemde-editor";
export const CustomEventListeners = () => {
const [value, setValue] = useState("Initial value");
const onChange = useCallback((value: string) => {
setValue(value);
}, []);
// Make sure to always `useMemo` all the `options` and `events` props to ensure best performance!
const events = useMemo(() => {
return {
focus: () => console.log(value),
} as SimpleMdeToCodemirrorEvents;
}, []);
return <SimpleMdeReact events={events} value={value} onChange={onChange} />;
};
Autosaving
export const Autosaving = () => {
const delay = 1000;
const autosavedValue = localStorage.getItem(`smde_demo`) || "Initial value";
const anOptions = useMemo(() => {
return {
autosave: {
enabled: true,
uniqueId: "demo",
delay,
},
};
}, [delay]);
return (
<SimpleMdeReact id="demo" value={autosavedValue} options={anOptions} />
);
};
Retrieve easymde
, codemirror
or cursor
info to be able to manipulate it.
export const GetDifferentInstances = () => {
// simple mde
const [simpleMdeInstance, setMdeInstance] = useState<SimpleMDE | null>(null);
const getMdeInstanceCallback = useCallback((simpleMde: SimpleMDE) => {
setMdeInstance(simpleMde);
}, []);
useEffect(() => {
simpleMdeInstance &&
console.info("Hey I'm editor instance!", simpleMdeInstance);
}, [simpleMdeInstance]);
// codemirror
const [codemirrorInstance, setCodemirrorInstance] = useState<Editor | null>(
null
);
const getCmInstanceCallback = useCallback((editor: Editor) => {
setCodemirrorInstance(editor);
}, []);
useEffect(() => {
codemirrorInstance &&
console.info("Hey I'm codemirror instance!", codemirrorInstance);
}, [codemirrorInstance]);
// line and cursor
const [lineAndCursor, setLineAndCursor] = useState<Position | null>(null);
const getLineAndCursorCallback = useCallback((position: Position) => {
setLineAndCursor(position);
}, []);
useEffect(() => {
lineAndCursor &&
console.info("Hey I'm line and cursor info!", lineAndCursor);
}, [lineAndCursor]);
return (
<div>
<h4>Getting instance of Mde and codemirror and line and cursor info</h4>
<SimpleMdeReact
value="Go to console to see stuff logged"
getMdeInstance={getMdeInstanceCallback}
getCodemirrorInstance={getCmInstanceCallback}
getLineAndCursor={getLineAndCursorCallback}
/>
</div>
);
};
Basic testing
Here is how you do it. It requires mock of certain browser pieces to work, but this is whole example.import { act, render, screen } from "@testing-library/react";
import { useState } from "react";
import { SimpleMdeReact } from "react-simplemde-editor";
import userEvent from "@testing-library/user-event";
// @ts-ignore
Document.prototype.createRange = function () {
return {
setEnd: function () {},
setStart: function () {},
getBoundingClientRect: function () {
return { right: 0 };
},
getClientRects: function () {
return {
length: 0,
left: 0,
right: 0,
};
},
};
};
const Editor = () => {
const [value, setValue] = useState("");
return <SimpleMdeReact value={value} onChange={setValue} />;
};
describe("Renders", () => {
it("succesfully", async () => {
act(() => {
render(<Editor />);
});
const editor = await screen.findByRole("textbox");
userEvent.type(editor, "hello");
expect(screen.getByText("hello")).toBeDefined();
});
});
API
Props
export interface SimpleMDEReactProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
id?: string;
onChange?: (value: string, changeObject?: EditorChange) => void;
value?: string;
extraKeys?: KeyMap;
options?: SimpleMDE.Options;
events?: SimpleMdeToCodemirrorEvents;
getMdeInstance?: GetMdeInstance;
getCodemirrorInstance?: GetCodemirrorInstance;
getLineAndCursor?: GetLineAndCursor;
placeholder?: string;
textareaProps?: Omit<
React.HTMLAttributes<HTMLTextAreaElement>,
"id" | "style" | "placeholder"
>;
}
All exports list
default
- SimpleMdeReact SimpleMdeReact
- same as default
but namedTypes:
SimpleMdeReactProps
- props of the component DOMEvent
- certain events that are used to get events exported below CopyEvents
- only copy codemirror events GlobalEvents
- some other global codemirror events DefaultEvent
- default codemirror event handler function IndexEventsSignature
- index signature that expects string as key and returns DefaultEvent
SimpleMdeToCodemirrorEvents
- manually crafted events (based off @types/codemirror@0.0.109
that easymde
uses internally) +
all the above merged together into whole mapping between Codemirror event names and actual handlers for
events
prop GetMdeInstance
- signature of the callback function that retrieves mde instance GetCodemirrorInstance
- signature of the callback function that retrieves codemirror instance GetLineAndCursor
- signature of the callback function that retrieves line and cursor info Changelog
New in v4
instead ofsimplemde
itself. Possible breaking changes, so I bumped version to v4.- One obvious breaking change. Is how CSS is have to be imported. It used to be
simplemde/dist/simplemde.min.css
now it will beeasymde/dist/easymde.min.css
New in v3
- The
initialValue
prop has been removed and replaced with avalue
prop, allowing direct changes to the value to be made after the component mounts. - v3.6.8 if rendering server-side, you can set static ids to avoid errors in rendering synchronization.
- v3.6.17 TypeScript typings added.
- v3.6.19 All props will be passed to the wrapper now (except a id, onChange and few others that are ignored)
- v3.6.21 React 17 support (UNSAFE methods are no longer used)