In today’s fast-paced digital world, a document on which multiple users can collaborate allows for improved efficiency and speed. A collaborative document ensures that all the parties involved in editing the document are kept in the loop.
This tutorial article will guide users in creating a real-time collaboration document editing tool using Strapi, WebSockets, and Vue.js.
To follow through with the article, you should have the following:
Node.js and a package manager, yarn, or npm installed on your computer
Basic understanding of JavaScript and Vue.js
Knowledge of WebSockets
A code editor (Vscode, Sublime)
Strapi is an open-source, highly customizable headless content management system (CMS). It allows developers to create, edit, and manage digital content via its APIs. Strapi provides a flexible platform that can deliver content to any front-end framework through its APIs. The benefits of using Strapi as a headless CMS in front-end development include its ease of use, robust customization, and scalability, among others. It also has strong community support and comprehensive documentation to guide new users. Do check out the Strapi docs.
In this article, we’ll leverage Strapi’s features and walk through a Strapi tutorial to build a collaborative document editing tool. We will use its headless CMS capabilities to deliver content to our front end, its customization options to define the fields and inputs we want in our project, and its API flexibility to fetch data using RESTful APIs.
Vue is the progressive JavaScript framework for building interactive user interfaces and single-page applications. It is known for its simplicity, flexibility, and reactive data binding. These features make it the ideal choice for this tutorial as it allows for efficient and dynamic updates, which is essential for a real-time document editing tool.
In this Vue.js tutorial, Vue.js will be used to implement the UI components to display document content and active users, providing seamless editing.
WebSockets are a communication protocol that enhances bi-directional, real-time communication between a client and server. Unlike HTTP, which is unidirectional and request-server based, WebSockets allow for persistent communication. WebSockets are a more efficient alternative for real-time interactions than other communication protocols like HTTP. HTTP supports only client-initiated requests, and Server-Sent Events (SSE) manage updates from server to client. On the other hand, WebSockets support two-way simultaneous communication, which is ideal for applications requiring persistent communication.
In this WebSockets and Vue.js tutorial, you will learn how to implement this communication protocol to achieve seamless real-time communication in a document editing tool.
At the backend, Strapi will be used to manage the storage and retrieval of document data. We will create the document with Strapi and define the content type.
To create a new Strapi project, have Node.js and your preferred package manager installed on your computer. Here, we will use npm to manage the packages and dependencies required for the project.
To start, create a folder named document-editing-tool
. This folder will be the project folder. On your terminal, navigate to the directory you created.
Next up, Paste the following command in your terminal:
npx create-strapi-app@latest strapi
After running the command, you will be prompted to choose an installation type between Quickstart (recommended) and Custom (manual settings). The recommended option is to opt for the Quickstart installation. All necessary dependencies will be installed, and a new Strapi project will be created in a directory with what you named it. You are now ready to develop your application.
Note: If the Strapi project does not run after being bootstrapped. Do the following:
1cd strapi
2npm run develop
Your browser will direct you to a site where you must register your credentials.
After signing up, you will be able to access the dashboard.
From the admin panel you’re on, we will create the Document content type, which allows multiple users to make edits.
In the left panel, click on Content-Types Builder, then click the create new collection type button in the dropdown.
Give it a display name of "Document.” Next, we will define the fields for the document editing tool.
Click the Finish button and then Add new field.
Select Text and input Title
in the name attribute. Select Short text as the Type.
We will add another field, so click on Add another field and select Text. In the name attribute, input Content
and choose Long text as the type.
Click on Advanced settings and mark the Required field checkbox.
This will ensure that the content information is provided when updating the document. Now, click Finish to be done with the fields. Click save at the top right corner to create the new content type.
We want to disable Strapi's default Authentication and Authorization service for our application for the Document
Content-Type we created previously.
To do this, go to Settings in the left panel, under the Users and Permissions Plugin, and click on Roles. From the list of roles, we will focus on the Public role to allow any user to edit the document. Click Public and scroll down to the Permissions section, then select our content type, which is Document.
Click on the dropdown and check the Select all box to allow public users to create new documents, find individual documents, update documents, and delete documents. You can now save the changes at the top right.
WebSockets play a significant role in enabling real-time communication in web applications by facilitating two-way communication between a client and a server over a single TCP connection, allowing messages to be sent back and forth simultaneously.
WebSockets will be used to implement real-time collaboration features in our document editing tool and to track changes made to the document.
Here, we will set up a WebSocket server using a Strapi plugin. The Strapi plugin strapi-plugin-io is a plugin for Strapi CMS that allows Socket IO integration.
We will use this to implement WebSocket server functionality.
Navigate to your Strapi directory in your terminal and input the following to install strapi-plugin-io
.
npm install strapi-plugin-io
strapi-plugin-io
with StrapiWe will now integrate the Strapi plugin strapi-plugin-io
into our project.
To integrate strapi-plugin-io
with Strapi, input the following code in the ./config/plugins.js
file of the strapi
directory, which is the backend of the editing tool. The relative path should be like this strapi/config/plugins.js
1module.exports = () => ({
2 io: {
3 enabled: true,
4 config: {
5 // This will listen for all supported events on the document content type
6 contentTypes: ['api::document.document'],
7 socket: {
8 serverOptions: {
9 cors: {
10 origin: "*",
11 methods: ["GET", "POST"],
12 },
13 },
14 },
15 events: [
16 {
17 name: 'connection',
18 handler: ({ strapi }, socket) => {
19 strapi.log.info(`[io] user with socket ${socket.id} connected.`);
20 strapi.activeUsers = strapi.activeUsers || []
21 strapi.$io.raw({ event: 'active-users', data: strapi.activeUsers.map(s => s.name) });
22 }
23 },
24 {
25 name: 'user-joined',
26 handler: ({ strapi }, socket, name) => {
27 strapi.log.info(`[io] trigger update for socket ${socket.id}.`);
28 socket.name = name;
29 strapi.activeUsers.push({ id: socket.id, name });
30 strapi.$io.raw({ event: 'active-users', data: strapi.activeUsers.map(s => s.name) });
31 },
32 },
33 {
34 name: "update-history",
35 handler: ({ strapi }, socket, name) => {
36 strapi.$io.raw({ event: 'document-history', data: `${name} just made a change` });
37 }
38 },
39 {
40 name: 'disconnect',
41 handler: ({ strapi }, socket) => {
42 strapi.log.info(`[io] user with socket ${socket.id} disconnected.`);
43 strapi.activeUsers = strapi.activeUsers.filter(s => s.id !== socket.id);
44 strapi.$io.raw({ event: 'active-users', data: strapi.activeUsers.map(s => s.name) });
45 }
46 }
47 ]
48 },
49
50 },
51});
Here, we've enabled the Strapi plugin to set up events for various actions of the document
content type. This allows us to easily manage events for create
, update
, and delete
actions.
We also configured the plugin's default behavior to set up extra events to manage the active users currently editing the document.
Vue.js is a performant JavaScript framework that allows us to create the interactive interface that contributors will use to edit the document.
Follow the following steps to set up Vue and integrate it with the Strapi backend.
In a new project terminal, input the following to create a new directory.
mkdir frontend
Navigate into the new frontend directory.
cd frontend
Create a new Vue project in the frontend directory.
npx @vue/cli create my-vue-app
You will be prompted with some options for setting up the project. Choose the default options.
We have now completely set up the Vue project. In your document-editing-tool
project directory, you will find the frontend directory and the my-vue-app
folder within it.
Navigate to the frontend
project in your terminal and run the command below:
cd my-vue-app
Next, install Axio to make HTTP requests to the Strapi backend. Go ahead with the following. Input the next command
npm install axios
Create a folder named ' services ' in your Vue project (my-vue-app
directory). Create a file within that folder and name it strapi.js
So your file path should be like this my-vue-app/src/services/strapi.js
Paste this code into the strapi.js
file
1import axios from "axios";
2
3const api = axios.create({
4 baseURL: "http://localhost:1337/api",
5});
6
7export default api;
Now that we’ve configured Axios to communicate with the Strapi backend, the next step is to set up the component responsible for displaying and editing the document.
Create a new component
in your components
folder and call it DocumentEditor.vue
. Input the following code.
1<template>
2 <div>
3 <h1>{{ document.Title }}</h1>
4 <textarea v-model="document.Content" @input="updateDocument"></textarea>
5 </div>
6</template>
7
8<script>
9import axios from "../services/strapi.js";
10
11export default {
12 data() {
13 return {
14 socket: null,
15 id: null,
16 document: {
17 Title: "",
18 Content: "",
19 },
20 };
21 },
22 methods: {
23 fetchDocument() {
24 axios
25 .get("/documents")
26 .then(({ data }) => {
27 if (data.data.length > 0) {
28 this.id = data.data[0].id;
29 this.document = data.data[0].attributes;
30 } else {
31 this.createDocument();
32 }
33 })
34 .catch((err) => {
35 console.log(err.response.status);
36 let status = err.response.status;
37 if (status === 404) {
38 this.createDocument();
39 }
40 });
41 },
42 createDocument() {
43 axios
44 .post("/documents", {
45 data: {
46 Title: "New Document",
47 Content: "start editing",
48 },
49 })
50 .then(({ data }) => {
51 this.id = data.data.id;
52 this.document = data.data.attributes;
53 });
54 },
55 },
56 mounted() {
57 this.fetchDocument();
58 },
59};
60</script>
This snippet above is a Vue.js component that fetches data from the Strapi backend and creates a new one if it does not already exist.
Go along with the following prompt to integrate WebSocket functionality into the DocumentEditor.vue
component.
In your Vue project, go to your terminal and install socket.io-client
.
npm install socket.io-client
Update the DocumentEditor.vue
component code with the one below.
1<template>
2 <div class="document-container">
3 <div class="input-container">
4 <label for="title">Title</label>
5 <input
6 id="title"
7 v-model="document.Title"
8 class="input-field"
9 @input="updateDocument"
10 />
11 </div>
12 <div class="textarea-container">
13 <label for="content">Content</label>
14 <textarea
15 id="content"
16 v-model="document.Content"
17 @input="updateDocument"
18 class="textarea-field"
19 ></textarea>
20 </div>
21 </div>
22</template>
23
24<style scoped>
25.document-container {
26 width: 100%;
27 max-width: 600px;
28 margin: 0 auto;
29 padding: 20px;
30 background-color: #f9f9f9;
31 border-radius: 8px;
32 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
33}
34
35.input-container,
36.textarea-container {
37 margin-bottom: 15px;
38}
39
40label {
41 display: block;
42 margin-bottom: 5px;
43 font-weight: bold;
44 color: #333;
45}
46
47.input-field,
48.textarea-field {
49 width: 100%;
50 padding: 10px;
51 font-size: 16px;
52 border: 1px solid #ccc;
53 border-radius: 4px;
54}
55
56.input-field:focus,
57.textarea-field:focus {
58 border-color: #007bff;
59 outline: none;
60}
61
62.textarea-field {
63 min-height: 100px;
64 resize: vertical;
65}
66</style>
67
68<script>
69import axios from "../services/strapi.js";
70import io from "socket.io-client";
71
72export default {
73 beforeMount() {
74 this.socket = io("http://localhost:1337");
75 this.socket.on("document:update", (data) => {
76 this.document = data.data.attributes;
77 });
78 },
79 data() {
80 return {
81 socket: null,
82 id: null,
83 document: {
84 Title: "",
85 Content: "",
86 },
87 };
88 },
89 methods: {
90 fetchDocument() {
91 axios
92 .get("/documents")
93 .then(({ data }) => {
94 if (data.data.length > 0) {
95 this.id = data.data[0].id;
96 this.document = data.data[0].attributes;
97 } else {
98 this.createDocument();
99 }
100 })
101 .catch((err) => {
102 console.log(err.response.status);
103 let status = err.response.status;
104 if (status === 404) {
105 this.createDocument();
106 }
107 });
108 },
109 createDocument() {
110 axios
111 .post("/documents", {
112 data: {
113 Title: "New Document",
114 Content: "start editing",
115 },
116 })
117 .then(({ data }) => {
118 this.id = data.data.id;
119 this.document = data.data.attributes;
120 });
121 },
122 updateDocument() {
123 axios
124 .put(`/documents/${this.id}`, {
125 data: {
126 Title: this.document.Title,
127 Content: this.document.Content,
128 },
129 })
130 .then(() => {});
131 },
132 },
133 mounted() {
134 this.fetchDocument();
135 },
136};
137</script>
This snippet above will get the document from the Strapi backend and display it to enable real-time collaboration by broadcasting updates through socket.io. It also re-fetches the document for each update event to get recent updates.
Replace the App.vue
in the src
folder with the following:
1<template>
2 <DocumentEditor />
3</template>
4
5<script>
6import DocumentEditor from "./components/DocumentEditor.vue";
7
8export default {
9 name: "App",
10 components: {
11 DocumentEditor,
12 },
13};
14</script>
We implemented a simple application to edit a sample document and broadcast its changes to all connected users. But we can make it even better. We will improve the user interface, add features for viewing other users currently on the file, and add the document’s content history at the side of the page.
Update the Vue.js component DocumentEditor.vue
file to the following:
1<template>
2 <div class="container">
3 <div class="sidebar">
4 <div>
5 <h3>Current User</h3>
6 <div>
7 <input type="text" v-model="username" :disabled="loggedIn" />
8 </div>
9 <button @click="login" v-if="!loggedIn">Log in as an editor</button>
10 </div>
11
12 <div>
13 <!-- Display all active users editing the document -->
14 <h3>Active Users</h3>
15 <ul id="active-users-list">
16 <li v-for="user in activeUsers" :key="user">
17 {{ user }}
18 </li>
19 </ul>
20 </div>
21
22 <div>
23 <h3>Document History</h3>
24 <ul id="history-list">
25 <li v-for="log in history" :key="log">
26 {{ log }}
27 </li>
28 </ul>
29 </div>
30 </div>
31 <div class="content">
32 <div class="document-info">
33 <input type="text" v-model="document.Title" :disabled="!loggedIn" @input="updateDocument"/>
34 <!-- <p>Created by: User Name (date)</p> -->
35 </div>
36 <div id="editor" style="position: relative">
37 <p v-if="!loggedIn">Log in to make edits to this document</p>
38 <textarea
39 class="textarea"
40 ref="textareaRef"
41 :disabled="!loggedIn"
42 @click="getCoordinates"
43 @input="updateDocument"
44 v-model="document.Content"
45 >
46 </textarea>
47 </div>
48 </div>
49 </div>
50</template>
51
52<script>
53import axios from "../services/strapi.js";
54import io from "socket.io-client";
55
56export default {
57 beforeMount() {
58 this.socket = io("http://localhost:1337");
59
60 this.socket.on("document:update", (data) => {
61 this.document = data.data.attributes;
62 });
63
64 this.socket.on("active-users", (data) => {
65 this.activeUsers = data.data;
66 });
67
68 this.socket.on("document-history", ({ data }) => {
69 // only keeps track of the 10 latest actions
70 if (this.history.length >= 10) {
71 this.history.pop();
72 }
73 this.history.unshift(data);
74 });
75 },
76 data() {
77 return {
78 socket: null,
79 id: null,
80 document: {
81 Title: "",
82 Content: "",
83 },
84 loggedIn: false,
85 activeUsers: [],
86 history: [],
87 username: "",
88 };
89 },
90 methods: {
91 fetchDocument() {
92 axios
93 .get("/documents")
94 .then(({ data }) => {
95 if (data.data.length > 0) {
96 this.id = data.data[0].id;
97 this.document = data.data[0].attributes;
98 } else {
99 this.createDocument();
100 }
101 })
102 .catch((err) => {
103 let status = err.response.status;
104 if (status === 404) {
105 this.createDocument();
106 }
107 });
108 },
109 createDocument() {
110 axios
111 .post("/documents", {
112 data: {
113 Title: "New Document",
114 Content: "start editing",
115 },
116 })
117 .then(({ data }) => {
118 this.id = data.data.id;
119 this.document = data.data.attributes;
120 });
121 },
122 updateDocument() {
123 this.socket.emit("update-history", this. username);
124 axios
125 .put(`/documents/${this.id}`, {
126 data: {
127 Title: this.document.Title,
128 Content: this.document.Content,
129 },
130 })
131 .then(() => {});
132 },
133 login() {
134 this.socket.emit("user-joined", this.username);
135 this.loggedIn = true;
136 },
137 },
138 mounted() {
139 this.fetchDocument();
140 },
141};
142</script>
143
144<style>
145body {
146 font-family: sans-serif;
147 margin: 0;
148 padding: 0;
149 display: flex;
150 min-height: 100vh;
151}
152
153.container {
154 display: flex;
155 flex: 1;
156 min-height: 100vh;
157}
158
159.sidebar {
160 width: 40%;
161 padding: 20px;
162 border-right: 1px solid #ddd;
163 background-color: #f5f5f5;
164 min-height: 100vh;
165}
166
167.sidebar h2 {
168 margin-top: 0;
169 margin-bottom: 10px;
170}
171
172.sidebar ul {
173 list-style: none;
174 padding: 0;
175}
176
177.sidebar li {
178 margin-bottom: 5px;
179}
180
181.content {
182 flex: 1;
183 padding: 20px;
184 height: 100%;
185}
186
187.document-info {
188 margin-bottom: 20px;
189}
190
191.document-info h3 {
192 font-size: 18px;
193 margin-bottom: 5px;
194}
195
196#editor .textarea {
197 width: 60vw;
198 height: calc(100vh - 160px); /* Account for header and padding */
199 border: 1px solid #ddd;
200 padding: 10px;
201 font-size: 16px;
202}
203
204input {
205 padding: 8px;
206 border-radius: 3px;
207}
208
209button {
210 margin-top: 10px;
211 padding: 6px;
212 border: none;
213 outline: none;
214 background-color: #0d6aad;
215 color: white;
216 border-radius: 5px;
217}
218
219.cursor {
220 display: inline-block;
221 font-size: 20px;
222 animation: blink 0.5s step-end infinite;
223}
224
225.cursor span {
226 font-size: 12px;
227 position: absolute;
228 bottom: 0;
229 visibility: hidden;
230}
231
232.cursor:hover span {
233 visibility: visible;
234}
235
236@keyframes blink {
237 0% {
238 visibility: hidden;
239 }
240 50% {
241 visibility: visible;
242 }
243}
244</style>
The code has been updated to keep track of users editing the document. Users have to set their usernames and log in before accessing the document. The active users editing the document are now displayed along with the history of their changes.
To run the application's frontend, navigate to your terminal's my-vue-app
directory.
Run the command below in the terminal to start the frontend of our application. The application will start on localhost: http://localhost:8080/.
npm run serve
To ensure the backend service is also running, run the command below while in the strapi
project folder.
npm run develop
Here is what the fronntend looks like:
Here, we can see that "Jane", a current user, has logged into the document and can tell from the document history that another active user, Joe, has just made changes.
Similarly, from this demo, "Jane," who logged in as a current user, can also see that Joe is an active user and can tell from the document history that Joe has just made an edit.
Overall, this entire code block handles the user logins, displays active users and document history, and syncs document updates in real-time using strapi-plugin-io
. The editor only allows logged-in users to edit the document.
These features enhance the collaborative editing experience supported by the Strapi backend.
In this article, we successfully built a collaborative document editing tool by integrating Strapi for backend management, Vue.js for the frontend interface, and WebSockets for real-time collaboration. We covered setting up the document content types and Strapi's easy configuration for plugins. We also demonstrated how to use WebSockets to manage document edits and track user presence, while the frontend in Vue.js provided a responsive editing experience.
You are encouraged to continue exploring with Strapi Docs to gain further firsthand insight and enhance your projects. You can also join the Discord community to access shared resources and connect with other developers.
I'm a Front-end developer and technical writer who takes delight in translating complex information into easy-to-read articles, writing software documentation, how-to guides, and tutorials.