npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@quandis/qbo4.ui

v4.0.1-CI-20240515-222031

Published

The `qbo4.ui` package comprises the following features:

Downloads

566

Readme

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 the apiEndpoint; useful for a GET operation.
  • POST operations will post the json 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 the ShadowDOM.

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 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; 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 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:

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'}"/>