How Visual Components Handle Typed Parameters and Slot Synchronization in Instatic
Visual Components in Instatic use a TypeBox schema for typed parameters and a dedicated slot synchronization module to keep page instances in sync with component definition changes.
Visual Components (VCs) are the reusable building blocks of Instatic's visual editing system. They expose a strongly-typed parameter interface through a declarative schema, while slot-based composition allows nested content. Understanding how typed parameters and slot synchronization work together is essential for anyone extending or debugging the Instatic editor.
Typed Parameters Through TypeBox Schemas
Every Visual Component is defined by a TypeBox schema located in src/core/visualComponents/schemas.ts. This schema establishes the contract between component authors, the editor UI, and the runtime engine.
The VCParam Schema
Each parameter in a VC's params array conforms to the VCParam schema. The supported types include:
string– plain text inputnumber– numeric values with optional min/max constraintsboolean– toggle switchesurl– validated external linksenum– dropdown selections with predefined optionscolor– color picker valuesimage– asset referencesrichText– formatted content blocksslot– nested content areas
During parsing, the parseVisualComponent function validates raw data against this schema. Unknown types gracefully fall back to string, while helper logic normalizes default values, enum options, and required flags. The output is a typed VCParam[] that both the editor and runtime can rely on without additional runtime checks.
import { VisualComponentSchema } from '@core/visualComponents/schemas'
const myButtonVC = {
id: 'btn-01',
name: 'Button',
tree: { /* VC node tree */ },
params: [
{ id: 'p1', name: 'label', type: 'string', defaultValue: 'Click me', required: true },
{ id: 'p2', name: 'href', type: 'url', defaultValue: '', required: false },
{ id: 'p3', name: 'icon', type: 'slot', defaultValue: null, required: false },
],
classIds: [],
createdAt: Date.now(),
}
How Slots Are Represented in the VC Tree
Slot-type parameters have a special representation. Unlike other parameters, slots do not appear as entries in the params array with storage semantics. Instead, each slot is represented by a base.slot-outlet node embedded directly in the VC's own node tree.
The props.slotName property on each base.slot-outlet node serves as the canonical identifier for that slot. This design separates slot definitions (what slots exist) from slot instances (what content occupies them on a specific page).
Slot Synchronization When Definitions Change
When a Visual Component definition changes—adding a new slot, renaming an existing one, or removing a slot—the editor must reconcile all existing VC references on pages. This is handled by the slot synchronization module in src/core/visualComponents/slotSync.ts.
Extracting Slot Names from the VC Definition
The collectSlotOutletNames function walks the VC's flat node map and extracts all unique slot names from base.slot-outlet nodes. This produces the authoritative set of slots that should exist for any reference to this VC.
Computing Required Synchronization Operations
The syncSlotInstances function performs a pure comparison between the VC's slot definitions and the existing base.slot-instance children of a VC reference. It returns a SyncResult describing the minimal operations needed to bring the page tree into alignment:
| Operation | Trigger | Implementation |
|---|---|---|
| Insert | New slot in VC not present in VC reference | Create locked base.slot-instance node with matching slotName |
| Rename | Slot name changed in VC definition | Update props.slotName on existing base.slot-instance |
| Delete | Slot removed from VC definition | Remove orphaned base.slot-instance nodes including all subtrees |
Applying Synchronization Results
The applySlotSyncResult function executes these operations within a Mutative recipe. It mutates the page tree draft by adding new nodes, applying rename operations, deleting orphans, and finally re-ordering the VC reference's children array to match the VC's slot definition order.
import { syncSlotInstances, applySlotSyncResult } from '@core/visualComponents/slotSync'
import { mutateActiveTree } from '@admin/pages/site/store/slices/site/helpers'
// vcRefNode = node representing the VC reference on the page
// vc = the VisualComponent definition (as parsed above)
// treeNodes = mutable draft of the page's node map
mutateActiveTree((draft) => {
const result = syncSlotInstances(vcRefNode, vc, draft)
applySlotSyncResult(draft, result, vcRefNode.id)
})
Resolving Parameter Origins
The origin lookup system in src/core/visualComponents/origin.ts connects a VC parameter back to the specific node that provides its binding. This enables the editor to highlight source nodes and validate bindings.
For non-slot parameters, findParamOrigin scans node.propBindings for a matching paramId. This reveals which node property is bound to the parameter.
For slot parameters, the system locates the base.slot-outlet node whose props.slotName matches the parameter name. This bridges the semantic parameter name to the structural node in the VC tree.
import { findParamOrigin } from '@core/visualComponents/origin'
const origin = findParamOrigin(vc, 'p1')
if (origin) {
console.log('Parameter lives on node', origin.nodeId, 'prop key', origin.propKey)
}
Integration with the Editor Store
Slot synchronization is triggered automatically when VC definitions change. The visualComponentsSlice.ts file integrates this into the Redux store, calling syncSlotInstances whenever parameters are modified and ensuring all page instances stay consistent.
Summary
- TypeBox schemas in
src/core/visualComponents/schemas.tsenforce typed parameters with graceful fallbacks for unknown types - Slot definitions exist as
base.slot-outletnodes in the VC tree rather than parameter entries - Slot synchronization (
src/core/visualComponents/slotSync.ts) uses pureSyncResultoperations for insert, rename, and delete actions - Origin resolution (
src/core/visualComponents/origin.tsdistinguishes binding lookup for slot vs. non-slot parameters - Mutative recipes ensure all tree mutations remain traceable and undoable
Frequently Asked Questions
What happens when a Visual Component adds a new slot parameter?
The editor detects the change in the VC definition and triggers syncSlotInstances. For each VC reference on the page, it creates a new locked base.slot-instance node with the appropriate slotName. Existing page content remains untouched, and the new slot appears empty and ready for content.
How does Instatic handle unknown or invalid parameter types?
During parseVisualComponent, any type not recognized in the VCParam schema falls back to string. This prevents crashes while maintaining forward compatibility. Component authors can see the fallback behavior in the editor and correct the type definition.
Can slot instances contain their own nested Visual Components?
Yes. Slot instances are full nodes in the page tree with their own children array. They can contain any valid node type, including other VC references. When a slot is deleted during synchronization, the entire subtree—including any nested components—is removed.
What's the difference between base.slot-outlet and base.slot-instance?
base.slot-outlet appears in the VC definition and declares that the component accepts content in a named slot. base.slot-instance appears in page trees as a child of a VC reference, representing the actual container where editors place content for that slot.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →