Notion like Placeholders for every line — Slatejs

Santosh Viswanatham
3 min readSep 17, 2021

--

I have recently worked on building a module that includes a google docs kind of functionality. I was exploring several JavaScript frameworks around building editors, and I finally choosed Slatejs

One feature of my editor was to have a placeholder for every line similar to Notion. By default, Slatejs provides a placeholder but it will be rendered if the document only contains a single empty block.

However, Slate.js doesn’t have the feature that I needed. I was able to figure out a solution with the help of the amazing Slate.js community members and thought to share this with you.

The solution I implemented was to check if a node is empty and then display a dummy text using CSS tricks.

.selected-empty-element {
position: relative;
}
.selected-empty-element:after {
content: " Use '/' to create question";
color: #aaa;
position: absolute;
top: 0;
}

In the render element method, I will add this class selected-empty-element if the node is empty.

const Element = (props): JSX.Element => {
const { children } = props;
const isEmpty =
children.props.node.children[0].text === “” &&
children.props.node.children.length === 1;
return (
<p {…props} className={isEmpty ? “selected-empty-element” : “”}>
{children}
</p>
);
};

But this gives out a weird user experience when you have multiple empty rows. All empty lines will show the placeholders.

So one solution I could think of was to check if that particular line has focus. I added a check using useSelected, and it looked exactly like the solution I needed.

const selected = useSelected();
return (
<p {...props}
className={selected && isEmpty ? "selected-empty-element" : ""}
>
{children}
</p>
);

However, there was one problem that I found later on. If I select the whole document, then the useSelected is true for all nodes and I could see the placeholder for all nodes.

Now I had to add another check to see if my selection is empty or not. I used the available library methods to see if the Range is collapsed or not using

const editor = useSlate();
const selection = editor.selection;
let isSelectionCollapsed = true;
if (selection !== null)
isSelectionCollapsed = Range.isCollapsed(editor.selection);

So now the final Element code is

const Element = (props): JSX.Element => {
const { children } = props;
const selected = useSelected();
const editor = useSlate();
const selection = editor.selection;
let isSelectionCollapsed = true;
if (selection !== null)
isSelectionCollapsed = Range.isCollapsed(editor.selection);

const isEmpty =
children.props.node.children[0].text === “” &&
children.props.node.children.length === 1;
return (
<p {…props}
className={ selected && isSelectionCollapsed && isEmpty
? “selected-empty-element” : “”
}
>
{children}
</p>
);
};

This was the final solution that I needed. You can find the complete editor code in this sandbox here -

I hope this was helpful. Share with me some of the interesting features you built using Slatejs.

--

--

Santosh Viswanatham

Javascript Engineer | Product Developer | Tech Speaker | Angular | React | WebVR | Browser Add-ons