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: stringmeans theidproperty takes astringtype 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>
)
}