Skip to content

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.

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
app-startup.ts
import { services, UrlFileUploadService } from '@streamline-pulse/formkrafter-core'
services.fileUploadService = new UrlFileUploadService({
url: '/api/uploads',
credentials: 'include',
})

The default turns every file into a base64 data URL — fine for a demo, wrong for production. UrlFileUploadService POSTs each file as multipart instead:

  1. Point it at your endpoint.

    services.fileUploadService = new UrlFileUploadService({
    url: '/api/uploads',
    headers: { 'X-Tenant': 'acme' },
    credentials: 'include',
    })
  2. Return the stored location from your endpoint. The service reads the first of url, path, location or href it finds in the JSON response.

    { "url": "https://cdn.example.com/f/8a2c.pdf" }
  3. 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.

A select can load its options from a URL, interpolating {key} placeholders from the runtime context and the current form data — context first:

Brick config
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:

  1. 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' })
  2. Context token — when the API is cross-origin. The host injects it through the renderer’s context prop.

    <FkFormRender spec={spec} context={{ auth: { token } }} />

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.

A project by Streamline Pulse