Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/webviewer-commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@proofkit/webviewer": minor
"@proofkit/docs": minor
---

Add typed Web Viewer command registry.
148 changes: 148 additions & 0 deletions apps/docs/content/docs/webviewer/commands.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
title: "Run JS from FileMaker"
description: How to use the Perform JavaScript in Web Viewer script step to trigger functions inside your web viewer app.
---

FileMaker can call a global Web Viewer namespace. Your app registers typed handlers on that namespace.

## Initialize

Call `initWebViewerCommands()` once in browser code.

```ts
import { initWebViewerCommands } from "@proofkit/webviewer/commands";

initWebViewerCommands();
```

The module is import-safe on the server. `initWebViewerCommands()` returns `undefined` when `window` is unavailable.

## Type commands

Declare commands in a `.d.ts` file. Extending `DefineWebViewerCommandRegistry` makes TypeScript check that every command is a function that accepts only string parameters.

```ts
import type { DefineWebViewerCommandRegistry } from "@proofkit/webviewer/commands";

export {};

declare module "@proofkit/webviewer/commands" {
interface WebViewerCommandRegistry
extends DefineWebViewerCommandRegistry<{
openCustomer: (recordId: string) => void;
refreshDashboard: () => void;
}> {}
}
```

FileMaker passes string parameters, so command parameters must be strings.

You can also wrap individual entries with `WebViewerCommandHandler`.

```ts
import type { WebViewerCommandHandler } from "@proofkit/webviewer/commands";

declare module "@proofkit/webviewer/commands" {
interface WebViewerCommandRegistry {
openCustomer: WebViewerCommandHandler<(recordId: string) => void>;
}
}
```

## Static commands

Use `registerWebViewerCommand` for handlers that do not depend on component state. This example lets FileMaker ask the Web Viewer to load a customer snapshot through `fmFetch`.

```ts
import { fmFetch } from "@proofkit/webviewer";
import { registerWebViewerCommand } from "@proofkit/webviewer/commands";

registerWebViewerCommand("loadCustomerSummary", async (recordId) => {
const summary = await fmFetch("Load Customer Summary", { recordId });
window.dispatchEvent(
new CustomEvent("customer-summary-loaded", {
detail: summary,
})
);
});
```

## React commands

Use `useWebViewerCommand` when a handler depends on UI state, routing, or component data.

```tsx
import { useWebViewerCommand } from "@proofkit/webviewer/react";

export function CustomerScreen() {
useWebViewerCommand("openCustomer", (recordId) => {
console.log(recordId);
});

return null;
}
```

The hook registers on mount, unregisters on unmount, and keeps the latest callback after rerenders.

## Next.js

Initialize from Client Components only.

```tsx
"use client";

import { initWebViewerCommands } from "@proofkit/webviewer/commands";
import { useEffect } from "react";

export function WebViewerCommandBootstrap() {
useEffect(() => {
initWebViewerCommands();
}, []);

return null;
}
```

Use import-time initialization only in client-only modules.

```tsx
"use client";

import { initWebViewerCommands } from "@proofkit/webviewer/commands";

initWebViewerCommands();
```

## Early calls

Missing commands buffer by default. If FileMaker calls before React mounts, the call replays when the command registers.

```ts
initWebViewerCommands({
missingCommand: "buffer",
maxBufferedCallsPerCommand: 100,
});
```

Other missing-command modes are `drop`, `warn`, and `throw`.

## Migration

Avoid direct `window.proofkit` assignment. It bypasses buffering, cleanup, and type checks.

Before:

```tsx
useEffect(() => {
window.proofkit = {
openDialog: () => setOpen(true),
};
}, []);
```

After:

```tsx
useWebViewerCommand("openDialog", () => setOpen(true));
```
1 change: 1 addition & 0 deletions apps/docs/content/docs/webviewer/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"runtime-under-the-hood",
"data-access",
"filemaker-scripts-as-backend",
"commands",
"platform-notes",
"deployment-methods",
"---Reference---",
Expand Down
29 changes: 29 additions & 0 deletions packages/webviewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,26 @@
"default": "./dist/cjs/nextjs.cjs"
}
},
"./commands": {
"import": {
"types": "./dist/esm/commands.d.ts",
"default": "./dist/esm/commands.js"
},
"require": {
"types": "./dist/cjs/commands.d.cts",
"default": "./dist/cjs/commands.cjs"
}
},
"./react": {
"import": {
"types": "./dist/esm/react.d.ts",
"default": "./dist/esm/react.js"
},
"require": {
"types": "./dist/cjs/react.d.cts",
"default": "./dist/cjs/react.cjs"
}
},
"./package.json": "./package.json"
},
"dependencies": {
Expand All @@ -60,13 +80,22 @@
"next": ">=13.0.0",
"react": ">=18.0.0"
},
"peerDependenciesMeta": {
"next": {
"optional": true
},
"react": {
"optional": true
}
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.4",
"@proofkit/fmdapi": "workspace:*",
"@tanstack/intent": "^0.0.19",
"@tanstack/vite-config": "^0.2.1",
"@types/filemaker-webviewer": "^1.0.3",
"@types/node": "^22.19.5",
"@types/react": "19.2.7",
"@types/uuid": "^10.0.0",
"knip": "^5.80.2",
"publint": "^0.3.16",
Expand Down
Loading
Loading