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.
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.
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.
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
.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;
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;
});
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>
);
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>
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.
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";
}}
/>
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.
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.
Now lets add a little bit of spice...
Put emoji-picker in a popover, add message bubbles and some styling.
You can use this setup in React-Native with WebView.
Top comments (0)