4.0.1-CI-20241031-000311 • Published 9 months ago

@quandis/qbo4.configuration v4.0.1-CI-20241031-000311

Weekly downloads
-
License
-
Repository
-
Last release
9 months ago

Overview

The @quandis/qbo4.Configuration.Web package offers:

  • Management of options classes via dependency injection
  • A QboTemplate web component base class that supports configuration driven rendering options

Options Classes

Similar to Microsoft's IConfiguration, the qbo4.Configuration package's classes manage configuration settings for a web application via dependency injection.

Including the qbo4.Configuration.js script will provide:

qbo4.services: a container for dependency injection (including qbo4.services.container for direct access to the tsyringe container) qbo4.configuration: a set of classes to manage configuration settings IConfiguration: an interface to manage configuration settings

It provides for dependency injections via the tsyringe package.

Typescript Usage

import 'reflect-metadata';
import { Configuration, IConfiguration, IConfigurationSource, IConfigurationSourceToken, IConfigurationToken, JsonConfigurationSource, services } from '@quandis/qbo4.configuration';


class OptionsA { public name: string = ''; }
class OptionsB { public count: number = 0; }

const source1 = new JsonConfigurationSource({ A: { name: 'Alice' }, B: { count: 27 } });
const source2 = new JsonConfigurationSource({ A: { name: 'Bob' }, C: { enabled: true } });
services.container.register<IConfigurationSource>(IConfigurationSourceToken, { useValue: source1 });
services.container.register<IConfigurationSource>(IConfigurationSourceToken, { useValue: source2 });

const config: IConfiguration = services.container.resolve<IConfiguration>(IConfigurationToken);
expect(config).not.null;
const a = config.getSection('A').bind(OptionsA);
expect(a.name).equal('Bob');

const b = config.getSection('B').bind(OptionsB);
expect(b.count).equal(27);

Browser Usage

<html>
  <head>
	<script src="//configuration/js/qbo4.Configuration.js"></script>
	<script type="text/javascript">
        const aiConfig = new qbo4.logging.JsonConfigurationSource({ 'ApplicationInsights': { instrumentationKey: '651cc99f-0b30-4f27-8918-e53dfed1a2c2' } });
        qbo4.services.container.register(qbo4.logging.IConfigurationSourceToken, { useValue: aiConfig });
		// now the configuration settings are available for dependency injection

		// later in your code, web components, or scripts:
		const instances = qbo4.services.container.resolveAll<SomeInterface>(SomeInterfaceToken);

    </script>
  </head>
  <body>
  </body>
</html>

Example: ApplicationInsights Logger

Assume we want to inject an ApplicationInsights logger, where the InstrumentationKey is stored in the configuration settings.

// An options class
export class ApplicationInsightsOptions {
	public InstrumentationKey: string = '';
}

// Prepare the options for dependency injection (a tsyring pattern)
export const ApplicationInsightsOptionsToken: InjectionToken<ApplicationInsightsOptions> = 'ApplicationInsightsOptions';

// The logger class
@injectable()
export class ApplicationInsights {
	constructor(
		@inject(ApplicationInsightsOptionsToken) private options: ApplicationInsightsOptions
	) {
		// Initialize the logger
		// ...
	}
}

// Prepare the class for dependency injection
export const ApplicationInsightsToken: InjectionToken<ApplicationInsights> = 'ApplicationInsights';

QboTemplate Web Component

The QboTemplate web component enables power users to configure the rendering of a web component a runtime. Assume we have a Contact web component that renders a contact's name, address, and other information, and we choose to support different layouts based on a type attribute:

<qbo-contact type="fullname"></qbo-contact>

should render:

<input name="first" placeholder="First name"/>
<input name="middle" placeholder="Middle name"/>
<input name="last" placeholder="Last name"/>

Such a web component might look like this:

import { html } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('qbo-contact')
export class ContactComponent extends LitElement {
    jsonData: object = {
        'Contact': {
            'First': 'James',
            'Middle': 'Tiberious',
			'Last': 'Kirk'
        }
    }

	render() {
		return html`
			<input name="first" placeholder="First name" value="${this.jsonData['Contact']['First']}"/>
			<input name="middle" placeholder="Middle name" value="${this.jsonData['Contact']['Middle']}"/>
			<input name="last" placeholder="Last name" value="${this.jsonData['Contact']['Last']}"/>
		`;
	}
}

Futher assume that you want to allow a power user to configure the layout of the qbo-contact web component at runtime. To accomplish this, shift the ContactComponent to derive from a QboTemplate class:

import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { templates, TemplateFunction, QboTemplate } from '@quandis/qbo4.configuration';

export class ContactComponent extends QboTemplate {
    constructor() {
        super();  
        this.type ??= 'default'; 
    }

    jsonData: object = {
        'Contact': {
            'First': 'James',
            'Middle': 'Tiberious',
			'Last': 'Kirk'
        }
    }

	// this will map to the 'default' type, specified by the decorator parameter.
	@template('default')
	renderDefault(component: any) {
		return html`<input name="first" placeholder="First name" value="${component.jsonData['Contact']['First']}"/>
			<input name="middle" placeholder="Middle name" value="${component.jsonData['Contact']['Middle']}"/>
			<input name="last" placeholder="Last name" value="${component.jsonData['Contact']['Last']}"/>`;
	}

	// this will map to the 'reverse' type, becaue no parameter is passed to the decorator
	@template()
	reverse(component: any) {
		return html`<input name="last" placeholder="Last name" value="${component.jsonData['Contact']['First']}"/>,
			<input name="first" placeholder="Last name" value="${component.jsonData['Contact']['Last']}"/>
			<input name="middle" placeholder="Middle name" value="${component.jsonData['Contact']['Middle']}"/>`;
	}
	// Notice: no render method here - see below.
}

Now you can render the qbo-contact web component with a type attribute:

<!-- This will render last name, first name, middle name -->
<qbo-contact type="reverse"></qbo-contact>

<!-- This will render first name, middle name, last name -->
<qbo-contact type="default"></qbo-contact>

<!-- So will this, because we set type = 'default' in the constructor -->
<qbo-contact></qbo-contact>

Note that the template functions accept a component parameter, which is the instance of the ContactComponent class. Ensure you reference your component's properties via component rather than this.

Mapping type to different rendering functions is all well and good, but not particularly interesting.

The interesting part is allowing a power user to create new rendering functions (or edit existing ones) at runtime.

The QboTemplate class will listen for a ctrl-dblclick event if it is contained in an element with a qbo-design class (indicating that we are in 'design' mode).

When the ctrl-dblclick event is triggered, a dialog will open, presenting the user with something like:


Edit Template

default <- a datalist of available templates

1 <input name="first" placeholder="First name" value="${component.jsonData.Contact.First]}"/>
2 <input name="middle" placeholder="Middle name" value="${component.jsonData.Contact.Middle]}"/>
3 <input name="last" placeholder="Last name" value="${component.jsonData.Contact.Last}"/>

^ an editor for editing the rendering function

Ask AI to code...

Save


Feature include:

  • As the power user modifies the template, the underlying UI will update in real-time.
    • Any syntax errors will be trapped and displayed in the editor
  • The editor can be dragged and resized as needed
  • The type at the top can be selected from a datalist, and new types can be entered.
  • The Ask AI to code... input will apply Generative AI to modify the code as you describe.
  • Clicking Save will save the template in configuration (server-side), and be available for use.
  • Clicking Cancel will discard any changes, reverting to the original rendering.

How it Works

Deployment of @qbo4.Configuration web components is paired with deployment of the qbo4.Configuration.Web Razor Class Library, which includes server-side functionality to store are retrieve template code. The QboTemplate class will interact with the server-side /template endpoint to store and retrieve templates.

The QboTemplate class will render the web component based on the type attribute, using the templates map. If the type is not found in the templates map, the component will fetch templates stored in configuration from the /template/search/{ComponentClassName} endpoint.

If a component's Typescript class defines a type that also exists in configuration, the configuration will take precedence.

Spiderman Clause

With great power comes great responsibility. Only trusted users should be allowed to edit templates. The ultimate control of this remains server-side with the authorization policies of the /template endpoints. For the front-end, the QboTemplate class will only allow editing of templates if the containing element has a qbo-design class. We recommend that the qbo-design class be added to the body for trusted power users only. This will prevent accidental editing attempts of templates by regular users.

As with all qbo-based configuration, enable and test in a lower environment before deploying to production.

RoadMap

  • add intellisense to CodeMirror for component-specific properties (including Json data)
  • shift the Editor to a separate web component
  • enable custom Editors, to simplify changes to complex controls
  • create a GUI designer that detects available web components via DI
  • propagate changes to a components type property to parent components
    • if a user creates a new type of an address control called streetview, and the address control is part of a property control, save the property control such that it uses <qbo-address type="streetview">