← README.md | 🌱
A ViewModel extends the base Widget class to group multiple widgets with shared state and behavior. It separates presentation logic from business logic by encapsulating both the widget tree structure and the state management.
Core Methods:
| Method | Required | Description |
|---|---|---|
template() |
Yes | Returns the widget tree configuration object. Called once during ViewModel creation. |
setProps(newProps) |
No | Updates the ViewModel's props. Triggers onUpdateView() as default. |
Lifecycle Hooks:
All lifecycle hooks are optional and should be declared with the override keyword.
| Hook | When Called | Purpose |
|---|---|---|
onCreateView() |
After ViewModel creation, before template() | Initialize state, setup listeners, configure initial values |
onTemplateCreated() |
After template() returns, before rendering | Access rendered widgets, setup widget-specific logic |
onUpdateView() |
When ViewModel updates (after setProps or update props via render) | React to prop changes, re-render template by default |
onDestroyView() |
Before ViewModel destruction | Cleanup resources, remove listeners, clear references |
Note on onUpdateView():
The onUpdateView() hook is called when props are updated via setProps() or when the ViewModel is updated via render(). By default, onUpdateView() calls m.render() which automatically re-renders the ViewModel using its template() function.
' Default implementation in BaseViewModel
sub onUpdateView()
m.render() ' Re-renders using template()
end subYou can override onUpdateView() to customize update behavior:
- Call
m.render()to re-render the entire template (default behavior) - Call
m.render(partialTemplate)to update specific parts - Skip rendering entirely for performance optimization
- Add custom logic before/after re-rendering
Note on m.render() without arguments:
When m.render() is called without arguments on a ViewModel, it automatically calls m.template() and re-renders the ViewModel with the returned configuration. This provides a simple way to refresh the entire UI based on current props and viewModelState.
Shared Properties:
| Property | Scope | Mutability | Description |
|---|---|---|---|
props |
All widgets in ViewModel | Read-only | Configuration passed during render, available as m.props in all widgets |
viewModelState |
All widgets in ViewModel | Mutable | Shared state, available as m.viewModelState in all widgets |
Note: The FocusPlugin automatically manages
viewModelState.isFocusedonly on ViewModel widgets (isViewModel = true). It is initialized tofalseif not already present and updated when focus state changes. Simple child widgets within a ViewModel template do NOT get this behavior because they share the parent ViewModel'sviewModelStateby reference, which would cause conflicts. Use the injectedm.isFocused()widget method for reliable per-widget focus queries on any widget.
How it works:
- ViewModel class is instantiated
- ViewModel's root widget is added to virtual tree
- Default
propsandviewModelStateare extended from config. - Widget is decorated with framework methods and plugins
onCreateView()is called (if defined)template()is called to get widget tree configuration- Plugins are applied to configure widget behavior (fields, focus, observer, etc.)
- All child widgets in the tree share the same
propsandviewModelStatereferences - Widgets can access
m.propsandm.viewModelStatein field expressions and hooks - When ViewModel is destroyed,
onDestroyView()is called and references are cleared
A ViewModel extends the base Widget class, which means the ViewModel instance IS the root widget of its template. When template() is called, the returned root-level configuration (nodeType, fields, focus, focusGroup, etc.) is deep-merged into the ViewModel instance itself.
This has an important consequence for the m scope in callbacks:
Root widget callbacks — m refers to the ViewModel instance directly:
override function template() as object
return {
nodeType: "Group",
focusGroup: {
' m = this ViewModel instance (the root widget)
' ViewModel methods can be called directly
back: sub() m.goBack() : end sub
},
children: [...]
}
end functionChild widget callbacks — m refers to the individual child widget, NOT the ViewModel:
children: [{
id: "backButton",
nodeType: "Group",
focus: {
' m = the backButton widget
' Use getViewModel() to access the owning ViewModel
onSelect: sub() m.getViewModel().goBack() : end sub
}
}]| Context | m refers to |
How to access ViewModel |
|---|---|---|
| Root widget callbacks (focus, focusGroup, observer, fields, lifecycle) | ViewModel instance | m (direct) |
| Child widget callbacks | The child widget | m.getViewModel() |
| Child ViewModel callbacks (nested ViewModel) | The child ViewModel | m.getParentViewModel() for parent |
Note: All child widgets (non-ViewModel) within the template tree share references to the ViewModel's
propsandviewModelState. This meansm.propsandm.viewModelStatework in any widget callback within the tree, regardless of depth.
Structure:
class MyViewModel extends Rotor.ViewModel
' Shared configuration (default, extended during via config)
props = {}
' Shared mutable state (default, extended during via config)
viewModelState = {}
' Required: Returns widget tree configuration
override function template() as object
return { ... }
end function
' Lifecycle hooks (optional)
override sub onCreateView()
override sub onTemplateCreated()
override sub onDestroyView()
' onUpdateView - has default implementation that calls m.render()
' Override to customize update behavior
override sub onUpdateView()
' Default: m.render() - re-renders using template()
' Custom: add logic, skip render, or render partial updates
end sub
' Method for updating props - calls onUpdateView() after updating
override sub setProps(newProps as object)
end classViewModel example:
class ButtonViewModel extends Rotor.ViewModel
props = {
label: "Click Me",
enabled: true
}
viewModelState = {
clickCount: 0
}
override function template() as object
return {
nodeType: "Group",
children: [{
id: "background",
nodeType: "Rectangle",
fields: {
width: 200,
height: 60,
color: function() as string typecast m as Rotor.ViewModel
if m.isFocused()
return "#FF5500"
else if m.props.enabled
return "#0055FF"
else
return "#CCCCCC"
end if
end function
}
}, {
id: "text",
nodeType: "Label",
fields: {
text: m.props.label,
width: 200,
height: 60,
horizAlign: "center",
vertAlign: "center",
color: "#FFFFFF"
}
}]
}
end function
override sub onCreateView()
' Initialize ViewModel
print "ButtonViewModel created"
' Access dispatcher state using convenience methods
dispatcher = m.connectDispatcher("appState")
currentState = dispatcher.getState()
' Or use m.getStateFrom() convenience method
currentState = m.getStateFrom("appState")
' Dispatch using convenience method
m.dispatchTo("appState", { type: "BUTTON_CREATED" })
end sub
end class
' Render the ViewModel
frameworkInstance.render({
viewModel: ButtonViewModel
})NEXT STEP: Fields Plugin
Reference Documentation:
- Framework Initialization - Configuration, task synchronization, and lifecycle
- ViewBuilder Overview - High-level architecture and concepts
- Widget Reference - Complete Widget properties, methods, and usage patterns
Plugin Documentation:
- Fields Plugin - Field management with expressions and interpolation
- FontStyle Plugin - Typography and font styling
- Observer Plugin - Field observation patterns
- Focus Plugin - Focus management and navigation
Additional Documentation:
- Cross-Thread MVI design pattern - State management across threads
- Internationalization support - Locale-aware interface implementation