Embedding youtube videos in rich text documents with slate js
April 05, 2021
Embedding media such as youtube or vimeo links in a rich text document is a very common feature in rich text editors (in my case, it’s an often requested feature by clients).
In this post I’ll go through a pattern that I see used across projects, which is to render embedded media in iframes. In this case it’s a youtube video, but it could really be anything, like a tweet for example.
The finished example is available here
Okay, let’s get started ⬇️
1. Setup
At the time of writing, I’m using slate version ^0.59
, make sure you’re using this version to ensure nothing breaks (currently version ^0.60
is actually broken)
If you don’t have a react app already, please use create-react-app
(or something similar) to get started. I always include typescript for my projects but this is entirely optional.
npx create-react-app my-awesome-editor --template typescript
cd my-awesome-editor
Add the dependencies slate
, slate-react
and slate-history
to your React app.
yarn add slate slate-react slate-history
Now let’s add the boilerplate for your editor component, importing all the right dependencies and handling onChange events.
import React, { useMemo, useState } from "react";
import { createEditor, Node } from "slate";
import { withHistory } from "slate-history";
import { Editable, ReactEditor, Slate, withReact } from "slate-react";
export function MyEditor() {
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
const [value, setValue] = useState<Node[]>([
{
children: [{
text: ""
}],
},
]);
return <Slate editor={editor} onChange={setValue} value={value}>
<Editable placeholder="Write something..."/>
</Slate>
}
2. Add a slate element for youtube embeds
One of the three fundamental building blocks of a slate document are Block Elements. In their simplest form Block Elements are lines of text (or paragraphs), but they can also be non-text elements. All block elements are derived from this shape:
{
children: [{
text: ''
}]
}
To create our youtube element we add our own properties to this element. Youtube videos have id’s, so we add a videoId
alongside a type
for clarity.
{
type: 'youtube',
videoId: 'CvZjupLir-8',
children: [{
text: ''
}]
}
Update your default slate value to include this block. Next, we’ll tackle rendering this element ⬇
3. Rendering embeddable elements
In order to render the iframe we need to define the aptly named renderElement
prop of slate’s Editable
component like this:
<Editable
renderElement={({ attributes, element, children }) => {
if (element.type === 'youtube' && element.videoId != null) {
return <div
{...attributes}
contentEditable={false}
>
<iframe
src={`https://www.youtube.com/embed/${element.videoId}`}
aria-label="Youtube video"
frameBorder="0"
></iframe>
{children}
</div>
} else {
return <p {...attributes}>{children}</p>
}
}}
/>
If you’ve followed the steps so far, you should now see a youtube embed appear in your editor. Let’s break down what’s happening with our renderElement
method as shown above.
- In our
renderElement
method we check if the type of element is'youtube'
and if it is, we render our iframe. We construct the iframe src attribute by concatenating youtube’s embed url with with the video id. - Our
renderElement
callback must always render thechildren
prop as well as the elementattributes
which can be spread over a html element (Otherwise slate.js will error when you try to interact with the element). - If the element type isn’t
'youtube'
therenderElement
prop renders a paragraph by default. Slate will use therenderElement
method to render everyelement
in your document. - For non-text elements, we need to add
contentEditable={false}
to prevent the browser from adding a cursor to our content. - Don’t forget to add an
aria-label
or atitle
attribute to your iframe, otherwise screen-readers will not be able to make sense of it.
4. Treat 'youtube'
blocks as voids
By default slate assumes that every element has editable text. This is not the case for our youtube block.
To make sure slate behaves appropriately we need to override the editor.isVoid
method like so:
editor.isVoid = (el) => el.type === 'video'
For completeness, here’s the entire useMemo callback producing the editor prop for the Slate
component:
const editor = useMemo(() => {
const _editor = withHistory(withReact(createEditor()))
_editor.isVoid = (el) => el.type === 'youtube'
return _editor
}, [])
Now we’re rendering and handling this block correctly, but how does a user actually add a youtube block?
5. Inserting youtube blocks
To insert an element - we use slate’s Transforms
library, in particular, the insertNodes
method:
Transforms.insertNodes([{
type: 'youtube',
videoId,
children: [{
text: ''
}]
}])
However we still need the user interaction for input. Let’s add an onPaste
prop to our Editable component for this.
<Editable
onPaste={(event) => {
const pastedText = event.clipboardData?.getData('text')?.trim()
const youtubeRegex = /^(?:(?:https?:)?\/\/)?(?:(?:www|m)\.)?(?:(?:youtube\.com|youtu.be))(?:\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(?:\S+)?$/
const matches = pastedText.match(youtubeRegex)
if (matches != null) {
// the first regex match will contain the entire url,
// the second will contain the first capture group which is our video id
const [_, videoId] = matches
event.preventDefault()
Transforms.insertNodes(editor, [{
type: 'youtube',
videoId,
children: [{
text: ''
}]
}])
}
}}
renderElement={...}
/>
Let’s break this down:
First we retrieve the text we pasted:
const pastedText = event.clipboardData?.getData('text')?.trim()
To test if our pasted url is a youtube url and to capture the id from the url we use a regex. It’s not pretty but I prefer examples with as few dependencies as possible. If you do want something easier to read, you could use libraries like get-youtube-id
for this purpose.
If the regex matches we call event.preventDefault()
to prevent the pasted text from being inserted as text. Instead we insert a slate element of type 'youtube'
and with a video id. Now we can embed youtube videos in our document by simply pasting the link, anywhere.
That’s it, I hope you enjoyed this tutorial. If you have questions or an idea of what you’d like me to cover in my next tutorial, reach out on twitter - I’m always happy to hear from the community!