npm package: @quandis/qbo4.ui
Description: The `qbo4.ui` package comprises the following features.
Author: Quandis, Inc.
Latest version: 4.0.1-CI-20260409-145529

﻿# Overview

The `qbo4.ui` package comprises the following features.

# Api Endpoint Registration

If your web page needs to interface with multiple APIs, you can register them with the `qbo-api` component:

```html
<qbo-api name="myService" method="POST" apiEndpoint="https://api.mysite.com/some/endpoint">
    <header name="Accept">application/json</header>
</qbo-api>

<qbo-api name="addresses" method="GET" apiEndpoint="https://nominatim.openstreetmap.org/search?q=${value}&format=json">
    <header name="Accept">application/xml</header>
</qbo-api>
```

Then, in typescript:

```typescript
import { getApiService } from '@quandis/qbo4.ui';

const myApi = getApiService('myService');
const myResult = await myApi.fetch('/more/route/data', { Foo: 'Bar' });

// Alternatively, you can access and augment api endpoints with api://{name}/{path}, like so:
const myApiEndpoint = getApiService('api://myService/more/route/data'); 
// POST to https://api.mysite.com/some/endpoint/more/route/data
myApiEndpoint.fetch('', { Foo: 'Bar' }); 

const geoApi = getApiService('addresses');
const address = getApiService('geoApi').fetch('', { value: '1600 Pennsylvania Ave NW, Washington, DC' });
```


> A few items to point out:
> - The `IApiService.fetch(relativePath, json)` can substitute against the `apiEndpoint`. This is useful for a `GET` operation. 
> - A `method="POST"` will post the `json` object as the body of the request.
> - If `method` is not defined, a `GET` will be used if `json` is null. Otherwise, a `POST` will be used.
> - The `json` object can be a string, or an object. If it is an object, it will be stringified.
> - Additional headers can be specified in the `qbo-api` component.

## Default API Endpoint

The `qbo4-ui` package will automatically register a default API endpoint using the window.location.
So, if your endpoints are on the same page as your web page, you can use the default API endpoint.

```typescript
// This will point to the same website you are on.
const defaultApi = getApiService('default');
```

## Reusing the same API with different paths

You may register an API, and then reuse it with different paths:

```html
<qbo-api name="qms" method="POST" apiEndpoint="https://services.quandis.io/api/military/">
    <header name="Accept">application/json</header>
</qbo-api>
```

Then, in typescript:

```typescript
import { getApiService } from '@quandis/qbo4.ui';
const qmsSearch = getApiService('api://qms/scra/instant/{clientID}');
const qmsHealth = getApiService('api://qms/scra/healthcheck');
```

In this example, the `qms` is cloned in `getApiService`, and the `relativePath` is substituted into the `apiEndpoint`:

|API Syntax|Url Returned|
|-|-|
|`api://qms/scra/instant/{clientID}`|`https://services.quandis.io/api/military/scra/instant/{clientID}`|
|`api://qms/scra/healthcheck`       |`https://services.quandis.io/api/military/scra/healthcheck`|


## Custom API Services

You can write your own `IApiService` class, and register it to qbo's DI container:

```typescript
import { services, IApiService } from "@quandis/qbo4.ui";

@injectable()
export class MyApi implements IApiService {

    async fetch(relativePath: string | null, payload: Record<string, string> | null = null): Promise<any> {
        // implement your own fetch logic here
    }
}

// Register your service to qbo's DI container
services.container.registerInstance<IApiService>('myApi', new MyApi());
```

# QboFormElement

The `QboFormElement` web component is used to wrap form elements, 
and present them to a <code>form</code> element upon submission.

For example, a credit card components might look like this:

```typescript
export class QboPayment extends QboFormElement {
    render() {
        return html`<slot>
    <input type="text" name="Number" placeholder="Card Number" required />
    <input type="text" name="Expiry" placeholder="MM/YY" required />
    <input type="text" name="CVC" placeholder="CVC" required />
    <input type="text" name="Name" placeholder="Name on Card" required />
</slot>`;
    }
}
```

> This is a simple example without any credit card validation logic.

This can be used within a form multiple times:

```html
<form id='fact'>
    <qbo-payment name="primary"></qbo-payment>
    <qbo-payment name="backup"></qbo-payment>
</form>
```

Upon submission, the form will contain the following fields:

```json
{
	"primaryNumber": "...",
	"primaryExpiry": "...",
	"primaryCVC": "...",
	"primaryName": "..."
	"backupNumber": "...",
	"backupExpiry": "...",
	"backupCVC": "...",
	"backupName": "..."
}
```

> The `name` attribute is used to prefix the field names for any fields in the `ShadowDOM`.

If you render elements as children of a `QboFormElement`, they will be directly visible to the form:

```html
<form id='fact'>
    <qbo-payment name="primary">
      Number: <input type="text" name="myNumber" placeholder="Card Number" required value="1234567890">
    </qbo-payment>
    <qbo-payment name="backup"></qbo-payment>
</form>
```

Upon submission, the form will contain the following fields:

```json
{
	"myNumber": "...",
	"backupNumber": "...",
	"backupExpiry": "...",
	"backupCVC": "...",
	"backupName": "..."
}
```

> The `myNumber` field is slotted into the `ShadowDOM` from the normal markup, and is visible to the form.
Thus, `myNumber` is not prefixed with `primary`.

# Form Validation

Modern browers support a very rich set of [form valiation functionality](https://www.w3.org/TR/2009/WD-html5-20090825/forms.html#attr-input-pattern); use it!
Custom functionality can be introduced as follows:

```html
<form>
    <qbo-validate></qbo-validate>
    <div>
        <label for="city">City</label>
        <input type="text" name="address" required data-exists="addresses">
    </div>
    <button type="submit">Submit form</button>
</form>
```

> Note that the `data-exists` tag has a value of `addresses`, which corresponds to the `qbo-api` name attribute.

This markup, combined with our custom `ExistsValidator` class, will validate that the value entered in the `input` field exists in the `addresses` API.

Here is our `ExistsValidator` class:

```typescript
import { injectable, InjectionToken } from 'tsyringe';

@injectable()
export class ExistsValidator implements IValidate {
    async validate(input: HTMLElement): Promise<boolean> {
        var url = input.getAttribute('data-exists');
        if (!url) return false;
        var path = input.getAttribute('data-exists-path');
        const service: IApiService = container.isRegistered(url) ? container.resolve<IApiService>(url) : new RestApiService(url);

        if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement || input instanceof HTMLTextAreaElement) {
            const response = await service.fetch(path, { value: input.value });
            const json = getArray(response);
            if (json == null)
                return false;
            return json.length > 0;
        }
        return false;
    };
    message = 'This value does not appear to exist.';
    selector = '[data-exists]';
}
container.register(ValidateToken, { useClass: ExistsValidator });
```

The `IValidate` interface is:

```typescript
export interface IValidate {
    // Called by `QboValidate` when fields change or a form is submitted.
    validate(input: HTMLElement): Promise<boolean>; 
    // Called by `QboValidate` when the component is connected to the DOM.
    connect(form: HTMLFormElement): void { };
    // Called by `QboValidate` when the component is disconnected from the DOM.
    disconnect(): void { };
    // Message to display when validation fails.
    message: string;
    // Selector to use to find elements to apply the IValidate implementation to.
    selector: string;
}
```

Paired with the `qbo-validate` component, you can decorate your HTML markup with simple attributes, 
and automatically trigger form validate against any `IValidate` class registered with the DI container.

It's important that your selector be unique amont all registered `IValidate` classes.
We recommend the selector be `[data-{IValidate class name}]` for consistency.

## Dependencies

It's common for form controls (and other UI components) to depend on the values of each other.

For example:

```html
<input type="text" name="This" placeholder="Enter something here" />
<input type="text" name="That" placeholder="Or something here" />
<input type="text" name="Other" placeholder="To enable this" data-depend-options='{"depends": "This,That"}'/>
```

In this example, the `Other` field will only be enabled if either the `This` or `That` field have a value.

### DependValidator Options 

|Option|Default|Description|
|-|-|-|
|`depends`|`undefined`|A comma-delimited list of field names (or ids) that the element is dependent upon.|
|`condition`|`or`|If `or`, just 1 dependency must be met. If `and`, every dependency must be met.|
|`emptyOnDisable`|`false`|If `true`, dependent values will be set to empty if dependencies are not met.|
|`resetOnDisable`|`true`|If `true`, dependent values will be reset to their original value if dependencies are not met.|
|`disabledClass`|`disabled`|Css class to apply to an element if dependencies are not met.|
|`disableChildren`|`true`|If `true`, all child elements will be disabled if dependencies is not met.|

### Depends examples

|Example|Description|
|-|-|
|`{"depends": "This,That"}`|Either `This` or `That` must have a non-empty value.|
|`{"depends": "This,That", "condition": "and"}`|Both `This` and `That` must have a non-empty value.|
|`{"depends": "This=Foo"}`|`This` must have a value `Foo`.|
|`{"depends": "This!=Foo"}`|`This` must **not** have a value `Foo`.|
|`{"depends": "This="}`|`This` must have an empty value.|
|`{"depends": "!This"}`|`This` must have an empty value.|
|`{"depends": "This=Foo*"}`|`This` must have a value that **starts with** `Foo`.|
|`{"depends": "This=*Bar"}`|`This` must have a value that **ends with** `Bar`.|
|`{"depends": "This=*oo*"}`|`This` must have a value that **contains** `oo`.|

The `depends` attribute may reference elements by `id` or by `name`. 
In the case of a conflict, the `id` will be used.
The following expression is used find the target element:

```typescript
const target = this.form.querySelector(`#${CSS.escape(selector)}`)
    ?? this.form.querySelector(`[name="${CSS.escape(selector)}"]`);
```

HTML markup and browser standards require that attributes containing JSON be double-quoted:

```html
<input type="text" data-depend-options='{"depends": "This"}'/>
```

is valid, but the following is not:

```html
<input type="text" data-depend-options="{'depends': 'This'}"/>
```