Not sure how to add a new XML fragment
nyacg opened this issue · comments
Hello!
I'm not sure how to add a new XML fragment to my array of objects.
The use case is around having multiple 'pages' in a rich text editor. I would then have a separate object in the synced store that would maintain the folder structure.
My store currently looks like this:
type Todo = { completed: boolean; title: string };
type Doc = { id: string; fragment: "xml" };
export const collaborativeStore = syncedStore({ todos: [] as Todo[], docs: [] as Doc[], exampleDoc: "xml" });
It works great for the todo list:
const state = useSyncedStore(collaborativeStore);
...
state.todos.push({ completed: false, title: target.value });
...
But if I try and add a new doc I'm not sure what I should be pushing to the array...
If I use a fragment that's created at initialization of the synced store, everything works (collaboration cursor, collaborative editing, persistence, etc.) for a single document:
// from getCollaborationExtensions() function
const fragment = collaborativeStore.exampleDoc;
return [
Collaboration.configure({
fragment: fragment,
}),
CollaborationCursor.configure({
provider: provider,
user: {
name: username,
color: getRandomColor(),
},
}),
];
I've tried to populate the array collaborativeStore.docs
in various ways and each has failed. They all follow this pattern:
const docReference = collaborativeStore.docs.find((doc) => doc.id === documentId);
if (!docReference) {
const newDocReference = { id: documentId, fragment: <some way of populating the fragment> };
collaborativeStore.docs.push(newDocReference);
}
const fragment = collaborativeStore.docs.find((doc) => doc.id === documentId).fragment;
return [ ... ];
-
const newDocReference = { id: documentId, fragment: "xml" };
Gives this error:
Uncaught TypeError: Cannot read properties of undefined (reading 'on') at new UndoManager (yjs.mjs:3612:1) at Plugin.init (undo-plugin.js:38:1) at EditorState.reconfigure (index.js:868:1) at Editor.createView (index.js:3617:1) at new Editor (index.js:3456:1)
-
const newDocReference = { id: documentId, fragment: new Y.XmlFragment() };
Gives no error but the collaboration does not work (no cursor, no collaborative editing, no save)
-
const ydoc = getYjsDoc(collaborativeStore); const newDocReference = { id: documentId, fragment: ydoc.getXmlFragment(documentId) };
Gives the following error
Uncaught TypeError: Cannot read properties of null (reading 'forEach') at typeListInsertGenericsAfter (yjs.mjs:5296:1) at typeListInsertGenerics (yjs.mjs:5353:1) at eval (yjs.mjs:7676:1) at transact (yjs.mjs:3358:1) at YXmlFragment.insert (yjs.mjs:7675:1) at YXmlFragment._integrate (yjs.mjs:7524:1) at ContentType.integrate (yjs.mjs:9312:1) at Item.integrate (yjs.mjs:9873:1) at typeMapSet (yjs.mjs:5510:1) at eval (yjs.mjs:6072:1)
Note that I am doing this outside of the react lifecycle (i.e. not using useSyncedStore
just operating on the store directly but from what I understand that's not likely to be the issue. Oh and we're using TipTap for the rich text editor
What should I be doing? 🙏
Ok, change of plan, I'm now using the 'field' property inside the TipTap Collaboration
extension for each new document and it seems to work.
Collaboration.configure({
document: getYjsDoc(collaborativeStore),
field: documentId,
}),
I need to do a little more reading to understand the implications of this but it seems to work! Happy for any comments on this approach (rich text documents stored in the fields of the ydoc and folder structure managed in an object on the synced store).
I’m figuring this out too, right now. For those following along, this is what I’ve figured out after playing around with the library for a day or two:
It’s stated in the docs that the shape
argument passed into the syncedStore function is meant only for describing the shape of the top-level types on the underlying Y.Doc
.
The part where you declare a top-level type as "xml"
seems to be a bit of syntactic sugar on the part of SyncedStore, which creates a Y.XmlFragment
for you as a top-level type.
This only works in the shape
argument passed to SyncedStore in my understanding; you cannot assign "xml"
as a value when manipulating any nested data structures, and expect SyncedStore to do the same thing as if you were declaring it as a top-level type in the shape
object.
I’ve learned that if you want to store a Y.XmlFragment
nested inside of a top-level type, one level or deeper, you can mutate the store’s proxy object and add a new Y.XmlFragment()
, like so:
state.topLevelMap.nestedArray?.push({
id: createId(),
title: "Untitled",
editor: new Y.XmlFragment(),
});
This creates an Y.XmlFragment
shared type deep in the underlying Y.Doc
, and you can freely access it from the SyncedStore proxy object when binding it to your editor(s).
Of course, you can also directly create a new Y.XmlFragment()
by getting the underlying Y.Doc
from SyncedStore, then use Yjs methods (as is what happens if you use the field
property in TipTap: TipTap’s collaboration plugin will handle it for you using Yjs APIs).
But by creating the XmlFragment using the SyncedStore proxy, everything is tracked by SyncedStore which might be more convenient, especially if manipulating the Y.Doc
state from within React.
I don’t know if this approach is recommended or if there are any downsides. @YousefED if you could comment on this?
I think passing a nested XmlFragment like you're doing should be fine @andrictham !