mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-04-22 14:21:54 +02:00
add rendering and schema guides
This commit is contained in:
parent
56a1f69a2f
commit
c9f1168cd7
@ -16,8 +16,10 @@
|
||||
## Guides
|
||||
|
||||
- [Changes](./guides/changes.md)
|
||||
- [Plugins](./guides/plugins.md)
|
||||
- [Data Model](./guides/data-model.md)
|
||||
- [Plugins](./guides/plugins.md)
|
||||
- [Rendering](./guides/rendering.md)
|
||||
- [Schemas](./guides/schemas.md)
|
||||
|
||||
|
||||
## General
|
||||
|
148
docs/guides/rendering.md
Normal file
148
docs/guides/rendering.md
Normal file
@ -0,0 +1,148 @@
|
||||
|
||||
# Rendering
|
||||
|
||||
One of the best parts of Slate is that it is built with React, so it fits right into your existing application. It doesn't re-invent its own view layer that you have to learn. It tries to keep everything as React-y as possible.
|
||||
|
||||
To that end, Slate gives you control over the rendering behavior of every node and mark in your document, any placeholders you want to render, and even the top-level editor itself.
|
||||
|
||||
You can define these behaviors by passing `props` into the editor, or you can define them in Slate plugins.
|
||||
|
||||
|
||||
## Nodes & Marks
|
||||
|
||||
Using custom components for the nodes and marks is the most common rendering need. Slate makes this easy to do, you just define a `renderNode` function.
|
||||
|
||||
The function is called with the node's props, including `props.node` which is the node itself. You can use these to determine what to render. For example, you can render nodes using simple HTML elements:
|
||||
|
||||
```js
|
||||
function renderNode(props) {
|
||||
const { node, attributes, children } = props
|
||||
|
||||
switch (node.type) {
|
||||
case 'paragraph': return <p {...attributes}>{children}</p>
|
||||
case 'quote': return <blockquote {...attributes}>{children}</blockquote>
|
||||
case 'image': {
|
||||
const src = node.data.get('src')
|
||||
return <img {...attributes} src={src} />
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 🤖 Be sure to mix in `props.attributes` and render `props.children` in your node components! The attributes required for utilities like Slate's `findDOMNode`, and the children are the actual text content of your nodes.
|
||||
|
||||
Or course, you don't have to use simple HTML elements, you can use your own custom React components too:
|
||||
|
||||
```js
|
||||
function renderNode(props) {
|
||||
switch (props.node.type) {
|
||||
case 'paragraph': <ParagraphComponent {...props} />
|
||||
case 'quote': <QuoteComponent {...props} />
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And you can just as easily put that `renderNode` logic into a plugin, and pass that plugin into your editor instead:
|
||||
|
||||
```js
|
||||
function SomeRenderingPlugin() {
|
||||
return {
|
||||
renderNode(props) {
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const plugins = [
|
||||
SomeRenderingPlugin(),
|
||||
...
|
||||
]
|
||||
|
||||
<Editor
|
||||
plugins={plugins}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
Marks work the same way, except they invoke the `renderMark` function. Like so:
|
||||
|
||||
```js
|
||||
function renderMark(props) {
|
||||
const { children, mark } = props
|
||||
switch (mark.type) {
|
||||
case 'bold': return <strong>{children}</strong>
|
||||
case 'italic': return <em>{children}</em>
|
||||
case 'code': return <code>{children}</code>
|
||||
case 'underline': return <u>{children}</u>
|
||||
case 'strikethrough': return <strike>{children}</strike>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That way, if you happen to have a global stylesheet that defines `strong`, `em`, etc. styles then your editor's content will already be formatted!
|
||||
|
||||
> 🤖 Be aware though that marks aren't guaranteed to be "contiguous". Which means even though a **word** is bolded, it's not guaranteed to render as a single `<strong>` element. If some of its characters are also italic, it might be split up into multiple elements—one `<strong>wo</strong>` and one `<em><strong>rd</strong></em>`.
|
||||
|
||||
|
||||
## Placeholders
|
||||
|
||||
By default Slate will render a placeholder for you which mimics the native DOM `placeholder` attribute of `<input>` and `<textarea>` elements—it's in the same typeface as the editor, and it's slightly translucent. And as soon as the document has any content, the placeholder disappears.
|
||||
|
||||
However sometimes you want to customize things. Or maybe you want to render placeholders inside specific blocks like inside an image caption. To do that, you can define your own `renderPlaceholder` function:
|
||||
|
||||
```js
|
||||
function renderPlaceholder(props) {
|
||||
const { node, editor } = props
|
||||
if (node.kind != 'block') return
|
||||
if (node.type != 'caption') return
|
||||
if (node.text != '') return
|
||||
|
||||
return (
|
||||
<span
|
||||
contenteditable={false}
|
||||
style={{ display: 'inline-block', width: '0', whiteSpace: 'nowrap', opacity: '0' }}
|
||||
>
|
||||
{editor.props.placeholder}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
<Editor
|
||||
renderPlaceholder={renderPlaceholder}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
That will render a simple placeholder element inside all of the your `caption` blocks until someone decides to write in a caption.
|
||||
|
||||
|
||||
## The Editor Itself
|
||||
|
||||
Not only can you control the rendering behavior of the components inside the editor, but you can also control the rendering of the editor itself.
|
||||
|
||||
This sounds weird, but it can be pretty useful if you want to render additional top-level elements from inside a plugin. To do so, you use the `renderEditor` function:
|
||||
|
||||
```js
|
||||
function renderEditor(props) {
|
||||
const { children, editor } = props
|
||||
const wordCount = countWords(editor.value.text)
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
<span className="word-count">{wordCount}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<Editor
|
||||
renderEditor={renderEditor}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
Here we're rendering a small word count number underneath all of the content of the editor. Whenever you change the content of the editor, `renderEditor` will be called, and the word count will be updated.
|
||||
|
||||
This is very similar to how higher-order components work! Except it allows each plugin in Slate's plugin stack to add to wrap the editor's children.
|
||||
|
||||
> 🤖 Be sure to remember to render `children` in your `renderEditor` functions, because that contains the editor's own elements!
|
114
docs/guides/schemas.md
Normal file
114
docs/guides/schemas.md
Normal file
@ -0,0 +1,114 @@
|
||||
|
||||
# Schemas
|
||||
|
||||
One of Slate's principles is that it doesn't assume anything about the type of content you're building an editor for. Some editors will want **bold**, _italic_, ~~strikethrough~~, and some won't. Some will want comments and highlighting, some won't. You _can_ build all of these things with Slate, but Slate doesn't assume anything out of the box.
|
||||
|
||||
This turns out to be extremely helpful when building complex editors, because it means you have full control over your content—you are never fighting with assumptions that the "core" library has made.
|
||||
|
||||
That said, just because Slate is agnostic doesn't mean you aren't going to need to enforce a "schema" for your documents.
|
||||
|
||||
To that end, Slate provides a `Schema` model, which allows you to easily define validations for the structure of your documents, and to fix them if the document ever becomes invalid. This guide will show you how they work.
|
||||
|
||||
|
||||
## Basic Schemas
|
||||
|
||||
Slate schemas are defined as Javascript objects, with properties that describe the document, block nodes, and inlines nodes in your editor. Here's a simple schema:
|
||||
|
||||
```js
|
||||
const schema = {
|
||||
document: {
|
||||
nodes: [
|
||||
{ types: ['paragraph', 'image'] }
|
||||
]
|
||||
},
|
||||
blocks: {
|
||||
paragraph: {
|
||||
nodes: [
|
||||
{ kinds: ['text'] }
|
||||
]
|
||||
},
|
||||
image: {
|
||||
isVoid: true,
|
||||
data: {
|
||||
src: v => v && isUrl(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 🤖 Internally, Slate instantiates schemas as immutable `Schema` models, but you don't have to worry about that. In user-land schemas can always be defined as plain Javascript objects, and you can let Slate handle the rest.
|
||||
|
||||
Hopefully just by reading this definition you'll understand what kinds of blocks are allowed in the document and what properties they can have—schemas are designed to prioritize legibility.
|
||||
|
||||
This schema defines a document that only allows `paragraph` and `image` blocks. In the case of `paragraph` blocks, they can only contain text nodes. And in the case of `image` blocks, they are always void nodes with a `data.src` property that is a URL. Simple enough, right?
|
||||
|
||||
That magic is that by passing a schema like this into your editor, it will automatically "validate" the document when changes are made, to make sure the schema is being adhered to. If it is, great. But if it isn't, and one of the nodes in the document is invalid, the editor will automatically "normalize" the node, to make the document valid again.
|
||||
|
||||
This way you can guarantee that the data is in a format that you expect, so you don't have to handle tons of edge-cases or invalid states in your own code.
|
||||
|
||||
|
||||
## Custom Normalizers
|
||||
|
||||
By default, Slate will normalize any invalid states to ensure that the document is valid again. However, since Slate doesn't have that much information about your schema, its default normalization techniques might not always be what you want.
|
||||
|
||||
For example, with the above schema, if a block that isn't a `paragraph` or an `image` is discovered in the document, Slate will simply remove it.
|
||||
|
||||
But you might want to preserve the node, and instead just convert it to a `paragraph`, this way you aren't losing whatever the node's content was. Slate doesn't know those kinds of specifics about your data model, and trying to express all of these types of preferences in a declarative schema is a huge recipe for complexity.
|
||||
|
||||
Instead, Slate lets you define your own custom normalization logic.
|
||||
|
||||
```js
|
||||
const schema = {
|
||||
document: {
|
||||
nodes: [
|
||||
{ types: ['paragraph', 'image'] }
|
||||
],
|
||||
normalize: (change, reason, context) => {
|
||||
if (reason == 'child_type_invalid') {
|
||||
change.setNodeByKey(context.child.key, { type: 'paragraph' })
|
||||
}
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
That's an example of defining your own custom `normalize` option for the document validation. If the invalid reason is `child_type_invalid`, it will set the child to be a `paragraph`.
|
||||
|
||||
When Slate discovers and invalid child, it will first check to see if your custom normalizer handles that case, and if it does Slate won't do any of its default behavior. That way you can opt-in to customizing the normalization logic for specific cases, without having to re-implement all of the defaults yourself.
|
||||
|
||||
This gives you the best of both worlds. You can write simple, terse, declarative validation rules that can be highly optimized. But you can still define fine-grained, imperative normalization logic for when invalid states occur.
|
||||
|
||||
> 🤖 For a full list of validation `reason` arguments, check out the [`Schema` reference](../reference/slate/schema.md).
|
||||
|
||||
|
||||
## Custom Validations
|
||||
|
||||
Sometimes though, the declarative validation syntax isn't fine-grained enough to handle a specific piece of validation. That's okay, because you can actually define schema validations in Slate as regular functions when you need more control, using the `validateNode` property of plugins and editors.
|
||||
|
||||
> 🤖 Actually, under the covers the declarative schemas are all translated into `validateNode` functions too!
|
||||
|
||||
When you define a `validateNode` function, you either return nothing if ths node's already valid, or you return a normalizer function that will make the node valid if it isn't. Here's an example:
|
||||
|
||||
```js
|
||||
function validateNode(node) {
|
||||
if (node.kind != 'block') return
|
||||
if (node.isVoid) return
|
||||
|
||||
const { nodes } = node
|
||||
if (nodes.size != 3) return
|
||||
if (nodes.first().kind != 'text') return
|
||||
if (nodes.last().kind != 'text') return
|
||||
|
||||
return (change) => {
|
||||
change.removeNodeByKey(node.key)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This validation defines a very specific (honestly, useless) behavior, where if a node is block, non-void and has three children, the first and last of which are text nodes, it is removed. I don't know why you'd ever do that, but the point is that you can get very specific with your validations this way. Any property of the node can be examined.
|
||||
|
||||
When you need this level of specificity, using the `validateNode` property of the editor or plugins is handy.
|
||||
|
||||
However, only use it when you absolutely have to. And when you do, you need to be aware of its performance. `validateNode` will be called **every time the node changes**, so it should be as performant as possible. That's why the example above returns early, so that the smallest amount of work is done as possible each time it is called.
|
Loading…
x
Reference in New Issue
Block a user