Merging changes from one document into another

Hi all,

I’ve got a scenario where I’m trying to create a document that contains content from multiple template documents. The user should be able to customize the new document as they see fit. I’d also like to be able to modify the template documents and have a mechanism for those changes to flow into the new document (based on an id key or something).

I’m not quite sure how to accomplish this or if this is possible. Below is a scenario that I’m trying to get working. Any thoughts / questions / suggestions would be greatly appreciated!

  // Template Doc
  const templateDoc = new Y.Doc()
  const templateArray = templateDoc.getArray('array')
  const templateMap = new Y.Map()
  templateMap.set('id', 'template')
  templateMap.set('deleteKey', 'deleteValue')
  templateArray.push([templateMap])

  // Listen to changes
  let templateUpdates: Uint8Array | undefined
  templateDoc.on('update', (update: Uint8Array) => {
    templateUpdates = templateUpdates ? Y.mergeUpdates([templateUpdates, update]) : update
  })

  // Custom Doc
  const customDoc = new Y.Doc()
  const customArray = customDoc.getArray('array')
  const customMap = new Y.Map()
  customMap.set('id', 'b')
  customArray.push([customMap])

  // Add Template Map to Custom Doc
  customArray.push([templateMap.clone()])

  // Modify the template
  templateMap.delete('deleteKey')
  templateMap.set('name', 'Template Name')
  templateMap.set('title', 'Template Title')

  // Modify the customized template
  const customTemplateMap = customArray.get(1) as Y.Map<unknown>
  customTemplateMap.set('customKey', 'customValue')

  // Apply Template Map changes to Custom Map...
  // Y.applyUpdate(customTemplateMap, templateUpdates)
  expect(customArray.toJSON()).toEqual([
    {
      id: 'b'
    },
    {
      id: 'template',
      customKey: 'customValue',
      name: 'Template Name',
      title: 'Template Title'
    }
  ])

Hey Sam,

Sure you can do that. Maybe I misunderstood, but it might make sense to change the perspective a bit and work purely with document updates.

Yjs documents can be transformed to document updates (representing some state of the document). E.g. using update = Y.encodeStateAsUpdate(ydoc). Document updates are commutative (which allows, for example, p2p network protocols) and idempotent. Idempotency is a neat property because it allows you to create a powerful templating engine. Basically, it says that updates can be merged even if they contain duplicate information (the duplicate information is then stripped away in the output).

Here is a templating engine for text documents:

import * as Y from 'yjs'

// basic template that should be inherited by all
const basicTemplate  = new Y.Doc()
basicTemplate.getText().insert(0, '# Headline\n')

console.log('Basic template: \n' + basicTemplate.getText(''))

// meeting notes template change the headline and add a bullet list
const meetingNotes = new Y.Doc()
// inherit changes from basicTemplate
Y.applyUpdate(meetingNotes, Y.encodeStateAsUpdate(basicTemplate))
meetingNotes.getText().applyDelta([
  { retain: 2 }, // skip "# "
  { delete: 8 }, // delete headline
  { insert: "Meeting Notes" }, // add new headline
  { retain: 1 }, // skip newline
  { insert: "• \n• \n" } // add two bullet items
])

console.log('Meeting notes template: \n' + meetingNotes.getText())

// Now we create a document based on the meetingNotes template
const ydoc = new Y.Doc()
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(meetingNotes))
console.log('Document state:\n' + ydoc.getText())

// A client can perform some operations on any template. E.g. add a citation to the basic template
basicTemplate.getText().insert(10, "\n> citation")

// We need to apply the changes on all documents that use basicTemplate
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(basicTemplate))

// the new paragraph is added in the final document
console.log('New state:\n' + ydoc.getText())

Maybe you can adapt this for your own scenario. The above example works on text because it is the most complex scenario. But I think you will see that working with updates makes sense in this scenario. Let me know what you think.

2 Likes

@dmonad, thanks for the quick reply. Your example makes sense, but I think I need to clarify my issue a bit. It’s quite possible I’m making assumptions / assertions that don’t make sense / are incorrect.

I am trying to insert Slate Nodes (Y.Map) from a Template Document into a Customized Slate Document with other Nodes. At the root of a Slate Document (and I imaging several other editors) is an array of Nodes (Y.Array of Y.Maps). Those Nodes can potentially be moved (delete/insert) around and nested under other Nodes. I want my users to be able to insert a Node from a Template Document and then customize / move that Node to their liking inside their Customized Document. I then want to be able to change the Node in the Template Document and apply those changes to the corresponding Node in the Customized Document, which would involve finding the original Node by searching the Y.Array/Y.Map hierarchy and applying the updates from the Template Document.

I took a look at Subdocuments, but I’m worried about inserting a Y.Doc into the hierarchy, because slate / slate-yjs is expecting a Map at that position.

Please let me know if the description is still unclear. I can try to create some supporting diagrams.

You can’t move nodes around in Yjs. Not even within Yjs documents. This feature is not planned for a long time and is tracked in the Yjs discussion board: Moving elements in lists

The best method is to copy a node from one place to another. This can be nicely expressed using events and doesn’t have the drawbacks discussed in the above thread. However, even in this case, you need to implement the copy semantic manually.

Subdocuments might be an interesting solution to your case, because it would allow you to use the same “template” in many different documents and also allow you to basically move a node to a different place.

But you are right, slate doesn’t know how to handle subdocuments.

I think you could implement the approach you described manually. It might be helpful to give every node a unique GUID that maps from template to customized document. But this is not something where Yjs can help.

1 Like

@dmonad,

Thank you for the thorough response. It’s very helpful to know what isn’t going to work on the journey to hopefully finding what will work. I have a few more ideas to try out in the next few days. I’ll post back here with any findings for the community if any of them work out.