DEV Community

Burak Tarım
Burak Tarım

Posted on

How to build chat input with Slate and Emoji-Mart

In this article we are going to build a chat text input with emojis using Slate and Emoji-Mart.

First create an React project with create-react-app.

npx create-react-app chat-input --template typescript
Enter fullscreen mode Exit fullscreen mode

Install necessary packages.

npm i slate slate-react emoji-mart @emoji-mart/data @emoji-mart/react
Enter fullscreen mode Exit fullscreen mode

Clear App.tsx, it should look like this.

import React from "react";

function App() {
  return <div></div>;
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now import the packages and create an initial value for the Slate editor.

import React from "react";
import { createEditor, Transforms } from "slate";
import { Slate, Editable, withReact, DefaultElement, useSelected } from "slate-react";
import data from "@emoji-mart/data";
import Picker from "@emoji-mart/react";

const initialValue = [
  {
    type: "paragraph",
    children: [{ text: "" }],
  },
];

function App()...
Enter fullscreen mode Exit fullscreen mode

Create the editor instance and add classes for styling.

function App() {
  const [editor] = React.useState(() => withReact(createEditor()));

  return (
    <div className="container">
      <Picker data={data} onEmojiSelect={console.log} />
      <Slate editor={editor} initialValue={initialValue}>
        <Editable placeholder="Your message..." className="editable" />
      </Slate>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Lets add some styles to the index.css


.editable {
  outline: none;
  border: 1px solid rgba(0, 0, 0, 0.1);
  padding: 0.5rem;
}

.container {
  padding-right: 15px;
  padding-left: 15px;
  margin-right: auto;
  margin-left: auto;
}

@media (min-width: 768px) {
  .container {
    width: 750px;
  }
}

@media (min-width: 992px) {
  .container {
    width: 970px;
  }
}

@media (min-width: 1200px) {
  .container {
    width: 1170px;
  }
}

Enter fullscreen mode Exit fullscreen mode

Start the app with npm run start
Open http://localhost:3000 and it should look like this;

Slate and emoji-mart

To render and insert the emojis, we need to define a custom emoji element.

Custom emeoji element should be void and inline. To do that we will modify the editor instance.

const [editor] = React.useState(() => {
    const editor = withReact(createEditor());
    const { isVoid, isInline } = editor;

    editor.isVoid = (element: any) =>
      element.type === "emoji" ? true : isVoid(element);

    editor.isInline = (element: any) =>
      element.type === "emoji" ? true : isInline(element);

    return editor;
  });
Enter fullscreen mode Exit fullscreen mode

Slate editor needs renderElement function to render custom elements. Create Emoji component and renderElement function.

const Emoji = (props: any) => (
    <span {...props.attributes}>
      {props.children}
      <img
        width={20}
        src={`https://cdn.jsdelivr.net/npm/emoji-datasource-apple@14.0.0/img/apple/64/${props.element.emoji.unified}.png`}
      />
    </span>
  );

const renderElement = React.useCallback((props: any) => {
    switch (props.element.type) {
      case "emoji":
        return <Emoji {...props} />;
      default:
        return <DefaultElement {...props} />;
    }
  }, []);

return (
    <div className="container">
      <Picker data={data} onEmojiSelect={console.log} />
      <Slate editor={editor} initialValue={initialValue}>
        <Editable renderElement={renderElement} placeholder="Your message..." className="editable" />
      </Slate>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

To insert emojis to Slate we will create a function and use it on Emoji-mart's onEmojiSelect callback.

const insertEmoji = (emoji: any) => {
    Transforms.insertNodes(editor, [
      {
        type: "emoji",
        emoji,
        children: [{ text: "" }],
      } as any,
    ]);
  };

return (
    <div className="container">
      <Picker data={data} onEmojiSelect={insertEmoji} />
      <Slate editor={editor} initialValue={initialValue}>
        <Editable
          renderElement={renderElement}
          placeholder="Your message..."
          className="editable"
        />
      </Slate>
    </div>
Enter fullscreen mode Exit fullscreen mode

Now we can select an emoji from emoji-picker and insert it to Slate.
But there is an issue.
Since our emoji element is void and its selectable. When you move with arrow keys Slate will focus to emojis.

Emoji picker with slate

To prevent that we will use useSelected hook in our Emoji component and keep track of anchor direction with useRef.

To define if the direction is reverse or not we are going to create a ref and update its value on onKeyDownCapture.

function App(){
...
const reverse = React.useRef(false);
...


return (...
<Editable
   renderElement={renderElement}
   placeholder="Your message..."
   className="editable"
   onKeyDownCapture={(e) => {
     reverse.current = e.key === "ArrowLeft";
   }}
  />
Enter fullscreen mode Exit fullscreen mode

Now lets modify Emoji component and add useEffect hook to move the anchor when its selected.

When selected status is changed, useEffect hook will execute.
We want to move the anchor when its selected and anchor.pathlength is 3.
Otherwise it will move when user selects all with ctrl+a etc.

const Emoji = (props: any) => {
    const selected = useSelected();

    React.useEffect(() => {
      const selection = editor.selection;
      if (selected && selection?.anchor.path.length === 3) {
        Transforms.move(editor, {
          unit: "offset",
          reverse: reverse.current,
        });
      }
    }, [selected]);

    return (
      <span {...props.attributes}>
        {props.children}
        <img
          width={20}
          src={`https://cdn.jsdelivr.net/npm/emoji-datasource-apple@14.0.0/img/apple/64/${props.element.emoji.unified}.png`}
        />
      </span>
    );
  };
Enter fullscreen mode Exit fullscreen mode

With the new Emoji component, it works pretty good.

Emoji picker with slate

Now lets add a little bit of spice...

Put emoji-picker in a popover, add message bubbles and some styling.

Slate with emoji-mart

You can use this setup in React-Native with WebView.

Top comments (0)