Skip to content
This repository was archived by the owner on Jan 30, 2026. It is now read-only.

Commit 601e9ed

Browse files
committed
feat: update exportTemplate functionality to support async operations and improve user experience
1 parent 67aaad4 commit 601e9ed

6 files changed

Lines changed: 98 additions & 85 deletions

File tree

README.md

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ ref.current.previousField(); // Shift+Tab behavior
123123

124124
// Get data
125125
const fields = ref.current.getFields();
126-
const template = ref.current.exportTemplate();
126+
const template = await ref.current.exportTemplate();
127127
```
128128

129129
## Custom Components
@@ -201,15 +201,8 @@ function TemplateEditor() {
201201
Get the complete template data for saving:
202202
203203
```jsx
204-
const handleSave = () => {
205-
const data = ref.current?.exportTemplate();
206-
207-
// Save to backend
208-
await api.saveTemplate({
209-
name: 'Invoice Template',
210-
fields: data.fields,
211-
document: data.document
212-
});
204+
const handleSave = async () => {
205+
await ref.current?.exportTemplate({ fileName: 'invoice.docx' });
213206
};
214207
```
215208

demo/src/App.tsx

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
import React, { useState, useRef, useCallback, useMemo } from 'react';
2-
import SuperDocTemplateBuilder from '@superdoc-dev/template-builder';
1+
import React, { useState, useRef, useCallback, useMemo } from "react";
2+
import SuperDocTemplateBuilder from "@superdoc-dev/template-builder";
33
import type {
44
SuperDocTemplateBuilderHandle,
55
TemplateField,
6-
FieldDefinition
7-
} from '@superdoc-dev/template-builder';
8-
import 'superdoc/dist/style.css';
9-
import './App.css';
6+
FieldDefinition,
7+
} from "@superdoc-dev/template-builder";
8+
import "superdoc/dist/style.css";
9+
import "./App.css";
1010

1111
const availableFields: FieldDefinition[] = [
1212
// Contact Information
13-
{ id: 'customer_name', label: 'Customer Name', category: 'Contact' },
14-
{ id: 'customer_email', label: 'Customer Email', category: 'Contact' },
15-
{ id: 'customer_phone', label: 'Customer Phone', category: 'Contact' },
16-
{ id: 'customer_address', label: 'Customer Address', category: 'Contact' },
13+
{ id: "customer_name", label: "Customer Name", category: "Contact" },
14+
{ id: "customer_email", label: "Customer Email", category: "Contact" },
15+
{ id: "customer_phone", label: "Customer Phone", category: "Contact" },
16+
{ id: "customer_address", label: "Customer Address", category: "Contact" },
1717

1818
// Company Information
1919
{ id: 'company_name', label: 'Company Name', category: 'Company' },
@@ -45,8 +45,7 @@ const availableFields: FieldDefinition[] = [
4545
export function App() {
4646
const [fields, setFields] = useState<TemplateField[]>([]);
4747
const [events, setEvents] = useState<string[]>([]);
48-
const [showExport, setShowExport] = useState(false);
49-
const [exportData, setExportData] = useState<any>(null);
48+
const [isDownloading, setIsDownloading] = useState(false);
5049
const builderRef = useRef<SuperDocTemplateBuilderHandle>(null);
5150

5251
const log = useCallback((msg: string) => {
@@ -82,11 +81,25 @@ export function App() {
8281
log('⌨ Trigger detected');
8382
}, [log]);
8483

85-
const handleExportTemplate = useCallback(() => {
86-
const data = builderRef.current?.exportTemplate();
87-
setExportData(data);
88-
setShowExport(true);
89-
log('📤 Template exported');
84+
const handleExportTemplate = useCallback(async () => {
85+
if (!builderRef.current) {
86+
return;
87+
}
88+
89+
try {
90+
setIsDownloading(true);
91+
92+
await builderRef.current.exportTemplate({
93+
fileName: "template.docx",
94+
});
95+
96+
log("📤 Template exported");
97+
} catch (error) {
98+
log("⚠️ Export failed");
99+
console.error("Failed to export template", error);
100+
} finally {
101+
setIsDownloading(false);
102+
}
90103
}, [log]);
91104

92105
const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -147,8 +160,12 @@ export function App() {
147160
<span className="hint">Tab/Shift+Tab to navigate</span>
148161
</div>
149162
<div className="toolbar-right">
150-
<button onClick={handleExportTemplate} className="export-button">
151-
Export Template
163+
<button
164+
onClick={handleExportTemplate}
165+
className="export-button"
166+
disabled={isDownloading}
167+
>
168+
{isDownloading ? "Exporting..." : "Export Template"}
152169
</button>
153170
</div>
154171
</div>
@@ -177,27 +194,6 @@ export function App() {
177194
</div>
178195
)}
179196

180-
{/* Export Modal */}
181-
{showExport && (
182-
<div className="modal-overlay" onClick={() => setShowExport(false)}>
183-
<div className="modal" onClick={e => e.stopPropagation()}>
184-
<h2>Exported Template</h2>
185-
<div className="export-content">
186-
<h3>Fields ({exportData?.fields?.length || 0})</h3>
187-
<pre>{JSON.stringify(exportData?.fields || [], null, 2)}</pre>
188-
{exportData?.document && (
189-
<>
190-
<h3>Document Structure</h3>
191-
<pre>{JSON.stringify(exportData.document, null, 2).substring(0, 500)}...</pre>
192-
</>
193-
)}
194-
</div>
195-
<button onClick={() => setShowExport(false)} className="modal-close">
196-
Close
197-
</button>
198-
</div>
199-
</div>
200-
)}
201197
</div>
202198
</div>
203199
);

src/__tests__/SuperDocTemplateBuilder.test.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -224,14 +224,11 @@ describe("SuperDocTemplateBuilder component", () => {
224224
await waitForBuilderReady();
225225
await waitFor(() => expect(ref.current).toBeTruthy());
226226

227-
const exportData = ref.current?.exportTemplate();
228-
229-
expect(exportData).toMatchObject({
230-
fields: [
231-
{ id: "field1", alias: "Field 1", tag: "contact" },
232-
{ id: "field2", alias: "Field 2", tag: "invoice" },
233-
],
234-
document: expect.any(Object),
227+
await ref.current?.exportTemplate({ fileName: "export.docx" });
228+
229+
expect(superDocMock.mockExport).toHaveBeenCalledWith({
230+
exportType: ["docx"],
231+
exportedName: "export.docx",
235232
});
236233
});
237234

src/index.tsx

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -66,25 +66,25 @@ const SuperDocTemplateBuilder = forwardRef<
6666
const success =
6767
mode === "inline"
6868
? editor.commands.insertStructuredContentInline?.({
69-
attrs: {
70-
id: fieldId,
71-
alias: field.alias,
72-
tag: field.metadata
73-
? JSON.stringify(field.metadata)
74-
: field.category,
75-
},
76-
text: field.defaultValue || field.alias,
77-
})
69+
attrs: {
70+
id: fieldId,
71+
alias: field.alias,
72+
tag: field.metadata
73+
? JSON.stringify(field.metadata)
74+
: field.category,
75+
},
76+
text: field.defaultValue || field.alias,
77+
})
7878
: editor.commands.insertStructuredContentBlock?.({
79-
attrs: {
80-
id: fieldId,
81-
alias: field.alias,
82-
tag: field.metadata
83-
? JSON.stringify(field.metadata)
84-
: field.category,
85-
},
86-
text: field.defaultValue || field.alias,
87-
});
79+
attrs: {
80+
id: fieldId,
81+
alias: field.alias,
82+
tag: field.metadata
83+
? JSON.stringify(field.metadata)
84+
: field.category,
85+
},
86+
text: field.defaultValue || field.alias,
87+
});
8888

8989
if (success) {
9090
const newField: Types.TemplateField = {
@@ -339,12 +339,31 @@ const SuperDocTemplateBuilder = forwardRef<
339339
selectField(templateFields[prevIndex].id);
340340
}, [templateFields, selectedFieldId, selectField]);
341341

342-
const exportTemplate = useCallback(() => {
343-
return {
344-
fields: templateFields,
345-
document: superdocRef.current?.activeEditor?.exportDocx(),
346-
};
347-
}, [templateFields]);
342+
const exportTemplate = useCallback(
343+
async (options?: { fileName?: string }): Promise<void> => {
344+
try {
345+
const documentBlob = await superdocRef.current?.export({
346+
exportType: ["docx"],
347+
exportedName: options?.fileName || "document.docx",
348+
});
349+
350+
if (!documentBlob) return;
351+
352+
const blobUrl = URL.createObjectURL(documentBlob as Blob);
353+
const link = globalThis.document.createElement("a");
354+
link.href = blobUrl;
355+
link.download = options?.fileName || "document.docx";
356+
globalThis.document.body.appendChild(link);
357+
link.click();
358+
globalThis.document.body.removeChild(link);
359+
URL.revokeObjectURL(blobUrl);
360+
} catch (error) {
361+
console.error("Failed to export DOCX", error);
362+
throw error;
363+
}
364+
},
365+
[],
366+
);
348367

349368
// Imperative handle
350369
useImperativeHandle(ref, () => ({

src/test/setup.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const mockUpdateStructuredContentById = vi.fn();
77
const mockDeleteStructuredContentById = vi.fn();
88
const mockSelectStructuredContentById = vi.fn();
99
const mockDestroy = vi.fn();
10+
const mockExportDocx = vi.fn(async () => new Blob());
1011

1112
const mockEditor = {
1213
commands: {
@@ -37,6 +38,7 @@ const mockEditor = {
3738
dispatch: vi.fn(),
3839
coordsAtPos: vi.fn(() => ({ left: 0, top: 0 })),
3940
},
41+
exportDocx: mockExportDocx,
4042
on: vi.fn(),
4143
};
4244

@@ -48,6 +50,7 @@ const SuperDocMock = vi.fn((options: any = {}) => {
4850
return {
4951
destroy: mockDestroy,
5052
activeEditor: mockEditor,
53+
exportDocx: mockExportDocx,
5154
on: vi.fn((event: string, handler: (data?: any) => void) => {
5255
if (event === "editorCreate") {
5356
queueMicrotask(() => handler({ editor: mockEditor }));
@@ -70,6 +73,7 @@ const SuperDocMock = vi.fn((options: any = {}) => {
7073
(SuperDocMock as any).mockSelectStructuredContentById =
7174
mockSelectStructuredContentById;
7275
(SuperDocMock as any).mockDestroy = mockDestroy;
76+
(SuperDocMock as any).mockExportDocx = mockExportDocx;
7377

7478
vi.mock("superdoc", () => ({
7579
SuperDoc: SuperDocMock,

src/types.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ export interface FieldMenuProps {
2828
allowCreate?: boolean;
2929
onSelect: (field: FieldDefinition) => void;
3030
onClose: () => void;
31-
onCreateField?: (field: FieldDefinition) => void | Promise<FieldDefinition | void>;
31+
onCreateField?: (
32+
field: FieldDefinition,
33+
) => void | Promise<FieldDefinition | void>;
3234
}
3335

3436
export interface FieldListProps {
@@ -73,7 +75,9 @@ export interface SuperDocTemplateBuilderProps {
7375
onFieldDelete?: (fieldId: string) => void;
7476
onFieldsChange?: (fields: TemplateField[]) => void;
7577
onFieldSelect?: (field: TemplateField | null) => void;
76-
onFieldCreate?: (field: FieldDefinition) => void | Promise<FieldDefinition | void>;
78+
onFieldCreate?: (
79+
field: FieldDefinition,
80+
) => void | Promise<FieldDefinition | void>;
7781

7882
// UI
7983
className?: string;
@@ -92,5 +96,5 @@ export interface SuperDocTemplateBuilderHandle {
9296
nextField: () => void;
9397
previousField: () => void;
9498
getFields: () => TemplateField[];
95-
exportTemplate: () => { fields: TemplateField[]; document?: any };
99+
exportTemplate: (options?: { fileName?: string }) => Promise<void>;
96100
}

0 commit comments

Comments
 (0)