Skip to main content

Data Sources

Data sources provide options for field.select fields. They can be static (defined in the schema) or remote (fetched from an API at runtime).

Static data sources

Define options inline in the schema. No async loading — options are always available immediately.

import { datasource, field, buildForm } from "formwright/schema";
import { registerBasicPlugins } from "formwright/plugins";

const rolesSource = datasource.static("roles", [
{ value: "admin", label: "Admin" },
{ value: "member", label: "Member" },
{ value: "viewer", label: "Viewer" },
]);

const roleField = field.select("role", {
label: "Role",
dataSource: "roles", // matches the datasource name
});

const form = buildForm({
form: defineForm({ id: "invite" }),
fields: [roleField],
layout: layout.stack("root", [layout.field(roleField)]),
datasources: [rolesSource],
});

// Static datasources need registerAsyncPlugins() to load:
const runtime = createFormRuntime({
form,
plugins: [...registerBasicPlugins(), ...registerAsyncPlugins()],
});

Remote data sources

Fetch options from an API. Requires registerAsyncPlugins().

import { registerAsyncPlugins } from "formwright/plugins";

const countriesSource = datasource.remote("countries", {
endpoint: "/api/countries",
method: "GET",
labelKey: "name",
valueKey: "code",
});

const runtime = createFormRuntime({
form,
plugins: [...registerBasicPlugins(), ...registerAsyncPlugins()],
});

Pass a baseUrl if your endpoints are relative:

registerAsyncPlugins({ baseUrl: "https://api.example.com" })

Remote datasource with query parameters

Map field values to query parameters using queryMap. Use {{fieldPath}} template syntax:

datasource.remote("cities", {
endpoint: "/api/cities",
method: "GET",
labelKey: "name",
valueKey: "id",
dependsOn: ["country"], // re-fetches when country changes
queryMap: {
country_code: "{{country}}", // passes current country value as ?country_code=...
},
})

Remote datasource with POST body

datasource.remote("products", {
endpoint: "/api/products/search",
method: "POST",
labelKey: "title",
valueKey: "sku",
dependsOn: ["category"],
bodyMap: {
category: "{{category}}",
},
})

Loading state in custom renderers

When you build a custom renderer for a select field, access the loading state:

import type { FieldRendererComponent } from "formwright/react";

const MySelect: FieldRendererComponent = ({ value, onChange, options, loading, error }) => {
if (loading) return <div>Loading...</div>;

return (
<select value={String(value ?? "")} onChange={(e) => onChange(e.target.value)}>
{(options ?? []).map((opt) => (
<option key={String(opt.value)} value={String(opt.value)}>
{opt.label}
</option>
))}
</select>
);
};

useDatasourceOptions hook

Access options and loading state directly in custom components:

import { useDatasourceOptions } from "formwright/react";

function CountryDropdown({ fieldPath }: { fieldPath: string }) {
const { options, loading, error } = useDatasourceOptions(fieldPath);

if (loading) return <span>Loading...</span>;
if (error) return <span>Failed to load options</span>;

return (
<select>
{options?.map((opt) => (
<option key={String(opt.value)} value={String(opt.value)}>
{opt.label}
</option>
))}
</select>
);
}

Custom datasource plugin

Build a datasource plugin when you need custom fetching logic — authentication headers, GraphQL, SDK calls:

import type { DataSourcePlugin } from "formwright/core";

const myApiDatasourcePlugin: DataSourcePlugin = {
kind: "datasource",
identity: { name: "my-api-datasource", version: "1" },
sourceType: "my-api", // matches datasource definition type

async load(input) {
const source = input.source as { type: "my-api"; resourceType: string };

const response = await myApiClient.fetch(source.resourceType, {
token: input.context.authToken as string,
});

return {
options: response.items.map((item) => ({
value: item.id,
label: item.displayName,
})),
};
},
};

// Register it alongside the built-in plugins:
const runtime = createFormRuntime({
form,
plugins: [
...registerBasicPlugins(),
myApiDatasourcePlugin,
],
});

Define the datasource type in your schema:

// Use raw datasource definition for custom source types:
import { buildForm } from "formwright/schema";

const form = buildForm({
// ...
datasources: [{
name: "products",
definition: {
type: "my-api",
resourceType: "product",
},
}],
});