# 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.
# 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 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.
For an embed() or an iframe of your own, listen(iframe, handlers) gives the
same events: onReady, onResult, onError, 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, 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. 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.