Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
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:
1
2
cd strapi
npm 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
module.exports = () => ({
io: {
enabled: true,
config: {
// This will listen for all supported events on the document content type
contentTypes: ['api::document.document'],
socket: {
serverOptions: {
cors: {
origin: "*",
methods: ["GET", "POST"],
},
},
},
events: [
{
name: 'connection',
handler: ({ strapi }, socket) => {
strapi.log.info(`[io] user with socket ${socket.id} connected.`);
strapi.activeUsers = strapi.activeUsers || []
strapi.$io.raw({ event: 'active-users', data: strapi.activeUsers.map(s => s.name) });
}
},
{
name: 'user-joined',
handler: ({ strapi }, socket, name) => {
strapi.log.info(`[io] trigger update for socket ${socket.id}.`);
socket.name = name;
strapi.activeUsers.push({ id: socket.id, name });
strapi.$io.raw({ event: 'active-users', data: strapi.activeUsers.map(s => s.name) });
},
},
{
name: "update-history",
handler: ({ strapi }, socket, name) => {
strapi.$io.raw({ event: 'document-history', data: `${name} just made a change` });
}
},
{
name: 'disconnect',
handler: ({ strapi }, socket) => {
strapi.log.info(`[io] user with socket ${socket.id} disconnected.`);
strapi.activeUsers = strapi.activeUsers.filter(s => s.id !== socket.id);
strapi.$io.raw({ event: 'active-users', data: strapi.activeUsers.map(s => s.name) });
}
}
]
},
},
});
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
1
2
3
4
5
6
7
import axios from "axios";
const api = axios.create({
baseURL: "http://localhost:1337/api",
});
export 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<template>
<div>
<h1>{{ document.Title }}</h1>
<textarea v-model="document.Content" @input="updateDocument"></textarea>
</div>
</template>
<script>
import axios from "../services/strapi.js";
export default {
data() {
return {
socket: null,
id: null,
document: {
Title: "",
Content: "",
},
};
},
methods: {
fetchDocument() {
axios
.get("/documents")
.then(({ data }) => {
if (data.data.length > 0) {
this.id = data.data[0].id;
this.document = data.data[0].attributes;
} else {
this.createDocument();
}
})
.catch((err) => {
console.log(err.response.status);
let status = err.response.status;
if (status === 404) {
this.createDocument();
}
});
},
createDocument() {
axios
.post("/documents", {
data: {
Title: "New Document",
Content: "start editing",
},
})
.then(({ data }) => {
this.id = data.data.id;
this.document = data.data.attributes;
});
},
},
mounted() {
this.fetchDocument();
},
};
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
<template>
<div class="document-container">
<div class="input-container">
<label for="title">Title</label>
<input
id="title"
v-model="document.Title"
class="input-field"
@input="updateDocument"
/>
</div>
<div class="textarea-container">
<label for="content">Content</label>
<textarea
id="content"
v-model="document.Content"
@input="updateDocument"
class="textarea-field"
></textarea>
</div>
</div>
</template>
<style scoped>
.document-container {
width: 100%;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.input-container,
.textarea-container {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.input-field,
.textarea-field {
width: 100%;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
}
.input-field:focus,
.textarea-field:focus {
border-color: #007bff;
outline: none;
}
.textarea-field {
min-height: 100px;
resize: vertical;
}
</style>
<script>
import axios from "../services/strapi.js";
import io from "socket.io-client";
export default {
beforeMount() {
this.socket = io("http://localhost:1337");
this.socket.on("document:update", (data) => {
this.document = data.data.attributes;
});
},
data() {
return {
socket: null,
id: null,
document: {
Title: "",
Content: "",
},
};
},
methods: {
fetchDocument() {
axios
.get("/documents")
.then(({ data }) => {
if (data.data.length > 0) {
this.id = data.data[0].id;
this.document = data.data[0].attributes;
} else {
this.createDocument();
}
})
.catch((err) => {
console.log(err.response.status);
let status = err.response.status;
if (status === 404) {
this.createDocument();
}
});
},
createDocument() {
axios
.post("/documents", {
data: {
Title: "New Document",
Content: "start editing",
},
})
.then(({ data }) => {
this.id = data.data.id;
this.document = data.data.attributes;
});
},
updateDocument() {
axios
.put(`/documents/${this.id}`, {
data: {
Title: this.document.Title,
Content: this.document.Content,
},
})
.then(() => {});
},
},
mounted() {
this.fetchDocument();
},
};
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<DocumentEditor />
</template>
<script>
import DocumentEditor from "./components/DocumentEditor.vue";
export default {
name: "App",
components: {
DocumentEditor,
},
};
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
<template>
<div class="container">
<div class="sidebar">
<div>
<h3>Current User</h3>
<div>
<input type="text" v-model="username" :disabled="loggedIn" />
</div>
<button @click="login" v-if="!loggedIn">Log in as an editor</button>
</div>
<div>
<!-- Display all active users editing the document -->
<h3>Active Users</h3>
<ul id="active-users-list">
<li v-for="user in activeUsers" :key="user">
{{ user }}
</li>
</ul>
</div>
<div>
<h3>Document History</h3>
<ul id="history-list">
<li v-for="log in history" :key="log">
{{ log }}
</li>
</ul>
</div>
</div>
<div class="content">
<div class="document-info">
<input type="text" v-model="document.Title" :disabled="!loggedIn" @input="updateDocument"/>
<!-- <p>Created by: User Name (date)</p> -->
</div>
<div id="editor" style="position: relative">
<p v-if="!loggedIn">Log in to make edits to this document</p>
<textarea
class="textarea"
ref="textareaRef"
:disabled="!loggedIn"
@click="getCoordinates"
@input="updateDocument"
v-model="document.Content"
>
</textarea>
</div>
</div>
</div>
</template>
<script>
import axios from "../services/strapi.js";
import io from "socket.io-client";
export default {
beforeMount() {
this.socket = io("http://localhost:1337");
this.socket.on("document:update", (data) => {
this.document = data.data.attributes;
});
this.socket.on("active-users", (data) => {
this.activeUsers = data.data;
});
this.socket.on("document-history", ({ data }) => {
// only keeps track of the 10 latest actions
if (this.history.length >= 10) {
this.history.pop();
}
this.history.unshift(data);
});
},
data() {
return {
socket: null,
id: null,
document: {
Title: "",
Content: "",
},
loggedIn: false,
activeUsers: [],
history: [],
username: "",
};
},
methods: {
fetchDocument() {
axios
.get("/documents")
.then(({ data }) => {
if (data.data.length > 0) {
this.id = data.data[0].id;
this.document = data.data[0].attributes;
} else {
this.createDocument();
}
})
.catch((err) => {
let status = err.response.status;
if (status === 404) {
this.createDocument();
}
});
},
createDocument() {
axios
.post("/documents", {
data: {
Title: "New Document",
Content: "start editing",
},
})
.then(({ data }) => {
this.id = data.data.id;
this.document = data.data.attributes;
});
},
updateDocument() {
this.socket.emit("update-history", this. username);
axios
.put(`/documents/${this.id}`, {
data: {
Title: this.document.Title,
Content: this.document.Content,
},
})
.then(() => {});
},
login() {
this.socket.emit("user-joined", this.username);
this.loggedIn = true;
},
},
mounted() {
this.fetchDocument();
},
};
</script>
<style>
body {
font-family: sans-serif;
margin: 0;
padding: 0;
display: flex;
min-height: 100vh;
}
.container {
display: flex;
flex: 1;
min-height: 100vh;
}
.sidebar {
width: 40%;
padding: 20px;
border-right: 1px solid #ddd;
background-color: #f5f5f5;
min-height: 100vh;
}
.sidebar h2 {
margin-top: 0;
margin-bottom: 10px;
}
.sidebar ul {
list-style: none;
padding: 0;
}
.sidebar li {
margin-bottom: 5px;
}
.content {
flex: 1;
padding: 20px;
height: 100%;
}
.document-info {
margin-bottom: 20px;
}
.document-info h3 {
font-size: 18px;
margin-bottom: 5px;
}
#editor .textarea {
width: 60vw;
height: calc(100vh - 160px); /* Account for header and padding */
border: 1px solid #ddd;
padding: 10px;
font-size: 16px;
}
input {
padding: 8px;
border-radius: 3px;
}
button {
margin-top: 10px;
padding: 6px;
border: none;
outline: none;
background-color: #0d6aad;
color: white;
border-radius: 5px;
}
.cursor {
display: inline-block;
font-size: 20px;
animation: blink 0.5s step-end infinite;
}
.cursor span {
font-size: 12px;
position: absolute;
bottom: 0;
visibility: hidden;
}
.cursor:hover span {
visibility: visible;
}
@keyframes blink {
0% {
visibility: hidden;
}
50% {
visibility: visible;
}
}
</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.