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

TiptapCollab blog post series: first WIP #4023

Merged
merged 2 commits into from
May 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions demos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@
"@types/uuid": "^8.3.4",
"@vitejs/plugin-react": "^1.3.1",
"@vitejs/plugin-vue": "^1.10.2",
"autoprefixer": "^10.4.2",
"autoprefixer": "^10.4.14",
"iframe-resizer": "^4.3.2",
"postcss": "^8.4.6",
"postcss-import": "^15.1.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"sass": "^1.49.7",
"svelte": "^3.49.0",
"tailwindcss": "^2.2.19",
"tailwindcss": "^3.3.2",
"typescript": "4.7.4",
"uuid": "^8.3.2",
"vite": "^2.9.13",
Expand Down
2 changes: 2 additions & 0 deletions demos/postcss.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module.exports = {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {},
},
Expand Down
19 changes: 19 additions & 0 deletions demos/src/Posts/1-1-textarea/Vue/Note.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">

import { ref, watch } from 'vue'

import { TNote } from './types'

const props = defineProps<{ note: TNote }>()

const modelValueProxy = ref('')

watch(props, () => modelValueProxy.value = props.note?.content, {
immediate: true,
})

</script>

<template>
<textarea v-model="modelValueProxy" class="p-2 border border-black rounded-lg"></textarea>
</template>
Empty file.
20 changes: 20 additions & 0 deletions demos/src/Posts/1-1-textarea/Vue/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script setup lang="ts">
import './styles.css'

import Note from './Note.vue'
import { TNote } from './types'

const notes: TNote[] = [
{ id: 'note-1', content: 'some random note text' },
{ id: 'note-2', content: 'some really random note text' },
]

</script>

<template>
<div class="p-3">
<div v-for="note in notes" :key="note.id">
<Note :note="note"/>
</div>
</div>
</template>
3 changes: 3 additions & 0 deletions demos/src/Posts/1-1-textarea/Vue/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
4 changes: 4 additions & 0 deletions demos/src/Posts/1-1-textarea/Vue/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type TNote = {
id: string;
content: string;
};
26 changes: 26 additions & 0 deletions demos/src/Posts/1-2-tiptap/Vue/Note.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup lang="ts">

import StarterKit from '@tiptap/starter-kit'
import { EditorContent, useEditor } from '@tiptap/vue-3'
import { ref, watch } from 'vue'

import type { TNote } from './types'

const props = defineProps<{note: TNote}>()

const modelValueProxy = ref('')

watch(props, () => modelValueProxy.value = props.note.content)

const editor = useEditor({
content: props.note.content,
extensions: [
StarterKit,
],
})

</script>

<template>
<editor-content :editor="editor"></editor-content>
</template>
Empty file.
17 changes: 17 additions & 0 deletions demos/src/Posts/1-2-tiptap/Vue/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">

import Note from './Note.vue'
import { TNote } from './types'

const notes: TNote[] = [
{ id: 'note-1', content: 'some random note text' },
{ id: 'note-2', content: 'some really random note text' },
]

</script>

<template>
<div v-for="note in notes" :key="note.id">
<Note :note="note"/>
</div>
</template>
4 changes: 4 additions & 0 deletions demos/src/Posts/1-2-tiptap/Vue/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type TNote = {
id: string;
content: string;
};
30 changes: 30 additions & 0 deletions demos/src/Posts/1-3-yjs/Vue/Note.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">

import { Collaboration } from '@tiptap/extension-collaboration'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent, useEditor } from '@tiptap/vue-3'
import * as Y from 'yjs'

import type { TNote } from './types'

const props = defineProps<{note: TNote}>()

const doc = new Y.Doc()

const editor = useEditor({
content: props.note.defaultContent,
extensions: [
StarterKit.configure({
history: false, // important because history will now be handled by Y.js
}),
Collaboration.configure({
document: doc,
}),
],
})

</script>

<template>
<editor-content :editor="editor"></editor-content>
</template>
Empty file.
17 changes: 17 additions & 0 deletions demos/src/Posts/1-3-yjs/Vue/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">

import Note from './Note.vue'
import { TNote } from './types'

const notes: TNote[] = [
{ id: 'note-1', defaultContent: 'some random note text' },
{ id: 'note-2', defaultContent: 'some really random note text' },
]

</script>

<template>
<div v-for="note in notes" :key="note.id">
<Note :note="note"/>
</div>
</template>
4 changes: 4 additions & 0 deletions demos/src/Posts/1-3-yjs/Vue/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type TNote = {
id: string;
defaultContent: string;
};
54 changes: 54 additions & 0 deletions demos/src/Posts/1-4-collab/Vue/Note.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script setup lang="ts">

import { TiptapCollabProvider } from '@hocuspocus/provider'
import { Collaboration } from '@tiptap/extension-collaboration'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent, useEditor } from '@tiptap/vue-3'
import { fromBase64 } from 'lib0/buffer'
import { onMounted, onUnmounted } from 'vue'
import * as Y from 'yjs'

import type { TNote } from './types'

const props = defineProps<{ note: TNote }>()

let provider: TiptapCollabProvider | undefined

const createDocFromBase64 = (base64Update: string) => {
const doc = new Y.Doc()

Y.applyUpdate(doc, fromBase64(base64Update))

return doc
}

// usually, you'd just do `new Y.Doc()` here. We are doing some magic to make sure you can just switch to your APP and you have the same document
const doc = createDocFromBase64(props.note.documentBase64)

onMounted(() => {
provider = new TiptapCollabProvider({
name: props.note.id, // any identifier - all connections sharing the same identifier will be synced
appId: '7j9y6m10', // replace with YOUR_APP_ID
token: 'notoken', // replace with your JWT
document: doc,
})
})

onUnmounted(() => provider?.destroy())

const editor = useEditor({
extensions: [
StarterKit.configure({
history: false, // important because history will now be handled by Y.js
}),
Collaboration.configure({
document: doc,
}),
],
})

</script>

<template>
<editor-content :editor="editor"></editor-content>
</template>
Empty file.
17 changes: 17 additions & 0 deletions demos/src/Posts/1-4-collab/Vue/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">

import Note from './Note.vue'
import { TNote } from './types'

const notes: TNote[] = [
{ id: 'note-1', documentBase64: 'AgHaj462BgCE4If+hAIbEHJhbmRvbSBub3RlIHRleHQG4If+hAIABwEHZGVmYXVsdAMJcGFyYWdyYXBoBwDgh/6EAgAGBADgh/6EAgEFc29tZSCE4If+hAIGD3RvdGFsbHkgcmFuZG9tIIHgh/6EAhUChOCH/oQCFwRub3RlAeCH/oQCAQcV' },
{ id: 'note-2', documentBase64: 'AgHiy6OpCACE4If+hAIbF3JlYWxseSByYW5kb20gbm90ZSB0ZXh0BuCH/oQCAAcBB2RlZmF1bHQDCXBhcmFncmFwaAcA4If+hAIABgQA4If+hAIBBXNvbWUghOCH/oQCBg90b3RhbGx5IHJhbmRvbSCB4If+hAIVAoTgh/6EAhcEbm90ZQHgh/6EAgEHFQ==' },
]

</script>

<template>
<div v-for="note in notes" :key="note.id">
<Note :note="note"/>
</div>
</template>
4 changes: 4 additions & 0 deletions demos/src/Posts/1-4-collab/Vue/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type TNote = {
id: string;
documentBase64: string;
};
4 changes: 2 additions & 2 deletions demos/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const defaultTheme = require('tailwindcss/defaultTheme')

module.exports = {
mode: 'jit',
purge: [
content: [
'./preview/**/*.{vue,js,ts,jsx,tsx}',
'./src/**/*.{vue,js,ts,jsx,tsx}',
],

theme: {
Expand Down
64 changes: 64 additions & 0 deletions docs/posts/1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
Hi there!

Welcome to the first post of a series of blog posts about collaboration in Tiptap using TiptapCollab. This series will start covering the basics, and cover more specific use-cases in the next posts. For today, we’ll start moving from a simple textarea box to a fully collaborative Tiptap editor instance.

Imagine that you are building a simple sticky note app, where a user can create notes. Just like Apple Notes, but with better collaboration.

So you have like a textarea and a button 'create new note'. Depending on your framework (Vue, React, ..), the code probably looks similar to this:
(for simplicity, we haven't added the 'new note' logic here)

<tiptap-demo name="Posts/1-1-textarea"></tiptap-demo>

In order to incorporate the Tiptap editor instance for better collaboration and formatting options, you start by modifying your code to include Tiptap in the Note component.

You begin by importing the necessary Tiptap components and creating a new editor instance within the Note component.

```bash
npm install @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit
```

<tiptap-demo name="Posts/1-2-tiptap"></tiptap-demo>

Now your Note component has a fully functional Tiptap editor instance! The user can now format their text (see https://tiptap.dev/guide/menus on how to add a menu bar, in our example you can make text bold using cmd+b). But what about collaboration?

To enable collaboration, you need to add the Collaboration extension to your editor instance. This extension allows multiple users to edit the same document simultaneously, with changes being synced in real-time.


To add the Collaboration extension to your editor instance, you first need to install the `@tiptap/extension-collaboration` package:

```bash
npm install @tiptap/extension-collaboration @yjs/yjs
```

Then, you can import the `Collaboration` extension and add it to your editor extensions:

<tiptap-demo name="Posts/1-3-yjs"></tiptap-demo>

ok, so what have we done?

We just added the collaboration extension as well as the technology behind it, Yjs. Basically instead of text we are passing the Y.Doc which basically takes care of merging changes. But so far, there is no collaboration...

To enable real-time collaboration, we need to connect Yjs with the HocuspocusProvider. The HocuspocusProvider is a package that provides a simple way to share Yjs documents across different clients. It sets up a Yjs room and connects all participants to that room.

To start using HocuspocusProvider, we need to create a new instance of the HocuspocusProvider class and pass it our Yjs document. We also need to provide a document name to connect all participants.

To get started, let's sign up for a Tiptap Pro account, which comes with a free licence for Tiptap Collab: https://tiptap.dev/pricing

After you signed up, go to tiptap.dev/pro and click "Join the Beta". Just follow the instructions and you'll be set up within a few minutes.

Your app ID is shown in the collab admin interface: https://collab.tiptap.dev/ - just copy that and also already get the JWT from the settings area. It's valid for two hours, so more than enough for our quick test. We'll cover generating JWTs using your secret later..


Now, back to our application:

```bash
npm install @hocuspocus/provider
```

Let's now create the TiptapCollabProvider to finally get syncing:

<tiptap-demo name="Posts/1-4-collab"></tiptap-demo>

And that's it! With these changes, our Tiptap note-taking application is now fully collaborative. Notes will get synced to other users in real-time.

Of course, this is just the beginning of what is possible with TiptapCollab and Hocuspocus. In future articles, we'll explore more advanced use cases, such as permissions, presence indicators, and more. Stay tuned!
Loading