How to build chat input with Slate and Emoji-Mart
Published: 2024-01-04 22:34:23

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
```

Install necessary packages.

```
npm i slate slate-react emoji-mart @emoji-mart/data @emoji-mart/react
```

Clear `App.tsx`, it should look like this.

```tsx
import React from "react";

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

export default App;
```

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

```tsx
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()...
```

Create the editor instance and add classes for styling.

```tsx
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>
  );
}
```

Lets add some styles to the `index.css`

```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;
  }
}

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

![Slate and emoji-mart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w5b0lcvn9duek94150k1.png)

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. 

```tsx
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;
  });
```

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

```tsx
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>
  );
```

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

```tsx
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>
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v2ekybnhb5brn1g681ly.gif)

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`.

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


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

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.path `length is 3. 
Otherwise it will move when user selects all with ctrl+a etc.

```tsx
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>
    );
  };
```

With the new Emoji component, it works pretty good.

![Emoji picker with slate](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/azqld01hnk7k3s761fqz.gif)

Now lets add a little bit of spice...

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


![Slate with emoji-mart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9tzfqbgov2k4alhnfc40.gif)

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