@quandis/qbo4.ui v4.0.1-CI-20240507-231949
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' });
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
; useful for aGET
operation.POST
operations will post thejson
object as the body of the request.- 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.
// 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
.
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
name
attribute 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
myNumber
field is slotted into theShadowDOM
from the normal markup, and is visible to the form. Thus,myNumber
is 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-exists
tag has a value ofaddresses
, which corresponds to theqbo-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:
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'}"/>
4 days ago
4 days ago
4 days ago
10 days ago
11 days ago
12 days ago
12 days ago
12 days ago
16 days ago
16 days ago
16 days ago
15 days ago
20 days ago
20 days ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago