<!--
  Formcentric Documentation
  @formcentric/client: 4.6.0
  @formcentric/client-react: 1.3.1-beta
  @formcentric/client-vue: 1.3.1-beta
-->

## Table of Contents

- [Terminology](#terminology)
- [1.0 Description](#10-description)
- [2.0 Embedding](#20-embedding)
  - [2.1 General Embedding](#21-general-embedding)
  - [2.1.1 Example](#211-example)
  - [2.2 React](#22-react)
  - [2.3 Vue](#23-vue)
  - [2.4 Angular](#24-angular)
  - [2.5 SDK / Module Integration](#25-sdk--module-integration)
  - [2.6 Local Development](#26-local-development)
  - [2.6.1 Using Docker and Caddy](#261-using-docker-and-caddy)
  - [2.6.2 Bundler and Framework Proxy Configuration](#262-bundler-and-framework-proxy-configuration)
  - [2.7 Troubleshooting](#27-troubleshooting)
- [3.0 Configuration](#30-configuration)
  - [3.1 Configuration Sources](#31-configuration-sources)
  - [3.2 Window Object](#32-window-object)
  - [3.2.1 Structure](#321-structure)
  - [3.2.2 Examples](#322-examples)
  - [3.3 Heights and Widths](#33-heights-and-widths)
  - [3.4 Parent URL](#34-parent-url)
  - [3.5 Captcha Provider](#35-captcha-provider)
  - [3.6 Pre-filling Forms with Dynamic Values](#36-pre-filling-forms-with-dynamic-values)
  - [3.6.1 Pre-filling via URL Parameters](#361-pre-filling-via-url-parameters)
  - [3.6.2 Pre-filling via Configuration Attribute](#362-pre-filling-via-configuration-attribute)
  - [3.7 Including Custom Translations](#37-including-custom-translations)
  - [3.7.1 Adding New Languages and Translation Keys](#371-adding-new-languages-and-translation-keys)
- [4.0 Debugging](#40-debugging)
  - [4.1 Local Debugging via Attribute](#41-local-debugging-via-attribute)
  - [4.2 Global Activation](#42-global-activation)
- [5.0 Custom Display Variants](#50-custom-display-variants)
  - [5.1 Using Display Variants](#51-using-display-variants)
  - [5.2 Creating and Using Custom Display Variants](#52-creating-and-using-custom-display-variants)
- [6.0 Developing Custom Themes](#60-developing-custom-themes)
  - [6.1 Setting Up the Workspace](#61-setting-up-the-workspace)
  - [6.2 Documentation](#62-documentation)
- [7.0 Event API](#70-event-api)
  - [7.1 Basic Setup](#71-basic-setup)
  - [7.2 Event Structure](#72-event-structure)
  - [7.3 Available Events](#73-available-events)
  - [7.4 Wildcard Listeners](#74-wildcard-listeners)
  - [7.5 Integration with Matomo](#75-integration-with-matomo)
  - [7.6 Integration with Google Analytics 4](#76-integration-with-google-analytics-4)
  - [7.7 Custom Analytics Solution](#77-custom-analytics-solution)
  - [7.8 Debugging](#78-debugging)

# Formcentric Client (General)

## Terminology

| Term | Description |
| --- | --- |
| Formcentric Client | The client family for embedding Formcentric forms in the browser. Includes Static integration, SDK mount path, and framework adapters. |
| Formcentric Cloud | The Formcentric web application for creating, configuring, and publishing forms. |
| Headless Server | The Formcentric server for delivery, validation, and processing of form data. |
| Static Integration | Classic embedding via `formcentric.js`, `formapp.js`, `data-fc-*` attributes, and `window.formcentric`. |
| SDK / Module Integration | Programmatic embedding via `mount(element, config)` from `@formcentric/client`. |
| Form Container | The `div` into which the form is dynamically rendered. |
| Theme | Design template for colors, typography, layout, and templates of a form. |
| Display Variant | Additional CSS classes or theme-specific variants that adjust the appearance of individual elements. |
| SPA | Single-page application where content is updated without full page reloads. |

## 1.0 Description

The Formcentric Client is a modular JavaScript solution for dynamically embedding and rendering Formcentric forms in websites, CMS systems, and modern frontends.

Technically, the runtime consists of two main parts:

- `formcentric.js` handles wrapper tasks such as initialization, configuration capture, asset load paths, and lifecycle orchestration.
- `formapp.js` is the actual form application. It renders the form, manages states, processes interactions, and communicates with the headless server.

The form application is internally based on Preact. Several integration paths are currently available:

- Static via script tag and `data-fc-*`
- SDK via `mount()`
- React via `@formcentric/client-react`
- Vue via `@formcentric/client-vue`
- Angular currently only via a project-specific wrapper based on the SDK

## 2.0 Embedding

### 2.1 General Embedding

For classic websites and CMS integrations, embedding is done via a script tag and a form container.

Important:

- The domain of the embedding website must be registered in Formcentric.
- The script tag should be loaded with `defer`.
- The form container must be a `<div>`.
- `data-fc-id` must match the form ID from Formcentric.

The complete Static documentation can be found in [client.md](./02-client.md).

### 2.1.1 Example

```html
<script
    src="https://form.formcentric.com/form/formcentric.js"
    defer
></script>
<div data-fc-id="YOUR-FORM-ID"></div>
```

### 2.2 React

For React, the component-based integration via `@formcentric/client-react` should be used. The old, DOM-driven SPA embedding via `window.formcentric.initFormcentric()` is no longer the recommended path for new React integrations.

Details can be found in [client-react.md](./04-client-react.md).

### 2.3 Vue

For Vue, the component-based integration via `@formcentric/client-vue` should be used. Here too, the modern wrapper path is preferable to the old DOM-driven approach.

Details can be found in [client-vue.md](./05-client-vue.md).

### 2.4 Angular

There is currently no officially supported component for Angular. The sensible approach is therefore a project-specific wrapper based on the SDK mount path.

Details and an example can be found in [client-angular.md](./06-client-angular.md).

### 2.5 SDK / Module Integration

For direct JavaScript or TypeScript integrations without a framework component, the SDK path via `mount()` is available.

Details can be found in [client-sdk.md](./03-client-sdk.md).

Note: If this path has not yet been publicly rolled out, the associated external documentation should only be published together with the release.

### 2.6 Local Development

Formcentric Cloud integrations also require a domain registered in Formcentric during local development. In practice, this usually means:

- local development via HTTPS
- development under a registered dev domain
- optionally a reverse proxy in front of the actual dev server

If your bundler or framework can run directly under a suitable HTTPS domain, that's often sufficient. Otherwise, a reverse proxy is the simplest solution.

### 2.6.1 Using Docker and Caddy

A simple approach is a local proxy with Docker and Caddy.

Example `Caddyfile`:

```text
DOMAIN.TLD {
    tls internal
    reverse_proxy http://docker.host.internal:4200
    header {
        Access-Control-Allow-Origin *
        Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
        Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept"
    }
}
```

Example `docker-compose.yml`:

```yaml
services:
    caddy:
        image: caddy:2.4.6-alpine
        ports:
            - '80:80'
            - '443:443'
        volumes:
            - ./Caddyfile:/etc/caddy/Caddyfile
        extra_hosts:
            - 'docker.host.internal:host-gateway'
```

Next, create a suitable host entry in `/etc/hosts` so that the desired dev domain points to your computer locally.

Example:

```text
127.0.0.1 DOMAIN.TLD
```

Then the domain must be released in your Formcentric Cloud admin account. Only then can the proxy be started with `docker compose up -d`. Your application should then be accessible at `https://DOMAIN.TLD`. `DOMAIN.TLD` is a placeholder for your desired dev domain.

### 2.6.2 Bundler and Framework Proxy Configuration

Many bundlers and frameworks support proxy or host configuration directly:

- [Vite](https://vitejs.dev/config/server-options#server-proxy)
- [Webpack Dev Server](https://webpack.js.org/configuration/dev-server/#devserverproxy)
- [Angular](https://v17.angular.io/guide/build#proxying-to-a-backend-server)

What matters is less the specific technology than the target condition:

- the dev server must be accessible under a registered domain
- HTTPS should work correctly
- asset and runtime URLs must match the chosen host

### 2.7 Troubleshooting

Typical general error sources are:

- wrong `embedId`
- form is not yet published
- domain is not registered in Formcentric
- wrong `srcUrl`, `dataUrl`, `formappUrl`, or `designUrl`
- runtime, theme, or templates are not loading
- missing `defer` in Static
- SPA or SDK integrations expecting a reinit without remounting
- local development running without a suitable domain or without HTTPS

## 3.0 Configuration

### 3.1 Configuration Sources

Most Formcentric configuration values are not integration-specific. What differs is mainly:

- the source from which a value is read
- the syntax in the respective integration path
- a few Static- or wrapper-specific additional APIs

#### 3.1.1 Priority and Default Chains

Depending on the integration path, different configuration sources with clear priority are used:

| Integration Path | Local Configuration | Shared Defaults | Browser-global Defaults | Note |
| --- | --- | --- | --- | --- |
| Static | `data-fc-*` on form container | - | `window.formcentric` or `configure(...)` | local container attributes override browser-global defaults |
| SDK | `mount(element, config)` | - | `window.formcentric` or `configure(...)` | `mount()` is config-authoritative |
| React | Props on `FormcentricForm` | `FormcentricConfigProvider` | `window.formcentric` or `configure(...)` | props override provider defaults and browser globals |
| Vue | Props on `FormcentricForm` | `provideFormcentricConfig(...)` | `window.formcentric` or `configure(...)` | props override provider defaults and browser globals |
| Angular wrapper on SDK basis | project-specific inputs or wrapper config | project-specific | `window.formcentric` or `configure(...)` | typically oriented towards SDK |

For all paths additionally:

| Topic | Behavior |
| --- | --- |
| Object values | `requestHeaders`, `themeVariables`, and `configuration` are merged between default and local configuration. In case of identical keys, the closer or local configuration wins. |
| Running instances | Changes to defaults do not affect already running instances retroactively. They only take effect on a new initialization or controlled remount. |
| Browser-global defaults | `configure(...)` replaces previously set browser-global defaults instead of extending them incrementally. |
| Static-only | `data-fc-watch`, `dynamicInit`, and `initFormcentric()` have no direct equivalent in SDK or component mount. |

#### 3.1.2 Shared Formcentric Keys

The shared semantics of Formcentric keys are largely identical for Static, SDK, React, Vue, and SDK-based Angular wrappers. React and Vue adopt SDK key names as top-level props. In Vue templates, they are usually written in `kebab-case`.

Identification and form source:

| Purpose | Static local | Browser-global | SDK / React / Vue / Angular wrapper | Notes |
| --- | --- | --- | --- | --- |
| Form ID | `data-fc-id` | - | `embedId` | must be set locally |
| Form Definition | `data-fc-form-definition` | - | `formDefinition` | alternative to `embedId`; not browser-global |
| Base URL | `data-fc-src-url` | `srcUrl` | `srcUrl` | delivery base URL |
| Data URL | `data-fc-data-url` | `dataUrl` | `dataUrl` | overrides headless endpoint |
| Runtime URL | `data-fc-formapp-url` | `formappUrl` | `formappUrl` | overrides path to `formapp.js` |
| Design URL | `data-fc-design-url` | `designUrl` | `designUrl` | overrides design or theme load paths |

Theme and Assets:

| Purpose | Static local | Browser-global | SDK / React / Vue / Angular wrapper | Notes |
| --- | --- | --- | --- | --- |
| Theme Directory | `data-fc-theme-dir` | `themeDir` | `themeDir` | for directory-based theme loading |
| Theme Name | `data-fc-theme` | `theme` | `theme` | together with `themeDir` |
| Theme CSS | `data-fc-theme-url` | `themeUrl` | `themeUrl` | explicit stylesheet URL |
| Theme Variables URL | `data-fc-theme-variable-url` | `themeVariableUrl` | `themeVariableUrl` | explicit URL to `_variables.json` |
| Template URL | `data-fc-template-url` | `templateUrl` | `templateUrl` | explicit URL to theme script |
| Theme Variables | `data-fc-theme-variables` | `themeVariables` | `themeVariables` | object values are merged with defaults |
| Skip Theme Load | `data-fc-skip-theme-load` | `skipThemeLoad` | `skipThemeLoad` | only effective when explicitly set |
| Skip Templates Load | `data-fc-skip-templates-load` | `skipTemplatesLoad` | `skipTemplatesLoad` | only effective when explicitly set |
| Skip Form Load | `data-fc-skip-form-load` | `skipFormLoad` | `skipFormLoad` | advanced option |

Request, Language, and Runtime:

| Purpose | Static local | Browser-global | SDK / React / Vue / Angular wrapper | Notes |
| --- | --- | --- | --- | --- |
| Variables | `data-fc-vars` | - | `vars` | not browser-global; URL parameters can override values |
| Request Parameters | `data-fc-params` | - | `params` | not browser-global |
| Refs | `data-fc-refs` | - | `refs` | not browser-global |
| Bearer Token | `data-fc-token` | `token` | `token` | Authorization header |
| Additional Headers | `data-fc-request-headers` | `requestHeaders` | `requestHeaders` | object values are merged and used for all requests of the instance |
| Language | `data-fc-language` | `language` | `language` | preferred language |
| Locale | `data-fc-locale` | `locale` | `locale` | fallback for `Accept-Language` if `language` is missing |
| Translation File | `data-fc-locales-path` | `localesPath` | `localesPath` | path to a custom JavaScript file with translations |
| Translation Overrides | - | - | `locales` | in-memory overrides object (carries `date-fns`/`uppy` by reference); SDK / React / Vue / Angular only |
| Form Name | `data-fc-name` | - | `formName` | not browser-global |
| Instance ID | `data-fc-instance-id` | - | `instanceId` | not browser-global |
| Environment | `data-fc-env` | `env` | `env` | `preview`, `live`, `vestibule_live` |
| Parent URL | `data-fc-parent-url` | `parentUrl` | `parentUrl` | return or double-opt-in context |
| Additional Configuration | `data-fc-configuration` | `configuration` | `configuration` | object values are merged |

Layout, Debugging, and Translations:

| Purpose | Static local | Browser-global | SDK / React / Vue / Angular wrapper | Notes |
| --- | --- | --- | --- | --- |
| Max Width | `data-fc-max-width` | `maxWidth` | `maxWidth` | layout hint for container |
| Fixed Height | `data-fc-height` | `height` | `height` | not always strictly enforced on mobile devices |
| Debugging | `data-fc-debug` | `debug` | `debug` | additional debug output |

Not part of the shared keys are especially:

- Static-only: `data-fc-watch`, `data-fc-dynamic-init`, `window.formcentric.dynamicInit`, `window.formcentric.initFormcentric()`
- SDK-only: `conflictBehavior`, `onReady`, `onError`
- React-only: `FormcentricConfigProvider`, `remountKey`, `containerProps`
- Vue-only: `provideFormcentricConfig(...)`, `remountKey`, `containerProps`, `@ready`, `@error`

For syntax- or integration-specific details, see:

- [client.md](./02-client.md) for Static markup, script tag, and `window.formcentric`
- [client-sdk.md](./03-client-sdk.md) for `mount()` and SDK-specific options
- [client-react.md](./04-client-react.md) for React-specific wrapper topics
- [client-vue.md](./05-client-vue.md) for Vue-specific wrapper topics

### 3.2 Window Object

The global object `window.formcentric` is the central control interface of the Static integration. Parts of it are also used by the runtime and the Event API.

Important:

- `configure()` validates browser-global defaults and replaces previously set default values.
- `stopAll`, `unmountAll`, and lifecycle methods of running instances are asynchronous and return `Promise<void>`.
- `initFormcentric()` triggers a Static scan but does not itself return a `Promise`.
- `setInstanceOptions()` only takes effect before initialization. For already running instances, it is ignored.
- `on`, `once`, and `off` for the Event API are provided with the Formapp runtime.
- `on()` and `once()` each return an unsubscribe function.

### 3.2.1 Structure

The current structure can be summarized as follows:

```ts
window.formcentric = {
    configure?: (config: Record<string, unknown>) => void,
    debug?: boolean,
    localesPath?: string,
    dynamicInit?: boolean,
    observer?: MutationObserver,
    initFormcentric?: () => void,
    getInstance?: (id: string) => FcInstance | undefined,
    setInstanceOptions?: (id: string, options: Record<string, unknown>) => void,
    stopAll?: () => Promise<void>,
    unmountAll?: () => Promise<void>,
    on?: (eventType: string | string[], listener: (event: unknown) => void) => () => void,
    once?: (eventType: string | string[], listener: (event: unknown) => void) => () => void,
    off?: (eventType: string | string[], listener: (event: unknown) => void) => void,
    formapp?: {
        state?: 'idle' | 'loading' | 'ready' | 'error',
        start?: (element: HTMLElement, options: Record<string, unknown>) => void,
        templates?: Record<string, unknown>,
        instances?: {
            [embedId: string]: {
                state?: string,
                initElement?: HTMLElement,
                options?: Record<string, unknown>,
                stop?: () => Promise<void>,
                unmount?: () => Promise<void>,
                reload?: () => Promise<void>,
                on?: (eventType: string | string[], listener: (event: unknown) => void) => () => void,
                once?: (eventType: string | string[], listener: (event: unknown) => void) => () => void,
                off?: (eventType: string | string[], listener: (event: unknown) => void) => void,
            },
        },
    },
}
```

### 3.2.2 Examples

Global activation of debugging, dynamic init, and translations:

```html
<script>
    window.formcentric ??= {}
    window.formcentric.debug = true
    window.formcentric.dynamicInit = true
    window.formcentric.localesPath = '/locales/custom.js'
</script>
```

Set browser-global defaults validated after loading the client:

```html
<script>
    window.formcentric.configure?.({
        language: 'de',
        requestHeaders: {
            'X-App': 'website',
        },
        debug: true,
    })
</script>
```

Set instance options before initialization:

```html
<script>
    window.formcentric ??= {}
    window.formcentric.setInstanceOptions?.('YOUR-FORM-ID', {
        debug: true,
        locale: 'de_DE',
    })
</script>
```

Read and stop running instance:

```html
<script>
    const instance = window.formcentric?.getInstance?.('YOUR-FORM-ID')
    void instance?.stop?.()
</script>
```

### 3.3 Heights and Widths

By default, a form grows with its content. If fixed dimensions or limits are desired, they can be set differently depending on the integration path:

- Static via `data-fc-height` and `data-fc-max-width`
- SDK and components via `height`, `maxWidth`, or styles on the wrapper element
- alternatively directly via custom CSS on the form container

Example in Static markup:

```html
<div
    data-fc-id="YOUR-FORM-ID"
    data-fc-height="800px"
    data-fc-max-width="600px"
></div>
```

Important:

- On mobile devices, a fixed height is not always strictly enforced.
- Limited containers can affect scrolling behavior.
- Many confirmation and error views center themselves within the container.

### 3.4 Parent URL

`parentUrl` or `data-fc-parent-url` is used to restore states of the embedding application in double-opt-in or return scenarios.

If no value is set, the current page URL is used as fallback.

Example:

```html
<div
    data-fc-id="YOUR-FORM-ID"
    data-fc-parent-url="https://my-website.com/?form=open"
></div>
```

### 3.5 Captcha Provider

Captcha-related settings are passed via the additional configuration:

- Static via `data-fc-configuration` as JSON string
- SDK and components via `configuration` as object

Under `captcha.providers[].properties`, provider-specific runtime options are set:

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `src` | `string` | - | URL of the provider SDK. `{{language}}` is replaced with the language code. The value can remain empty if the SDK is provided host-side or if automatic reloading should not be configured for compatibility reasons. |
| `loadSdk` | `boolean` | `true` | Controls whether the client reloads the provider SDK itself. If `false`, the SDK must already be present before client initialization by the host page. |

Example:

```ts
const configuration = {
    captcha: {
        providers: [
            {
                name: 'friendlyCaptcha',
                properties: {
                    src: 'https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk/site.min.js',
                },
            },
        ],
    },
}
```

Example with host-side loaded SDK:

```ts
const configuration = {
    captcha: {
        providers: [
            {
                name: 'friendlyCaptcha',
                properties: {
                    loadSdk: false,
                },
            },
        ],
    },
}
```

### 3.6 Pre-filling Forms with Dynamic Values

Forms can be pre-filled via URL parameters as well as via configuration.

An important shared behavior is:

- URL parameters are additionally taken into account
- if URL parameters and configuration set the same key, the URL parameter overrides the configuration value

### 3.6.1 Pre-filling via URL Parameters

Example:

```text
https://your-site.com/form?name=Max&product=Premium
```

### 3.6.2 Pre-filling via Configuration Attribute

Static:

```html
<div
    data-fc-id="YOUR-FORM-ID"
    data-fc-vars='{"name":"Max","product":"Premium"}'
></div>
```

SDK or components:

```ts
{
    vars: {
        name: 'Max',
        product: 'Premium',
    }
}
```

### 3.7 Including Custom Translations

Custom translations can be supplied in two ways, depending on the integration:

- **`locales` object** (SDK, React, Vue, Angular) — pass the overrides directly as an in-memory object. Your bundler inlines `date-fns` and `@uppy/locales` from the npm packages, so the date-picker and file-upload locales (which are functions) are carried by reference. This is the only way to add a **full new-language module** in a bundled integration. The `locales` object is not available as a `data-fc-*` attribute or on `window.formcentric`.
- **`localesPath` string** — a URL to a JavaScript module, loaded at runtime. Use it for Static and legacy integrations (`data-fc-locales-path` or `window.formcentric.localesPath`); it is also accepted by the SDK and components (`localesPath`). A page without a bundler can supply translations this way, but for the date-picker and file-upload locales the module has to import browser-ready files (see below).

If both are supplied, the `locales` object wins.

Each language key uses one of two shapes:

- a **translation patch** — either a bare translation map like `{ cancel_label: 'Avbryt' }`, or the same keys nested under `resources`. Use this to patch the texts of a language Formcentric already ships;
- a **structured locale module** — an object that uses the locale-module fields `resources`, `dateLocale`, and `uploadLocale`. This shape can be partial for a shipped language, or complete for a language Formcentric does **not** ship.

If one of `resources`, `dateLocale`, or `uploadLocale` is present, the entry is treated as the structured locale-module shape. In that case, translation keys must live under `resources`; stray top-level translation keys are ignored and logged as a warning.

For example, `{ cancel_label: 'Avbryt', dateLocale }` ignores `cancel_label`; write `{ resources: { cancel_label: 'Avbryt' }, dateLocale }` instead.

For a language Formcentric does **not** ship, provide a full locale module with `resources`, `dateLocale`, and `uploadLocale`; otherwise the date-picker and file-upload stay English.

`dateLocale` is a [`date-fns` locale](https://date-fns.org/docs/I18n) and `uploadLocale` is an [`@uppy/locales`](https://uppy.io/docs/locales/) pack. Their keys are owned by those libraries — see their documentation for the exact contents.

**Passing the `locales` object (SDK, React, Vue).** In a bundled integration, import the locale objects from the packages and pass the overrides via the `locales` prop (or the SDK `mount` option):

```jsx
import { sv as dateLocale } from 'date-fns/locale/sv'
import uploadLocale from '@uppy/locales/lib/sv_SE'

const locales = {
    'sv-SE': {
        resources: {
            page: 'Sida',
            continue_label: 'Fortsätt',
            back_label: 'Tillbaka',
            cancel_label: 'Avbryt',
        },
        dateLocale,
        uploadLocale,
    },
}

// React (a Vue prop or an SDK mount() option works the same way)
<FormcentricForm embedId="…" locales={locales} />
```

**Using a `localesPath` file (Static, legacy).** The file must be a JavaScript module that uses `export default`, with the same per-language shapes as above. Without a bundler, `localesPath` points to your `locales.js` module, which can import browser-ready locale files from a CDN, or from the same files downloaded and self-hosted on your domain:

```js
import 'https://cdn.jsdelivr.net/npm/@date-fns/cdn@4.4.0/locale/sv/cdn.min.js'
import uploadLocale from 'https://cdn.jsdelivr.net/npm/@uppy/locales@5.1.1/lib/sv_SE.js'

export default {
    'sv-SE': {
        resources: {
            page: 'Sida',
            continue_label: 'Fortsätt',
            back_label: 'Tillbaka',
            cancel_label: 'Avbryt',
        },
        dateLocale: window.dateFns.locale.sv,
        uploadLocale,
    },
}
```

Alternatively, preload downloaded browser files before Formcentric starts and read the globals from `locales.js`:

```html
<script>
    window.Uppy = window.Uppy || { locales: {} }
</script>
<script src="/vendor/date-fns/locale/sv/cdn.min.js"></script>
<script src="/vendor/uppy/locales/sv_SE.min.js"></script>
```

```js
export default {
    'sv-SE': {
        resources: {
            page: 'Sida',
            continue_label: 'Fortsätt',
            back_label: 'Tillbaka',
            cancel_label: 'Avbryt',
        },
        dateLocale: window.dateFns.locale.sv,
        uploadLocale: window.Uppy.locales.sv_SE,
    },
}
```

To verify locale behavior while integrating, enable debug mode (`data-fc-debug="true"` in Static/legacy, `debug: true` in SDK, or the `debug` prop in React/Vue). Formcentric then logs locale-loading problems such as invalid `dateLocale` / `uploadLocale` objects or incomplete unshipped-language modules to the browser console.

The complete set of overridable Formcentric keys is below; it mirrors the `locales.js` template shipped in the package. Copy it, keep the language keys you need, and uncomment the entries you want to change:

```js
// To add a language Formcentric does NOT ship, also localize the date-picker and file-upload, not
// only the texts. In bundled integrations, import date-fns and @uppy/locales from npm packages.
// In Static integrations without a bundler, import browser-ready CDN/self-hosted files as shown above,
// or preload them before Formcentric starts and read them from window.dateFns/window.Uppy.

export default {
    // Remove the comment and add a custom value to the keys you are seeking to override.
    en: {
        // exit_label: 'Exit',
        // cancel_label: 'Cancel',
        // submit_label: 'Send',
        // continue_label: 'Continue',
        // back_label: 'Back',
        // empty: 'Not specified',
        // page: 'Page',
        // progress: 'Form progress',
        // header_description: 'Page Directory',
        // datePicker: {
        //     time: 'Time',
        // },
        // fieldError: {
        //     error_unfocused: 'Your input for the field {{field}} is invalid: {{error}}',
        //     error_unfocused_no_label: 'Your input in a field is invalid: {{error}}',
        //     error_focused: '{{error}}',
        // },
        // globalError: {
        //     title: 'Error in the form',
        // },
        // notice: {
        //     insecureContext: {
        //         title: 'This form is submitted over an unsecured connection.',
        //         description: 'Your data is not encrypted and may be viewed or altered by third parties.',
        //     },
        // },
        // signature: {
        //     button_label: 'Sign form',
        //     close_label: 'Close window',
        //     modal_label: 'Use your finger or mouse to sign.',
        //     canvas_label: 'To close the modal press Escape.',
        //     actions_label: 'Signature actions',
        //     confirm_label: 'Apply',
        //     clear_label: 'Clear',
        //     orientation_dialog:
        //         'Switch to landscape mode to sign. Rotate your device or adjust the size of your browser window.',
        // },
        // fileUpload: {
        //     button_label: 'Upload file',
        //     uploaded_files_description: 'Use your assistive technology’s forward navigation to select and delete files',
        //     uploaded_files_description_empty: 'Empty list',
        //     uploaded_files_label: 'Uploaded files',
        //     uploaded_files_delete_label: 'Delete file {{filename}}',
        //     uploaded_files_deleted: '{{filename}} deleted',
        //     uploaded_files_delete_error: '{{filename}} could not be deleted',
        //     uploaded_files_download_label: 'Download file {{filename}}',
        //     // Keys are taken from @uppy/locale by default. To customize a key remove the comment of the key you want to change.
        //     uploadModal: {},
        // },
        // summary: {
        //     list_image_signature: 'Image of the entered signature',
        // },
        // comboBox: {
        //     choose: 'Choose an option',
        //     chooseMultiple: 'Choose one or more options',
        //     empty: 'All options are selected',
        //     add: 'Add option',
        //     descriptionSingle:
        //         'Combobox. Press the spacebar or down arrow to open the dropdown list. Use the up and down arrow keys to navigate through the options. Press PageUp and PageDown to navigate through the options in steps of 10. Press Home and End to navigate to the beginning or end of all options. Press Enter or Spacebar to select an option. Press Esc to close the list without making a selection. Use the Tab key to jump to the next element.',
        //     descriptionSingleInput:
        //         'Combobox with input. Press the spacebar or down arrow to open the dropdown list. Use the up and down arrow keys to navigate downwards from the input for individual entries through the options. Press PageUp and PageDown to navigate downwards in steps of 10 from the input for individual entries through the options. Press Home and End to navigate to the beginning or into the input for individual entries, or to the end of all options. Press Enter or Spacebar to select an option or confirm the input for individual entries. Press Esc to close the list without making a selection. Use the Tab key to jump to the next element.',
        //     descriptionMulti:
        //         'Multi Combobox. Press the spacebar or down arrow to open the dropdown list. Use the up and down arrow keys to navigate through the options. Press PageUp and PageDown to navigate through the options in steps of 10. Press Home and End to navigate to the beginning or end of all options. Press Enter or Spacebar to select or deselect an option. Selecting multiple options is possible. Press Esc to close the list without making a selection. Use the Tab key to jump to the next element.',
        //     descriptionMultiInput:
        //         'Multi Combobox with input. Press the spacebar or down arrow to open the dropdown list. Use the up and down arrow keys to navigate downwards from the input for individual entries through the options. Press PageUp and PageDown to navigate downwards in steps of 10 from the input for individual entries through the options. Press Home and End to navigate to the beginning or into the input for individual entries, or to the end of all options. Press Enter or Spacebar to select or deselect an option or confirm the input for individual entries. Selecting multiple options is possible. Press Esc to close the list without making a selection. Use the Tab key to jump to the next element. Press Backspace when the input is focused to remove existing individual entries.',
        // },
        // captcha: {
        //     valid: 'Captcha solved.',
        //     editMode: {
        //         title: 'Captcha',
        //         description: 'Switch to test mode to test the captcha service you have set up.',
        //     },
        //     image: {
        //         alt: 'Captcha image consisting of letters.',
        //     },
        //     refresh: {
        //         label: 'Refresh Captcha.',
        //     },
        // },
        // formcentric: {
        //     error: {
        //         invalidmail: 'Please check the email address.',
        //         invaliddate: 'Please check the date.',
        //         date: {
        //             range: 'Please enter a date that falls between {{0}} and {{1}}.',
        //             min: 'Please enter a date from {{0}}.',
        //             max: 'Please enter a date up to {{0}}.',
        //         },
        //         invalidcaptcha: 'Please solve the Captcha.',
        //         invalidnumber: 'Please enter a number.',
        //         number: {
        //             min: 'Please enter a number that is greater than or equal to {{0}}',
        //             max: 'Please enter a number that is less than or equal to {{0}}.',
        //         },
        //         invalidzipcode: 'Please check the postal code.',
        //         invalidpassword: 'Please enter a password in the specified format.',
        //         invalidretypepassword: 'Please make sure both passwords match. Please check your input.',
        //         invalidformat: 'Please enter a valid format.',
        //         required: 'Please fill in this field.',
        //         filerequired: 'Please upload a file.',
        //         wrongfiletype: 'The uploaded file type is not permitted.',
        //         toolargefile: 'The file is too large. Allowed maximum size is {{0}} KB.',
        //         filevalidation: 'There has been an error while validating the file. Please try again.',
        //         toomanyfiles: 'Please select {{0}} files maximum.',
        //         typeMismatch: 'Error processing your data.',
        //         invalidvat: 'Please enter a valid value added tax identification number.',
        //         illegaloption: 'Please enter a valid value.',
        //         illegalvalue: 'Please enter a valid value.',
        //         illegalfield: 'The specified field does not exist in the form.',
        //         length: {
        //             min: 'Please enter at least {{0}} characters.',
        //             max: 'Please enter a maximum of {{0}} characters.',
        //         },
        //         invalidiban: 'Please check the IBAN.',
        //         invalidbic: 'Please check the BIC.',
        //         invalidphone: 'Please check the phone number.',
        //         validation: 'There has been an error while validating the form.',
        //         unknown: 'There has been an error while processing the form.',
        //         notequal: 'The entered values in the fields {{1}} and {{0}} do not match.',
        //         invalidlicense: 'Your Formcentric license is invalid or has expired.',
        //         sessionexpired: 'Sorry, your session has expired. Please refresh the page to continue.',
        //         uploadsizeexceeded: 'The size of the uploaded file exceeds max size allowed.',
        //         methodnotsupported: 'The specified HTTP method is not supported.',
        //         creditcard: {
        //             invalidlength: 'Please check the length of your credit card number.',
        //             invalidchecksum: 'The credit card number is invalid. Please check your input and try again.',
        //             invalidcompany: 'The credit card number cannot be matched to any card issuer.',
        //             companymismatch: 'This credit card is not accepted. Please use a different one.',
        //         },
        //         fieldcount: {
        //             min: 'Please fill in at least {{0}} fields.',
        //             max: 'Please fill in a maximum of {{0}} fields.',
        //         },
        //         mailsizeexceeded: 'The total size of the uploaded files ({{0}}) exceeds the allowed limit of {{1}}.',
        //         formsubmit: {
        //             title: 'A technical error occurred',
        //             message: 'A technical error occurred while submitting the form. Please try again later.',
        //         },
        //     },
        // },
        // errorPage: {
        //     header: 'Oops?!',
        //     desc: 'Looks like this page is still getting into form.',
        // },
        // hint: {
        //     button: {
        //         label_open: 'Open notice',
        //         label_close: 'Close notice',
        //     },
        // },
        // datepicker: {
        //     describedBy:
        //         'Datepicker: Use the arrow keys to navigate through the days. Arrow right navigates to the next day in the same month. Arrow left navigates to the previous day in the same month. Arrow up navigates to the same weekday in the previous week. Arrow down navigates to the same weekday in the next week. Page up navigates to the same day in the previous month (if that day does not exist, focus moves to the last day of the previous month). Page down navigates to the same day in the next month (if that day does not exist, focus moves to the last day of the next month). Shift + Page up navigates to the same day and month of the previous year (if that day does not exist, focus moves to the last day of the month). Shift + Page down navigates to the same day and month of the next year (if that day does not exist, focus moves to the last day of the month). Home navigates to the first day (e.g., Sunday) of the current week. End navigates to the last day (e.g., Saturday) of the current week. Press Enter or Esc to close the calendar.',
        //     label_close: 'Close',
        //     label_nextYear: 'Next Year',
        //     label_chooseDayPrefix: 'Focus on',
        //     label_nextMonth: 'Next Month',
        //     label_disabledDayPrefix: 'Not available',
        //     label_weekPrefix: 'Month',
        //     label_monthPrefix: 'Year',
        //     label_previousYear: 'Previous Year',
        //     label_previousMonth: 'Previous Month',
        //     label_nextMonthButton: 'Next Month',
        //     label_nextYearButton: 'Next Year',
        //     label_previousMonthButton: 'Previous Month',
        //     label_previousYearButton: 'Previous Year',
        //     label_timeInput: 'Time',
        //     label_week: '#',
        //     label_chooseDate: 'Choose Date',
        //     label_chooseTime: 'Choose Time',
        // },
    },
    // Add more languages here (e.g. en-US, de, de-DE, etc.)
    //
    // Patch a shipped language — override only the keys you need (same structure as above):
    // 'en-US': {
    //     // Take keys from above
    // },
    //
    // Add a language Formcentric does NOT ship — supply a full locale module so the date-picker and
    // file-upload are localized too (uses the imports shown at the top of this file):
    // 'xx-XX': {
    //     resources: {
    //         // same key structure as above, in your language
    //         submit_label: '…',
    //     },
    //     dateLocale: yourDateLocale,
    //     uploadLocale: yourUploadLocale,
    // },
}
```

### 3.7.1 Adding New Languages and Translation Keys

Note:

- Language keys should be named consistently, for example `de`, `de-DE`, or `en-US`.
- Existing namespaces like `formcentric.error.required` should be kept.
- Custom keys should be created in the same structure as existing translations.
- Only the integration path differs for different integration paths, not the structure of the localization file.
- To add a language Formcentric does not ship, provide a full locale module (`resources` plus `dateLocale` and `uploadLocale`); a texts-only entry leaves the date-picker and file-upload in English.

## 4.0 Debugging

### 4.1 Local Debugging via Attribute

In Static integrations, debugging can be activated directly on the script tag or form container:

```html
<div
    data-fc-id="YOUR-FORM-ID"
    data-fc-debug="true"
></div>
```

### 4.2 Global Activation

Additionally, debugging can be activated globally:

```html
<script>
    window.formcentric ??= {}
    window.formcentric.debug = true
</script>
```

In SDK and component paths, this corresponds to the flag `debug: true`.

## 5.0 Custom Display Variants

### 5.1 Using Display Variants

Many themes support display variants via classes that are taken from the editor configuration into the markup.

Important basic rules:

- `style_class` is intended for additional CSS classes.
- `field_width` is a separate property and should be used for width classes.
- Comma-separated classes from the editor are normalized to spaces at runtime.

Common examples from the included themes:

- `mwf-s`, `mwf-m`, `mwf-l` for field widths
- `mwf-separator` for separator variants in supported themes
- `fc-file__dashboard--inline` and `fc-file__dashboard--modal` for file upload display variants

Important: Not every arbitrary class automatically has an effect. The class must either already be supported by the theme or be taken into account by you via CSS or template.

### 5.2 Creating and Using Custom Display Variants

If you want to use custom display variants, you need three things:

- a class or variant assignment in the editor
- a theme or CSS that actually evaluates this class
- optionally templates that pass `style_class` to the appropriate DOM element

#### 5.2.1 Define CSS Class in Editor

Define a class in the editor or in your field configuration, for example:

- `my-highlight`
- `my-compact`
- `fc-file__dashboard--modal`

#### 5.2.2 Create and Include CSS File

Step 1: Create CSS file

```css
.my-highlight {
    border: 2px solid #0f62fe;
    border-radius: 8px;
}
```

Step 2: Include CSS file in HTML or build

- classically via `<link>`
- in the framework via normal CSS or SCSS import
- in the theme directly in the corresponding partial files

Step 3: Define CSS rules

Work against stable theme or wrapper classes if possible, not against arbitrary DOM depths.

#### 5.2.3 Targeting Specific Elements

For more precise adjustments, we recommend:

- open browser developer tools
- inspect the actually rendered element and its classes
- evaluate whether the class lands on the outer field container or on an inner wrapper
- only then formulate CSS specifically

Especially for file uploads or more complex fields, you should check which element in the template receives the `style_class`.

## 6.0 Developing Custom Themes

You can design your own themes with the Formcentric Theme Customisation Workspace. This was specially developed for customizing Formcentric themes. Here you can modify existing Formcentric themes or create your own theme. With functions like adjusting theme variables, SCSS partials, and JavaScript templates, you can design the appearance, markup, and styling of forms according to your ideas. You also have the option to create custom themes, add fonts and images, and export your customizations for different target environments.

Good knowledge of frontend development, especially SCSS and JavaScript, is required for editing themes. Note that modified themes may need to be updated after Formcentric updates so that they continue to function correctly.

### 6.1 Setting Up the Workspace

To start developing your own theme, clone the Formcentric Theme Customisation Workspace with the following command:

```bash
git clone git@github.com:Formcentric/theme-customisation-workspace.git
```

So that you can use all the advantages of Git during development and set up deployments of your themes with GitHub Actions, we recommend forking the workspace. Follow the corresponding GitHub documentation.

### 6.2 Documentation

Important general components of a theme are:

- `definition.json` for cloud and design definitions
- `styles.scss` as central style entry
- `templateEntry.js` as entry for template bundling
- `_variables.json` for runtime variables in non-SCSS integrations
- `variables/_variables.scss` for SCSS-based theme integrations

Second-generation themes additionally work with a token-based design system. This facilitates:

- consistent color and spacing systems
- central maintenance of tokens
- variant creation
- long-term maintainability

## 7.0 Event API

With the Formcentric Event API, you can see when certain actions take place in your forms - for example, a submission or an error. These events can be automatically forwarded to tracking services like Matomo, Google Analytics, or other analytics tools.

The API provides a unified interface through which you can subscribe to relevant events and use them in your own tracking solutions.

Note: The use of the Event API is subject to data protection regulations (GDPR). Obtain user consent before collecting personal data or forwarding it to external services.

### 7.1 Basic Setup

The Event API is automatically available as soon as the Formcentric script is loaded. To reliably register event listeners, use the `formcentric:loaded` custom event:

Important: Your tracking script must be included before the Formcentric wrapper script and must not have a `defer` attribute.

```html
<!-- Your form embed -->
<div data-fc-id="my-form"></div>
<!-- Your tracking script (MUST be before the wrapper script, without defer!) -->
<script>
    window.addEventListener('formcentric:loaded', () => {
        console.log('Formcentric Event API is ready!')

        // Register event listener
        window.formcentric.on('form:init:complete', event => {
            console.log('Form loaded:', event.formName)
        })
    })
</script>
<!-- Formcentric wrapper script (can use defer) -->
<script
    defer
    src="https://form.formcentric.com/form/formcentric.js"
></script>
```

### 7.2 Event Structure

Each event follows a unified structure with the following basic properties:

| Property | Type | Description |
| --- | --- | --- |
| `eventType` | `string` | Event identifier (e.g., `form:init:complete`) |
| `timestamp` | `number` | Unix timestamp in milliseconds |
| `embedId` | `string` | Unique ID of the form instance |
| `formId` | `string` (optional) | Form UID (available after initialization) |
| `formName` | `string` (optional) | Form name (set via `data-fc-name` or form name selected in Formcentric Editor, if available) |

### 7.3 Available Events

The Formcentric Event API provides various categories of events to track user interactions, validation, and form lifecycles.

#### 7.3.1 Form Lifecycle Events

These events track the lifecycle of a form instance.

| Event | Description | Available Properties |
| --- | --- | --- |
| `form:init:start` | Wrapper starts form initialization | `eventType`, `embedId`, `timestamp` |
| `form:init:complete` | Form fully loaded and rendered | `eventType`, `embedId`, `formId`, `formName`, `pageCount`, `timestamp` |
| `form:stop` | Formapp instance was stopped | `eventType`, `embedId`, `reason` (`user`, `error`, `navigation`, `system`), `timestamp` |
| `form:unmount` | Formapp instance removed from DOM | `eventType`, `embedId`, `timestamp` |
| `form:reload` | Form was reloaded | `eventType`, `embedId`, `trigger`, `timestamp` |
| `form:cancel` | Form was cancelled | `eventType`, `embedId`, `formId`, `formName`, `currentPage`, `timestamp` |
| `form:submitted` | Form was successfully submitted | `eventType`, `embedId`, `formId`, `formName`, `fromPage`, `view`, `submissionTime`, `timestamp` |

#### 7.3.2 Navigation Events

| Event | Description | Available Properties |
| --- | --- | --- |
| `page:change:start` | Page change started | `eventType`, `embedId`, `formId`, `formName`, `fromPage`, `toPage`, `direction` (`forward`, `backward`), `timestamp` |
| `page:change:success` | Page change successful | `eventType`, `embedId`, `formId`, `formName`, `fromPage`, `toPage`, `direction`, `submissionTime`, `timestamp` |
| `page:change:failed` | Page change failed | `eventType`, `embedId`, `formId`, `formName`, `fromPage`, `toPage`, `direction`, `reason` (`validation`, `server`, `network`, `timeout`), `errorMessage` (optional), `timestamp` |

#### 7.3.3 Field Interaction Events

These events capture interaction with individual form fields.

| Event | Description | Available Properties |
| --- | --- | --- |
| `field:input` | User enters data in field | `eventType`, `embedId`, `formId`, `formName`, `fieldName`, `fieldLabel`, `fieldType`, `valueLength`, `timestamp` |
| `field:focus` | Field receives focus | `eventType`, `embedId`, `formId`, `formName`, `fieldName`, `fieldLabel`, `fieldType`, `timestamp` |
| `field:blur` | Field loses focus | `eventType`, `embedId`, `formId`, `formName`, `fieldName`, `fieldLabel`, `fieldType`, `timestamp` |

#### 7.3.4 Validation Events

These events report validation results for individual form fields.

| Event | Description | Available Properties |
| --- | --- | --- |
| `validation:success` | Field successfully validated | `eventType`, `embedId`, `formId`, `formName`, `currentPage`, `fieldName`, `fieldLabel`, `timestamp` |
| `validation:error` | Validation error occurred | `eventType`, `embedId`, `formId`, `formName`, `currentPage`, `fieldName`, `fieldLabel`, `errorCode`, `errorMessage`, `timestamp` |
| `validation:cleared` | Validation error resolved | `eventType`, `embedId`, `formId`, `formName`, `currentPage`, `fieldName`, `fieldLabel`, `timestamp` |

#### 7.3.5 User Behavior Events

These events capture specific user behavior for analyzing the user experience.

**Shared Properties**
All events contain at least these basic properties:

| Property | Type | Description |
| --- | --- | --- |
| `eventType` | `string` | Event identifier (e.g., `form:init:complete`) |
| `timestamp` | `number` | Unix timestamp in milliseconds |
| `embedId` | `string` | Unique ID of the form instance |
| `formId` | `string` (optional) | Form UID (available after initialization) |
| `formName` | `string` (optional) | Form name (set via `data-fc-name` or default `formId`) |

### 7.4 Wildcard Listeners

With wildcards, you can capture multiple events with a single handler:

```js
window.addEventListener('formcentric:loaded', () => {
    // Output all events of a category
    window.formcentric.on('validation:*', event => {
        console.log('[Form Event]', event.eventType, event)
    })
    // Output all events (not recommended in production environments)
    window.formcentric.on('*', event => {
        console.log('[Form Event]', event.eventType, event)
    })
})
```

### 7.5 Integration with Matomo

Complete setup for Matomo tracking:

```html
<!-- Matomo Tracking Code -->
<script>
    var _paq = (window._paq = window._paq || [])
    _paq.push(['trackPageView'])
    _paq.push(['enableLinkTracking'])
    ;(function () {
        var u = '//your-matomo-domain.com/'
        _paq.push(['setTrackerUrl', u + 'matomo.php'])
        _paq.push(['setSiteId', '1'])
        var d = document,
            g = d.createElement('script'),
            s = d.getElementsByTagName('script')[0]
        g.async = true
        g.src = u + 'matomo.js'
        s.parentNode.insertBefore(g, s)
    })()
</script>
<!-- Formcentric Event Tracking -->
<script>
    window.addEventListener('formcentric:loaded', () => {
        const startedForms = new Set()
        window.formcentric.on('form:init:complete', event => {
            _paq.push(['trackEvent', 'Form', 'Loaded', event.formName, event.pageCount])
        })
        window.formcentric.on('field:focus', event => {
            if (startedForms.has(event.embedId)) return
            startedForms.add(event.embedId)
            _paq.push(['trackEvent', 'Form', 'Started', event.formName || event.formId || event.embedId])
        })
        window.formcentric.on('page:change:success', event => {
            _paq.push(['trackEvent', 'Form', 'Page Change', `${event.fromPage} -> ${event.toPage}`, event.toPage])
        })
        window.formcentric.on('form:submitted', event => {
            _paq.push(['trackEvent', 'Form', 'Submitted', event.formName, event.submissionTime])
            _paq.push(['trackGoal', 1])
        })
        window.formcentric.on('validation:error', event => {
            _paq.push(['trackEvent', 'Form', 'Validation Error', `${event.fieldLabel}: ${event.errorCode}`])
        })
        window.formcentric.on(['form:stop', 'form:unmount'], event => {
            startedForms.delete(event.embedId)
        })
    })
</script>
```

### 7.6 Integration with Google Analytics 4

Complete setup for Google Analytics 4 tracking:

```html
<!-- Google Analytics 4 -->
<script
    async
    src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
></script>
<script>
    window.dataLayer = window.dataLayer || []
    function gtag() {
        dataLayer.push(arguments)
    }
    gtag('js', new Date())
    gtag('config', 'G-XXXXXXXXXX')
</script>
<!-- Formcentric Event Tracking -->
<script>
    window.addEventListener('formcentric:loaded', () => {
        const startedForms = new Set()
        window.formcentric.on('form:init:complete', event => {
            gtag('event', 'form_load', {
                form_id: event.formId,
                form_name: event.formName,
                embed_id: event.embedId,
                page_count: event.pageCount,
            })
        })
        window.formcentric.on('field:focus', event => {
            if (startedForms.has(event.embedId)) return
            startedForms.add(event.embedId)
            gtag('event', 'form_start', {
                form_id: event.formId,
                form_name: event.formName,
                embed_id: event.embedId,
            })
        })
        window.formcentric.on('page:change:success', event => {
            gtag('event', 'form_page_change', {
                form_id: event.formId,
                form_name: event.formName,
                embed_id: event.embedId,
                from_page: event.fromPage,
                to_page: event.toPage,
                direction: event.direction,
            })
        })
        window.formcentric.on('form:submitted', event => {
            gtag('event', 'form_submit', {
                form_id: event.formId,
                form_name: event.formName,
                embed_id: event.embedId,
                submission_time_ms: event.submissionTime,
            })
            gtag('event', 'conversion', {
                send_to: 'G-XXXXXXXXXX/your-conversion-id',
                transaction_id: event.embedId,
            })
        })
        window.formcentric.on('validation:error', event => {
            gtag('event', 'form_validation_error', {
                form_id: event.formId,
                form_name: event.formName,
                embed_id: event.embedId,
                field_name: event.fieldName,
                field_label: event.fieldLabel,
                error_code: event.errorCode,
            })
        })
        window.formcentric.on(['form:stop', 'form:unmount'], event => {
            startedForms.delete(event.embedId)
        })
    })
</script>
```

### 7.7 Custom Analytics Solution

Send events to your own API:

```html
<script>
    window.addEventListener('formcentric:loaded', () => {
        const ANALYTICS_ENDPOINT = 'https://your-api.com/events'
        const startedForms = new Set()
        const sendEvent = async (eventName, payload) => {
            try {
                await fetch(ANALYTICS_ENDPOINT, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        event_name: eventName,
                        ...payload,
                    }),
                })
            } catch (error) {
                console.error('Analytics error:', error)
            }
        }
        window.formcentric.on('form:init:complete', event => {
            void sendEvent('form_load', {
                embed_id: event.embedId,
                form_id: event.formId,
                form_name: event.formName,
                page_count: event.pageCount,
                timestamp: event.timestamp,
            })
        })
        window.formcentric.on('field:focus', event => {
            if (startedForms.has(event.embedId)) return
            startedForms.add(event.embedId)
            void sendEvent('form_start', {
                embed_id: event.embedId,
                form_id: event.formId,
                form_name: event.formName,
                timestamp: event.timestamp,
            })
        })
        window.formcentric.on('page:change:success', event => {
            void sendEvent('form_page_change', {
                embed_id: event.embedId,
                form_id: event.formId,
                form_name: event.formName,
                from_page: event.fromPage,
                to_page: event.toPage,
                direction: event.direction,
                timestamp: event.timestamp,
            })
        })
        window.formcentric.on('form:submitted', event => {
            void sendEvent('form_submit', {
                embed_id: event.embedId,
                form_id: event.formId,
                form_name: event.formName,
                from_page: event.fromPage,
                view: event.view,
                submission_time_ms: event.submissionTime,
                timestamp: event.timestamp,
            })
        })
        window.formcentric.on('validation:error', event => {
            void sendEvent('form_validation_error', {
                embed_id: event.embedId,
                form_id: event.formId,
                form_name: event.formName,
                field_name: event.fieldName,
                field_label: event.fieldLabel,
                error_code: event.errorCode,
                error_message: event.errorMessage,
                timestamp: event.timestamp,
            })
        })
        window.formcentric.on(['form:stop', 'form:unmount'], event => {
            startedForms.delete(event.embedId)
        })
    })
</script>
```

### 7.8 Debugging

During development, you can output all events to the console for debugging:

```js
// Only in development environment!
if (window.location.hostname === 'localhost') {
    window.addEventListener('formcentric:loaded', () => {
        window.formcentric.on('*', event => {
            console.log('[Formcentric Event]', event.eventType, event)
        })
    })
}
```
