DEV Community

Cover image for Tackle building a Rich Text Editor with Remirror and React
Conner Ow
Conner Ow

Posted on

Tackle building a Rich Text Editor with Remirror and React

Sweat. Blood. Tears.

Staring at a screen for countless hours

Rich Text Editors are one of the most challenging aspects of frontend development

I pushed through the pain and built it so you won't have to

Am fine btw

Let's dive in

What we're building

Sike.

Let's get a topological view of what you'll build first

You will build a <ContentEditor/> component that connects to a useContentEditor hook

The hook will allow getting and setting of the markdown content

Component usage

Live Demo. Take it for a spin

Seems pretty straightforward, right?

Let's build.

Environment Setup

Spin up a React Repl on Replit.

Alternatively, create a new react app with create-react-app.

The Component

Install @remirror/react and remirror via npm.

npm install remirror @remirror/react
Enter fullscreen mode Exit fullscreen mode

Copy. Paste. Docs if you want

TL;DR, we're just importing and rendering the Remirror component from @remirror/react

The heavy lifting will be done by the useContentEditor hook

import { EditorComponent, Remirror, ReactExtensions, UseRemirrorReturn } from "@remirror/react";
import { AnyExtension, RemirrorEventListener } from "remirror";

export function ContentEditor({
  editor: { manager, state },
  onChange,
}: {
  editor: UseRemirrorReturn<ReactExtensions<AnyExtension>>;
  onChange: RemirrorEventListener<AnyExtension>;
}) {
  return (
    <div>
      <Remirror
        manager={manager}
        state={state}
        onChange={onChange}
        autoFocus
      >
        <EditorComponent />
      </Remirror>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Extensions

Editor functionality is based on Remirror Extensions.

BoldExtension allows us to use bold text formatting

ItalicExtension likewise

Self-explanatory.

Create a file extensions.ts and paste the extension config for allowing most aspects of basic markdown syntax.

import { textblockTypeInputRule } from "prosemirror-inputrules";
import { AnyExtension, ExtensionPriority } from "remirror";
import {
  BlockquoteExtension,
  BoldExtension,
  BulletListExtension,
  CodeBlockExtension,
  CodeExtension,
  HardBreakExtension,
  HeadingExtension,
  ItalicExtension,
  LinkExtension,
  ListItemExtension,
  MarkdownExtension,
  OrderedListExtension,
  PlaceholderExtension,
  StrikeExtension,
  TextHighlightExtension,
  TrailingNodeExtension,
} from "remirror/extensions";
import languages from "./languages";

/**
 * Modifies the Code Block extension creation behavior. Creates a code block after language specification + space/line break.
 */
class ExtendedCodeBlockExtension extends CodeBlockExtension {
  createInputRules() {
    const regexp = /^```
{% endraw %}
([a-zA-Z]+)?(\n|\s)/;
    return [textblockTypeInputRule(regexp, this.type)];
  }
}

/**
 * A list of allowed Remirror Extensions
 */
const extensions =
  (placeholder: string = "") =>
    (): Array<AnyExtension> => [
      new BoldExtension(),
      new ItalicExtension(),
      new HeadingExtension(),
      new CodeExtension(),
      new BlockquoteExtension(),
      new LinkExtension({ autoLink: true }),
      new StrikeExtension(),
      new BulletListExtension({ enableSpine: true }),
      new OrderedListExtension(),
      new ListItemExtension({
        priority: ExtensionPriority.High,
        enableCollapsible: true,
      }),
      new ExtendedCodeBlockExtension({
        supportedLanguages: languages,
      }),
      new TrailingNodeExtension(),
      new MarkdownExtension({
        copyAsMarkdown: false,
      }),
      new HardBreakExtension(),
      new TextHighlightExtension(),
      new PlaceholderExtension({
        placeholder,
      }),
    ];

export default extensions;
{% raw %}

Enter fullscreen mode Exit fullscreen mode

Why am I extending CodeBlockExtension?

The default config is less user-friendly and a bit whacky.

A little bit of placeholder drama, but it makes future configuration easy.

Syntax Highlighting

This was half the headache for me.

Took forever to figure it out, but I did it.

Create a file languages.ts and paste the following.


tsx
import clike from "refractor/lang/clike.js";
import go from "refractor/lang/go.js";
import java from "refractor/lang/java.js";
import kotlin from "refractor/lang/kotlin.js";
import md from "refractor/lang/markdown.js";
import php from "refractor/lang/php.js";
import python from "refractor/lang/python.js";
import ruby from "refractor/lang/ruby.js";
import rust from "refractor/lang/rust.js";
import swift from "refractor/lang/swift.js";
import typescript from "refractor/lang/typescript.js";
import html from "refractor/lang/xml-doc.js";

const languages = [
  typescript,
  md,
  python,
  html,
  clike,
  java,
  php,
  swift,
  kotlin,
  ruby,
  go,
  rust,
];

export default languages;


Enter fullscreen mode Exit fullscreen mode

Straightforward.

refractor should have been pre-installed with remirror.

The hook

This is where everything comes together.

Let's start with imports.


ts
import remirrorExtensions from "./extensions";
import { ReactExtensions, useRemirror, UseRemirrorReturn } from "@remirror/react";
import { useCallback, useState } from "react";
import { AnyExtension, RemirrorEventListener, RemirrorEventListenerProps } from "remirror";


Enter fullscreen mode Exit fullscreen mode

And I'm sorry for not separating type imports.

Oh no, anyway

Next, return type.


ts
export interface UseContentEditorReturnType {
  editor: UseRemirrorReturn<ReactExtensions<AnyExtension>>;
  onChange: RemirrorEventListener<AnyExtension>;
  content: string;
  setContent: (content: string) => void;
}


Enter fullscreen mode Exit fullscreen mode

editor: Modified Remirror utility

onChange: Also Modified Remirror utility

content: markdown content

setContent: to control and set the editor content state.

Create the hook function.


ts
export function useContentEditor(
  // Initial value
  value: string,
  // Optional arguments
  args?: {
    placeholder?: string;
  }
): UseContentEditorReturnType {

}


Enter fullscreen mode Exit fullscreen mode

Red squiggly lines.

Endure it.

Create a useState hook for the markdown content

Load the extensions

Configure a new remirror editor instance


ts
const [content, setMarkdownContent] = useState(value);

const extensions = remirrorExtensions(args?.placeholder);

const editor = useRemirror({
  extensions,
  stringHandler: "markdown",
  content: value,
  selection: "start",
});

const { onChange, manager } = editor;


Enter fullscreen mode Exit fullscreen mode

Create a modified onEditorChange function to update the markdown string state



const onEditorChange = useCallback(
  (
    parameter: RemirrorEventListenerProps<
      ReactExtensions<ReturnType<typeof extensions>[number]>
    >,
  ) => {
    const markdownContent = parameter.helpers.getMarkdown(parameter.state);
    setMarkdownContent(markdownContent);
    onChange(parameter);
  },
  [onChange],
);


Enter fullscreen mode Exit fullscreen mode

Create a setContent function that sets the markdown state and updates the editor


ts
const setContent = useCallback(
  (value: string) => {
    manager.view.updateState(
      manager.createState({
        content: value,
        selection: "end",
        stringHandler: "markdown",
      }),
    );
    setMarkdownContent(value);
  },
  [manager],
);


Enter fullscreen mode Exit fullscreen mode

Return.



return {
  editor,
  onChange: onEditorChange,
  setContent,
  content,
};


Enter fullscreen mode Exit fullscreen mode

Wrapping up

Let's put everything together.

A little different than the overview in the beginning.

Adjust import paths if necessary.


tsx
import { ContentEditor } from './ContentEditor'
import { useContentEditor } from './ContentEditor/useContentEditor'

const initialValue = `This is a rich text editor demo.

It supports **bold**, _italic_, \`code\`, and ~~strikethrough~~ formatting.

> Blockquotes are supported as well

\`\`\`js
// And so are
console.log("Code blocks with syntax highlighting");
\`\`\`

- Lists
- Work
- As well

1. And numbered ones
2. Behave similarly`

export default function App() {
  const { editor, onChange, content, setContent } = useContentEditor(initialValue);

  return (
    <div className="container">
      <h1>Rich text editor demo</h1>

      <div>
        <button onClick={() => setContent(initialValue)}>Reset</button>
      </div>
      <div className="content">
        <ContentEditor editor={editor} onChange={onChange} />

        <pre className="markdown-content">{content}</pre>
      </div>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

I forgot the CSS.

That's yours to take care of

Or go ahead and use mine

We're done

You've just built on of the most challenging things in the entirety of web development.


Let's get in touch.

Top comments (0)