Portive

Slate Cloud

Saving to a Database

Introduction to Saving

The Slate Documentation on Saving to a Database takes the Editor value and saves that value which is plain JSON to a database.

Slate Cloud supports asynchronous uploads which allows users to keep editing while files are still uploading which is a useful feature; however, one side effect of this is that you can't just save the value because there may be unfinished uploads in the Slate document.

Slate Cloud includes functions which help you get a valid value from Slate.

Saving in Slate Cloud

Saving methods

With Slate Cloud there are two methods which return a valid value which you can save to a database:

  1. editor.cloud.normalize: This returns the Slate value with all unfinished uploads removed.

  2. editor.cloud.save: This is an async function (i.e. it returns a Promise) that waits until all uploads are finished before returning the Slate value. It also has a timeout option to prevent a save from taking too long. If the timeout is reached, it returns a valid version of the document with any unfinished uploads removed.

If you do not use one of these two methods and instead just save Slate's value, then after you open the document again, the images and attachments will be invalid. This is because in addition to making sure the document is in a valid state, it replaces the id which is a non-sensical reference in a lookup like #i3h54 to the actual url of the uploaded file like https://files.portive.com/f/demo/oqcjnuoy7w65ltojqkwdx--508x362.png.

Using editor.cloud.normalize

This is a good way to get Slate's value for saving when you don't want to wait for uploads to finish.

The normalize method returns the Slate Editor's value with any elements that haven't finished uploading removed.

This is useful in a scenario where you want a snapshot of the document right now like if you want to save a draft of the document.

JavaScript
import { useCallback, useState } from "react"
import { createEditor } from "slate"
import { withHistory } from "slate-history"
import { Editable, Slate, withReact } from "slate-react"
import { withCloud } from "slate-cloud"
import { CloudComponents } from "slate-cloud/cloud-components"
// ✅ Add `CloudComponents.withRenderElement` plugin on `renderElement`
const renderElement = CloudComponents.withRenderElement((props) => {
const { element } = props
if (element.type === "paragraph") {
return <p {...props.attributes}>{props.children}</p>
}
throw new Error(`Unhandled element type ${element.type}`)
})
export default function Page() {
const [editor] = useState(() => {
const basicEditor = withHistory(withReact(createEditor()))
// ✅ Add `withCloud` plugin on `Editor` object to enable uploads
const cloudEditor = withCloud(basicEditor, {
apiKey: "MY_API_KEY",
})
// ✅ Add `CloudComponents.withEditor` plugin on `Editor` object
CloudComponents.withEditor(cloudEditor)
return cloudEditor
})
const normalize = useCallback(() => {
// get the `editor.value` with incomplete uploads removed
const value = editor.cloud.normalize()
console.log(value)
}, [editor])
return (
<Slate
editor={editor}
value={[{ type: "paragraph", children: [{ text: "Hello World" }] }]}
>
<button onClick={normalize}>Normalize</button>
<Editable
renderElement={renderElement}
// ✅ Add `editor.cloud.handlePaste` to `Editable onPaste`
onPaste={editor.cloud.handlePaste}
// ✅ Add `editor.cloud.handleDrop` to `Editable onDrop`
onDrop={editor.cloud.handleDrop}
/>
</Slate>
)
}

Using editor.cloud.save

This is a way to get Slate's value when you want to wait for uploads to complete first. Call await editor.cloud.save which returns an object that contains the document value.

function save(options?: SaveOptions) => Promise<SaveResult>

type SaveOptions = { maxTimeooutInMs?: number }

type SaveResult =
  | { status: "timeout"; value: Descendant[]; finishes: Promise<Upload>[] }
  | { status: "complete"; value: Descendant[] }

The method takes an optional { maxTimeoutInMs: number } option. If the files aren't finished uploading by the timeout, a normalized document value will be return with the unfinished Elements removed.

Even though some incomplete elements are remove, the document value is in a valid state.

🌞 Note that when a normalized document is returned, it won't remove the Elements from the Editor. Any uploads will continue uploading after save is called.

JavaScript
import { useCallback, useState } from "react"
import { createEditor } from "slate"
import { withHistory } from "slate-history"
import { Editable, Slate, withReact } from "slate-react"
import { withCloud } from "slate-cloud"
import { CloudComponents } from "slate-cloud/cloud-components"
// ✅ Add `CloudComponents.withRenderElement` plugin on `renderElement`
const renderElement = CloudComponents.withRenderElement((props) => {
const { element } = props
if (element.type === "paragraph") {
return <p {...props.attributes}>{props.children}</p>
}
throw new Error(`Unhandled element type ${element.type}`)
})
export default function Page() {
const [editor] = useState(() => {
const basicEditor = withHistory(withReact(createEditor()))
// ✅ Add `withCloud` plugin on `Editor` object to enable uploads
const cloudEditor = withCloud(basicEditor, {
apiKey: "MY_API_KEY",
})
// ✅ Add `CloudComponents.withEditor` plugin on `Editor` object
CloudComponents.withEditor(cloudEditor)
return cloudEditor
})
const normalize = useCallback(async () => {
// waits for up to 10s for uploads to finish and returns a result object
const result = await editor.cloud.save({ maxTimeoutInMs: 10000 })
console.log(result.value)
}, [editor])
return (
<Slate
editor={editor}
value={[{ type: "paragraph", children: [{ text: "Hello World" }] }]}
>
<button onClick={normalize}>Normalize</button>
<Editable
renderElement={renderElement}
// ✅ Add `editor.cloud.handlePaste` to `Editable onPaste`
onPaste={editor.cloud.handlePaste}
// ✅ Add `editor.cloud.handleDrop` to `Editable onDrop`
onDrop={editor.cloud.handleDrop}
/>
</Slate>
)
}

We also recommend putting the editor into readOnly mode so the user can't editing while a save is in progress.