@quandis/qbo4.configuration
v4.0.1-CI-20241207-191913
Published
The `@quandis/qbo4.Configuration.Web` package offers:
Downloads
629
Keywords
Readme
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 theContactComponent
class. Ensure you reference your component's properties viacomponent
rather thanthis
.
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 ] [ Cancel ]
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 adatalist
, 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 atype
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 calledstreetview
, and the address control is part of a property control, save the property control such that it uses<qbo-address type="streetview">
- if a user creates a new