Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Not sure how to add a new XML fragment #126

Open
nyacg opened this issue Feb 6, 2024 · 3 comments
Open

Not sure how to add a new XML fragment #126

nyacg opened this issue Feb 6, 2024 · 3 comments

Comments

@nyacg
Copy link

nyacg commented Feb 6, 2024

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 [ ... ];
  1.  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)
    
  2.  const newDocReference = { id: documentId, fragment: new Y.XmlFragment() };

    Gives no error but the collaboration does not work (no cursor, no collaborative editing, no save)

  3.  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? 🙏

@nyacg
Copy link
Author

nyacg commented Feb 6, 2024

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).

@andrictham
Copy link

andrictham commented Mar 21, 2024

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?

@YousefED
Copy link
Owner

I think passing a nested XmlFragment like you're doing should be fine @andrictham !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants