TinyMCE is a cool text formatting editor that gives users the ability to create structured HTML content. It has good support, documentation and the ability to extend it with various plugins, both from official developers and proprietary ones. One such plugin is comments within HTML content.
In this article we will consider the principle, features and some difficulties of TinyMCE comments when integrated into applications written in Vue and Laravel.
Comments plugin overview and features
It's worth mentioning right away that the plugin is a paid one and is included in the Essential subscription. You will have to pay about $700 per year for the subscription.
A full list of features for that money can be found at pricing.
The editor's user interface allows users to collaborate on content by creating and responding to comments.
Main Features
- Create comment
- Edit comment
- Reply to comment
- Lookup a comment
- Resolve a comment thread
- Delete а comment/s
The plugin has two modes of operation: Callback
mode and Embedded
mode. Each has its own features. We will be interested in Callback mode, because we want to store comments on our server in our database and be able to manage them from our application.
The principle of operation of this mode is based on callback functions, which is what its name says. To integrate the plugin we need to implement the following callback functions inside our application:
- tinycomments_create
- tinycomments_reply
- tinycomments_edit_comment
- tinycomments_delete_comment
- tinycomments_delete
- tinycomments_delete_all
- tinycomments_lookup
Getting started
To integrate with our Vue application, we will use a ready-made component from the editor developer himself, which is focused on it.
npm install --save tinymce "@tinymce/tinymce-vue@^5"
.
The component has already been written for us, we just need to configure it correctly. If you look in the source code of the package you can find the component itself.
import { Editor } from './components/Editor';
.
So, to get the plugin started it should, first of all:
- Copy the API key from the TinyMCE personal cabinet and write it in the .env file.
VITE_TINY_API_KEY=api_key_value
.
- Write in main.js
app.provide('tinyApiKey', import.meta.env.VITE_TINY_API_KEY)
You can access environment variables (prefixed with VITE_) from your Vue application using the import.meta.env object.
- Add our domain to the whitelist, also from your personal cabinet. To remove the annoying inscription inside the editor.
Import the Editor component and specify our API key.
Enable the comments premium plugin by writing in init.plugins the value
tinycomments
, as well as add in init.toolbar -addcomment showcomments
, to be able to open the sidebar to create and show comments.Create an dummy function for other comments functions.
Declare
tinycomments_mode
and define callback functions
Let's fill in tinycomments_author by hand, for starters
The final code will look like this:
<script setup>
import Editor from '@tinymce/tinymce-vue';
import { inject, ref } from 'vue';
const tinyApiKey = inject('tinyApiKey');
const content = ref('Hello, CoderDen!');
const noop = () => {};
</script>
<template>
<main>
<Editor
:api-key="tinyApiKey"
v-model="content"
:init="{
plugins: 'lists link image table code wordcount tinycomments',
toolbar: 'undo redo | bold italic underline strikethrough | fontfamily fontsize blocks | addcomment showcomments',
tinycomments_mode: 'callback',
tinycomments_author: 'Denis Sinyukov',
tinycomments_create: noop,
tinycomments_reply: noop,
tinycomments_edit_comment: noop,
tinycomments_delete: noop,
tinycomments_delete_all: noop,
tinycomments_delete_comment: noop,
tinycomments_lookup: noop,
}"
/>
</main>
</template>
Create comment
The plugin uses the tinycomments_create
function to create a comment. The function receives as input a request object (req), a done callback, and a fail callback.
The req argument has the following fields:
- content - The content of the comment to create.
- createdAt - The date the comment was created.
After successful creation it returns a unique conversation identifier via the Done callback, otherwise the fail function is called.
Creating a comment on the server
Laravel and MySQL was chosen as the backend.
- Let's design a simple database table to store comments.A polymorphic table
conversations
is used to store comments, which can be used for different entities.
- Create Laravel web routes to work with comments
Route::get('/editor/conversations', [EditorController::class, 'getConversations']);
Route::post('/editor/conversations', [EditorController::class, 'createConversation']);
- Create the logic for saving the conversation. entityId - id of the entity to which the comment belongs.
public function createConversation(Request $request): JsonResponse
{
$payload = $request->validate([
'content' => 'required|string',
'entityId' => 'required|numeric',
]);
$user = $request->user();
$userId = $user->id;
$conversationId = app(ConversationRepository::class)->create([
'user_id' => $userId,
'content' => $payload['content'],
'entity_type' => EntityType::Template->value, // php8 enum 'template'
'entity_id' => $payload['entityId'],
'uuid' => $conversationUid = Str::uuid(),
]);
return response()->json([
'data' => [
'conversationId' => $conversationId,
'conversationUid' => $conversationUid
],
'meta' => [
'code' => 200
]
]);
}
Example of a successful server response:
{
"meta": {
"code": 200
},
"data": {
"conversationId": 1,
"conversationUid": "4ae419e1-79d5-44f0-9c96-ca842959f7a4"
}
}
- Let's create a function to receive comments
public function getConversations(Request $request): JsonResponse
{
$payload = $request->validate([
'entityId' => 'required|numeric',
]);
$conversations = DB::table('conversations', 'c')
->selectRaw( 'c.*')
->selectRaw('u.first_name, u.last_name, u.email')
->join('users as u', 'u.id', 'c.user_id')
->where('entity_id', $payload['entityId'])
->where('entity_type', EntityType::Template->value)
->get();
$conversations = $conversations->map(fn($conversation) => [
'id' => $conversation->id,
'uuid' => $conversation->uuid,
'author' => trim($conversation->first_name . ' ' . $conversation->last_name),
'content' => $conversation->content,
'created_at' => $conversation->created_at,
'updated_at' => $conversation->updated_at,
'entity_id' => $conversation->entity_id,
'entity_type' => $conversation->entity_type,
]);
return response()->json([
'data' => [
'conversations' => $conversations,
],
'meta' => [
'code' => 200
]
]);
}
Creating a comment on the client
Let's create our comment creation function that will send the created comment to our backend.
Replace the function in init.tinycomments_create with createComment.
:init="{
...
tinycomments_create: createComment,
...
}"
We will use the axios utility to send data to the server.
After that we need to declare the createComment function, which will have the following form:
const createComment = (ref, done, fail) => {
ref.entityId = 1; // You can substitute any identifier here.
axios.post('/editor/conversations', ref, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then(response => response.data?.data)
.then(response => {
let conversationUid = response.conversationUid;
conversations.value.push({
id: response.conversationId,
author: 'Denis Sinyukov', // skip logic
content: ref.content,
uuid: conversationUid,
entity_id: ref.entityId,
updated_at: ref.createdAt,
created_at: ref.createdAt,
});
done({conversationUid: conversationUid});
})
.catch((e) => {
fail(e);
});
};
The point is that after successful execution of tinycomments_create - tinycomments_lookup is called to display it. Now let's do it, let's create the lookupComment function and get the list of comments from the server.
- We get a list of comments before the component is rendered.
import { inject, ref, computed, onBeforeMount } from 'vue';
const conversations = ref([]);
...
const getConversations = () => {
axios.get('/editor/conversations', {
params: {
entityId: 1
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then(response => response.data?.data)
.then(response => conversations.value = response.conversations);
}
onBeforeMount(() => {
getConversations();
});
const mappedConversations = computed(() => {
return conversations.value.map(conversation => {
return {
author: conversation.author || '',
createdAt: conversation.created_at || new Date().toISOString(),
content: conversation.content,
modifiedAt: conversation.created_at || new Date().toISOString(),
uid: conversation.uuid
};
})
});
It is important to adapt the format of our data so that the editor learns to understand it. mappedConversations - Format the data to fit the editor's display format.
- Display the created comment
After successful creation, the array with comments goes to the tinycomments_lookup
function and looks for us newly created comment.
const lookupComment = ({conversationUid}, done, fail) => {
const conversation = mappedConversations.value.find(c => c.uid === conversationUid);
if (!conversation) {
fail();
}
done({
conversation: {
uid: conversationUid,
comments: [
conversation,
]
}
})
}
If we look at the HTML source code of the editor content, we see a data attribute with the uid of our comment -data-mce-annotation-uid="425cb9a8-6fdc-4739-8331-be63e0b08dbf"
.
This attribute is used to show where the comments were created and highlight them.
The Bottom Line
The article covered the basic workings of using comments and integrating the plugin into existing applications written in Vue and Laravel.
TinyMCE comment plugin integration makes your applications look more functional without much wasted development time. The plugin facilitates commenting, reviewing and collaboration between application users.
The full Vue component:
<script setup>
import Editor from '@tinymce/tinymce-vue';
import { inject, ref, computed, onBeforeMount } from 'vue';
import axios from "axios";
const tinyApiKey = inject('tinyApiKey');
const content = ref('Hello, CoderDen!');
const conversations = ref([]);
const noop = () => {};
const createComment = (ref, done, fail) => {
ref.entityId = 1; // You can substitute any identifier here
axios.post('/editor/conversations', ref, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then(response => response.data?.data)
.then(response => {
let conversationUid = response.conversationUid;
done({conversationUid: conversationUid});
conversations.value.push({
id: response.conversationId,
author: 'Denis Sinyukov',
content: ref.content,
uuid: conversationUid,
entity_id: ref.entityId,
updated_at: ref.createdAt,
created_at: ref.createdAt,
});
})
.catch((e) => {
fail(e);
});
};
const lookupComment = ({conversationUid}, done, fail) => {
const conversation = mappedConversations.value.find(c => c.uid === conversationUid);
if (!conversation) {
fail();
}
done({
conversation: {
uid: conversationUid,
comments: [
conversation,
]
}
})
}
const getConversations = () => {
axios.get('/editor/conversations', {
params: {
entityId: 1
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then(response => response.data?.data)
.then(response => conversations.value = response.conversations);
}
const mappedConversations = computed(() => {
return conversations.value.map(conversation => {
return {
author: conversation.author || '',
createdAt: conversation.created_at || new Date().toISOString(),
content: conversation.content,
modifiedAt: conversation.created_at || new Date().toISOString(),
uid: conversation.uuid
};
})
});
onBeforeMount(() => {
getConversations();
});
</script>
<template>
<main>
<Editor
:api-key="tinyApiKey"
v-model="content"
:init="{
plugins: 'lists link image table code wordcount tinycomments',
toolbar: 'undo redo | bold italic underline strikethrough | fontfamily fontsize blocks | addcomment showcomments',
tinycomments_mode: 'callback',
tinycomments_author: 'Denis Sinyukov',
tinycomments_create: createComment,
tinycomments_reply: noop,
tinycomments_edit_comment: noop,
tinycomments_delete: noop,
tinycomments_delete_all: noop,
tinycomments_delete_comment: noop,
tinycomments_lookup: lookupComment,
}"
/>
</main>
</template>
Top comments (3)
is it persistent. i mean when you refresh the page does tinymce loads the previous comments?
Yes, they will be loaded from the database and inserted into the editor
@dnsinyukov Great tutorial! Would you be interested in contributing this to the TinyMCE blog as well?