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:
incomplete→in-progress→completed - 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:
@mercura-aps:registry=https://npm.pkg.mercura.ioThen install the SDK and its required peer dependencies.
React:
npm install @mercura-aps/frontend-form-sdk react react-dom zustand @tanstack/query-core zod @mercura-aps/frontend-schemasVue:
npm install @mercura-aps/frontend-form-sdk vue zustand @tanstack/query-core zod @mercura-aps/frontend-schemasImport 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.
// Reactimport { 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.
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
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 formsconst paginatedForms = sdk.useForms((s) => s.paginatedFormsData);
// Add config when user selects a formfunction selectForm(formId: number, formName: string) { sdk.useConfigs((s) => s.addConfig)({ formId, name: "Config 1", formName, configQuantity: 1, });}
// Current step and navigationconst 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);
// Valuesconst handleChange = sdk.useValuesByCurrentConfigId((s) => s?.handleChangeInput);
// Submitconst 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 componentsHow a value change flows through the system:
- User interacts with an input →
handleChangeInputis called valuesSubstoreupdatesvalues[configId][elementId]formSubstoredetects the change via a subscription- Constraints are re-evaluated against the new values using
checkConstraints - Effects are applied: fields are shown/hidden, values set, prices updated, messages triggered
- The updated
steps,messages, anddynamicPricesare written back to the store - Your components re-render via the selector hooks
Config status is derived automatically:
incomplete— no options selected yetin-progress— some selections made but required fields still missingcompleted— 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 draftsetDraftName("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 triggeredAPI 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.
| Hook | Substore | What it gives you |
|---|---|---|
useConfigs | configsSubstore | Config list, current config id, add/remove/copy |
useForms | formsSubstore | Paginated form catalog, search, category filter |
useFormByCurrentConfigId | formSubstore[currentConfigId] | Steps, navigation, messages, loading state |
useFormByConfigId(id, sel) | formSubstore[id] | Same as above for a specific config |
useValuesByCurrentConfigId | valuesSubstore[currentConfigId] | Values, quantities, input handlers |
useValuesByConfigId(id, sel) | valuesSubstore[id] | Same as above for a specific config |
useSubmit | submitSubstore | submit, saveOrUpdateDraft, isSubmitEnabled |
useAuth | authSubstore | isAuth, user data |
useLocalization | localizationSubstore | Language / country selection |
useFormCategories | formCategoriesSubstore | Category hierarchy navigation |
useContactDetails | contactDetailsSubstore | Name, email, custom request fields |
useFinishedConfigs | finishedConfigsSubstore | Aggregated bill of materials across configs |
useNumberFormatter | numberFormatterSubstore | Localized price formatting |
useAppearance | appearanceSubstore | UI theming data |
useAddons | addonsSubstore | Optional addon extensions |
useReset | resetSubstore | Register reset callbacks |
useOptions | optionsSubstore | Dynamic option registry |
Key types
| Type | Description |
|---|---|
TConfig | A config instance: id, formId, name, status, configQuantity, missingRequiredElements |
TForm | Form metadata: id, name, description, layout_type |
TStep | Union of TElement and TGroup |
TElement | A form field: id, label, type, options, required |
TOption / TProduct | A selectable choice within an element |
ConfigStatusType | "incomplete" | "in-progress" | "completed" |
FormStoreItem | Per-config form state: steps, currentStep, messages, dynamicPrices, etc. |
ValuesStoreItem | Per-config values state: values, quantities, handleChangeInput, etc. |
Key methods
configsSubstore
| Method | Signature | Description |
|---|---|---|
addConfig | (config: Omit<TConfig, "id" | "status" | "missingRequiredElements">) => void | Create a new config and start loading its form |
setCurrentConfigId | (configId: number) => void | Set which config is active |
removeConfig | (configId: number) => void | Remove a config and clean up its state |
copyConfig | (configId: number) => void | Duplicate a config with all its values |
updateConfig | (configId, partial) => void | Update name or quantity of a config |
formSubstore (per config via useFormByCurrentConfigId)
| Method | Description |
|---|---|
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)
| Method | Signature | Description |
|---|---|---|
handleChangeInput | ({ type, elementId, optionIdOrValue }) => void | Recommended: handles checkbox toggle logic automatically |
setElementValue | (elementId, value: string[]) => void | Directly set a value |
setOptionQuantity | (elementId, optionId, quantity) => void | Set quantity for a quantifiable option |
submitSubstore
| Method | Description |
|---|---|
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 |