Skip to main content

Widget Architecture in tSM

Widgets are the essential building blocks of forms in tSM. They are responsible for rendering fields, capturing user input, organizing layout, and supporting dynamic logic and data binding. This section explains the architectural principles of widgets — their classification, structure, lifecycle, and how they behave at runtime.

Widget Categories

Widgets in tSM fall into three primary categories:

CategoryPurposeExamples
Data WidgetsBind directly to values and persist datatsm-text, tsm-datepicker, tsm-lov
Layout WidgetsStructure the UI, group fieldsdtl-fluent-card, dtl-fluent-tab
Functional WidgetsProvide composite behaviors or module integrationstsm-ticket-history, tsm-comments

These categories define how the widget interacts with the schema, runtime engine, and user.

Note: Legacy terminology sometimes refers to all these as “components”, but for clarity, this documentation consistently uses “widget” for renderable units within the form engine.

Widget Structure

Each widget is configured using three main building blocks:

  1. Widget Type
    • Defines the widget renderer (e.g. tsm-text, dtl-fluent-card)
    • Referenced under the widget.type property in JSON Schema
  2. Config Object
    • Contains custom properties for styling, conditional logic, and layout
    • Passed into the Angular component via inputs
  3. Binding Definition
    • Data path: where the value is read from or written to (e.g. chars.priority)
    • Configured implicitly by form engine based on schema structure and entity binding

Example:

"priority": {
  "type": "string",
  "widget": {
    "type": "tsm-select",
    "config": {
      "label": "Priority",
      "options": ["Low", "Medium", "High"],
      "readonly": "${$context.user.role !== 'admin'}"
    }
  }
}

Widget Registration and Resolution

Each widget is registered via a widget registry mechanism during application startup. At runtime:

  1. The form engine parses the JSON Schema
  2. It identifies widget types via widget.type
  3. It resolves the widget component from the registry (usually an Angular component)
  4. It injects config and data bindings
  5. It evaluates JEXL expressions and event hooks (e.g. onChange, onInit)

Custom widgets must follow specific interface contracts:

  • Accept a config input
  • Support FormControl binding (for reactive forms)
  • Expose standard hooks for change detection and dynamic updates

Layout Widgets and Nesting

Layout widgets (e.g. dtl-fluent-columns, dtl-fluent-tab, dtl-fluent-section) define hierarchical structure. They do not bind to data but group and organize child widgets.

Nested layouts can form arbitrary hierarchies such as:

Tab → Card → Column → Section → Field

Each layout widget supports its own configuration:

Layout WidgetKey Config Options
dtl-fluent-tabtabs, label, hidden, readonly
dtl-fluent-cardcollapsible, header, style
dtl-fluent-columnscolumns, width, spacing, responsive
dtl-fluent-sectionlegend, margin, padding

Use conditional config (e.g. config.hidden, config.collapsible) for runtime optimization.

Runtime Behavior and Evaluation

Widgets dynamically respond to the following at runtime:

  1. JEXL expressions
    • For visibility (config.hidden), read-only, validation, default values
  2. $context resolution
    • Access to form, entity, user, lov, related, etc.
  3. Reactive updates
    • Via NGRX selectors or programmatic store subscriptions
  4. Lazy initialization
    • Not rendered until visible or triggered

tSM form engine watches JEXL-bound properties and re-evaluates expressions on relevant state changes.

Widget Storage Modes

Each data widget can define its storage behavior, which determines how and whether its value is persisted:

ModeEditableVisibleStored?
readonlyBased
always
never
computed - saved
computed - transient

Use these modes to control hidden calculations or display-only logic without bloating the saved data.

Dynamic Behavior and Config Examples

Most widgets accept a config object that supports:

Config FieldDescription
readonlyJEXL-based or static toggle for editing
hiddenWhether to display the widget
defaultDefault value expression
tooltip / placeholderUX helpers
labelCss, inputCssCustom CSS for layout

Advanced widgets like LOVs and Listings support:

FieldPurpose
dataSourceName of the source entity or API
selectPropertyWhich field is stored (e.g. ID, code, full object)
filtersStatic or dynamic filters using JEXL
lazyLoadWhether to defer query until open
selectFirstValueAuto-select default value

Example: Conditional Required Field

"discount": {
  "type": "number",
  "widget": {
    "type": "tsm-number",
    "config": {
      "label": "Discount",
      "validationMessages": {
        "custom": {
          "expression": "${$context.form.customerType === 'VIP' && ($value < 10 || $value > 50)}",
          "message": "VIP discount must be between 10 and 50"
        }
      }
    }
  }
}

This field:

  • Applies a dynamic validation only for VIP customers

  • Uses $value and $context references

  • Is evaluated in real time on input

Summary

Widgets are highly configurable, context-aware components that form the core rendering engine of forms in tSM. Understanding their architecture helps to:

  • Build reusable, performant form layouts
  • Control behavior with minimal code
  • Integrate deeply with entity, user, and backend context
  • Enable declarative configuration and rapid iteration