@quandis/qbo4.ui v4.0.1-CI-20241017-004102
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:
<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:
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 theapiEndpoint. This is useful for aGEToperation.- A
method="POST"will post thejsonobject as the body of the request.- If
methodis not defined, aGETwill be used ifjsonis null. Otherwise, aPOSTwill be used.- The
jsonobject can be a string, or an object. If it is an object, it will be stringified.- Additional headers can be specified in the
qbo-apicomponent.
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.
// 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:
<qbo-api name="qms" method="POST" apiEndpoint="https://services.quandis.io/api/military/">
<header name="Accept">application/json</header>
</qbo-api>Then, in 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:
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 form element upon submission.
For example, a credit card components might look like this:
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:
<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:
{
"primaryNumber": "...",
"primaryExpiry": "...",
"primaryCVC": "...",
"primaryName": "..."
"backupNumber": "...",
"backupExpiry": "...",
"backupCVC": "...",
"backupName": "..."
}The
nameattribute is used to prefix the field names for any fields in theShadowDOM.
If you render elements as children of a QboFormElement, they will be directly visible to the form:
<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:
{
"myNumber": "...",
"backupNumber": "...",
"backupExpiry": "...",
"backupCVC": "...",
"backupName": "..."
}The
myNumberfield is slotted into theShadowDOMfrom the normal markup, and is visible to the form. Thus,myNumberis not prefixed withprimary.
Form Validation
Modern browers support a very rich set of form valiation functionality; use it! Custom functionality can be introduced as follows:
<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-existstag has a value ofaddresses, which corresponds to theqbo-apiname 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:
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:
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:
<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:
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:
<input type="text" data-depend-options='{"depends": "This"}'/>is valid, but the following is not:
<input type="text" data-depend-options="{'depends': 'This'}"/>12 months ago
1 year ago
1 year ago
12 months ago
12 months ago
12 months ago
12 months ago
7 months ago
1 year ago
1 year ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
11 months ago
1 year ago
11 months ago
7 months ago
1 year ago
12 months ago
1 year ago
11 months ago
1 year ago
1 year ago
12 months ago
1 year ago
1 year ago
1 year ago
12 months ago
1 year ago
1 year ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
12 months ago
12 months ago
1 year ago
1 year ago
12 months ago
1 year ago
1 year ago
7 months ago
12 months ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago