rotor-framework 0.7.6 β†’ 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -175,7 +175,7 @@
175
175
 
176
176
  END OF TERMS AND CONDITIONS
177
177
 
178
- Copyright 2025 Balazs Molnar
178
+ Copyright Β© 2025-2026 Balazs Molnar
179
179
 
180
180
  Licensed under the Apache License, Version 2.0 (the "License");
181
181
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -96,7 +96,7 @@ You can find [🌱](./docs/ai/readme.opt.yaml) symbols in all documentation page
96
96
 
97
97
  ## πŸ“š Learn More
98
98
 
99
- ![Version](https://img.shields.io/badge/version-v0.7.6-blue?label=Documents%20TAG)
99
+ ![Version](https://img.shields.io/badge/version-v0.8.0-blue?label=Documents%20TAG)
100
100
 
101
101
  ### Framework Core
102
102
 
@@ -185,6 +185,6 @@ This helps others discover the project and supports the open source community. T
185
185
 
186
186
  ---
187
187
 
188
- **Copyright 2025 Balazs Molnar**
188
+ **Copyright Β© 2025-2026 Balazs Molnar**
189
189
 
190
190
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rotor-framework",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
4
4
  "description": "Roku toolkit library providing a ViewBuilder, full UI lifecycle with focus handling and many core features, plus MVI-based state management.",
5
5
  "author": "BalΓ‘zs MolnΓ‘r",
6
6
  "license": "Apache-2.0",
@@ -52,8 +52,9 @@
52
52
  },
53
53
  "homepage": "https://github.com/mobalazs/rotor-framework#readme",
54
54
  "devDependencies": {
55
- "@rokucommunity/bslint": "^1.0.0-alpha.48",
56
- "brighterscript": "^1.0.0-alpha.48",
57
- "rooibos-roku": "file:./vendor/rooibos-roku-6.0.0-alpha.48-fixed354.tgz"
55
+ "@rokucommunity/bslint": "1.0.0-alpha.50",
56
+ "brighterscript": "1.0.0-alpha.50",
57
+ "roku-deploy": "^3.16.1",
58
+ "rooibos-roku": "6.0.0-alpha.50"
58
59
  }
59
60
  }
@@ -4,8 +4,8 @@
4
4
  ' β–β–›β–€β–šβ––β–β–Œ β–β–Œ β–ˆ β–β–Œ β–β–Œβ–β–›β–€β–šβ–– β–β–›β–€β–€β–˜β–β–›β–€β–šβ––β–β–›β–€β–œβ–Œβ–β–Œ β–β–Œβ–β–›β–€β–€β–˜β–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–›β–€β–šβ––β–β–›β–šβ––
5
5
  ' β–β–Œ β–β–Œβ–β–šβ–„β–žβ–˜ β–ˆ β–β–šβ–„β–žβ–˜β–β–Œ β–β–Œ β–β–Œ β–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–™β–„β–„β––β–β–™β–ˆβ–Ÿβ–Œβ–β–šβ–„β–žβ–˜β–β–Œ β–β–Œβ–β–Œ β–β–Œ
6
6
  ' Rotor Frameworkβ„’
7
- ' Version 0.7.6
8
- ' Β© 2025 BalΓ‘zs MolnΓ‘r β€” Apache License 2.0
7
+ ' Version 0.8.0
8
+ ' Β© 2025-2026 BalΓ‘zs MolnΓ‘r β€” Apache License 2.0
9
9
  ' =========================================================================
10
10
 
11
11
  ' constants
@@ -86,7 +86,7 @@ namespace Rotor
86
86
  class Framework
87
87
 
88
88
  name = "Rotor Framework"
89
- version = "0.7.6"
89
+ version = "0.8.0"
90
90
 
91
91
  config = {
92
92
  tasks: invalid, ' @array (optional)
@@ -333,6 +333,20 @@ namespace Rotor
333
333
  return m.builder.nodePool.presetNodePool(config)
334
334
  end function
335
335
 
336
+ ' ---------------------------------------------------------------------
337
+ ' registerSourceObject - Not available on render thread
338
+ '
339
+ public sub registerSourceObject(objectId as string, dispatcherId as string, sourceObject as object, eventFilter = invalid as dynamic)
340
+ throw "registerSourceObject() is only available on the task thread. Source objects require a task thread message port for event routing."
341
+ end sub
342
+
343
+ ' ---------------------------------------------------------------------
344
+ ' unregisterSourceObject - Not available on render thread
345
+ '
346
+ public sub unregisterSourceObject(objectId as string)
347
+ throw "unregisterSourceObject() is only available on the task thread."
348
+ end sub
349
+
336
350
  ' =====================================================================
337
351
  ' INTERNAL METHODS - INITIALIZATION AND TASK SYNCING
338
352
  ' =====================================================================
@@ -4,8 +4,8 @@
4
4
  ' β–β–›β–€β–šβ––β–β–Œ β–β–Œ β–ˆ β–β–Œ β–β–Œβ–β–›β–€β–šβ–– β–β–›β–€β–€β–˜β–β–›β–€β–šβ––β–β–›β–€β–œβ–Œβ–β–Œ β–β–Œβ–β–›β–€β–€β–˜β–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–›β–€β–šβ––β–β–›β–šβ––
5
5
  ' β–β–Œ β–β–Œβ–β–šβ–„β–žβ–˜ β–ˆ β–β–šβ–„β–žβ–˜β–β–Œ β–β–Œ β–β–Œ β–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–™β–„β–„β––β–β–™β–ˆβ–Ÿβ–Œβ–β–šβ–„β–žβ–˜β–β–Œ β–β–Œβ–β–Œ β–β–Œ
6
6
  ' Rotor Frameworkβ„’
7
- ' Version 0.7.6
8
- ' Β© 2025 BalΓ‘zs MolnΓ‘r β€” Apache License 2.0
7
+ ' Version 0.8.0
8
+ ' Β© 2025-2026 BalΓ‘zs MolnΓ‘r β€” Apache License 2.0
9
9
  ' =========================================================================
10
10
 
11
11
  ' constants
@@ -70,7 +70,7 @@ namespace Rotor
70
70
  class FrameworkTask
71
71
 
72
72
  name = "Rotor Framework"
73
- version = "0.7.6"
73
+ version = "0.8.0"
74
74
 
75
75
  config = {
76
76
  tasks: invalid, ' optional
@@ -86,8 +86,10 @@ namespace Rotor
86
86
  taskNode as object
87
87
  dispatcherProvider as object
88
88
  port as object
89
- asyncTransferRegistry = {} ' transferId -> { dispatcherId, context }
90
- deviceInfoRegistry = {} ' deviceInfoId -> { dispatcherId, context, deviceInfo }
89
+ sourceObjectRegistry = {} ' identity -> { dispatcherId, objectId, eventFilter }
90
+ sourceObjectIdIndex = {} ' objectId -> identity (reverse index for unregistration)
91
+ sourceObjectTypeRegistry = [] ' [{ dispatcherId, objectId, eventFilter }] - broadcast routing
92
+ private _eventFilterFn = invalid as dynamic
91
93
  onTick as function
92
94
 
93
95
  ' ---------------------------------------------------------------------
@@ -132,42 +134,59 @@ namespace Rotor
132
134
  end function
133
135
 
134
136
  ' ---------------------------------------------------------------------
135
- ' registerAsyncTransfer - Registers an async transfer with dispatcher context
137
+ ' registerSourceObject - Registers a source object for event routing
136
138
  '
137
- ' This method associates a roUrlTransfer identity with a dispatcher ID and
138
- ' optional context data. When the transfer completes and sends a roUrlEvent,
139
- ' the framework will route it to the correct dispatcher's asyncReducerCallback.
139
+ ' Generic registry for any Roku source object. Auto-detects routing mode:
140
+ ' - Identity-based: If sourceObject implements ifSourceIdentity, routes via
141
+ ' GetSourceIdentity() on the event (roUrlTransfer, roChannelStore).
142
+ ' - Broadcast: If sourceObject does NOT implement ifSourceIdentity, broadcasts
143
+ ' to all registered dispatchers (roDeviceInfo, roInput, roAppManager).
140
144
  '
141
- ' @param {string} transferId - The transfer.GetIdentity().ToStr() value
142
- ' @param {string} dispatcherId - Dispatcher ID that initiated the transfer
143
- ' @param {object} context - Optional user data to pass back in callback
145
+ ' @param {string} objectId - Unique identifier for this registration
146
+ ' @param {string} dispatcherId - Dispatcher ID that will handle events
147
+ ' @param {object} sourceObject - The source object (SetMessagePort will be called)
148
+ ' @param {function} eventFilter - Optional filter function. Receives msg, returns boolean.
144
149
  '
145
- public sub registerAsyncTransfer(transferId as string, dispatcherId as string, context = invalid as dynamic)
146
- m.asyncTransferRegistry[transferId] = {
147
- dispatcherId: dispatcherId,
148
- context: context
149
- }
150
+ public sub registerSourceObject(objectId as string, dispatcherId as string, sourceObject as object, eventFilter = invalid as dynamic)
151
+ sourceObject.SetMessagePort(m.port)
152
+ if FindMemberFunction(sourceObject, "GetIdentity") <> invalid
153
+ ' Identity-based routing
154
+ identity = sourceObject.GetIdentity().ToStr()
155
+ m.sourceObjectRegistry[identity] = {
156
+ dispatcherId: dispatcherId,
157
+ objectId: objectId,
158
+ eventFilter: eventFilter
159
+ }
160
+ m.sourceObjectIdIndex[objectId] = identity
161
+ else
162
+ ' Broadcast routing
163
+ m.sourceObjectTypeRegistry.push({
164
+ dispatcherId: dispatcherId,
165
+ objectId: objectId,
166
+ eventFilter: eventFilter
167
+ })
168
+ end if
150
169
  end sub
151
170
 
152
171
  ' ---------------------------------------------------------------------
153
- ' registerDeviceInfo - Registers a roDeviceInfo for event listening
154
- '
155
- ' This method associates a roDeviceInfo object with a dispatcher ID.
156
- ' When device info events occur (linkStatus, etc.), the framework will
157
- ' route them to the correct dispatcher's asyncReducerCallback.
172
+ ' unregisterSourceObject - Unregisters a port-based object by its objectId
158
173
  '
159
- ' @param {string} deviceInfoId - Unique identifier for this registration
160
- ' @param {string} dispatcherId - Dispatcher ID that will handle events
161
- ' @param {object} deviceInfo - The roDeviceInfo object (must have SetMessagePort called)
162
- ' @param {object} context - Optional user data to pass back in callback
174
+ ' @param {string} objectId - The unique identifier used during registration
163
175
  '
164
- public sub registerDeviceInfo(deviceInfoId as string, dispatcherId as string, deviceInfo as object, context = invalid as dynamic)
165
- deviceInfo.SetMessagePort(m.port)
166
- m.deviceInfoRegistry[deviceInfoId] = {
167
- dispatcherId: dispatcherId,
168
- deviceInfo: deviceInfo,
169
- context: context
170
- }
176
+ public sub unregisterSourceObject(objectId as string)
177
+ ' Try identity-based registry
178
+ if m.sourceObjectIdIndex.DoesExist(objectId)
179
+ identity = m.sourceObjectIdIndex[objectId]
180
+ m.sourceObjectRegistry.Delete(identity)
181
+ m.sourceObjectIdIndex.Delete(objectId)
182
+ return
183
+ end if
184
+ ' Try broadcast registry
185
+ for i = m.sourceObjectTypeRegistry.count() - 1 to 0 step -1
186
+ if m.sourceObjectTypeRegistry[i].objectId = objectId
187
+ m.sourceObjectTypeRegistry.Delete(i)
188
+ end if
189
+ end for
171
190
  end sub
172
191
 
173
192
  ' ---------------------------------------------------------------------
@@ -241,53 +260,63 @@ namespace Rotor
241
260
 
242
261
  end if
243
262
  else
244
-
263
+ ' Cross-thread state change: notify task-side listeners
245
264
  data = msg.getData()
246
- extraInfo = msg.GetInfo() ' Info AA passed during observeFieldScoped
247
-
248
- if extraInfo?.dispatcherId <> invalid and m.dispatcherProvider.get(extraInfo?.dispatcherId) <> invalid
249
- ' Catch by dispatcherId
250
- m.dispatcherProvider.get(extraInfo?.dispatcherId).asyncReducerCallback(msg, extraInfo?.context as dynamic)
251
- else
252
- dispatcherId = fieldId
253
- dispatcherInstance = m.dispatcherProvider.get(dispatcherId)
265
+ dispatcherId = fieldId
266
+ dispatcherInstance = m.dispatcherProvider.get(dispatcherId)
267
+ if dispatcherInstance <> invalid
254
268
  dispatcherInstance.notifyListeners(data)
255
269
  end if
256
270
 
257
271
  end if
258
- else if msgType = "roUrlEvent"
259
- ' Handle async transfer responses
260
- transferId = msg.GetSourceIdentity().ToStr()
261
-
262
- if m.asyncTransferRegistry.DoesExist(transferId)
263
- transferData = m.asyncTransferRegistry[transferId]
264
- dispatcherId = transferData.dispatcherId
265
-
266
- ' Cleanup registry entry (Note: order is important - this make it reusable immediately)
267
- m.asyncTransferRegistry.delete(transferId)
268
-
269
- dispatcherInstance = m.dispatcherProvider.get(dispatcherId)
270
- if dispatcherInstance <> invalid
271
- ' Route to dispatcher with wrapped message
272
- dispatcherInstance.asyncReducerCallback(msg as roUrlEvent, transferData?.context as dynamic)
273
- end if
272
+ else
273
+ ' Generic source object routing
274
+ routed = false
275
+
276
+ ' Try identity-based routing
277
+ if m.sourceObjectRegistry.count() > 0
278
+ try
279
+ sourceIdentity = msg.GetSourceIdentity().ToStr()
280
+ if m.sourceObjectRegistry.DoesExist(sourceIdentity)
281
+ entry = m.sourceObjectRegistry[sourceIdentity]
282
+
283
+ ' Apply event filter if provided
284
+ allowed = true
285
+ if entry.eventFilter <> invalid
286
+ m._eventFilterFn = entry.eventFilter
287
+ allowed = m._eventFilterFn(msg)
288
+ end if
274
289
 
290
+ if allowed
291
+ dispatcherInstance = m.dispatcherProvider.get(entry.dispatcherId)
292
+ if dispatcherInstance <> invalid
293
+ dispatcherInstance.onSourceEvent(msg)
294
+ end if
295
+ end if
296
+ routed = true
297
+ end if
298
+ catch e
299
+ ' Event doesn't support GetSourceIdentity - fall through to broadcast
300
+ end try
275
301
  end if
276
- else if msgType = "roDeviceInfoEvent"
277
- ' Handle device info events (ignore appFocused, etc.)
278
- if m.deviceInfoRegistry.count() > 0
279
- eventInfo = msg.GetInfo()
280
- ' Only process linkStatus events, ignore appFocused and other events
281
- if eventInfo?.linkStatus <> invalid
282
- ' Route to all registered device info dispatchers
283
- for each deviceInfoId in m.deviceInfoRegistry
284
- registryEntry = m.deviceInfoRegistry[deviceInfoId]
285
- dispatcherInstance = m.dispatcherProvider.get(registryEntry.dispatcherId)
302
+
303
+ ' Broadcast to all non-identity registered dispatchers
304
+ if not routed
305
+ for each entry in m.sourceObjectTypeRegistry
306
+ ' Apply event filter if provided
307
+ allowed = true
308
+ if entry.eventFilter <> invalid
309
+ m._eventFilterFn = entry.eventFilter
310
+ allowed = m._eventFilterFn(msg)
311
+ end if
312
+
313
+ if allowed
314
+ dispatcherInstance = m.dispatcherProvider.get(entry.dispatcherId)
286
315
  if dispatcherInstance <> invalid
287
- dispatcherInstance.asyncReducerCallback(eventInfo, registryEntry?.context as dynamic)
316
+ dispatcherInstance.onSourceEvent(msg)
288
317
  end if
289
- end for
290
- end if
318
+ end if
319
+ end for
291
320
  end if
292
321
  end if
293
322
  end if
@@ -354,8 +383,9 @@ namespace Rotor
354
383
  ' Destroys dispatcher provider and clears global framework helper.
355
384
  '
356
385
  public sub destroy()
357
- m.asyncTransferRegistry.clear()
358
- m.deviceInfoRegistry.clear()
386
+ m.sourceObjectRegistry.clear()
387
+ m.sourceObjectIdIndex.clear()
388
+ m.sourceObjectTypeRegistry.clear()
359
389
  m.dispatcherProvider.destroy()
360
390
 
361
391
  globalScope = GetGlobalAA()
@@ -32,12 +32,12 @@ namespace Rotor
32
32
  dispatch as function ' Dispatch intent to owner dispatcher
33
33
  dispatchTo as function ' Dispatch intent to dispatcher by ID
34
34
  getStateFrom as function ' Get state from dispatcher by ID
35
- ownerDispatcher as object ' Owning dispatcher instance
36
- ownerDispatcherId as string ' Owning dispatcher ID
35
+ dispatcher as object ' Owning dispatcher instance
36
+ dispatcherId as string ' Owning dispatcher ID
37
37
 
38
38
  ' Internal
39
39
  middlewareFnScoped as dynamic ' Currently executing middleware
40
-
40
+
41
41
  ' =============================================================
42
42
  ' CONSTRUCTOR
43
43
  ' =============================================================
@@ -56,12 +56,12 @@ namespace Rotor
56
56
 
57
57
  ' Inject dispatch method for dispatching intents
58
58
  m.dispatch = sub(intent as object)
59
- m.ownerDispatcher.dispatch(intent)
59
+ m.dispatcher.dispatch(intent)
60
60
  end sub
61
61
 
62
62
  ' Inject getState method for accessing current state
63
63
  m.getState = function() as Rotor.Model
64
- return m.ownerDispatcher.getState()
64
+ return m.dispatcher.getState()
65
65
  end function
66
66
 
67
67
  ' Inject dispatchTo method for dispatching intents to other dispatchers
@@ -105,6 +105,19 @@ namespace Rotor
105
105
  return state
106
106
  end function
107
107
 
108
+ ' ---------------------------------------------------------------------
109
+ ' onCreateDispatcher - Lifecycle hook called after dispatcher is fully registered
110
+ '
111
+ ' Called by the framework after the dispatcher is created and registered
112
+ ' with the DispatcherProvider. At this point, dispatcherId and all
113
+ ' framework integrations are available.
114
+ '
115
+ ' Override this method to perform initialization that requires
116
+ ' the dispatcher to be fully set up (e.g., source object registration).
117
+ '
118
+ sub onCreateDispatcher()
119
+ end sub
120
+
108
121
  ' =============================================================
109
122
  ' MIDDLEWARE
110
123
  ' =============================================================
@@ -174,34 +187,37 @@ namespace Rotor
174
187
  end function
175
188
 
176
189
  ' =============================================================
177
- ' ASYNC TRANSFER SUPPORT
190
+ ' SOURCE OBJECT SUPPORT
178
191
  ' =============================================================
179
192
 
180
193
  ' ---------------------------------------------------------------------
181
- ' registerAsyncTransfer - Registers a roUrlTransfer with the framework
182
- '
183
- ' Helper method to simplify async HTTP request tracking. Call this after
184
- ' creating a roUrlTransfer but BEFORE calling AsyncGetToString() or other
185
- ' async methods.
194
+ ' registerSourceObject - Registers a source object with the framework
186
195
  '
187
- ' @param {object} transfer - The roUrlTransfer object
188
- ' @param {object} context - Optional data to receive in asyncReducerCallback
196
+ ' Helper method for registering any Roku source object (roUrlTransfer,
197
+ ' roDeviceInfo, roChannelStore, etc.). The framework will call
198
+ ' SetMessagePort and route events to this reducer's onSourceEvent.
199
+ ' Routing mode is auto-detected: identity-based if sourceObject has GetIdentity(),
200
+ ' broadcast otherwise.
189
201
  '
190
- ' Example:
191
- ' transfer = CreateObject("roUrlTransfer")
192
- ' transfer.SetUrl("https://api.example.com/data")
193
- ' transfer.SetMessagePort(m.port)
194
- ' m.registerAsyncTransfer(transfer, { userId: 123 })
195
- ' transfer.AsyncGetToString()
202
+ ' @param {object} sourceObject - The source object
203
+ ' @param {function} eventFilter - Optional filter function. Receives msg, returns boolean.
196
204
  '
197
- sub registerAsyncTransfer(transfer as object, context = invalid as dynamic)
205
+ sub registerSourceObject(sourceObject as object, eventFilter = invalid as dynamic)
198
206
  frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
199
- transferId = transfer.GetIdentity().ToStr()
200
207
 
201
- ' Only call if the method exists (task thread only)
202
- if Rotor.Utils.isFunction(frameworkInstance?.registerAsyncTransfer)
203
- frameworkInstance.registerAsyncTransfer(transferId, m.ownerDispatcherId, context)
204
- end if
208
+ objectId = m.getSourceObjectId(sourceObject)
209
+ frameworkInstance.registerSourceObject(objectId, m.dispatcherId, sourceObject, eventFilter)
210
+ end sub
211
+
212
+ ' ---------------------------------------------------------------------
213
+ ' unregisterSourceObject - Unregisters a source object from the framework
214
+ '
215
+ ' @param {object} sourceObject - The source object to unregister
216
+ '
217
+ sub unregisterSourceObject(sourceObject as object)
218
+ frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
219
+ objectId = m.getSourceObjectId(sourceObject)
220
+ frameworkInstance.unregisterSourceObject(objectId)
205
221
  end sub
206
222
 
207
223
  ' =============================================================
@@ -209,11 +225,28 @@ namespace Rotor
209
225
  ' =============================================================
210
226
 
211
227
  ' ---------------------------------------------------------------------
212
- ' asyncReducerCallback - Callback for async middleware operations
228
+ ' getSourceObjectId - Generates unique objectId for a source object
229
+ '
230
+ ' Identity-based objects (roUrlTransfer, etc.): dispatcherId + GetIdentity()
231
+ ' Broadcast objects (roDeviceInfo, etc.): dispatcherId + type name
232
+ '
233
+ ' @param {object} sourceObject - The source object
234
+ ' @returns {string} Unique objectId
235
+ '
236
+ private function getSourceObjectId(sourceObject as object) as string
237
+ if FindMemberFunction(sourceObject, "GetIdentity") <> invalid
238
+ return `${m.dispatcherId}_${sourceObject.GetIdentity()}`
239
+ else
240
+ return `${m.dispatcherId}_${type(sourceObject)}`
241
+ end if
242
+ end function
243
+
244
+ ' ---------------------------------------------------------------------
245
+ ' onSourceEvent - Callback for source object events
213
246
  '
214
- ' @param {object} msg - Message from async operation
247
+ ' @param {object} msg - Raw message from source object (roUrlEvent, roDeviceInfoEvent, etc.)
215
248
  '
216
- sub asyncReducerCallback(msg as roUrlEvent, context as dynamic)
249
+ sub onSourceEvent(msg as object)
217
250
  ' Override in subclass to handle async responses
218
251
  end sub
219
252
 
@@ -225,7 +258,7 @@ namespace Rotor
225
258
  ' destroy - Cleans up reducer references
226
259
  '
227
260
  sub destroy()
228
- m.ownerDispatcher = invalid
261
+ m.dispatcher = invalid
229
262
  m.port = invalid
230
263
  end sub
231
264
 
@@ -21,10 +21,10 @@ namespace Rotor
21
21
  ' MEMBER VARIABLES
22
22
  ' =============================================================
23
23
 
24
- isViewModel = true ' Identifies this as a ViewModel
24
+ isViewModel = true ' Identifies this as a ViewModel
25
25
 
26
- viewModelState = {} ' Mutable state shared across ViewModel's widgets
27
- props = {} ' Immutable props shared across ViewModel's widgets
26
+ viewModelState = {} ' Mutable state shared across ViewModel's widgets
27
+ props = {} ' Immutable props shared across ViewModel's widgets
28
28
 
29
29
  ' =============================================================
30
30
  ' PROPS MANAGEMENT
@@ -82,6 +82,8 @@ namespace Rotor
82
82
  ' Called automatically when setProps() is invoked.
83
83
  '
84
84
  sub onUpdateView()
85
+ ' Re-render template
86
+ m.render()
85
87
  end sub
86
88
 
87
89
  ' ---------------------------------------------------------------------
@@ -62,8 +62,8 @@ namespace Rotor
62
62
 
63
63
  ' Link reducer to this dispatcher
64
64
  m.reducerInstance = reducerInstance
65
- m.reducerInstance.ownerDispatcher = m
66
- m.reducerInstance.ownerDispatcherId = dispatcherId
65
+ m.reducerInstance.dispatcher = m
66
+ m.reducerInstance.dispatcherId = dispatcherId
67
67
  m.modelInstance = modelInstance
68
68
 
69
69
  ' Create field on task node for render thread communication
@@ -75,6 +75,10 @@ namespace Rotor
75
75
  ' Register with dispatcher provider
76
76
  dispatcherProvider = globalScope.rotor_framework_helper.frameworkInstance.dispatcherProvider
77
77
  dispatcherProvider.registerDispatcher(m, dispatcherId)
78
+
79
+ ' Lifecycle hook - called after dispatcher is fully registered
80
+ m.reducerInstance.onCreateDispatcher()
81
+
78
82
  end sub
79
83
 
80
84
  ' =============================================================
@@ -155,12 +159,12 @@ namespace Rotor
155
159
  ' =============================================================
156
160
 
157
161
  ' ---------------------------------------------------------------------
158
- ' asyncReducerCallback - Routes async callback to reducer
162
+ ' onSourceEvent - Routes async callback to reducer
159
163
  '
160
164
  ' @param {object} msg - Message from async operation
161
165
  '
162
- sub asyncReducerCallback(msg as roUrlEvent, context as dynamic)
163
- m.reducerInstance.asyncReducerCallback(msg as roUrlEvent, context as dynamic)
166
+ sub onSourceEvent(msg as object)
167
+ m.reducerInstance.onSourceEvent(msg)
164
168
  end sub
165
169
 
166
170
  ' =============================================================
@@ -91,26 +91,34 @@ namespace Rotor.ViewBuilder
91
91
  return m.getFrameworkInstance().builder.widgetTree.getSubtreeClone(searchPattern, configIncludeFilter, m.parentHID)
92
92
  end function
93
93
 
94
- ' Get i18n service
94
+ ' Get i18n service
95
95
  widget.i18n = function() as object
96
96
  return m.getFrameworkInstance().i18nService
97
97
  end function
98
98
 
99
99
  ' render - Renders widget updates (self, descendants, or children) *'
100
- widget.render = sub(payloads as dynamic, params = {} as object)
101
- for each payload in Rotor.Utils.ensureArray(payloads)
102
- if payload.DoesExist("id") = false
103
- ' Self update
104
- payload.id = m.id
105
- payload.HID = m.HID
106
- else if payload.id <> m.id
107
- ' Update descendants starting from this widget
108
- payload.parentHID = m.HID
109
- else
110
- ' Update descendants starting from parent widget
111
- payload.parentHID = m.parentHID
112
- end if
113
- end for
100
+ widget.render = sub(payloads = invalid as dynamic, params = {} as object)
101
+ if payloads = invalid and m.isViewModel = true
102
+ ' Self update (only for viewModels)
103
+ payloads = m.template()
104
+ ' Self update
105
+ payloads.id = m.id
106
+ payloads.HID = m.HID
107
+ else
108
+ for each payload in Rotor.Utils.ensureArray(payloads)
109
+ if payload.DoesExist("id") = false
110
+ ' Self update
111
+ payload.id = m.id
112
+ payload.HID = m.HID
113
+ else if payload.id <> m.id
114
+ ' Update descendants starting from this widget
115
+ payload.parentHID = m.HID
116
+ else
117
+ ' Update descendants starting from parent widget
118
+ payload.parentHID = m.parentHID
119
+ end if
120
+ end for
121
+ end if
114
122
  if Rotor.Utils.isValid(params.callback) then params.callbackScope = m
115
123
  m.getFrameworkInstance().builder.render(payloads, params)
116
124
  end sub