> For the complete documentation index, see [llms.txt](https://developer.vario-software.de/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.vario-software.de/documentation/apps/ui-integration.md).

# UI Integration

Your App runs in an **iframe** inside the ERP. The [manifest](/documentation/apps/app-manifest.md) declares *where* it appears; this page describes how the two sides talk once it is there.

Everything goes through `postMessage`, wrapped in two helpers:

```javascript
import { sendMain, receiveMain } from '@vario-software/vario-app-framework-frontend/script/communication.js';

sendMain({ height: 640 });

const stop = receiveMain({
  updateComponents: () => reload(),
  route: route => console.log(route.name),
});
```

`sendMain` automatically adds your `appIdentifier` and `integrationId` to every message — the ERP uses both to route it back to the right frame.

{% hint style="warning" %}
Because those two keys are added first and your payload is spread after them, a payload key called `appIdentifier` would overwrite the routing information. Don't reuse those names.

`receiveMain` calls your handler for **every** key present in an incoming message, and it checks nothing about the sender. Treat inbound data as untrusted.
{% endhint %}

## The ERP must hear from you within 15 seconds

The host waits for a first valid message as a loaded-handshake. If none arrives within **15 seconds**, your App is replaced by an "app not available" message.

The designated signal for this is:

```javascript
sendMain({ ready: true });
```

The framework sends it for you from `secureIntegration()`, which runs on `DOMContentLoaded`. That function does two things: it verifies your page is really embedded in a VARIO Cloud frame (by checking the referrer against the `*.vario.cloud` hosts and `localhost`), and then reports readiness.

{% hint style="info" %}
The host does not insist on that specific key — it treats **the first valid message** as the handshake, whatever it contains. So an App that immediately reports its height is also fine. `ready` is simply the explicit, guaranteed way to do it.

Don't confuse it with `customActivity`, which is unrelated: that one resets the ERP's idle timer during long interactions so the user isn't logged out.
{% endhint %}

{% hint style="warning" %}
**Send it yourself rather than relying on the framework.** In the current published version `secureIntegration()` throws before it reaches the `ready` message (see the warning at the end of this page), so nothing is sent. Apps still work because their own first message satisfies the handshake — but if your App renders slowly and sends nothing early, it will be marked unavailable.
{% endhint %}

{% hint style="danger" %}
**During installation the deadline is harsher.** The install dialog rolls the installation back after **120 seconds**. Send `installationComplete` when done, or keep sending `uninstallAlive` while you work — otherwise your App is uninstalled again.
{% endhint %}

## Sizing the frame

**An iframe cannot grow with its content.** There is no `height: auto` that means "as tall as the page inside" — the browser gives the frame a fixed box, and anything taller simply scrolls **inside** it. The result is the thing users complain about: a second scrollbar within the ERP page, and your content cut off at an arbitrary line.

The ERP cannot fix this on its own either. Your App is served from your own domain, so the host is not allowed to measure the document inside the frame. **Only you know how tall your content is.**

Hence the contract: *you measure, the host applies.*

```javascript
sendMain({ height: document.body.scrollHeight });
```

Send it once the content is rendered, and again whenever it changes — after loading data, expanding a section, opening a form. The framework can do this continuously for you via `initHeightTransfer()`, which watches the body and re-sends on every change.

{% hint style="warning" %}
That creates a feedback loop: you send a height → the host resizes the iframe → your layout changes → you send again. It is only damped by an "unchanged value" check, so content whose layout depends on the frame height can oscillate.

The robust alternative is to let the host own the height:

```javascript
sendMain({ useFullHeight: true });
receiveMain({ height: value => resizeTo(parseInt(value, 10)) });
```

The host then computes the height from the viewport and sends it to you. Note the **type flip**: outbound `height` is a number of pixels, inbound `height` is a CSS string such as `"650px"` or `"calc(100vh - 259px)"`.
{% endhint %}

Once the host owns the height — in full-height mode, in a dialog, or in a dashboard widget — your own `height` messages are **ignored**. Inside a dialog use `sendMain({ dialog: { fullHeight: true } })` instead.

## Dialogs

Your App can open another of its own integrations as a modal, and get a value back.

**Open it** (the `integrationId` must be declared in your manifest, typically without a `pointOfIntegration`):

```javascript
sendMain({
  dialog: {
    open: { integrationId: 'my-editor', title: 'Editor', additionalPayload: { id } },
    updateComponentsAfterDismiss: true,
  },
});
```

**Close it and return a value** — this is what the dialog's own code sends:

```javascript
sendMain({ dialog: { close: true, result: { confirmed: true, value } } });
```

**Receive the result** in the opener:

```javascript
receiveMain({
  dialogResult: result => apply(result),
});
```

{% hint style="info" %}
If the user dismisses the dialog with the close button instead, **no result is sent at all** — `dialogOk`/`dialogResult` never arrive. That is what `updateComponentsAfterDismiss` is for: it makes the ERP refresh its components anyway. Note it sits on `dialog`, not inside `dialog.open`.
{% endhint %}

Inside a dialog you can also send `dialog.disablePadding` and `stickynavButtons` (rendered into the dialog header).

## Using the ERP's own dialogs

You don't have to build pickers. Ask the host to open its own and send you the result:

| Send           | Get back                                           |
| -------------- | -------------------------------------------------- |
| `searchDialog` | `searchDialog-<key>` — `{ selected }`              |
| `datepicker`   | `datepicker-<key>` — JSON string of `{ date, to }` |
| `cron`         | `cron-<key>` — JSON string of `{ value }`          |
| `colorpicker`  | `colorpicker-<key>` — JSON string of `{ value }`   |
| `confirmation` | `confimation` — `{ key, values }`                  |

Each accepts an optional `key` so you can tell several instances apart; without it the reply key ends in `-result`.

{% hint style="danger" %}
Two things will cost you an afternoon:

1. **`confimation` is misspelled** — one `r` is missing, on both sides. Listening for `confirmation` receives nothing.
2. **`datepicker`, `cron` and `colorpicker` results arrive as JSON strings** and must be parsed. `searchDialog` only stringifies `selected` when multiple selection is enabled. `dialogResult` is passed through raw.

Cancelling a picker sends **nothing** — only `confirmation` reports a cancel, as `{ key: false }`.
{% endhint %}

## Integrating into the ERP shell

| Message                          | Effect                                                                                  |
| -------------------------------- | --------------------------------------------------------------------------------------- |
| `ready`                          | Confirms your integration has loaded — the handshake described above                    |
| `updateComponents`               | Tells the ERP to refresh its datagrids and badges                                       |
| `notify`                         | Shows an ERP toast; `route` adds an open-in-new-tab action                              |
| `badge`                          | Puts a badge on your tab                                                                |
| `backlink`                       | Custom back link; clicking it sends you `back`                                          |
| `stickynavButtons`               | Buttons in the sticky nav; a click sends you `button-<key>`                             |
| `stickynavTabs`                  | Sub-tabs bound to the route; the host rewrites the URL rather than reloading you        |
| `editmode`                       | Register, enter or leave the ERP's edit mode; you then receive `edit`, `save`, `cancel` |
| `documentTitle`                  | Sets the browser tab title                                                              |
| `copyToClipboard`                | Copies text and shows a confirmation                                                    |
| `customActivity`                 | Resets the idle timer, preventing auto-logout during long interactions                  |
| `minWidth`                       | Minimum width for your frame                                                            |
| `openInNewTab` / `openInSameTab` | Navigate the ERP (see below)                                                            |
| `updateSettings`                 | Change a shared setting — only the level-of-detail and UI-mode keys are accepted        |

Some of these are vetoed by the host depending on where you are embedded: `stickynavButtons`, `backlink` and `editmode` are ignored inside dialogs and widgets.

{% hint style="warning" %}
**Nothing is queued.** A message you send before the host has attached its listener is lost, and a message the host sends before your frame exists is dropped. Send state-establishing messages (buttons, tabs, badge, backlink) on `DOMContentLoaded` or later, and re-send them after you receive `routeChanged`.
{% endhint %}

## What the host sends you

| Key                         | Meaning                                           |
| --------------------------- | ------------------------------------------------- |
| `height`                    | New frame height (CSS string) in full-height mode |
| `updateComponents`          | Something changed in the ERP; refresh             |
| `changeLanguageCode`        | The user switched language                        |
| `route` + `routeChanged`    | The ERP route changed                             |
| `submit`                    | An ERP form is submitting — see below             |
| `dialogOk` / `dialogResult` | Your dialog resolved                              |
| `back`                      | Your custom backlink was clicked                  |
| `appToken`                  | A refreshed App token                             |
| `edit` / `save` / `cancel`  | Edit-mode actions                                 |
| `button-<key>`              | One of your sticky-nav buttons was clicked        |
| `beforeUnmount`             | Your integration is being torn down               |

### Answering a submit

When the ERP submits a form that contains your integration, it asks you to confirm:

```javascript
receiveMain({
  submit: ({ correlationId }) => {
    const ok = validate();
    sendMain(ok
      ? { submitConfirmed: true, correlationId }
      : { submitRejected: true, correlationId });
  },
});
```

{% hint style="warning" %}
`correlationId` must be sent back as a **sibling key**, not nested inside an object — otherwise the ERP cannot match your answer. And if you don't answer at all, the ERP proceeds anyway after its timeout.
{% endhint %}

## Navigating the ERP

```javascript
sendMain({ openInNewTab: { name: 'documents-sales.index' } });
sendMain({ openInNewTab: { name: 'documents-sales.detail', params: { id } } });
```

The target is a standard vue-router location, so `name` must be a route the ERP actually has.

{% hint style="info" %}
**Route names are not listed here** — there are many and they change per release. The ERP's route definitions are the source of truth; the names follow the visible hierarchy (`documents-sales.index`, `accounts.detail.tabs.masterdata`, `settings.general.users.index`). App integrations get generated names of the form `apps.<appIdentifier>.<integrationId>`.
{% endhint %}

`openInSameTab` navigates the ERP window itself and is deliberately restricted — a string target is only accepted for VARIO Cloud hosts.

## Parameters you receive

The host puts context on your integration URL. The framework exposes the common ones:

`appIdentifier`, `integrationId`, `language`, `supportMode`, `myCompanyId`, `uimode`, `detailDatagridTable`, `superUser`, `permissions`, `additionalPayload`

Also on the URL but not exposed by the framework — read them from the query string yourself: `userId`, `detailDatagrid`, `routeName`, and every parameter of the current ERP route.

{% hint style="warning" %}
**All values are strings.** `superUser` must be compared to `'true'`; using it as a boolean makes every user a super user.

`additionalPayload` is a JSON string when the host passes an object — parse it.

On installation and uninstallation pages the parameter set is smaller: there is no `integrationId`, no `superUser` and **no `permissions`**. Calling a permission check there fails, so guard it.
{% endhint %}

## Dashboard widgets

A widget is an integration mounted at one of the `dashboard.*` integration points. Its title and icon come from the manifest; the grid fixes the minimum size.

**Settings are exchanged, not stored by you.** Announce your defaults, and the host sends back the user's saved settings merged over them:

```javascript
receiveMain({
  widgetSettings: settings => apply(settings),
  widgeEditmode: active => setEditMode(active),
});

sendMain({ widget: { init: { showTotals: true } } });   // your defaults
```

When the user changes something, report it and the dashboard persists it:

```javascript
sendMain({ widget: { settings: { showTotals: false } } });
```

{% hint style="warning" %}
Two details:

* The inbound edit-mode key is spelled **`widgeEditmode`** — missing a `t`. Use it exactly as written.
* If you send `widget.init` **without** any defaults, the host concludes your widget has no settings and shows no settings affordance at all.
  {% endhint %}

To follow the ERP's own edit mode, report your state with `sendMain({ widget: { editmode: { active } } })`.

## Reusing the ERP's look

The framework ships **CSS only** — no JavaScript components. Link the stylesheet and write plain markup with `v-*` classes:

```html
<link rel="stylesheet" href="node_modules/@vario-software/vario-app-framework-frontend/style/index.css">
```

Controls rely on structure. A checkbox needs a wrapper, and the input must come **immediately before** its label, because the checked state is expressed with an adjacent-sibling selector:

```html
<div class="v-checkbox">
  <input id="agree" type="checkbox" />
  <label for="agree">Label</label>
</div>
```

Put anything between the input and the label and nothing renders. `v-toggle` has the same structure for switches.

Dark mode and responsive breakpoints are applied by `initSharedSettings()` and `initScreenSize()`, which set classes on `body`.

{% hint style="danger" %}
**Call those initialisers explicitly.** In the current published framework, `secureIntegration()` throws before it can start them, which silently costs you dark mode, responsive classes, height transfer and token refresh. Call `initHeightTransfer()`, `initAppTokenHandling()`, `initSharedSettings()` and `initScreenSize()` yourself rather than relying on the bootstrap.

Also: `initStickynav()` waits for a reply the ERP never sends. Do not `await` it.
{% endhint %}

## Related

* [App Manifest](/documentation/apps/app-manifest.md) — declaring where your integration appears
* [App Authentication](/documentation/apps/authentication.md) — the App token you receive
* [FAQ](broken://pages/DmBZgbUEXCMyhXGUtn1t)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developer.vario-software.de/documentation/apps/ui-integration.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
