-
Notifications
You must be signed in to change notification settings - Fork 456
Description
First of all thanks for this awesome package!
I wanted to create a collaborative text field. The closest one I could find was the textarea example located in /examples/textarea
and is using a third party package called sharedb-string-binding
. However it is using plain html with javascript, so by looking at the code I tried to recreate it using react (but could be done using angular/vue/svelte).
import { Stack, TextField } from "@mui/material";
import ReconnectingWebSocket from "reconnecting-websocket";
import { useMount, useUnmount } from "react-use";
import { useEffect, useState } from "react";
import { Connection } from "sharedb/lib/client";
import { cloneDeep } from "lodash";
import jsondiff from "json0-ot-diff";
import diffMatchPatch from "diff-match-patch";
const App = () => {
const [socket, setSocket] = useState(null);
const [doc, setDoc] = useState(null);
const [docData, setDocData] = useState(null);
useMount(() => {
const socket = new ReconnectingWebSocket(
process.env.REACT_APP_SOCKER_URL,
[],
{
maxRetries: 3,
}
); // connect to the socket
setSocket(socket); // save the socket instance
const connection = new Connection(socket); // create a connection
const document = connection.get("test-collection", "123"); // get the document
setDoc(document); // save the document to state
document.subscribe((e) => {
// subscribe to the document
if (e) {
console.error(e);
return;
}
// If document.type is undefined, the document has not been created, so let's create it
if (!document.type) {
document.create(
{
collaborativeField: "",
},
(e) => {
if (e) {
console.log(e);
}
}
);
}
setDocData(cloneDeep(document.data)); // save document data to state, cloneDeep is mandatory
});
});
useUnmount(() => {
socket?.close();
doc?.unsubscribe();
setDocData(null);
});
useEffect(() => {
if (!doc || !docData) return;
// setup the onOp listener.
const onOp = (_e, source) => {
if (source) return; // if source is true, meaning its acknowledging user's own operations, do nothing
setDocData(cloneDeep(doc.data)); // else save the updated document data to state
};
doc.on("op", onOp);
return () => doc.removeListener("op", onOp);
}, [doc, docData]);
const handleInputChange = (e) => {
let newValue = e.target.value; // the new value
const newDocData = cloneDeep(docData); // create a copy of the document data in order to modify it locally in the function
newDocData.collaborativeField = newValue; // update the field with the new value
const diff = jsondiff(docData, newDocData, diffMatchPatch);
// use the jsondiff package to generate the json0 operations ...
// to go from the docData to newDocData...
// use diffMatchPatch as third argument because our field is a string
console.log(diff);
setDocData(newDocData); // set the new docData to state because we do not acknowledging our own operations
doc.submitOp(diff); // submit the operation
};
return (
<Stack
sx={{ height: "100vh", alignItems: "center", justifyContent: "center" }}
>
{docData && ( // render if docData is initialized
<TextField
label="Collaborative text field"
variant="outlined"
size="small"
value={docData.collaborativeField}
onChange={handleInputChange}
/>
)}
</Stack>
);
};
export default App;
feel free to comment at any part of my implementation as I would like to make a PR to provide an example using any of the modern front-end frameworks. However the main issue is if it is possible to add a debounce in order to significantly reduce the number of operations that are submitted. Both in my implementation and in the provided textarea example, a new operation is submitted on every keypress meaning that a new document is created in the history collection. Imagine we have an application with many collaborators and many collaborative fields. The history will become enormous quite fast. A debouncer will significantly reduce the size of the history collection but even with a debouncer in place is there a recommended way of handling the history?
Thanks.