Set and Remove Editable Ranges in Word Documents with JavaScript in React

In documents such as contracts, forms, and official templates, you often want the person filling them in to modify only a few specific places — signature details, project name, acceptance conclusion — while every other clause has to stay exactly as it is. Setting an editable range on a document pins down "what may be changed" precisely and leaves everything else read-only. Spire.Doc for JavaScript does this entirely in the browser via WebAssembly, using a virtual file system (VFS) to manage fonts and file resources — no backend server required.

This article covers two core features:

For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume Spire.Doc is installed and the WebAssembly module is initialized.


Set an Editable Range

Setting an editable range involves three stages: first, load the font files and the target Word document into the WASM virtual file system via FetchFileToVFS; then instantiate a Document and load the file, call Protect to make the whole document read-only, and create a pair of PermissionStart and PermissionEnd markers that share the same id to mark the specified paragraph as an editable range; finally, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.

function App() {
  const SetEditableRange = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the input document into VFS
    const inputFileName = "SetEditableRange.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // Create a document object and load the document
    const doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // Protect the whole document: everything outside the editable range is read-only
    doc.Protect({ type: docModule.ProtectionType.AllowOnlyReading, password: "password" });

    // Create the permission markers: a start and an end with the same id form one editable range
    const start = new docModule.PermissionStart(doc, "testID");
    const end = new docModule.PermissionEnd(doc, "testID");

    // Insert the markers into the first paragraph: the start at the beginning, the end appended at the end
    doc.Sections.get_Item(0).Paragraphs.get_Item(0).ChildObjects.Insert(0, start);
    doc.Sections.get_Item(0).Paragraphs.get_Item(0).ChildObjects.Add(end);

    // Save the document
    const outputFileName = "Set Editable Range.docx";
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Set Editable Range in a Word Document</h1>
      <button onClick={SetEditableRange}>
        Generate
      </button>
    </div>
  );
}
export default App;

In the sample document, the places that need to be filled in are highlighted with light shading (a visual hint only — it has nothing to do with how the editable range is set). Once the editable range has been set, only the shaded paragraph can be modified, and the remaining clauses are read-only in Word.

The document after an editable range is set; the lightly shaded paragraph is the editable range


Remove an Editable Range

Removing an editable range takes a single pass: walk each section and each paragraph of the document in turn, look for the PermissionStart and PermissionEnd objects in the paragraph's ChildObjects collection, and remove each one you find from the collection.

There is one detail that is easy to trip over: ChildObjects.Remove shrinks the collection immediately, so the indexes of the remaining elements all shift forward. The index must therefore not be incremented while removing, or every marker you delete causes the one right behind it to be skipped, leaving markers behind.

function App() {
  const RemoveEditableRange = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

      // Load the input document into VFS
      const inputFileName = "RemoveEditableRange.docx";
      await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

      // Create a document object and load the document
      const doc = new docModule.Document();
      doc.LoadFromFile(inputFileName);

      // Iterate over every section and paragraph and delete the permission markers
      for (let i = 0; i < doc.Sections.Count; i++) {
        const section = doc.Sections.get_Item(i);
        for (let j = 0; j < section.Body.Paragraphs.Count; j++) {
          const paragraph = section.Body.Paragraphs.get_Item(j);

          // Remove on a match; the collection shrinks, so the index is not incremented
          for (let k = 0; k < paragraph.ChildObjects.Count;) {
            const obj = paragraph.ChildObjects.get_Item(k);
            if (obj instanceof docModule.PermissionStart || obj instanceof docModule.PermissionEnd) {
              paragraph.ChildObjects.Remove(obj);
            } else {
              k++;
            }
          }
        }
      }

      // Save the document
      const outputFileName = "Remove Editable Range.docx";
      doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    // Release resources
    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Remove Editable Ranges from a Word Document</h1>
      <button onClick={RemoveEditableRange}>
        Generate
      </button>
    </div>
  );
}
export default App;

Removing the markers only affects how the editable area is divided; the text content and the formatting of the document do not change in any way.

The document after the editable range markers are removed; the content and formatting stay unchanged


FAQ

The editable range is set, but the content inside it still cannot be edited

Cause: The permission markers only take effect together with the document's editing restriction. If PermissionStart and PermissionEnd are inserted without calling Protect, the document never enters the protected state and the markers have no effect whatsoever; on top of that, the ids of the two markers must match exactly before Word recognizes them as one single editable range.

Solution: Enable the editing restriction first, and then create the paired markers with the same id:

// Enable protection first so that the markers mean something
document.Protect({ type: wasmModule.ProtectionType.AllowOnlyReading, password: "password" });

// The start and the end must use the same id
const start = new wasmModule.PermissionStart(document, "testID");
const end = new wasmModule.PermissionEnd(document, "testID");

Some markers are missed when removing editable ranges

Cause: ChildObjects.Remove shifts the indexes of all the subsequent elements in the collection forward. If the index is incremented while removing inside a for loop, every object that is removed causes the one right behind it to be skipped, and the more markers are left in the document, the more obvious the misses become.

Solution: Switch to "remove on a match, do not increment the index", or collect the objects to be removed first and then iterate backwards:

for (let k = 0; k < paragraph.ChildObjects.Count;) {
  const obj = paragraph.ChildObjects.get_Item(k);
  if (obj instanceof wasmModule.PermissionStart || obj instanceof wasmModule.PermissionEnd) {
    paragraph.ChildObjects.Remove(obj);
    // Do not increment k here: check the new object at the current index
  } else {
    k++;
  }
}

The document is still read-only after the markers are removed

Cause: PermissionStart and PermissionEnd only mark "which areas may be edited"; removing them does not turn off the document's editing restriction. The protection is still in effect, so at that point the whole document cannot be edited.

Solution: If the protection is no longer needed, call Unprotect after removing the markers; if the document was protected with a password, pass the password that was used at the time:

document.Unprotect("password");

Get a Free License

Spire.Doc for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.