# sdk.js
A few plain JavaScript functions for using a space from your own frontend:
calling a function, uploading a file, and opening a function's page in a modal.
The space serves it at {prefix}/static/sdk.js; there is nothing to install or
build.
import { call, openModal } from "/tools/static/sdk.js";
const outputs = await call("/tools/divide", {a: 10, b: 2});
outputs[0].value; // "5.0"
// A "New user" button: the form, its validation and its endpoint come from Python
openModal("/tools/create_user", {prefill: {team: "sales"}});
Every function takes the URL it works on: a function (/tools/divide) or the
space (/tools). There is no client to configure.
# Calling
| Function | What it does |
|---|---|
call(url, args) |
runs the function; resolves to the list of outputs |
callStream(url, args, {onPrint}) |
the same, calling onPrint with what the function prints |
events(url, args) |
the raw stream: an async iterator of {name, data} events |
upload(spaceUrl, file, {reference}) |
uploads a file and resolves to its reference; reference reuses one you already have |
fileReference(filename) |
mints a reference, for an upload of your own with progress |
downloadUrl(spaceUrl, reference) |
the URL of a returned file |
formUrl(spaceUrl, output) |
the URL an OpenForm output points to |
doc(spaceUrl) |
the text of /doc |
outputsOf(envelope) |
the outputs of a response you fetched yourself |
const reference = await upload("/tools", input.files[0]);
await call("/tools/report", {source: reference});
call, callStream, events, upload and doc also take a signal, an
AbortSignal to cancel the request (the function itself still runs to the
end).
A failure throws FuncToWebError, with the server's message, its status, the
url and the envelope it received. The SDK does not validate: the server
does, and its 422 arrives as the error. A request that never reached the
server, or a stream cut before its end, has no status and keeps the error of
fetch as cause.
# Opening a function's page
Every function has a full page at /{slug}/ that can be embedded in any site.
embed() puts it in an element of yours, and openModal() in a modal that
closes with Escape, a click outside or its button. Both take the options of
prefill, spelled prefill, hidden, autorun, hideTitle,
hideDescription and hideSubmit, and title, the iframe's accessible name:
embed("#panel", "/tools/add", {prefill: {a: 9}, hidden: ["a"]});
openModal("/tools/monthly_report", {autorun: true, hideSubmit: true});
The modal fits its content, up to 760px wide and nine tenths of the window
tall; width, height, or the CSS variables --ftw-modal-width and
--ftw-modal-height change that. embed() also follows its content's height;
autoHeight: false turns that off for both. embed() returns the iframe it
added; pageUrl(url, options) gives the page's URL with its options, for an
iframe of your own.
# Knowing what happened
const modal = openModal("/tools/create_task", {closeOnResult: true});
const {completed, results} = await modal.closed;
if (completed) await refresh();
openModal() returns {element, iframe, close, closed}: close() closes it
from your code, and closed resolves when it closes by any route. completed
is true if a run finished, and results holds the outputs of the last one. onResult, onError,
onBusy and onClose are called as it happens. closeOnResult is off by
default, because most results (an image, a table, a download) are meant to be
read inside the modal.
onBusy(busy) is called with true when Submit starts a run, uploads
included, and with false when it ends, whatever the outcome, so a host can
tuck the modal away while it works and bring it back when it is done:
openModal("/tools/create_product", {
onBusy: (busy) => badge.hidden = !busy,
});
For an embed() or an iframe of your own, listen(iframe, handlers) gives the
same events: onReady, onResult, onError, onBusy, onNavigate (an
OpenForm is moving the page) and onResize (the content's height). It returns cache, the
last {ready, results, error} received, and stop().
How it works inside
The page posts a message to window.parent for each event, and nothing when it
is not embedded:
{"v": 1, "kind": "result", "slug": "create_task",
"outputs": [{"type": "text", "value": "Task 1 created"}]}
kind is ready, busy, result, error, navigate or resize. A
receiver ignores a v or a kind it does not know, so a page can learn new
kinds without breaking a host. error means a run failed (the error of the
envelope), never a field the browser rejected as the user typed. busy carries
"busy": true before a run and false after it, sent from a finally: a
failed upload or a broken response, which announce no error, still end the
bracket, and a form the browser rejected never opens one. navigate replaces
result when an OpenForm moves the page, since opening another form is not a
result. result carries the same outputs call() returns, so there is one
shape to learn.
The message goes out with a targetOrigin of "*": the page cannot know who
embedded it, so whoever can embed a page can read its results.