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:
Category | Purpose | Examples |
---|---|---|
Data Widgets | Bind directly to values and persist data | tsm-text , tsm-datepicker , tsm-lov |
Layout Widgets | Structure the UI, group fields | dtl-fluent-card , dtl-fluent-tab |
Functional Widgets | Provide composite behaviors or module integrations | tsm-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:
- Widget Type
- Defines the widget renderer (e.g.
tsm-text
,dtl-fluent-card
) - Referenced under the
widget.type
property in JSON Schema
- Defines the widget renderer (e.g.
- Config Object
- Contains custom properties for styling, conditional logic, and layout
- Passed into the Angular component via inputs
- 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
- Data path: where the value is read from or written to (e.g.
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:
- The form engine parses the JSON Schema
- It identifies widget types via
widget.type
- It resolves the widget component from the registry (usually an Angular component)
- It injects config and data bindings
- 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 Widget | Key Config Options |
---|---|
dtl-fluent-tab | tabs , label , hidden , readonly |
dtl-fluent-card | collapsible , header , style |
dtl-fluent-columns | columns , width , spacing , responsive |
dtl-fluent-section | legend , 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:
- JEXL expressions
- For visibility (
config.hidden
), read-only, validation, default values
- For visibility (
- $context resolution
- Access to
form
,entity
,user
,lov
,related
, etc.
- Access to
- Reactive updates
- Via NGRX selectors or programmatic store subscriptions
- 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:
Mode | Editable | Visible | Stored? |
---|---|---|---|
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 Field | Description |
---|---|
readonly | JEXL-based or static toggle for editing |
hidden | Whether to display the widget |
default | Default value expression |
tooltip / placeholder | UX helpers |
labelCss , inputCss | Custom CSS for layout |
Advanced widgets like LOVs and Listings support:
Field | Purpose |
---|---|
dataSource | Name of the source entity or API |
selectProperty | Which field is stored (e.g. ID, code, full object) |
filters | Static or dynamic filters using JEXL |
lazyLoad | Whether to defer query until open |
selectFirstValue | Auto-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