Learn how to create a Custom Image Component. For this example, we'll modify the ImageBlock
Component by adding a title
attribute to the Image.
Let's start by looking at then modifing the ImageBlockElementType
.
🌞 Even if you aren't using TypeScript, we recommend reading this section. You can probably follow the meaning of the type declarations (e.g.
id: string
means theid
property takes astring
type value).
Here is the type for the ImageBlockElementType
.
export type ImageBlockElementType = {
type: "image-block"
id: string
width: number
height: number
maxWidth: number
maxHeight: number
children: [{ text: "" }]
}
This Element
has its type
set to the exact string "image-block"
. This is a void
Element, so it has children
which is [{ text: "" }]
(a requirement for void
Elements).
We will be using some of Slate Cloud's built-in image handling (e.g. for image resizing and showing upload progress) which means that the Image element we create needs to follow the ImageFileInterface
. As you can see, all of the properties in the ImageFileInterface
are already present in the ImageBlockElementType
.
export interface ImageFileInterface {
id: string
width: number
height: number
maxWidth: number
maxHeight: number
}
Although the ImageFileInterface
is the minimum requirement we can add other properties we desire to our image Element
.
Let's create a new Element TitledImageBlockElementType
which renders an <img>
with a `title`` attribute.
Here's an type definition that includes a title
property:
export type TitledImageBlockElement = {
type: "titled-image-block"
id: string
width: number
height: number
maxWidth: number
maxHeight: number
children: [{ text: "" }]
}
Here's the Preset ImageBlock
Component.
import { RendeElementPropsFor, HostedImage } from "slate-portive"
export function ImageBlock({
attributes,
element,
children,
}: RenderElementPropsFor<ImageBlockElement>) {
return (
<div {...attributes} style={{ margin: "8px 0" }}>
<HostedImage
element={element}
style={{ borderRadius: element.size[0] < 100 ? 0 : 4 }}
/>
{children}
</div>
)
}
It may seem small. This is because the HostedImage
sub-component takes care of most of the hard work:
From the perspective of the ImageBlock
Component, we can treat it just like an img
tag and it can take any img
attributes like a "title"
attribute for example.
Let's modify this to create our Custom TitledImageBlock
Component:
import { RendeElementPropsFor, HostedImage } from "slate-portive"
export function TitledImageBlock({
attributes,
element,
children,
}: // ✅ Change `ImageBlockElement` to `TitledImageBlockElement`
RenderElementPropsFor<TitledImageBlockElement>) {
return (
<div {...attributes} style={{ margin: "8px 0" }}>
{/* ✅ Add `element.title` */}
<HostedImage
element={element}
style={{ borderRadius: element.size[0] < 100 ? 0 : 4 }}
title={element.title}
/>
{children}
</div>
)
}
Now our Custom Image can render the image with the title
attribute.
createImageFileElement
callbackWhen a user uploads an image, the createImageFileElement
function passed to withPortive
is called. In Getting Started withPortive
has these options:
const editor = withPortive(reactEditor, {
createImageFileElement: createImageBlock,
// ...
})
Here's the createImageBlock
method passed to the createImageFileElement
option:
export function createImageBlock(
e: CreateImageFileElementEvent
): ImageBlockElement {
return {
type: "image-block",
originKey: e.originKey, // ✅ sets originKey from the event
originSize: e.originSize, // ✅ sets originSize from the event
size: e.initialSize, // ✅ sets size from `initialSize` from the event
children: [{ text: "" }],
}
}
Here's what e
which is of type CreateImageFileElementEvent
looks like:
export type CreateImageFileElementEvent = {
type: "image"
originKey: string
originSize: [number, number]
initialSize: [number, number]
file: File
}
export type CreateImageFileElement = (
e: CreateImageFileElementEvent
) => Element & { originKey: string }
You can learn more about file
by reading the File
MDN web docs.
Let's use the file
object for our TitledImageBlock
:
export function createTitledImageBlock(
e: CreateImageFileElementEvent
// ✅ returns a `TitledImageBlockElement` instead
): TitledImageBlockElement {
return {
type: "titled-image-block",
title: e.file.name, // ✅ set the initial title value to the filename
originKey: e.originKey,
originSize: e.originSize,
size: e.initialSize,
children: [{ text: "" }],
}
}
Now it's just a matter of importing and using our new TitledImageBlock
. Here's the full source code...
import {
CreatedImageFileElementEvent,
RendeElementPropsFor,
HostedImage,
} from "slate-portive"
export type TitledImageBlockElement = {
type: "titled-image-block"
title: string // ✅ Add a `title` property for our titled image
originKey: string
originSize: [number, number]
size: [number, number]
children: [{ text: "" }]
}
export function createTitledImageBlock(
e: CreateImageFileElementEvent
// ✅ returns a `TitledImageBlockElement` instead
): TitledImageBlockElement {
return {
type: "titled-image-block",
title: e.file.name, // ✅ set the initial title value to the filename
originKey: e.originKey,
originSize: e.originSize,
size: e.initialSize,
children: [{ text: "" }],
}
}
export function TitledImageBlock({
attributes,
element,
children,
}: // ✅ Change `ImageBlockElement` to `TitledImageBlockElement`
RenderElementPropsFor<TitledImageBlockElement>) {
return (
<div {...attributes} style={{ margin: "8px 0" }}>
{/* ✅ Add `element.title` */}
<HostedImage
element={element}
style={{ borderRadius: element.size[0] < 100 ? 0 : 4 }}
title={element.title}
/>
{children}
</div>
)
}