Skip to content

Frontend Form SDK

The Mercura Frontend Form SDK lets you embed Mercura product configurator forms directly in your own application. It handles fetching form definitions, tracking user input, evaluating business rules, and submitting completed configurations — so you can focus on building the UI around it.


Key Concepts

Before writing any code, it helps to understand the five core abstractions the SDK is built around.

Form

A Form is the top-level definition of a product configurator. It contains metadata (name, description, categories) and a list of steps — which are either standalone fields or groups of fields. Forms are fetched from the Mercura API and rendered read-only; you never mutate them directly.

Step / Element

A Step is a single unit of the form. It can be:

  • An Element — a single input field (types: select, radio, checkbox, text, number, range)
  • A Group — a container that holds multiple elements

Each element has an id, a label, options (for choice-based types), and optional pricing metadata.

Config

A Config is an instance of a form. When a user picks a form to fill out, the SDK creates a Config to track their progress. You can have multiple configs simultaneously — for example, when a user is configuring several products in a single request.

Each config tracks:

  • Its associated form id
  • The user’s current progress
  • Status: incompletein-progresscompleted
  • Any missing required fields

Values

Values are what the user has entered or selected for each element, stored per config. Values are a flat map of elementId → string[]. The SDK also tracks option quantities and selected products separately.

Constraints

Constraints are formula-based rules defined in the Mercura back-office. They evaluate expressions against the current values and trigger effects — such as showing or hiding fields, setting default values, adjusting prices, or displaying messages. Constraints are evaluated automatically after every value change; you do not call them directly.


Installation

The SDK is published to the Mercura private npm registry. Add the registry to your project first:

.npmrc
@mercura-aps:registry=https://npm.pkg.mercura.io

Then install the SDK and its required peer dependencies.

React:

Terminal window
npm install @mercura-aps/frontend-form-sdk react react-dom zustand @tanstack/query-core zod @mercura-aps/frontend-schemas

Vue:

Terminal window
npm install @mercura-aps/frontend-form-sdk vue zustand @tanstack/query-core zod @mercura-aps/frontend-schemas

Import the stylesheet once at your application root:

import "@mercura-aps/frontend-form-sdk/dist/style.css";

Quick Start

This minimal example shows the essential pattern: create the SDK, add a config, and render the current step.

// React
import { createReactFormSDK } from "@mercura-aps/frontend-form-sdk/react";
import "@mercura-aps/frontend-form-sdk/dist/style.css";
const sdk = createReactFormSDK();
function App() {
const addConfig = sdk.useConfigs((s) => s.addConfig);
// Add a config for a known form when the component mounts
useEffect(() => {
addConfig({ formId: 42, name: "My Config", formName: "", configQuantity: 1 });
}, []);
return <FormView />;
}
function FormView() {
const currentStep = sdk.useFormByCurrentConfigId((s) => s?.currentStep);
const isLoading = sdk.useFormByCurrentConfigId((s) => s?.isLoading);
const goNext = sdk.useFormByCurrentConfigId((s) => s?.setNextCurrentStep);
const goPrev = sdk.useFormByCurrentConfigId((s) => s?.setPreviousCurrentStep);
if (isLoading) return <p>Loading…</p>;
if (!currentStep) return <p>No step available</p>;
return (
<div>
<h2>{currentStep.label}</h2>
{/* render inputs here */}
<button onClick={goPrev}>Back</button>
<button onClick={goNext}>Next</button>
</div>
);
}

Usage — React

1. Initialize the SDK

Create the SDK once at module level (outside any component) and export the hooks for use throughout your app.

sdk.ts
import { createReactFormSDK } from "@mercura-aps/frontend-form-sdk/react";
export const sdk = createReactFormSDK();

2. List available forms

Use useForms to fetch and display the form catalog. You can filter by category or search.

import { sdk } from "./sdk";
function FormList() {
const forms = sdk.useForms((s) => s.paginatedFormsData);
const setSearch = sdk.useForms((s) => s.setSearch);
const nextPage = sdk.useForms((s) => s.setNextPage);
if (forms.isLoading) return <p>Loading forms…</p>;
return (
<>
<input onChange={(e) => setSearch(e.target.value)} placeholder="Search…" />
{forms.data?.forms.map((form) => (
<div key={form.id}>
<h3>{form.name}</h3>
<button onClick={() => addConfigForForm(form.id, form.name)}>
Configure
</button>
</div>
))}
<button onClick={nextPage}>Load more</button>
</>
);
}

To filter by category, call setCategoryIdByType:

sdk.useForms((s) => s.setCategoryIdByType)({ categoryId: 5 });

3. Add a config and navigate to it

When the user selects a form, create a config and set it as current.

const addConfig = sdk.useConfigs((s) => s.addConfig);
const setCurrentConfig = sdk.useConfigs((s) => s.setCurrentConfigId);
const configs = sdk.useConfigs((s) => s.configs);
function onFormSelect(formId: number, formName: string) {
addConfig({ formId, name: "Configuration 1", formName, configQuantity: 1 });
// The new config gets the next id from configsCounter
const newId = configs.length + 1;
setCurrentConfig(newId);
}

4. Render form steps

Access the current config’s form data with useFormByCurrentConfigId.

function StepView() {
const step = sdk.useFormByCurrentConfigId((s) => s?.currentStep);
const isFirstStep = sdk.useFormByCurrentConfigId((s) => s?.isFirstStep);
const isLastStep = sdk.useFormByCurrentConfigId((s) => s?.isLastStep);
const isStepFinished = sdk.useFormByCurrentConfigId((s) => s?.isCurrentStepFinished);
const stepsAvailability = sdk.useFormByCurrentConfigId((s) => s?.stepsAvailability);
const goNext = sdk.useFormByCurrentConfigId((s) => s?.setNextCurrentStep);
const goPrev = sdk.useFormByCurrentConfigId((s) => s?.setPreviousCurrentStep);
if (!step) return null;
return (
<div>
<StepInputs step={step} />
<button onClick={goPrev} disabled={isFirstStep}>Back</button>
<button onClick={goNext} disabled={!isStepFinished}>Next</button>
</div>
);
}

stepsAvailability is a boolean array aligned to steps — an entry is true when the corresponding step is visible after constraint evaluation.

5. Handle user input

Use useValuesByCurrentConfigId to read and write values for the active config.

function ElementInput({ element }) {
const values = sdk.useValuesByCurrentConfigId((s) => s?.values);
const handleChange = sdk.useValuesByCurrentConfigId((s) => s?.handleChangeInput);
const currentValue = values?.[element.id] ?? [];
return (
<select
value={currentValue[0] ?? ""}
onChange={(e) =>
handleChange?.({ type: element.type, elementId: element.id, optionIdOrValue: e.target.value })
}
>
{element.options.map((opt) => (
<option key={opt.id} value={opt.id}>{opt.label}</option>
))}
</select>
);
}

handleChangeInput handles the toggle logic for checkbox elements automatically.

For direct assignment, use setElementValue:

const setVal = sdk.useValuesByCurrentConfigId((s) => s?.setElementValue);
setVal?.(elementId, ["option-id"]);

6. Submit

Collect contact details and submit when all configs are complete.

function Checkout() {
const isSubmitEnabled = sdk.useSubmit((s) => s.isSubmitEnabled);
const isLoading = sdk.useSubmit((s) => s.isLoadingSubmit);
const submit = sdk.useSubmit((s) => s.submit);
const error = sdk.useSubmit((s) => s.errorSubmit);
return (
<>
{error && <p className="error">{error}</p>}
<button onClick={() => submit()} disabled={!isSubmitEnabled || isLoading}>
{isLoading ? "Submitting…" : "Submit Request"}
</button>
</>
);
}

isSubmitEnabled is automatically false while forms are loading, required contact details are missing or invalid, or there are validation errors.


Usage — Vue

The Vue API is identical to React. Replace createReactFormSDK with createVueFormSDK and use the returned composables inside setup().

1. Initialize

sdk.ts
import { createVueFormSDK } from "@mercura-aps/frontend-form-sdk/vue";
export const sdk = createVueFormSDK();

2. Use in components

<script setup lang="ts">
import { sdk } from "./sdk";
const forms = sdk.useForms((s) => s.paginatedFormsData);
const addConfig = sdk.useConfigs((s) => s.addConfig);
const step = sdk.useFormByCurrentConfigId((s) => s?.currentStep);
const goNext = sdk.useFormByCurrentConfigId((s) => s?.setNextCurrentStep);
const values = sdk.useValuesByCurrentConfigId((s) => s?.values);
const handleChange = sdk.useValuesByCurrentConfigId((s) => s?.handleChangeInput);
</script>
<template>
<div v-if="step">
<h2>{{ step.label }}</h2>
<!-- render inputs -->
<button @click="goNext?.()">Next</button>
</div>
</template>

The composables return reactive refs — template bindings and watch work as expected.

3. Full form flow (Vue)

<script setup lang="ts">
import { sdk } from "./sdk";
// List forms
const paginatedForms = sdk.useForms((s) => s.paginatedFormsData);
// Add config when user selects a form
function selectForm(formId: number, formName: string) {
sdk.useConfigs((s) => s.addConfig)({
formId,
name: "Config 1",
formName,
configQuantity: 1,
});
}
// Current step and navigation
const currentStep = sdk.useFormByCurrentConfigId((s) => s?.currentStep);
const isCurrentStepDone = sdk.useFormByCurrentConfigId((s) => s?.isCurrentStepFinished);
const next = sdk.useFormByCurrentConfigId((s) => s?.setNextCurrentStep);
const prev = sdk.useFormByCurrentConfigId((s) => s?.setPreviousCurrentStep);
// Values
const handleChange = sdk.useValuesByCurrentConfigId((s) => s?.handleChangeInput);
// Submit
const isSubmitEnabled = sdk.useSubmit((s) => s.isSubmitEnabled);
const submit = sdk.useSubmit((s) => s.submit);
</script>

Data Flow & Architecture

Understanding the internals helps when debugging or extending the SDK.

API (Mercura backend)
└──(TanStack Query)──> Store fetch layer
Zustand Store ◄──── IndexedDB (persisted state)
┌──────────────────────────────────────────────┐
│ configsSubstore ← config list & status │
│ formSubstore ← form data per config │
│ valuesSubstore ← user input per config │
│ submitSubstore ← draft / submit flow │
│ formsSubstore ← paginated form catalog │
│ ... (16 substores total) │
└──────────────────────────────────────────────┘
selector hooks (React / Vue)
Your UI components

How a value change flows through the system:

  1. User interacts with an input → handleChangeInput is called
  2. valuesSubstore updates values[configId][elementId]
  3. formSubstore detects the change via a subscription
  4. Constraints are re-evaluated against the new values using checkConstraints
  5. Effects are applied: fields are shown/hidden, values set, prices updated, messages triggered
  6. The updated steps, messages, and dynamicPrices are written back to the store
  7. Your components re-render via the selector hooks

Config status is derived automatically:

  • incomplete — no options selected yet
  • in-progress — some selections made but required fields still missing
  • completed — all required fields filled

State is persisted to IndexedDB between page reloads. Only user-facing data (values, configs) is persisted; form definitions are always re-fetched.


Customization

Styling

Import the SDK stylesheet and override CSS custom properties in your own stylesheet:

/* Override SDK theme tokens */
:root {
--mercura-primary: #0047ab;
--mercura-border-radius: 4px;
}

Hooking into reset

Register a callback to run when the SDK is reset (e.g. after submission):

const addResetCallback = sdk.useReset((s) => s.addResetCallback);
addResetCallback((set) => {
// clean up your own state here
myLocalState.clear();
});

Draft saving

Drafts allow users to save progress and resume later:

const setDraftName = sdk.useSubmit((s) => s.setDraftName);
const saveOrUpdateDraft = sdk.useSubmit((s) => s.saveOrUpdateDraft);
// Name the draft
setDraftName("My product configuration");
// Save (creates new) or update (if draftId already exists)
const { ok, type } = await saveOrUpdateDraft();

Resume from a draft:

const openFromDraft = sdk.useSubmit((s) => s.openFromDraft);
await openFromDraft(draftId);

Copying a previous request

Let users re-use a previous order as a starting point:

const openFromRequestCopy = sdk.useSubmit((s) => s.openFromRequestCopy);
await openFromRequestCopy(requestId);

Constraint debugging

Access the constraint evaluation trace for a config:

const debugHistory = sdk.useFormByCurrentConfigId((s) => s?.debugHistory);
// debugHistory is an array of evaluation snapshots, each with a list of
// constraints, their expression result, and the effects that were triggered

API Reference

React hooks / Vue composables

All hooks follow the selector pattern: sdk.useX(selector). The selector receives the substore state and returns the slice you need.

HookSubstoreWhat it gives you
useConfigsconfigsSubstoreConfig list, current config id, add/remove/copy
useFormsformsSubstorePaginated form catalog, search, category filter
useFormByCurrentConfigIdformSubstore[currentConfigId]Steps, navigation, messages, loading state
useFormByConfigId(id, sel)formSubstore[id]Same as above for a specific config
useValuesByCurrentConfigIdvaluesSubstore[currentConfigId]Values, quantities, input handlers
useValuesByConfigId(id, sel)valuesSubstore[id]Same as above for a specific config
useSubmitsubmitSubstoresubmit, saveOrUpdateDraft, isSubmitEnabled
useAuthauthSubstoreisAuth, user data
useLocalizationlocalizationSubstoreLanguage / country selection
useFormCategoriesformCategoriesSubstoreCategory hierarchy navigation
useContactDetailscontactDetailsSubstoreName, email, custom request fields
useFinishedConfigsfinishedConfigsSubstoreAggregated bill of materials across configs
useNumberFormatternumberFormatterSubstoreLocalized price formatting
useAppearanceappearanceSubstoreUI theming data
useAddonsaddonsSubstoreOptional addon extensions
useResetresetSubstoreRegister reset callbacks
useOptionsoptionsSubstoreDynamic option registry

Key types

TypeDescription
TConfigA config instance: id, formId, name, status, configQuantity, missingRequiredElements
TFormForm metadata: id, name, description, layout_type
TStepUnion of TElement and TGroup
TElementA form field: id, label, type, options, required
TOption / TProductA selectable choice within an element
ConfigStatusType"incomplete" | "in-progress" | "completed"
FormStoreItemPer-config form state: steps, currentStep, messages, dynamicPrices, etc.
ValuesStoreItemPer-config values state: values, quantities, handleChangeInput, etc.

Key methods

configsSubstore

MethodSignatureDescription
addConfig(config: Omit<TConfig, "id" | "status" | "missingRequiredElements">) => voidCreate a new config and start loading its form
setCurrentConfigId(configId: number) => voidSet which config is active
removeConfig(configId: number) => voidRemove a config and clean up its state
copyConfig(configId: number) => voidDuplicate a config with all its values
updateConfig(configId, partial) => voidUpdate name or quantity of a config

formSubstore (per config via useFormByCurrentConfigId)

MethodDescription
setNextCurrentStep()Advance to the next visible step
setPreviousCurrentStep()Go back to the previous visible step
setCurrentProgress(n)Directly set the progress index

valuesSubstore (per config via useValuesByCurrentConfigId)

MethodSignatureDescription
handleChangeInput({ type, elementId, optionIdOrValue }) => voidRecommended: handles checkbox toggle logic automatically
setElementValue(elementId, value: string[]) => voidDirectly set a value
setOptionQuantity(elementId, optionId, quantity) => voidSet quantity for a quantifiable option

submitSubstore

MethodDescription
submit()Submit all configs as a request; returns { ok: boolean }
saveOrUpdateDraft()Save or update a draft; returns { ok, type: "save" | "update" }
openFromDraft(draftId)Load a saved draft into the store
openFromRequestCopy(requestId)Pre-fill the store from a previous request
setDraftName(name)Set the draft/configuration name