Guides
Services
FormKrafter never assumes how your infrastructure works. Anything that touches the outside world — uploading a file, fetching a list of options, loading a referenced form — goes through a service you can replace. Override once at app startup and every builder and renderer on the page picks it up.
The five services
Section titled “The five services”| Service | Default | Replace it when… |
|---|---|---|
jsRunnerService |
SandboxJsRunnerService (AST interpreter) |
you need full JS in a trusted context |
dataSourceService |
FetchDataSourceService |
remote select options need auth, a base URL or a retry policy |
fileUploadService |
Base64FileUploadService (data URLs) |
you want real uploads instead of inlined base64 |
specSourceService |
FetchSpecSourceService |
nested forms come from your own store |
optionSourceService |
FetchOptionSourceService |
shared option catalogs come from your own store |
import { services, UrlFileUploadService } from '@streamline-pulse/formkrafter-core'
services.fileUploadService = new UrlFileUploadService({ url: '/api/uploads', credentials: 'include',})File uploads
Section titled “File uploads”The default turns every file into a base64 data URL — fine for a demo, wrong
for production. UrlFileUploadService POSTs each file as multipart instead:
-
Point it at your endpoint.
services.fileUploadService = new UrlFileUploadService({url: '/api/uploads',headers: { 'X-Tenant': 'acme' },credentials: 'include',}) -
Return the stored location from your endpoint. The service reads the first of
url,path,locationorhrefit finds in the JSON response.{ "url": "https://cdn.example.com/f/8a2c.pdf" } -
Implement deletion if you want the remove button to clean up. The service calls
remove()when the user deletes a file.
A brick-level uploadUrl config overrides the service’s default URL, so one
form can send its attachments somewhere else.
Remote options and auth
Section titled “Remote options and auth”A select can load its options from a URL, interpolating {key} placeholders
from the runtime context and the current form data — context first:
URL {api.base}/employees?dept={department}Headers Authorization: Bearer {auth.token}The response may be a bare JSON array or an envelope: a payload whose data
property is an array is unwrapped automatically, which covers most paginated
APIs. For any other shape, point optionsPath at the array — result.items.
A payload matching none of these still fails loudly rather than showing an
empty select.
Tokens may be dotted paths, so nested context needs no flattening. A key containing a dot is matched first, then the path is walked; inherited properties are never reachable. A token that resolves to nothing becomes an empty string. That is the whole grammar — no expressions, no defaults, no escaping.
Two auth patterns, which compose:
-
httpOnly cookie — recommended for same-origin APIs. Nothing secret ever enters the spec or the property panel.
import { services, FetchDataSourceService } from '@streamline-pulse/formkrafter-core'services.dataSourceService = new FetchDataSourceService({ credentials: 'include' }) -
Context token — when the API is cross-origin. The host injects it through the renderer’s
contextprop.<FkFormRender spec={spec} context={{ auth: { token } }} />
A page-wide singleton, by design
Section titled “A page-wide singleton, by design”services lives on globalThis under
Symbol.for("formkrafter.core.services"). Bundlers routinely end up with
several copies of a module in one app — your bundle imports core directly, and
the Web Components bundle embeds its own. The shared store guarantees that an
override made from your app is seen by the components, whichever copy runs it.
The brick registry and the chrome translations use the same pattern.
Next steps
Section titled “Next steps”A project by Streamline Pulse