Comments
Read, create, reply to, and resolve DOCX comment threads with the packaged review rail or useReview.
Comments attach a discussion thread to a text range. They share the review rail, the useReview() hook, and the card layout with tracked changes. Comments require reviewModule() from @docx-editor.dev/pro, which uses the EigenPal Pro License.
Add a comment
comment(text, author?) comments on the current selection. This example keeps the draft when the editor refuses the write:
import { useState } from 'react';
import { useReview } from '@docx-editor.dev/pro/react';
function CommentBox() {
const { comment, selectionAnchorY } = useReview();
const [text, setText] = useState('');
// null when nothing is selected: there is nowhere to anchor a comment.
if (selectionAnchorY === null) return null;
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (comment(text)) setText('');
}}
>
<textarea value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit" disabled={!text.trim()}>
Comment
</button>
</form>
);
}<script setup lang="ts">
import { ref } from 'vue';
import { useReview } from '@docx-editor.dev/pro/vue';
const text = ref('');
const review = useReview();
function submit() {
if (review.comment(text.value)) {
text.value = '';
}
}
</script>
<template>
<form v-if="review.selectionAnchorY.value !== null" @submit.prevent="submit">
<textarea v-model="text" />
<button type="submit" :disabled="!text.trim()">Comment</button>
</form>
</template>Mount this component inside DocxEditorRoot.
A refused write returns false and does not throw. selectionAnchorY is the selection's document-space Y coordinate.
The packaged DocxEditorReview.AddComment and DocxEditorReview.Draft parts provide this workflow. For placement in custom cards, see Custom cards in the review rail.
Read and update threads
useReview().items holds comments and revisions. This example lists comments with a reply button:
import { useReview } from '@docx-editor.dev/pro/react';
function Threads() {
const { items, reply } = useReview();
return (
<ul>
{items
.filter((item) => item.kind === 'comment')
.map((item) => (
<li key={item.key}>
{/* File-derived. Render as text, never as markup. */}
{item.text}: {item.author}
<button onClick={() => reply(item, 'Agreed.')}>Reply</button>
</li>
))}
</ul>
);
}<script setup lang="ts">
import { computed } from 'vue';
import { useReview } from '@docx-editor.dev/pro/vue';
const review = useReview();
const comments = computed(() => review.items.value.filter((item) => item.kind === 'comment'));
</script>
<template>
<ul>
<li v-for="item in comments" :key="item.key">
{{ item.text }}: {{ item.author }}
<button type="button" @click="review.reply(item, 'Agreed.')">Reply</button>
</li>
</ul>
</template>Each action returns whether the editor applied it. commentResolutionDisabledReason explains why resolve and reopen are unavailable, such as in viewing mode:
| Action | Result | Refusal behavior |
|---|---|---|
comment(text, author?) | Adds a thread to the selection | Returns false; the caller keeps its draft |
reply(item, text, author?) | Adds a reply, or comments on a revision range | Returns false |
resolve(item) | Marks an open thread complete | Repeating it succeeds without a write. Returns false for a stale or non-comment item. |
reopen(item) | Reopens a complete thread | Repeating it succeeds without a write. Returns false for a stale or non-comment item. |
remove(item) | Deletes a comment thread or rejects a tracked change | Returns false when refused; a refused tracked change sets lastRefusal.action to reject |
Comment items include these fields:
| Field | Meaning |
|---|---|
key, id | Stable review and OOXML identifiers |
author, initials, date | Comment author metadata |
text | File-derived comment text |
replyIds | Reply identifiers in the thread |
resolved | Whether w15:commentsEx marks the thread complete |
parentId | Parent comment identifier; absent on a thread root |
anchorY, pageIndex | Document placement |
isActive | Whether the caret is in the item |
editor.getComments() on the core editor returns an empty array. For comment records, use getReviewItems() with the review module.
Show refused actions
lastRefusal from useReview() records the latest refused review action. This example shows the cause under a custom card:
import {
useReview,
useReviewRefusalMessage,
type ReviewItemView,
} from '@docx-editor.dev/pro/react';
function RefusalNotice({ item }: { item: ReviewItemView }) {
const { lastRefusal } = useReview();
const refusalMessage = useReviewRefusalMessage();
if (lastRefusal?.key !== item.key) return null;
return <p>{refusalMessage(lastRefusal)}</p>;
}<script setup lang="ts">
import { useReview, useReviewRefusalMessage, type ReviewItemView } from '@docx-editor.dev/pro/vue';
defineProps<{ item: ReviewItemView }>();
const review = useReview();
const refusalMessage = useReviewRefusalMessage();
</script>
<template>
<p v-if="review.lastRefusal.value && review.lastRefusal.value.key === item.key">
{{ refusalMessage(review.lastRefusal.value) }}
</p>
</template>lastRefusal names the refused action, the card key (null for a new comment), and the diagnostic code. final is true when the collaboration session ended, so a retry cannot help. sequence rises with each refusal, including repeats.
- Every
useReview()over one editor shareslastRefusal. The packaged card shows it withDocxEditorReview.Refusal. - The next applied action, or a change to the document, mode, editability, collaboration status, or author filter, clears it. A caret move does not.
- A mounted
DocxEditorReviewannounces every refusal in one polite live region. Do not add a second one. - Without
DocxEditorReview, announcelastRefusalyourself, keyed onlastRefusal.sequence.
For every code and its remedy, see Diagnostics.
Save comment changes
Each comment or reply write emits the editor's change event. Use it for autosave and dirty tracking:
useEditorEvent('change', (change) => void autosave(change.revision));The editor saves author, initials, date, reply threading, and resolved state in the comments part. Other DOCX applications can continue these threads.
Limits
- Author:
commentandreplyrequire a non-emptyauthor, because OOXML requiresw:author. - Browser writes: All writes need
reviewModule(), an attached editor, and an editable mode. - Protection: Comments-only protection permits comment actions. Read-only protection and
mode="view"refuse writes. See Document protection. - Ranges: Empty comment text and cross-cell anchors are refused, never approximated.
- Revisions: A reply to a revision becomes a comment over the revision range, because
w:insandw:delhave no body. - Undo: Resolve and reopen write one transaction and one Undo entry. A repeated call creates none.
- Suggesting mode: Resolve and reopen work in suggesting mode and create no tracked change.
- Security: Render file-derived comment text as text, never as HTML.