@shashimadushan/docx-editor-react-native
v0.7.4
Published
React Native (Expo + bare) wrapper for the docx-editor-toolkit editor — a touch-friendly DOCX editor rendered inside a WebView, with a typed postMessage bridge for loading/saving .docx bytes.
Maintainers
Readme
@shashimadushan/docx-editor-react-native
A touch-friendly DOCX editor for React Native (Expo managed + bare), built by
running @shashimadushan/docx-editor-editor's mobile layout
inside a react-native-webview. The whole editor UI ships as one prebuilt HTML
file baked into the package — no dev server, no CDN, no asset pipeline to
configure — so it works unmodified in Expo Go, EAS dev builds, and bare React
Native.
Optional features (the AI agent, the Sri Lankan legal templates) are separate plugin packages: install one and its code is in your app, don't and it isn't.
Full guide:
docs/REACT_NATIVE.md— every prop, the plugin model, native chrome, autosave, troubleshooting, and the 0.4 migration. This README is the overview.
Install
npx expo install @shashimadushan/docx-editor-react-native react-native-webview expo-file-systemBare RN: npm install @shashimadushan/docx-editor-react-native react-native-webview,
then pod install.
expo-file-system is an optional peer dependency but strongly recommended — it
backs the autosave adapter and lets the ~1.8 MB editor HTML be cached to a
local file and loaded via file:// rather than crossing the RN bridge as a
prop, which on Android risks a TransactionTooLargeException. Without it the
component still works, with a one-time warning.
Usage
import * as React from 'react';
import { View, Button } from 'react-native';
import { DocxEditorView, useDocxEditorHandle } from '@shashimadushan/docx-editor-react-native';
export default function App() {
const handle = useDocxEditorHandle();
const [isDirty, setIsDirty] = React.useState(false);
return (
<View style={{ flex: 1 }}>
<Button title="Load" onPress={() => handle.current?.loadDocx(SOME_BASE64_DOCX)} />
<Button
title="Save"
onPress={async () => {
const base64 = await handle.current?.saveDocx();
// persist `base64` (write to FileSystem, upload, share…)
}}
/>
<DocxEditorView
ref={handle}
onReady={() => console.log('editor ready')}
onDirtyChange={setIsDirty}
onError={(message) => console.warn('docx editor error', message)}
style={{ flex: 1 }}
/>
</View>
);
}See examples/expo-demo for a complete Expo app — document picker, save to
file, draft restore on launch, both plugins, both native sheets.
What you get
- The mobile editor — tabbed toolbar (Home / Insert / contextual Table / Image), folded header with mode switcher, status bar with word count and zoom, selection format bar, bottom sheets for File/View.
.docxload and save, base64 in and out.- Autosave drafts to on-device storage, with a pluggable adapter.
- Host-provided templates, extra toolbar buttons, comment persistence, theme tokens, and raw CSS overrides — all live-patchable from props without reloading the WebView.
modeprop — lock Editing/Suggesting/Viewing to a host-controlled value (e.g.'viewing'for a document opened from a view-only share), live-patchable like the rest. See Read-only / locked mode.fileNameprop — set the mobile header's title (e.g. from your own document record), live-patchable like the rest. A brand-new document also no longer shows a literal "Untitled" heading baked into the page — that was a bug (fixed in 0.7.3), not a feature.onFileNameChangeprop — fires on every keystroke when the user renames the document in the mobile header, so you can persist it (draft metadata, a server PATCH). Without a handler, an in-header rename is purely cosmetic and disappears on reload — that gap is fixed in 0.7.4, but persisting it is still your app's job.- Native chrome, optionally: render the agent and comments UIs yourself in React Native instead of inside the WebView.
- PDF export ingredients (
getExportSnapshot()) — no PDF library ships here, but you get everything needed to render one on-device (e.g.expo-print) or server-side. See Exporting to PDF. - Plugins for anything else.
Plugins
npx expo install @shashimadushan/docx-editor-react-native-agent
npx expo install @shashimadushan/docx-editor-react-native-legalimport { DocxEditorView } from '@shashimadushan/docx-editor-react-native';
import { agentPlugin } from '@shashimadushan/docx-editor-react-native-agent';
import { legalTemplatesPlugin } from '@shashimadushan/docx-editor-react-native-legal';
<DocxEditorView
plugins={[agentPlugin({ baseUrl: AGENT_URL }), legalTemplatesPlugin()]}
/>;Each package ships a prebuilt JS chunk plus its RN-side factory. The chunk is injected ahead of the core bundle, with React, ReactDOM and the editor package left external and bound to globals the core already exposes — so a plugin is tens of KB, and there's exactly one React/TipTap/ProseMirror instance in the page.
The set of plugins is fixed at mount; their options ride the config channel,
so changing them afterwards reaches the running plugin with no reload. A plugin
that fails to load reports one message through onError and is skipped — the
editor still mounts.
Writing your own uses the same mechanism (createPluginScript,
PLUGIN_CHUNK_EXTERNALS/PLUGIN_CHUNK_GLOBALS, a setup(ctx) export). See
the guide;
packages/react-native-legal is the smallest complete example.
Deprecated since 0.4.0
agentBaseUrl / agentHeaders / agentTools / agentPresentation and
legalTemplates are superseded by the two plugins. They still feed the
plugins' config as a fallback, but on their own they no longer enable
anything — that code isn't in the core bundle to switch on — and they warn once
naming the package to install. Removal no earlier than 0.5.0.
Migration diff.
API
<DocxEditorView />— the editor.refshould be aReact.RefObject<DocxEditorHandle | null>, typically fromuseDocxEditorHandle(). Full prop table.useDocxEditorHandle()— convenience hook returning that typed ref.DocxEditorHandle:loadDocx(base64): Promise<void>saveDocx(): Promise<string>— base64.docxsendAgentMessage(prompt): Promise<void>— with native agent chromesendCommentsHostCommand(command): Promise<void>— with native comments chromegetExportSnapshot(): Promise<DocxEditorExportSnapshot>— ingredients for a PDF export (HTML, live stylesheet, theme vars, watermark, header/footer). Pair withonExportPdfRequestedto implement "Export PDF…" — see Exporting to PDF for the exact DOM contract required and a workedexpo-printexample.
FileSystemDraftStorageAdapter,DraftStorageAdapter— autosave.AgentSheet,CommentsSheet,useAgentActivityLog— the optional native chrome implementations.createPluginScript,PLUGIN_CHUNK_EXTERNALS,PLUGIN_CHUNK_GLOBALS,HOST_RUNTIME_VERSIONand theWebPlugin*types — for authoring plugins.
How it works
webui/ Vite + React app rendering <ReactDocxEditor layout="mobile" />
with an RPC transport to the RN host.
↓ `vite build` (vite-plugin-singlefile inlines all JS/CSS into one HTML file)
scripts/build-webui.mjs
↓ reads webui/dist/index.html, writes it as an escaped template string
src/generated-html.ts `export const EDITOR_HTML = "...";`
↓ imported by
src/DocxEditorView.tsx cached to a local file, loaded as source={{ uri }}Communication between DocxEditorView and webui/src/main.tsx runs over a
versioned request/response/event RPC (src/protocol/, createRpc()) — a
correlation id on every call means concurrent loadDocx()/saveDocx() calls
are safe and a failure only ever rejects its own caller. (bridge.ts's
InMessage/OutMessage remain exported as deprecated aliases; nothing puts
that shape on the wire anymore.)
- RN → WebView methods:
doc.load,doc.save,config.patch,agent.send,comments.hostCommand - WebView → RN methods:
host.exportPdf,host.agent.tool,host.templates.get,host.comments.{list,save,remove} - WebView → RN events:
ready,dirty,error,toolbar.customPressed,menu.customPressed,agent.activity,comment.opened,plugin.error
Settings travel separately, over a config channel: DocxEditorView injects a
DocxEditorConfig before the page's own script runs, then patches it live via
config.patch when props change — which is why theme, injectedCss,
templates, toolbarItems, menuSections and every plugin's options update
without a reload.
webui/src/main.tsx imports protocol/ and config/ via relative paths so
the two sides can't drift apart; only the built HTML ships at runtime, so
this is a build-time-only shared contract.
Building from source
pnpm --filter @shashimadushan/docx-editor-react-native build
# node scripts/build-webui.mjs → vite build + regenerate generated-html.ts
# tsup → src/ → dist/ (ESM + CJS + types; wipes dist/ first)
# node scripts/copy-webui-assets.mjs → dist/webui.html + dist/webui.meta.jsonA change in packages/editor is invisible here until both packages are
rebuilt, editor first. That's the most common "I edited the toolbar and nothing
changed" trap. For faster iteration run pnpm --filter webui dev and preview at
phone width in a browser — it renders the identical component tree.
dist/webui.html is the same HTML generated-html.ts bakes in, as a real file,
for apps that would rather load it as a Metro asset than pay the string-in-JS
parse cost. This package can't require() it for you — Metro only resolves a
static literal path at the call site in your source. Pass the resolved URI to
htmlSource={{ uri }}. dist/webui.meta.json carries
{ packageVersion, protocolVersion, hash, byteLength } for sanity-checking
whatever you load.
Known limitations
- No native file picker or
expo-assetintegration ships here — obtain a base64.docxhowever suits your app (seeexamples/expo-demofor theexpo-document-picker+expo-file-systempattern). - No
branding(radius/density) tokens — seeARCHITECTURE_PLAN.md§5.2 for why a partial implementation would be worse than none. - Host templates support HTML content only, not base64
.docx. - Toolbar items are label-only and always append to the Insert tab.
- In native comments mode the toggle bubble and "Add comment" trigger still render in-page (their position depends on document layout), and the native agent composer has no "@" mention autocomplete.
CommentsPanel's drag-to-reposition isn't touch-adapted — the panel opens at a clamped, usable position but can't be dragged.
License
MIT
