@wincc-oa/wui-forms
v1.2.2
Published
WinCC Open Architecture Dashboard project.
Downloads
113
Maintainers
Readme
WinCCOA WebComponent Dashboard
This package is part of the workspace for the WinCC Open Architecture WebComponent Dashboard, built using Lit and managed with Nx.
Usage information and reference details can be found in the WinCC OA documentation.
Content
This library consists of core elements:
wui-json-form component
This is the component responsible for generating a form based on the provided information. The simpliest way to use it could be like that:
<script>
const data = {
name: 'Joe',
lastname: 'Doe',
age: 10
};
</script>
<wui-json-form .initData="${data}"></wui-json-form>This will generate a form consisting of 3 inputs and a submit button.
wui-json-form accepts following properties:
initData- initial data to populate the form. In case there is no schema provided the form will be generated based on this object,schema- JSON Schema containing all the entries of the form,uiSchema- JSON UI Schema containing the details about how to generate the form (order of elements, labels, types of elements),renderers- list of available functions to render form inputs or sections.
renderers
Functions rendering the JSON Schema registry entry.
Example:
export const inputRenderer = ({ path, element }: RendererConfig) => {
<ix-field-label for=${path}>${element.label}</ix-field-label>
<input type=${element.options.format} id=${path} name=${path}>
};testers
Functions used to determine which renderer should be used for which schema entry.
Example:
export const uiScopeIncludes = (text: string) => (uischema: any) => uischema.scope?.includes(text);wui-input-error component
Custom element that displays an error message for the closest input element. It also adds error class to the parent element.
<div>
<label for="firstname" class="typography-label">First name</label>
<input id="firstname" name="firstname" minlength="5" />
<wui-input-error></wui-input-error>
</div>Custom validation
The wui-json-form component accepts the additionalErrors object.
It can contain custom errors for each form property.
Example:
interface Address {
street: string;
city: string;
}
const formSchema = {
type: 'object',
properties: {
name: { type: 'string' },
address: { type: 'object', properties: { street: { type: 'string'}, city: { type: 'string' }}
},
};
const additionalErrors: AdditionalErrors = {
name: {
validator: (name: string) => name.length < 3 ? 'name is too short' : ''
}
address: {
validator: (address: Address): Record<keyof Address, string> => ({
street: address.street === 'street' ? 'incorrect street name' : ''
city: address.city === 'Cracow' ? '' : 'Error'
})
}
}
You can also rely on external libraries like ajv.
Example:
import Ajv from 'ajv';
const formSchema = {
type: 'object',
properties: {
range: {
type: 'object',
properties: {
min: { type: 'number', minimum: 20 },
max: { type: 'number', maximum: 50 }
},
required: ['min', 'max']
}
},
required: ['range']
};
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(customValidationFormSchema);
const additionalErrors: AdditionalErrors = {
range: {
validator: (data: RangeValue): Record<keyof RangeValue, string> => {
validate({ range: data });
const errors = validate.errors;
return {
min: errors?.find(({ instancePath, params }) => instancePath.includes('range/min') || (instancePath.includes('range') && params?.['missingProperty'] === 'min'))?.message || '',
max: errors?.find(({ instancePath, params }) => instancePath.includes('range/max') || (instancePath.includes('range') && params?.['missingProperty'] === 'max'))?.message || ''
};
}
}
};For new renderers custom validation has to be implemented if needed.
To use additionalErrors within your component you have to consume the JsonFormContext.
Example:
@customElement('wui-custom-renderer')
export class WuiCustomRenderer extends LitElement {
@consume({ context: jsonFormContext })
context!: JsonFormContext;
@property({ type: String, reflect: true }) name = '';
render() {
const customValidators = this.context?.additionalErrors?.[this.name];
if (customValidators.validator) {
const errors = customValidators.validator(value);
}
...
return html`...`
}
}Setting up form rules in a schema
You can find extended documentation here: https://jsonforms.io/docs/uischema/rules/
Rules allow for dynamic aspects for a form, e.g. by hiding or disabling UI schema elements.
A rule may be attached to any UI schema element and can be defined with the ==rule== property which looks like:
"rule": {
"effect": "HIDE" | "SHOW" | "ENABLE" | "DISABLE",
"condition": {
"scope": "<UI Schema scope>",
"schema": JSON Schema
}
}A rule basically works by first evaluating the ==condition== property and in case it evaluates to true, executing the associated ==effect==.
Rule Condition
The rule ==condition== object contains a ==scope== and a ==schema== property. The ==schema== property is a standard JSON schema object. This means, everything that can be specified using JSON schema can be used in the rule condition. The ==schema== is validated against the data specified in the ==scope== property. If the ==scope== data matches the ==schema== the rule evaluates to true and the rule effect is applied.
Note, ==SchemaBasedConditions== have been introduced with version 2.0.6 and have become the new default. The previous format via ==type== and ==expectedValue== properties is still supported for the time being.
Examples
Below are some common rule examples.
To match a scope variable to a specific value, "const" can be used:
"rule": {
"effect": "HIDE",
"condition": {
"scope": "#/properties/counter",
"schema": { const: 10 }
}
}Here, the control is hidden when the counter property is equal to 10.
Similar, to match multiple values, enum can be used:
"rule": {
"effect": "HIDE",
"condition": {
"scope": "#/properties/name",
"schema": { enum: ["foo", "bar"] }
}
}The rule evaluates to true if the scope property name is either "foo" or "bar".
A rule can be negated using "not":
"rule": {
"effect": "SHOW",
"condition": {
"scope": "#/properties/counter",
"schema": { not: { const: 10 } }
}
}The following rule evaluates to true if the counter property is 1 <= counter < 10:
"rule": {
"effect": "SHOW",
"condition": {
"scope": "#/properties/counter",
"schema": { minimum: 1, exclusiveMaximum: 10 }
}
}A rule can even operate on the full form data object and over multiple properties:
"rule": {
"effect": "SHOW",
"condition": {
"scope": "#",
"schema": {
"properties": {
"stringArray": { "contains": { "const": "Foo" } }
},
"required": ["stringArray", "otherProperty"]
}
}
}In this example, the condition is true if the properties "stringArray" and "otherProperty" are set in the form data and the "stringArray" property contains an element "Foo". Note, that the schema rule in this example looks more like a normal JSON schema as it is commonly used.
Rules for input state
The library has a service responsible for handling the input state ("SHOW"/"HIDE" and "DISABLE"/"ENABLE").
To use it you need to follow the steps:
- Import and resolve the
FormRulesservice. - Initialize context listener.
- Listen for status updates provided by the service and adjust your component.
Remember to dispose the context if it is not used anymore.
Example:
import { FormRules } from '@wincc-oa/wui-forms/services/form-rules/form-rules.service.js';
import { FormComponent } from '@wincc-oa/wui-forms/components/form-component.js';
import { type RendererConfig } from '@wincc-oa/wui-forms/interfaces/renderer-config.js';
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('custom-input-renderer')
export CustomInputRenderer extends FormComponent {
...
@property({ type: Object }) config?: RendererConfig;
@property({ type: Boolean, reflect: true }) disabled?: boolean;
private formRules = container.resolve(FormRules);
private subscriptions = new Subscription();
disconnectedCallback() {
this.subscriptions.unsubscribe();
this.formRules.disposeContext();
}
connectedCallback() {
const { element } = this.config;
if (element?.rule) {
this.formRules.initContext(this, element);
this.subscriptions.add(
this.formRules.status$.subscribe(([isDisabled, isVisible]) => {
this.disabled = isDisabled || !isVisible;
this.style.display = !isVisible ? 'none' : '';
})
);
}
}
render() {
return html`
<input ?disabled=${this.disabled}>
`;
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.subscriptions.unsubscribe();
}
}
Rules for an array element
When working with array elements, you need to provide context for the rules service to validate rules per array item. The context property enables the rules engine to evaluate conditions against the correct data scope for each array element.
Context Types
You can use either:
- Built-in context:
"series"for series array elements - Custom lit-context object for your own array implementations
Example: Series Array with Rules
UI Schema:
{
"type": "SeriesGroup",
"scope": "#/properties/dataSeries",
"elements": [
{
"type": "CheckboxControl",
"label": "Enable custom configuration",
"scope": "#/properties/enableCustomConfig"
},
{
"type": "Control",
"label": "Custom property",
"scope": "#/properties/customProperty",
"options": {
"context": "series" // Links to wuiSeriesItemContext
},
"rule": {
"effect": "ENABLE",
"condition": {
"scope": "#/properties/enableCustomConfig", // Relative to context root
"schema": {
"const": true
}
}
}
}
]
}Series Item Implementation:
import { provide } from '@lit/context';
import { TemplateResult, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { wuiSeriesItemContext } from '../../contexts/wui-series-context';
import { FormComponent } from '../form-component';
@customElement('wui-series-item')
export class WuiSeriesItem extends FormComponent {
@provide({ context: wuiSeriesItemContext })
private seriesItemContext?: string | Record<string, unknown>;
protected override render(): TemplateResult {
return html`<slot @input=${this.handleInput}></slot>`;
}
private async handleInput(): Promise<void> {
await this.updateComplete;
const inputs = this.querySelectorAll<HTMLInputElement | HTMLSelectElement>('[name]');
// Update context on every change - this triggers rule re-evaluation
this.seriesItemContext = [...inputs].reduce((total: { [key: string]: unknown }, { name, value, disabled }) => {
if (!disabled) {
total[name] = value;
}
return total;
}, {});
}
}Custom Context Implementation
For custom array elements, create your own context:
import { createContext } from '@lit/context';
export interface CustomArrayItemContext {
[key: string]: unknown;
}
export const customArrayItemContext = createContext<CustomArrayItemContext>('custom-array-item');
@customElement('wui-custom-array-item')
export class WuiCustomArrayItem extends FormComponent {
@provide({ context: customArrayItemContext })
private itemContext?: CustomArrayItemContext;
// Update context when form data changes
private updateContext(formData: Record<string, unknown>): void {
this.itemContext = formData;
}
}Key Points:
- Context must be updated whenever form data changes
- Rule scopes are relative to the context root object
- Each array item maintains its own context for independent rule evaluation
- The
options.contextproperty links the form element to the appropriate context provider
Layout renderers
There are 4 renderers to handle groups of multiple inputs:
WuiLayout
Adds styling to the group of elements. However, the form value is not grouped.
Example:
// UI Schema
{
"type": "WuiLayout",
"label": "Full name",
"elements": [
{
"type": "Control",
"label": "First name",
"scope": "#/properties/firstName",
},
{
"type": "Control",
"label": "Last name",
"scope": "#/properties/lastName",
}
]
},
// Form result
{
firstName: 'John',
lastName: 'Doe'
}HorizontalGroup
Adds styling to the group of elements and adds name property to the group, so in the form the results will be grouped.
Example:
// UI Schema
{
"type": "HorizontalGroup",
"label": "Full name",
"scope": "#/properties/fullName",
"elements": [
{
"type": "Control",
"label": "First name",
"scope": "#/properties/firstName",
},
{
"type": "Control",
"label": "Last name",
"scope": "#/properties/lastName",
}
]
},
// Form result
{
fullName: {
firstName: 'John',
lastName: 'Doe'
}
}VerticalGroup
The same as HorizontalGroup but the items are displayed in column.
SeriesGroup
With this group renderer you will be able to create a list of elements with the same structure.
Example:
// UI Schema
{
"type": "SeriesGroup",
"scope": "#/properties/participants",
"label": "List of participants",
"elements": [
{
"type": "Control",
"label": "First name",
"scope": "#/properties/firstName",
},
{
"type": "Control",
"label": "Last name",
"scope": "#/properties/lastName",
}
]
}
// Form result
{
participants: [
{ firstName: 'John', lastName: 'Doe' }
]
}JSON Schema description
Object should comply to the JSONSchema interface from @jsonforms/core library.
For more details please refer to their repository.
type PropertyType = 'string' | 'number' | 'object' | 'array' | 'boolean' | 'null';
interface Property {
type: PropertyType;
// validation properties that are used for example in the input-renderer (built-in form validation)
minimum?: number;
maximum?: number;
minLength?: number;
maxLength?: number;
// available values for the property
enum?: unknown[];
// nested properties
properties?: { [key: string]: Property };
}
interface JSONSchema {
type: PropertyType;
properties: { [key: string]: Property };
// list of required properties for example - ['propertyName.nestedPropertyName']
required?: string[];
}JSON UI Schema description
Object should comply to the Layout definition from @jsonforms/core library.
Renderers
Renderers are components responsible for rendering the elements of your form.
To create renderer you need a class extending the FormComponent.
FormComponent interface
While creating a class for the renderer it is recommended to extend the FormComponent to be sure that everything works correctly with other elements of the form.
// data representation for the custom element
interface Data {
value: number;
label: string;
}
@customElement('wui-custom-renderer')
export class WuiCustomRenderer extends FormComponent<Data> {
/**
* required to anchor the validation message assigned in the parent
* for instance if the child renderer has multiple inputs the parent should know which has validation error
*/
anchor?: HTMLElement;
...
protected override render(): TemplateResult {
return html``;
}
}Validation of a child component
If due to the ShadowDOM or any other reason it is not possible to handle the validation automatically you can still handle this manually.
Here you can find a simplified example of how to handle such cases:
@customElement('wui-parent-with-shadow-dom')
class ParentWithShadowDom extends FormComponent {
...
protected override render(): TemplateResult {
return html`
<wui-child-input [name]="input"></wui-child-input>
`;
}
private setValidity(): void {
let anchor;
const input = this.shadowRoot?.querySelector('[name="input"]');
this.validationMessage = input.validationMessage;
this.validity = input.validity;
// if the child is a single input it should be enough to provide the whole element
// however, if there are more than 1 inputs inside, you need to provide an anchor
// to the element that is invalid, so the error message is presented accordingly
anchor = input.anchor || input;
// internals are initialized in the FormComponent class
this.internals.setValidity(this.validity, this.validationMessage, anchor);
}
}
Variables
User can define variables for the widget. Currently, the list of variables is static, defined in the [widgetName]-ui-schema.json.
Define an element with scope set to "#/properties/variables". Inside define UI elements with the scope set to variable names - "#/properties/variableName". As a next step define variables you want to use in the options for any other control.
// NOTE - currently only works for DataPointControl
{
"type": "object",
"elements": [
{
"type": "Tab",
"scope": "#/properties/variables", // important to make it possible to save the variables properly
"elements": [
{
"type": "WuiLayout",
"label": "Variables",
"elements": [
{
"scope": "#/properties/sTimeRange", // variable name
"type": "TimeRangeControl"
}
]
}
]
},
{
"type": "WuiLayout",
"label": "WUI_Settings.Area.Content",
"elements": [
{
"type": "SeriesGroup",
"scope": "#/properties/series",
"label": "Series",
"elements": [
{
"type": "DataPointControl",
"scope": "#/properties/datapoint",
"label": "Datapoint",
"options": {
"fetchMethod": "historic",
"variables": {
"historic": {
"sTimeRange": "${sTimeRange}" // historic.sTimeRange will use a variable
}
}
}
}
]
}
]
}
]
}License
MIT
