Multi-Document and Subdocuments Support

Documents & room-names

Most Yjs connection providers (including the y-websocket provider) use a concept called room-names. The client will pass a room-name parameter to Hocuspocus which will then be used to identify the document which is currently being edited. We will call room-name throughout this documentation document name.

In a real-world app you would probably use the name of your entity and a unique ID. Here is how that could look like for a CMS:

const documentName = "page.140";

Now you can easily split this to get all desired information separately:

const [entityType, entityID] = documentName.split(".");

console.log(entityType); // prints "page"
console.log(entityID); // prints "140

This is a recommendation, of course you can name your documents however you want!

Nested documents

Introduction

We're currently evaluating feedback for subdocuments, but haven't implemented support yet.

In a lot of cases, instead of subdocuments, you can use different fragments of Yjs, so if you're thinking about a blog post with title/content, you can create a single yDoc and use different editors (or anything else) that are each binding to a different fragment, like this:

const ydoc = new Y.Doc();

const titleEditor = new Editor({
  extensions: [
    Collaboration.configure({
      document: this.ydoc,
      field: "title",
    }),
  ],
})

const bodyEditor = new Editor({
  extensions: [
    Collaboration.configure({
      document: this.ydoc,
      field: "body",
    }),
  ],
})

When using multiple fields you can simply merge different documents into the given document:

import { readFileSync } from "fs";
import { Server } from "@hocuspocus/server";
import { TiptapTransformer } from "@hocuspocus/transformer";
import Document from "@tiptap/extension-document";
import Paragraph from "@tiptap/extension-paragraph";
import Text from "@tiptap/extension-text";
const generateSampleProsemirrorJson = (text: string) => {
  return {
    type: "doc",
    content: [
      {
        type: "paragraph",
        content: [
          {
            type: "text",
            text,
          },
        ],
      },
    ],
  };
};
const server = Server.configure({
  async onLoadDocument(data) {
    // only import things if they are not already set in the primary storage
    if (data.document.isEmpty("default")) {
      // Get a Y-Doc for the 'default' field …
      const defaultField = TiptapTransformer.toYdoc(
        generateSampleProsemirrorJson("What is love?"),
        "default",
        [(Document, Paragraph, Text)]
      );
      // … and merge it into the given document
      data.document.merge(defaultField);
    }
    if (data.document.isEmpty("secondary")) {
      // Get a Y-Doc for the 'secondary' field …
      const secondaryField = TiptapTransformer.toYdoc(
        generateSampleProsemirrorJson("Baby don't hurt me…"),
        "secondary",
        [(Document, Paragraph, Text)]
      );
      // … and merge it into the given document
      data.document.merge(secondaryField);
    }
  },
});
server.listen();