DEV Community

Cover image for Using the TinyMCE comments with the Vue.js & Laravel frameworks
Denis Sinyukov
Denis Sinyukov

Posted on • Edited on • Originally published at coderden.dev

Using the TinyMCE comments with the Vue.js & Laravel frameworks

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.

Comments plugin overview and features

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.

TinyMCE API key

  • 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.

Approved Domains

  • 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

Editor Example

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>
Enter fullscreen mode Exit fullscreen mode

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.

Conversation database design

  • Create Laravel web routes to work with comments
Route::get('/editor/conversations', [EditorController::class, 'getConversations']);
Route::post('/editor/conversations', [EditorController::class, 'createConversation']);
Enter fullscreen mode Exit fullscreen mode
  • 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
        ]
    ]);
}
Enter fullscreen mode Exit fullscreen mode

Example of a successful server response:

{
  "meta": {
    "code": 200
  },
  "data": {
    "conversationId": 1,
    "conversationUid": "4ae419e1-79d5-44f0-9c96-ca842959f7a4"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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
        ]
    ]);
}
Enter fullscreen mode Exit fullscreen mode

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,
    ...
}"
Enter fullscreen mode Exit fullscreen mode

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);
        });
};
Enter fullscreen mode Exit fullscreen mode

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
        };
    })
});
Enter fullscreen mode Exit fullscreen mode

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

New 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,
            ]
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

Image description

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>
Enter fullscreen mode Exit fullscreen mode

Related links

Top comments (3)

Collapse
 
shehzar_abbasi_ecf018287a profile image
Shehzar Abbasi

is it persistent. i mean when you refresh the page does tinymce loads the previous comments?

Collapse
 
dnsinyukov profile image
Denis Sinyukov

Yes, they will be loaded from the database and inserted into the editor

Collapse
 
mrinasugosh profile image
Mrinalini Sugosh (Mrina)

@dnsinyukov Great tutorial! Would you be interested in contributing this to the TinyMCE blog as well?