contenteditable. All of the built-in logic in Slate depends on these constraints, so unfortunately you cannot omit them. They are...
Elementnodes must contain at least one
Textdescendant — even Void Elements. If an element node does not contain any children, an empty text node will be added as its only child. This constraint exists to ensure that the selection's anchor and focus points (which rely on referencing text nodes) can always be placed inside any node. With this, empty elements (or void elements) wouldn't be selectable.
paragraphblock cannot have another
paragraphblock element and a
linkinline element as children at the same time. The type of children allowed is determined by the first child, and any other non-conforming children are removed. This ensures that common richtext behaviors like "splitting a block in two" function consistently.
undefinedin your data model. This ensures that operations are also JSON-serializable, a property which is assumed by collaboration libraries.
null. Instead, you should use an optional property, e.g.
foo?: stringinstead of
foo: string | null. This limitation is due to
nullbeing used in operations to represent the absence of a property.
🤖 Although these constraints are the best we've come up with now, we're always looking for ways to have Slate's built-in constraints be less constraining if possible—as long as it keeps standard behaviors easy to reason about. If you come up with a way to reduce or remove a built-in constraint with a different approach, we're all ears!
normalizeNodefunction on the editor. The
normalizeNodefunction gets called every time an operation is applied that inserts or updates a node (or its descendants), giving you the opportunity to ensure that the changes didn't leave it in an invalid state, and correcting the node if so.
paragraphblocks only have text or inline elements as children:
normalizeNodegets called on a paragraph element, it loops through each of its children ensuring that none of them are block elements. And if one is a block element, it gets unwrapped, so that the block is removed and its children take its place. The node is "fixed".
normalizeNodeconstraints is that they are multi-pass.
returnthere, the original
normalizeNodeswill never be called, and the built-in constraints won't get a chance to run their own normalizations.
Editor.unwrapNodes, you're actually changing the content of the node that is currently being normalized. So even though you're ending the current normalization pass, by making a change to the node you're kicking off a new normalization pass. This results in a sort of recursive normalizing.
<paragraph c>. And it is valid, because it contains only text nodes as children.
<paragraph b>. This paragraph is invalid, since it contains a block element (
<paragraph c>). So that child block gets unwrapped, resulting in a new document of:
<paragraph a>changed. It gets normalized, and it is invalid, so
<paragraph b>gets unwrapped, resulting in:
normalizeNoderuns, no changes are made, so the document is valid!
🤖 For the most part you don't need to think about these internals. You can just know that anytime
normalizeNodeis called and you spot an invalid state, you can fix that single invalid state and trust that
normalizeNodewill be called again until the node becomes valid.
Elementnodes and makes sure they have at least one child. If it does not, an empty
Textdescendant is created.
Elementhas no children. For example, if a table element has no rows, you may wish to remove the table; however, this will never happen because a
Textnode would automatically be created before that normalization could run.
linkelements have a valid
linkelements have a
urlproperty string. But to fix invalid links it sets the
null, which is still not a string!
url == nullas well.
Editor.withoutNormalizingif the node tree should not be normalized between Transforms. This is frequently the case when you
wrapNodes. For example, you might write a function to change the type of a block as follows: