# Embedding Eximee Forms as a web component

The Eximee platform Forms presentation module can be launched in the variant:

* a standalone single page application hosted as a dedicated website, embedded within a webview or iframe,
* a web component library for embedding in any existing website or web application.

The forms application is functionally responsible for:

* presenting and handling forms defined using low-code in the Eximee platform,
* each form has a dynamic structure consisting of components and one or more pages defined using low-code,
* Eximee forms manage the form state, handle user interactions, and navigation within the presented form,
* the application can use browser URL navigation, exclusively using the part after #, or in-memory navigation that does not affect the browser URL state.

The documentation below describes how to embed and integrate Eximee forms using the web component library.

## General assumptions

To launch a form:

* JavaScript files providing the component implementation must be included in the website,
  * the component assets are served by the Eximee platform in a version compatible with the platform server,
  * the specific names and URLs of the assets come from the manifest hosted together with the other static platform files,
* ensure the correctness of CORS and CSP headers between the Eximee platform domains and the host application,
  * in particular, the ability to fetch static resources/assets of the form application in the host application,
* ensure access to the Eximee platform REST API from the host application's domain,
* create a DOM element for the component using HTML or, programmatically, using JavaScript,
* initiate form loading using the programmatic JavaScript API.

## Technologies used

The application is built using Angular version 20.x.x (subject to regular updates).

And exposes a web component in accordance with the web component specification in the scope of:

* custom elements,
* shadow DOM.

The web component can be embedded inside an open Shadow DOM, however this must be taken into account when embedding styles from the bundleStats.json manifest into the DOM (described below).

## Impact on the application's global execution context by zone.js

The application relies on the availability of the globally loaded zone.js library in a version compatible with the Angular version (and provided together with Angular/Angular CLI).

The zone.js library is the foundation of the Angular framework and is commonly used in applications built with it.

The library works by monkey-patching asynchronous user interaction APIs with the page in order to handle UI change detection during user interaction.

The Eximee forms application is currently not adapted to work in zoneless mode, and any adaptation would require dedicated work on the Eximee platform side.

However, previous experience shows that in the case of:

* using the web component inside a host application that uses a compatible version of zone.js (e.g. written in Angular),
* using the web component inside a host application that does not use zone.js (e.g. written in Vue or React).

We have not observed conflicts or operational problems with either of these applications (host, Eximee). However, it should be borne in mind that the compatibility of libraries embedded in the running application needs to be verified.

## Impact on the application's global execution context by polyfills

The web component library relies on the availability of the following polyfills (and includes them if they are not available):

* core-js/shim from core-js,
* @webcomponents/webcomponentsjs/custom-elements-es5-adapter.js from @webcomponents/webcomponentsjs,
* web-components/webcomponents-loader from polymer.

All polyfill dependencies comply with the standards for delivering polyfills, i.e.

* they do not overwrite native solutions available in the browser,
* they load only if they provide functionality that is not available natively and not provided by other means (e.g. by the host application),
* they are provided by commonly used open source libraries.

In practice, the indicated polyfills should not change behavior in the case of:

* using modern browsers,
* using the host application with modern web frameworks (such as Angular).

## Resource manifest and release cycle

The web component library assets depend on the current version of the Eximee platform deployed in a specific environment.

To make dependency management easier, the platform hosts a manifest file describing the resources required to be included on the page in order to launch the web component.

The manifest is in JSON format and looks like this, for example:

```json
{
  "format": 3,
  "scripts": [
    "polyfills.125595b8f8d58bce.js",
    "main.922b3d09b697675a.js"
  ],
  "globalStyles": [
    "global-styles.a9c4b7e18d2f03ab.css"
  ],
  "styles": [
    "styles.6e50ddf23fe0e270.css"
  ]
}
```

The manifest contains three types of resources that must be loaded in different ways:

* scripts – JavaScript scripts; they should be embedded in the `<head>` of the page hosting the web component (in `<script>`),
* globalStyles – global stylesheets; they should be embedded in the `<head>` of the page hosting the web component (in `<link>`). They contain only resources unrelated to layout or the appearance of DOM elements, such as font definitions (`@font-face`). They will not affect the styling of elements in the existing host application,
* styles – component-specific stylesheets; they should be attached directly to the component DOM element (or its shadowRoot, if the web component is embedded in Shadow DOM).

All resource names contain hashes based on file contents. This makes it possible at the same time to:

* ensure loading of the correct file according to the specific system version,
* reuse files from cache if their content does not change between versions.

Embedding resources can be done:

* dynamically by the application frontend before launching the form,
* as part of server-side rendering of the page hosting the host application (recommended).

Note: when embedding the form web component within shadow DOM (open) nodes, it is necessary to embed links to styles inside the appropriate shadowRoot. Styles embedded in the head of the entire document will not be able to style the component inside shadow DOM in the standard way.

## Creating a form instance

### Embedding the component and launching the form

The component can be embedded in the DOM using the HTML tag of the component or programmatically using the JavaScript API:

```typescript
var form = document.createElement("ex-forms-form");
document.body.appendChild(form)
```

The form component can be embedded anywhere in the application's DOM structure.

After embedding and obtaining a reference to the element, it is possible to launch the form using the loadForm method as shown in the example:

```typescript
container.loadForm({
  formId: 'demoFormularzJakoWebcomponent', /* form identifier */
  baseHref: '/api',  /* path where the Eximee REST API made available via proxy-pass is accessible */
  onError: function () {
    alert('An error occurred while processing the application');
  }
});
```

It is possible to pass additional parameters to the load form method, which will be described in the next sections of the documentation and provided in the reference API at the end of the document.

One such parameter is the ability to pass business parameters for launching a specific form, for example:

```typescript
container.loadForm({
  formId: 'demoFormularzJakoWebcomponent', /* form identifier */
  baseHref: '/api',  /* path where the Eximee REST API made available via proxy-pass is accessible */
  data: JSON.stringify({'param1': 'value1'}), /* Additional business parameters feeding the form in the form of a key-value object serialized to a JSON string */
  onError: function () {
    alert('An error occurred while processing the application');
  }
});
```

### Extending REST communication headers (including auth headers for API Gateway)

In many deployments it is necessary to extend the REST API request headers, especially in applications handling user authentication and relying on access control at the API Gateway level (e.g. OIDC tokens).

For this purpose, it is possible to specify a function that creates headers to be attached to each REST request:

```typescript
form.loadForm({
    formId: 'demoFormularzJakoWebcomponent',
    additionalRequestHeaders: () => ({
        'X-Custom-Header': 'value'
    })
})
```

The method creating headers is called every time, immediately before making a request to the REST API. Headers are not stored between calls, which is particularly important, for example, for OAuth headers that may change as a result of token refresh during form processing.

### Preserving form state between page refreshes / host application navigation

The complete form state and data entered by the user are stored on the Eximee platform server side within the user session.

This means that it is possible to restore/resume the user's active form even after navigating within the host application or completely refreshing the page in the browser.

The user's form on the server side is distinguished based on:

* the session identifier in the cookie,
* the form instance identifier in the session based on the form instance number (formInstanceNumber).

Assuming cookie handling is guaranteed and the cookie will not be deleted (except in situations where functional requirements require such deletion), restoring the form requires storing its instance number.

The form number can be passed to the load form method and retrieved from the instance after launch. Assuming the host application has a unambiguous way to store this value (e.g. in the page query, server-side, etc.), it is possible to write:

```typescript
let formInstanceNumber: string | undefined = restoreFormInstanceNumber();
form.loadForm({
    formId: 'demoFormularzJakoWebcomponent',
    formInstanceNumber: formInstanceNumber,
    onLoaded: (result, config) => storeFormInstanceNumber(result.data.formModel.formNumber),
});

```

## Access to the Eximee platform REST infrastructure

The component presenting forms requires access to the REST API served by the Eximee platform instance. All REST API endpoints are already hosted and exposed for standalone webforms instances (e.g. for web applications, webview or iframe embedding).

The recommended solution (most often chosen by customers) is to configure a proxy pass in the host application's domain that forwards traffic to the Eximee infrastructure. This solution offers the simplest setup in terms of infrastructure and security headers, as well as access control to the infrastructure, due to handling the REST API within the same domain in which the page / host application is presented.

Alternatively, it is possible to configure direct connections between the host application's domain and the Eximee REST server. However, this requires additional security/header configuration and testing verification of individual environments.

It is also possible to develop another communication mechanism, in particular one in which the host application intermediates every REST call. However, this requires analysis and developing a solution specific to the given deployment and entails planning additional development work in the platform.

## Form application cookie

The application uses a cookie describing:

* the user session,
* session affinity parameters for load balancers.

Cookies are created automatically by the server and infrastructure (load balancers) and are configured according to the parameters of the specific environment.

## Known functional limitations

* The library assumes that only one form is presented on a single screen at a time, and attempting to display two concurrently running form instances may cause errors.
* Changing the parameters of an initialized form requires reinitialization and means preparing a new instance (without any data previously entered by the user).

## Component interface

```typescript
export interface FormWebcomponentApi {
    loadForm(config: LoadFormConfig): void;
    hasActiveForm(): boolean;
 
    cancelCurrentForm(): void;
    handleAction(action: ExAction): void;
    proceedCurrentForm(): void;
    backCurrentForm(): void;
    isLastVisiblePage(): boolean;
    isFirstVisiblePage(): boolean;
    shouldShowForwardButton(): boolean;
    shouldShowBackwardButton(): boolean;
    getForwardButtonLabel(): string;
    onShowSpinner(callback: () => void): void;
    onHideSpinner(callback: () => void): void;
    getTranslation(key: string): string;
}
 
export interface LoadFormConfig extends LoadConfig {
    formId: string;
}
 
export interface LoadConfig {
    formInstanceNumber?: string;
    processId?: string;
    baseHref?: string;
    accessToken?: string;
    tokenType?: string;
    readonly?: boolean;
    data?: string;
    shadowRoot?: ShadowRoot;
    scrollOnErrorOffset?: number;
    additionalRequestHeaders?: () => { [header: string]: string };
    onLoaded?: (result: unknown, config: LoadConfig) => void;
    onActionDispatched?: (result: unknown, config: LoadConfig) => void;
    onCancelled?: (config: LoadConfig) => void;
    onSaved?: (result, config: LoadConfig) => void;
    onDraftSaved?: (result, config: LoadConfig) => void;
    onPageChanged?: (result, config: LoadConfig) => void;
    onModelChanged?: (result, config: LoadConfig) => void;
    onPageValidationErrors?: (result, config: LoadConfig) => void;
    onError?: (error, config: LoadConfig) => void;
    onAppEvent?: (event, config: LoadConfig) => void;
    onComponentValueChanged?: (result: unknown, config: LoadConfig) => void;
    onShowSpinner?: (immediate: boolean ) => void;
    onHideSpinner?: () => void;
    onClosed?: (config: LoadConfig) => void;
}
 
export interface ExAction {
    sourceId: string;
    event: ExActionEvent | string;
    detail?: object | string | number | boolean;
}
 
export enum ExActionEvent {
    SAVE = 'SAVE',
    CLOSE = 'CLOSE',
    NEXT = 'NEXT',
    EDIT = 'EDIT',
    CLICK_MORE_INFO = 'CLICK_MORE_INFO',
    CALL = 'CALL',
    CHECK = 'CHECK',
    UNCHECK = 'UNCHECK',
    CLICK = 'CLICK',
    ON_EXIT = 'ON_EXIT',
    TOOLTIP_CLICKED = 'TOOLTIP_CLICKED',
    AUTOCOMPLETE_NO_MATCH_BUTTON_CLICKED = 'AUTOCOMPLETE_NO_MATCH_BUTTON_CLICKED',
    EXPAND_STATEMENT = 'EXPAND_STATEMENT',
    ON_PAGE_ENTER = 'ON_PAGE_ENTER',
    POI_SELECTED = 'POI_SELECTED',
    HIDDEN = 'HIDDEN',
    CLOSE_POPUP = 'CLOSE_POPUP',
    RETRY = 'RETRY',
    SAVE_DRAFT = 'SAVE_DRAFT',
    VALUE_CHANGED = 'VALUE_CHANGED',
    FORWARD_PAGE = 'FORWARD_PAGE',
    BACKWARD_PAGE = 'BACKWARD_PAGE',
    PARK_FORM_WITH_PROVIDED_HASH = 'PARK_FORM_WITH_PROVIDED_HASH',
    SHOW_POPUP = 'SHOW_POPUP',
    SAVE_POPUP = 'SAVE_POPUP',
    TOGGLE = 'TOGGLE',
    CLEAR_UPLOAD_FILE = 'CLEAR_UPLOAD_FILE',
    REDIRECT_TO_RETURN_URL = 'REDIRECT_TO_RETURN_URL',
    REDIRECT = 'REDIRECT',
    CHECK_FED_STATEMENT = 'CHECK_FED_STATEMENT',
    POPUP_SAVED = 'POPUP_SAVED',
    POPUP_HIDDEN = 'POPUP_HIDDEN',
    START_PROCESS = 'START_PROCESS',
    COMPLETE_USER_TASK = 'COMPLETE_USER_TASK',
    TILE_CLICKED = 'TILE_CLICKED',
    START_APPLICATION = 'START_APPLICATION',
    EXIT_CONFIRMED = 'EXIT_CONFIRMED'
}
```

## Usage examples

The platform deployed in test environments hosts a sample HTML embedding a form using the web component:

* directly in the page DOM,
* wrapped in Shadow Root.

Knowing the Eximee platform address, both examples can be viewed at https\://\[environment-address]/webcomponent/\[deployment-skin]-webcomponent.html

HTML example:

```html
<html>
<head>
    <script>
        /*  Method for launching the application directly in the DOM tree. */
        function initFormPlain() {
            // Preparing the component in the DOM tree
            const formWrapper = document.getElementById('form-wrapper');
            const formWebcomponent = document.createElement("ex-forms-form");
            formWrapper.appendChild(formWebcomponent);
 
            // Launching the form instance
            formWebcomponent.loadForm({
                formId: 'demoFormularzJakoWebcomponent',
                baseHref: '/api',
                onError: function () {
                    alert('An error occurred while processing the application');
                }
            });
        }
    </script>
</head>
<body>
    <div id="form-wrapper"></div>
    <script src="polyfills.5310c9e539f37fb1.js" type="module"></script>
    <script src="main.71e5244a2a4d4f3d.js" type="module"></script>
</body>
</html>
```
