Using the TinyMCE comments with the Vue.js & Laravel frameworks
Tags: vue, laravel, php, tinymce
Published: 2023-11-04 22:08:52

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](https://www.tiny.cloud/pricing/).

The editor's user interface allows users to collaborate on content by creating and responding to comments.

![Comments plugin overview and features](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tt2s398nn159p5artuy8.png)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xj4r1z3dda5oneruhc7p.png)

- 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mbmihqdqqydb8d2givza.png)

- 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a9xm8qm0x9dsd8honivr.png)

The final code will look like this:

```js
<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.

![Conversation database design](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xxvxoa4a7fho2ugil4w5.png)

- Create Laravel web routes to work with comments

```php.
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.

```php
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

```php
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.

```js
: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:

```js
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.
```js
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

![New Comment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zyl3nklid2t3qe954opa.png)

After successful creation, the array with comments goes to the `tinycomments_lookup` function and looks for us newly created comment.

```js
const lookupComment = ({conversationUid}, done, fail) => {

    const conversation = mappedConversations.value.find(c => c.uid === conversationUid);

    if (!conversation) {
        fail();
    }

    done({
        conversation: {
            uid: conversationUid,
            comments: [
                conversation,
            ]
        }
    })
}
```
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dhl3gf8y9pflrjf1i88w.png)

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. 

<u>The full Vue component:</u>

```js
<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>
```


## Related links
- [Configuring the Comments plugin in callback mode](https://www.tiny.cloud/docs/tinymce/6/comments-callback-mode/#how-the-comments-plugin-works-in-callback-mode)