ViewController & ViewInstance
ViewController is “a controller to manage updating/switching views” (core/player/src/controllers/view/controller.ts:56). It watches the flow’s navigation state and the data model, and mints a ViewInstance — “a stateful view instance from an content” (core/player/src/view/view.ts:87) — for whichever view is currently active.
Key Files
Section titled “Key Files”core/player/src/controllers/view/controller.ts—ViewControllercore/player/src/controllers/view/store.ts—LocalStateStore(core/player/src/controllers/view/store.ts:19), used byAssetTransformCorePluginto persist per-node transform statecore/player/src/controllers/view/types.ts—TransformRegistry,TransformFunctions,BeforeTransformFunctioncore/player/src/view/view.ts—ViewInstance,CrossfieldProvider, and theViewPlugininterface
ViewController is constructed during player.start()’s controller wiring alongside the other core controllers — see Start to Render for where it sits in that sequence. Its constructor immediately taps flowController.hooks.flow (to hear about VIEW navigation states) and dataController.hooks.onUpdate/onDelete (to know when to re-render).
Why ViewController and ViewInstance are one page
Section titled “Why ViewController and ViewInstance are one page”ViewController is the long-lived manager: one instance per player.start() run, holding the map of authored views and the seven view plugins. ViewInstance is the short-lived, per-view object it mints — literally const view = new ViewInstance(source, this.viewOptions) inside ViewController.onView() (core/player/src/controllers/view/controller.ts:219), replaced every time navigation lands on a new VIEW state. This is the same “manager mints instance” pattern used by FlowController/FlowInstance (see Flow Controller) — the two classes are only meaningful together.
Resolving the active view
Section titled “Resolving the active view”ViewController’s constructor taps flowController.hooks.flow, and on that FlowInstance’s hooks.transition, checks newState.value.state_type === "VIEW" — if so, it calls onView(newState.value); otherwise it clears currentView.
onView(state: NavigationFlowViewState):
- Looks up the view for
state.refin the view map (getViewForRef) — falling back to resolving{{data}}references embedded in view ids, for content that names views dynamically. - Calls the
resolveViewhook with that lookup result, the raw ref, and the navigation state, giving plugins a chance to swap in a different view entirely (or throw if nothing resolves). - Constructs
new ViewInstance(source, this.viewOptions)and assigns it tocurrentView. - Applies the seven view plugins to it (
applyViewPlugins) — each plugin’sapply(view)tapsview.hooks.parser/view.hooks.resolverto install itself before anything resolves. - Calls the
viewhook with the new instance, so other plugins can attach their own listeners before the first resolve. - Calls
updateView()to trigger the first resolve.
ViewInstance: parser + resolver + plugins, lazily wired
Section titled “ViewInstance: parser + resolver + plugins, lazily wired”ViewInstance doesn’t build its Parser/Resolver in its constructor — it defers until the first update(changes?, nodeChanges?) call, when rootNode is still undefined:
- Constructs a
CrossfieldProvider(implementsValidationProvider) from the view’svalidationarray — grouping eachx-fieldvalidation reference by theBindingInstanceit targets (defaultingtrigger: "navigation",severity: "error"), sogetValidationsForBindingcan answer “what cross-field validations apply to this binding” in constant time. See Validation Controller for how this feeds into the rest of validation. - Fires
hooks.templatePluginwith the registeredTemplatePlugin(warning if none was set —TemplatePlugin.apply()callsview.setTemplatePlugin(this)during plugin application). - Creates a
Parser, fireshooks.parserwith it (this is what each view plugin’sapply()taps into), and callsparser.parseView(initialView)to build the root AST node. - Creates a
Resolverfrom that root node, fireshooks.resolverwith it, then callsresolver.update(changes, nodeChanges).
Every subsequent update() call skips straight to resolver.update(...), reusing the same Parser/Resolver. If the resolved result is referentially unchanged, hooks.onUpdate doesn’t fire again. updateAsync is a deprecated shim kept for compatibility — new code should use ViewController.updateViewAST instead.
Batched re-rendering
Section titled “Batched re-rendering”ViewController doesn’t call currentView.update() directly from its data-model taps. Both dataController.hooks.onUpdate/onDelete and the public updateViewAST(nodes) API route through a shared queueUpdate helper: pending bindings/nodes are merged into this.pendingUpdate, and — unless the update is silent — a single queueMicrotask is scheduled to flush them via updateView(). This is the same batching mechanism narrated in What Happens When Data Changes; this page won’t repeat it. If currentView.update() throws, ViewController captures the error via ErrorController and re-queues a silent update with the same bindings/nodes, so a later successful update still picks up whatever didn’t get applied.
optimizeUpdates (default true) is an escape hatch: when false, every data change synchronously calls updateView() instead of batching through queueUpdate.
Hooks / Extension Points
Section titled “Hooks / Extension Points”ViewController (viewController.hooks)
Section titled “ViewController (viewController.hooks)”| Hook | Type | Fires when |
|---|---|---|
resolveView | SyncWaterfallHook<[View | undefined, string, NavigationFlowViewState]> | Before a ViewInstance is constructed for a navigation VIEW state — lets plugins substitute a different view |
view | SyncHook<[ViewInstance]> | Right after a new ViewInstance has its view plugins applied, before the first resolve |
ViewInstance (view.hooks)
Section titled “ViewInstance (view.hooks)”| Hook | Type | Fires when |
|---|---|---|
parser | SyncHook<[Parser]> | Once, when the Parser is created on first update() — this is how view plugins tap parser hooks |
resolver | SyncHook<[Resolver]> | Once, when the Resolver is created on first update() — this is how view plugins tap resolver hooks |
templatePlugin | SyncHook<[TemplatePlugin]> | On first update(), with whichever TemplatePlugin registered itself via setTemplatePlugin |
onUpdate | SyncHook<[View]> | After every update() call that produces a new resolved value |
Relationship to Other Subsystems
Section titled “Relationship to Other Subsystems”- Plugin Implementation — walks through intercepting
resolveView(viavc.hooks.resolveView.intercept) to read view-state attributes before a view is resolved. - Writing a Plugin — shows tapping
vc.hooks.viewto modify aViewInstancebefore it resolves. - The Core View Plugins — the seven plugins applied to every
ViewInstance, and why their order is fixed. - The View Parser & Resolver — the
Parser/Resolverpair eachViewInstancecreates and exposes via itsparser/resolverhooks. - Validation Controller — consumes the
CrossfieldProviderbuilt into eachViewInstanceforx-fieldcross-binding validations. - Flow Controller — the transition hook
ViewControllertaps to know when to mint a newViewInstance. - What Happens When Data Changes — the full narration of the
queueUpdate/microtask batching mechanism this page only summarizes.