57.1 Architecture Widget Data Binding

Documentation for Sessions

57.1 Architecture: Widget-Module Data Binding System

Date: January 20, 2026 Type: Architecture Decision Confidence: 92% (Certain)

Context

The page-builder creates visual-only widgets, but these widgets need to connect to production module CRUD operations. The question was: how do we add data functionality without embedding business logic in visual widgets?

Decision

Create a declarative data binding type system with a runtime resolver service that connects visual widgets to module CRUD operations.

Options Considered

  1. Embed CRUD logic directly in widgets - Violates visual-only principle
  2. Create widget-module binding types + resolver service ✅ CHOSEN - Clean separation
  3. Use props drilling from parent containers - Too coupled, poor DX

Implementation

Type System

interface WidgetModuleBinding {
  id: string;
  widgetId: string;
  moduleRef: ModuleRef;           // { moduleId, collection, version? }
  action: ModuleAction;           // list, get, create, update, delete, etc.
  inputMappings: BindingInputMapping[];   // widget property → module parameter
  outputMappings: BindingOutputMapping[]; // module response → widget property
  refreshPolicy: RefreshPolicy;   // on-mount, interval, on-change, manual
  errorHandling?: ErrorHandlingConfig;
  caching?: CachingConfig;
  enabled: boolean;
}

interface WidgetEventBinding {
  id: string;
  widgetId: string;
  eventType: WidgetEventType;     // click, change, submit, etc.
  action: EventAction;            // module-action, navigate, emit-event
  confirmation?: ConfirmationConfig;
  enabled: boolean;
}

interface WidgetDataConfig {
  moduleBindings: WidgetModuleBinding[];
  eventBindings: WidgetEventBinding[];
  staticData?: Record<string, unknown>;
  loadingState?: LoadingStateConfig;
  emptyState?: EmptyStateConfig;
}

Runtime Resolver (WidgetDataBindingService)

Properties Panel Data Tab

Added new "Data" tab in Properties Panel with:

Example Usage

// Widget configuration
const tableWidget = {
  id: 'customers-table',
  type: 'table',
  config: {
    dataConfig: {
      moduleBindings: [{
        moduleRef: { moduleId: '@modules/customers-management', collection: 'customers' },
        action: 'list',
        inputMappings: [
          { source: '$.widget.page', target: 'page', defaultValue: 1 },
          { source: '$.widget.pageSize', target: 'pageSize', defaultValue: 10 }
        ],
        outputMappings: [
          { source: '$.data', target: '$.widget.rows' },
          { source: '$.total', target: '$.widget.totalCount' }
        ],
        refreshPolicy: { triggers: ['on-mount', 'on-change'], debounceMs: 300 },
        enabled: true
      }],
      eventBindings: [{
        eventType: 'click',
        action: { type: 'module-action', moduleAction: { action: 'delete', ... } },
        confirmation: { title: 'Delete?', message: 'Are you sure?', destructive: true }
      }]
    }
  }
};

Alignment with Session 56 Decisions

Session 56 decided:

This aligns because:

  1. Page-builder widgets remain visual-only (superior UI unchanged)
  2. Data operations are configured declaratively, not coded into widgets
  3. WidgetDataBindingService routes to existing backend module APIs
  4. When PhaseExecutor is integrated, generated widgets can include data binding configs

Files

Consequences

Positive:

Neutral:

Negative: