Report an issue

Integrating comments with your application

The comments feature provides an API that lets you add, remove, and update comments in the editor. To save and access all these changes in your database, you first need to integrate this feature.

This guide describes integrating comments as a standalone plugin (the asynchronous version of it). If you are using real-time collaboration, refer to the Real-time collaboration features integration guide.

# Integration methods

This guide will discuss two ways to integrate CKEditor 5 with your comments data source:

The adapter integration is the recommended one because it gives you better control over the data.

# Before you start

Complementary to this guide, we provide ready-to-use samples available for download. You may use the samples as an example or a starting point for your integration.

# Preparing a custom editor setup

To use the comments plugin, you need to prepare a custom editor setup with the asynchronous version of the comments feature included.

The easiest way to do that is by using the Builder. Pick a preset and start customizing your editor.

The Builder allows you to pick your preferred distribution method and framework. For this guide, we will use the “Vanilla JS” option with “npm” and a simple setup based on the “Classic Editor (basic)” preset, with the comments feature enabled.

In the “Features” section of the Builder (2nd step), make sure to:

  • turn off the “real-time” toggle next to the “Collaboration” group,
  • enable the “Collaboration → Comments” feature.

Once you finish the setup, the Builder will provide you with the necessary HTML, CSS, and JavaScript code snippets. We will use those code snippets in the next step.

# Setting up a sample project

Once we have a custom editor setup we need a simple JavaScript project to run it. For this, we recommend cloning the basic project template from our repository:

npx -y degit ckeditor/ckeditor5-tutorials-examples/sample-project sample-project
cd sample-project
npm install

Then, install the necessary dependencies:

npm install ckeditor5
npm install ckeditor5-premium-features

This project template uses Vite under the hood and contains 3 source files that we will use: index.html, style.css, and main.js.

It is now the time to use our custom editor setup. Go to the “Installation” section of the Builder and copy the generated code snippets to those 3 files.

# Activating the feature

To use this premium feature, you need to activate it with a license key. Refer to the License key and activation guide for details.

After you have successfully obtained the license key open the main.js file and update the your-license-key string with your license key.

# Building the project

Finally, build the project by running:

npm run dev

When you open the sample in the browser you should see the WYSIWYG editor with the comments plugin. However, it still does not load or save any data. You will learn how to add data to the comments plugin later in this guide.

Let’s now dive deeper into the structure of this setup.

# Basic setup’s anatomy

Examples below implement the wide sidebar display mode for comment threads. If you want to use the inline display mode, remove parts of the snippets that set up the sidebar.

Let’s now go through the key fragments of this basic setup.

# HTML structure

The HTML and CSS structure of the page creates two columns:

  • <div class="editor-container__editor"> is the container used by the editor.
  • <div class="editor-container__sidebar"> is the container used by the sidebar that holds the annotations (namely comments).

# JavaScript

The main.js file sets up the editor instance:

  • Loads all necessary editor plugins (including the Comments plugin).
  • Sets the licenseKey configuration option.
  • Sets the sidebar.container configuration option to the container mentioned above.
  • Adds the comment and commentsArchive buttons to the editor toolbar.
  • Defines the templates for the CommentsIntegration and UsersIntegrations plugins that we will use in the next steps of this tutorial.

If you use the ImageToolbar plugin, also add the comment button to the image toolbar.

# Comments API

The integration below uses the comments API. Making yourself familiar with the API may help you understand the code snippets. In case of any problems, refer to the comments API documentation.

# Next steps

We have set up a simple JavaScript project that runs a basic CKEditor 5 instance with the asynchronous version of the Comments feature. It does not yet handle loading or saving data, though. The next two sections cover the two available integration methods.

# A simple “load and save” integration

In this solution, users and comments data is loaded during the editor initialization, and comments data is saved after you finish working with the editor (for example when you submit the form containing the WYSIWYG editor).

This method is recommended if you can trust your users or if you provide additional validation of the submitted data. This way, we can make sure that the user changes their comments only.

Complementary to this guide, we provide ready-to-use samples available for download. You may use the samples as an example or as a starting point for your own integration.

# Loading the data

When the comments plugin is already included in the editor, you need to create a plugin which will initialize users and existing comments.

First, dump the users and comments data to a variable that will be available for your plugin.

If your application needs to request the comments data from the server asynchronously, instead of putting the data in the HTML source, you can create a plugin that will fetch the data from the database. In this case, your plugin should return a Promise from the Plugin.init() method to make sure that the editor initialization waits for your data.

If you have set up the sample project as recommended in the “Before you start” section, open the main.js file and add this variable right after the imports:

// Application data will be available under a global variable `appData`.
const appData = {
    // Users data.
    users: [
        {
            id: 'user-1',
            name: 'Mex Haddox'
        },
        {
            id: 'user-2',
            name: 'Zee Croce'
        }
    ],

    // The ID of the current user.
    userId: 'user-1',

    // Comment threads data.
    commentThreads: [
        {
            threadId: 'thread-1',
            comments: [
                {
                    commentId: 'comment-1',
                    authorId: 'user-1',
                    content: '<p>Are we sure we want to use a made-up disorder name?</p>',
                    createdAt: new Date( '09/20/2018 14:21:53' ),
                    attributes: {}
                },
                {
                    commentId: 'comment-2',
                    authorId: 'user-2',
                    content: '<p>Why not?</p>',
                    createdAt: new Date( '09/21/2018 08:17:01' ),
                    attributes: {}
                }
            ],
            context: {
                type: 'text',
                value: 'Bilingual Personality Disorder'
            },
            unlinkedAt: null,
            resolvedAt: null,
            resolvedBy: null,
            attributes: {}
        }
    ],

    // Editor initial data.
    initialData:
        `<h2>
            <comment-start name="thread-1"></comment-start>
            Bilingual Personality Disorder
            <comment-end name="thread-1"></comment-end>
        </h2>
        <p>
            This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth.
            As recent studies show, the language you speak has more effects on you than you realize.
            According to the studies, the language a person speaks affects their cognition,
            behavior, emotions and hence <strong>their personality</strong>.
        </p>
        <p>
            This shouldn’t come as a surprise
            <a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
            that different regions of the brain become more active depending on the activity.
            The structure, information and especially <strong>the culture</strong> of languages varies substantially
            and the language a person speaks is an essential element of daily life.
        </p>`
};

The Builder’s output sample already provides templates of two plugins: UsersIntegration and CommentsIntegration. Replace them with ones that read the data from appData and use the Users and CommentsRepository API:

class UsersIntegration extends Plugin {
    static get requires() {
        return [ 'Users' ];
    }

    static get pluginName() {
        return 'UsersIntegration';
    }

    init() {
        const usersPlugin = this.editor.plugins.get( 'Users' );

        // Load the users data.
        for ( const user of appData.users ) {
            usersPlugin.addUser( user );
        }

        // Set the current user.
        usersPlugin.defineMe( appData.userId );
    }
}

class CommentsIntegration extends Plugin {
    static get requires() {
        return [ 'CommentsRepository', 'UsersIntegration' ];
    }

    static get pluginName() {
        return 'CommentsIntegration';
    }

    init() {
        const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' );

        // Load the comment threads data.
        for ( const commentThread of appData.commentThreads ) {
            commentsRepositoryPlugin.addCommentThread( commentThread );
        }
    }
}

Update the editorConfig.initialData property to use the appData.initialData value:

const editorConfig = {
    // ...

    initialData: appData.initialData

    // ...
};

And build the project:

npm run dev

You should now we see an editor instance with one comment thread.

# Saving the data

To save the comments data, you need to get it using the CommentsRepository API first. To do this, use the getCommentThreads() method.

Then, use the comment threads data to save it in your database in the way you prefer. See the example below.

In index.html add:

<button id="get-data">Get data</button>

In main.js update the ClassicEditor.create() call with a chained then():

ClassicEditor
    .create( /* ... */ )
    .then( editor => {
        // After the editor is initialized, add an action to be performed after a button is clicked.
        const commentsRepository = editor.plugins.get( 'CommentsRepository' );

        // Get the data on demand.
        document.querySelector( '#get-data' ).addEventListener( 'click', () => {
            const editorData = editor.data.get();
            const commentThreadsData = commentsRepository.getCommentThreads( {
                skipNotAttached: true,
                skipEmpty: true,
                toJSON: true
            } );

            // Now, use `editorData` and `commentThreadsData` to save the data in your application.
            // For example, you can set them as values of hidden input fields.
            console.log( editorData );
            console.log( commentThreadsData );
        } );
    } );

It is recommended to stringify the attributes value to JSON, save it as a string in your database, and then parse the value from JSON when loading comments.

# Demo

Console

// Use the `Save data with comments` button to see the result...

# Adapter integration

Adapter integration uses an adapter object – provided by you – to immediately save changes in comments in your data store. It is the recommended way of integrating comments with your application because it lets you handle client-server communication more securely. For example, you can check user permissions, validate sent data, or update the data with information obtained on the server side, like the comment creation date. You will see how to handle the server response in the following steps.

Complementary to this guide, we provide ready-to-use samples available for download. You may use the samples as an example or as a starting point for your own integration.

# Implementation

First, define the adapter using the CommentsRepository#adapter property. Adapter methods are called after the user makes a change in the comments. The adapter allows you to save the change in your database immediately. Each comment action has a separate adapter method that you should implement.

On the UI side, each change in comments is performed immediately, however, all adapter actions are asynchronous and are performed in the background. Because of this, all adapter methods need to return a Promise. When the promise is resolved, it means that everything went fine and a local change was successfully saved in the data store. When the promise is rejected, the editor throws a CKEditorError error, which works nicely together with the watchdog feature. When you handle the server response you can decide if the promise should be resolved or rejected.

While any adapter action is being performed, a pending action is automatically added to the editor PendingActions plugin, so you do not have to worry that the editor will be destroyed before the adapter action has finished.

Now you are ready to implement the adapter.

If you have set up the sample project as recommended in the “Before you start” section, open the main.js file and add this variable right after the imports:

// Application data will be available under a global variable `appData`.
const appData = {
    // Users data.
    users: [
        {
            id: 'user-1',
            name: 'Mex Haddox'
        },
        {
            id: 'user-2',
            name: 'Zee Croce'
        }
    ],

    // The ID of the current user.
    userId: 'user-1',

    // Editor initial data.
    initialData:
        `<h2>
            <comment-start name="thread-1"></comment-start>
            Bilingual Personality Disorder
            <comment-end name="thread-1"></comment-end>
        </h2>
        <p>
            This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth.
            As recent studies show, the language you speak has more effects on you than you realize.
            According to the studies, the language a person speaks affects their cognition,
            behavior, emotions and hence <strong>their personality</strong>.
        </p>
        <p>
            This shouldn’t come as a surprise
            <a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
            that different regions of the brain become more active depending on the activity.
            The structure, information and especially <strong>the culture</strong> of languages varies substantially
            and the language a person speaks is an essential element of daily life.
        </p>`

};

The Builder’s output sample already provides templates of two plugins UsersIntegration and CommentsIntegration. Replace them with ones that read the data from appData and use the Users and CommentsRepository API:

class UsersIntegration extends Plugin {
    static get requires() {
        return ['Users'];
    }

    static get pluginName() {
        return 'UsersIntegration';
    }

    init() {
        const usersPlugin = this.editor.plugins.get( 'Users' );

        // Load the users data.
        for ( const user of appData.users ) {
            usersPlugin.addUser( user );
        }

        // Set the current user.
        usersPlugin.defineMe( appData.userId );
    }
}

class CommentsIntegration extends Plugin {
    static get requires() {
        return [ 'CommentsRepository', 'UsersIntegration' ];
    }

    static get pluginName() {
        return 'CommentsIntegration';
    }

    init() {
        const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' );

        // Set the adapter on the `CommentsRepository#adapter` property.
        commentsRepositoryPlugin.adapter = {
            addComment( data ) {
                console.log( 'Comment added', data );

                // Write a request to your database here. The returned `Promise`
                // should be resolved when the request has finished.
                // When the promise resolves with the comment data object, it
                // will update the editor comment using the provided data.
                return Promise.resolve( {
                    createdAt: new Date()       // Should be set on the server side.
                } );
            },

            updateComment( data ) {
                console.log( 'Comment updated', data );

                // Write a request to your database here. The returned `Promise`
                // should be resolved when the request has finished.
                return Promise.resolve();
            },

            removeComment( data ) {
                console.log( 'Comment removed', data );

                // Write a request to your database here. The returned `Promise`
                // should be resolved when the request has finished.
                return Promise.resolve();
            },

            addCommentThread( data ) {
                console.log( 'Comment thread added', data );

                // Write a request to your database here. The returned `Promise`
                // should be resolved when the request has finished.
                return Promise.resolve( {
                    threadId: data.threadId,
                    comments: data.comments.map( ( comment ) => ( { commentId: comment.commentId, createdAt: new Date() } ) ) // Should be set on the server side.
                } );
            },

            getCommentThread( data ) {
                console.log( 'Getting comment thread', data );

                // Write a request to your database here. The returned `Promise`
                // should resolve with the comment thread data.
                return Promise.resolve( {
                    threadId: data.threadId,
                    comments: [
                        {
                            commentId: 'comment-1',
                            authorId: 'user-2',
                            content: '<p>Are we sure we want to use a made-up disorder name?</p>',
                            createdAt: new Date(),
                            attributes: {}
                        }
                    ],
                    // It defines the value on which the comment has been created initially.
                    // If it is empty it will be set based on the comment marker.
                    context: {
                        type: 'text',
                        value: 'Bilingual Personality Disorder'
                    },
                    unlinkedAt: null,
                    resolvedAt: null,
                    resolvedBy: null,
                    attributes: {},
                    isFromAdapter: true
                } );
            },

            updateCommentThread( data ) {
                console.log( 'Comment thread updated', data );

                // Write a request to your database here. The returned `Promise`
                // should be resolved when the request has finished.
                return Promise.resolve();
            },

            resolveCommentThread( data ) {
                console.log( 'Comment thread resolved', data );

                // Write a request to your database here. The returned `Promise`
                // should be resolved when the request has finished.
                return Promise.resolve( {
                    resolvedAt: new Date(), // Should be set on the server side.
                    resolvedBy: usersPlugin.me.id // Should be set on the server side.
                } );
            },

            reopenCommentThread( data ) {
                console.log( 'Comment thread reopened', data );

                // Write a request to your database here. The returned `Promise`
                // should be resolved when the request has finished.
                return Promise.resolve();
            },

            removeCommentThread( data ) {
                console.log( 'Comment thread removed', data );

                // Write a request to your database here. The returned `Promise`
                // should be resolved when the request has finished.
                return Promise.resolve();
            }

        };
    }
}

Update the editorConfig.initialData property to use appData.initialData value:

const editorConfig = {
    // ...

    initialData: appData.initialData

    // ...
};

And build the project:

npm run dev

You should now we see an editor instance with one comment thread. Observe the browser console while you interact with the comments feature in the editor (add/remove threads and comments).

It is recommended to stringify the attributes value to JSON, save it as a string in your database, and then parse the value from JSON when loading comments.

# Demo

Pending adapter actions console

// Add, remove or update a comment to see the result...

Since the comments adapter saves the comment changes immediately after they are performed, it is also recommended to use the Autosave plugin to save the editor content after each change.

# Why is there no event when I remove comment thread markers from the content?

Note that no remove event is fired when you remove the marker corresponding to the comment thread. Instead, the comment thread is resolved which triggers CommentsRepository#resolveCommentThread event. This operation can be restored using undo (Cmd+Z or Ctrl+Z), which will fire CommentsRepository#reopenCommentThread event.

However, you can still remove the comment thread by using available buttons in an annotation. Remember that the removal operation cannot be undone.

When implementing adapter methods (getCommentThread() and getComment()), ensure they do not return deleted data, as removal is considered a permanent operation.

# Comments samples

Please visit the ckeditor5-collaboration-samples GitHub repository to find several sample integrations of the comments feature.