Logo

dev-resources.site

for different kinds of informations.

Building a Real-Time Collaborative Text Editor with Slate.js

Published at
12/4/2024
Categories
slatejs
react
realtime
editor
Author
priolo
Categories
4 categories in total
slatejs
open
react
open
realtime
open
editor
open
Author
6 person written this
priolo
open
Building a Real-Time Collaborative Text Editor with Slate.js

While slate-yjs provides a solid foundation for collaborative editing with Slate.js, I found it challenging to manage updates in a way that suited my project's requirements. To address this, I decided to build a simple synchronization system tailored to my needs, offering more control and flexibility over how updates are handled.

Jess (JavaScript Easy Sync System) is a lightweight library that enables real-time synchronization of shared objects between clients and a server. While it can be used for any type of collaborative editing.

Core Concepts

Jess is built around two main classes:

  • ClientObjects: Maintains local copies of shared objects on the client side and handles server communication
  • ServerObjects: Manages the authoritative state on the server and broadcasts changes to connected clients

The library uses a command-based approach where clients send commands to modify objects, which are then synchronized across all connected clients.

Basic Example with Slate Editor

Let's build a collaborative text editor using Jess and Slate.

First, we'll set up the server:

import { ServerObjects, SlateApplicator } from '@priolo/jess';
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 })
const server = new ServerObjects()
// Use the Slate-specific applicator
server.apply = SlateApplicator.ApplyCommands
let timeoutId: any = null

wss.on('connection', (ws) => {
    console.log('New client connected')

    server.onSend = async (client:WebSocket, message) => client.send(JSON.stringify(message))

    ws.on('message', (message) => {
        console.log(`Received message => ${message}`)

        server.receive(message.toString(), ws)

        clearTimeout(timeoutId)
        timeoutId = setTimeout(() => server.update(), 1000)
    })

    ws.on('close', () => server.disconnect(ws))
})

console.log('WebSocket server is running on ws://localhost:8080')
Enter fullscreen mode Exit fullscreen mode

On the client side, we need to set up the connection and Slate editor:

import { ClientObjects, SlateApplicator } from "@priolo/jess"

// create the local repository of objects
export const clientObjects = new ClientObjects()
// apply SLATE-friendly commands
clientObjects.apply = SlateApplicator.ApplyCommands

// create the socket
const socket = new WebSocket(`ws://${window.location.hostname}:${8080}/`);
// connection to the SERVER: observe the object with id = "doc"
socket.onopen = () => clientObjects.init("doc", true)
// receiving a message from SERVER: send it to the local repository
socket.onmessage = (event) => {
    console.log("received:", event.data)
    clientObjects.receive(event.data)
}

// specific function to send messages to the SERVER (in this case using the WEB SOCKET)
clientObjects.onSend = async (messages) => socket.send(JSON.stringify(messages))

// store COMMANDs and send them when everything is calm
let idTimeout: NodeJS.Timeout
export function sendCommands (command:any) {
    clientObjects.command("doc", command)
    clearTimeout(idTimeout)
    idTimeout = setTimeout(() => clientObjects.update(), 1000)
}
Enter fullscreen mode Exit fullscreen mode

Finally, integrate with Slate in your React component:

function App() {

    const editor = useMemo(() => {
        const editor = withHistory(withReact(createEditor()))
        const { apply } = editor;
        editor.apply = (operation: Operation) => {
            // synchronize everything that is NOT a selection operation
            if (!Operation.isSelectionOperation(operation)) {
                console.log("operation:", operation)
                sendCommands(operation)
            }
            apply(operation);
        }
        clientObjects.observe("doc", () => {
            const children = clientObjects.getObject("doc").valueTemp
            SlateApplicator.UpdateChildren(editor, children)
        })
        return editor
    }, [])


    return (
        <Slate
            editor={editor}
            initialValue={[{ children: [{ text: '' }] }]}
        >
            <Editable
                style={{ backgroundColor: "lightgray", width: 400, height: 400, padding: 5 }}
                renderElement={({ attributes, element, children }) =>
                    <div {...attributes}>{children}</div>
                }
                renderLeaf={({ attributes, children, leaf }) =>
                    <span {...attributes}>{children}</span>
                }
            />
        </Slate>
    )
}

export default App
Enter fullscreen mode Exit fullscreen mode

How It Works

1) When a user makes changes in Slate, operations are intercepted and sent as commands to the server via Jess
2) The server applies the commands and broadcasts updates to all connected clients
3) Clients receive updates and apply them to their local Slate editors
4) Changes are batched and synchronized with a delay to improve performance

Try It Out

You can find a complete working example in the repository. Clone it and run:

# Start the server
cd examples/websocket_slate/server
npm install
npm start

# Start the client
cd examples/websocket_slate/client  
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open multiple browser tabs and start typing - you'll see changes sync across all instances.

Link to GitHub Repository

editor Article's
30 articles in total
Favicon
Implementing Image Upload in React Quill
Favicon
Lazyvim version 14.x in WSL
Favicon
Adding and Customizing Tables in React Quill
Favicon
C Development with GNU Emacs
Favicon
Building a Real-Time Collaborative Text Editor with Slate.js
Favicon
SLATE Code editor with highlight
Favicon
Emacs, a simple tour
Favicon
AI Video Editor: Revolutionizing Video Editing
Favicon
Choosing the editor for the next decade
Favicon
Effortless Formatting for OpenTofu Files with LazyVim
Favicon
Store and Run your Javascript Online - tryjs.online
Favicon
Chosing the right code editor: A quick guide
Favicon
Magic Spell - An AI-powered text editor built with Next.js and the Vercel AI SDK
Favicon
Easy Access to Terminal Commands in Neovim using FTerm
Favicon
Encrypted Note Editor App In React Native
Favicon
Fully featured, modern and extensible editor
Favicon
Set Up Neovim with kickstart.nvim on Mac as a Vimginner
Favicon
Working with Zed for a week
Favicon
Guide to using ‘ed’ editor in Linux
Favicon
Elevating Your Video Editing Experience
Favicon
How do you use your VSCode profile?
Favicon
Live Editor with React, Quill, and Socket.IO
Favicon
The Spectacular Transformation: VFX’s Role in Redefining Cinema
Favicon
The Evolution of Emacs: A Journey Through Time
Favicon
React Markdown Editor with real-time preview
Favicon
Online Code Editors
Favicon
A light weight code editor that helps as to code efficiently & swiftly in a 360 world
Favicon
I created overbyte - An Online code editor
Favicon
Build a Neovim plugin in Lua 🌙
Favicon
Helix and Zellij

Featured ones: