INVYONE Component Spec v1.0
Draft — 2026-04-08 — Principle: Universality, Compatibility, Extensibility
Core Problem: In vex, ColumnConfig (354 lines), FilterConfig, and FormField are all different specs.
Data conversion between components causes errors. invyone unifies everything into FieldConfig (~30 lines).
Design Principles
1
One FieldConfig, Everywhere
Table columns, form inputs, search filters — all use the same FieldConfig. No conversion needed between components.
2
DataPort Communication
Components don't reference each other directly. Standard DataPorts handle all data exchange via output → input matching.
3
3-Layer Config
BaseConfig (common) → TypeConfig (per-type) → InstanceConfig (per-instance override). Clean separation.
Data Flow Overview
[table_type_columns DB]
| (auto load)
[Template.fields: FieldConfig[]] ← single source of truth
| (shared)
+----+----+
| | |
List Create Edit ← 3 views share same fields
| | |
[Component[]] ← each view's component array
|
[DataPort connections] ← Search → Table → Form auto flow
|
[Renderer] ← FieldConfig.type → appropriate UI
Extension Points
+ New FieldType
1. Add to FieldType union
2. Add renderer for table/form/search
Done. FieldConfig structure unchanged.
+ New ComponentType
1. Add to ComponentType union
2. Define TypeConfig
3. Define default DataPorts
4. Implement renderer
Done. No impact on existing specs.
+ New ActionType
1. Add to ActionType union
2. Implement handler
Done.
FieldType (10 Types)
Each FieldType determines how a field renders across table, form, and search contexts.
FieldConfig Properties
All properties of the unified FieldConfig interface (~30 lines replacing vex's 354-line ColumnConfig).
| Property |
Type |
Required |
Default |
Description |
Used By |
FieldConfig Live Editor
Edit the JSON on the left to see how the field renders as table cell, form input, and search filter.
ComponentType (11 Types)
Click a type to view its TypeConfig details.
Component Base Structure
Every component, regardless of type, follows this structure.
Component
+-- id: string // unique identifier
+-- type: ComponentType // 11 types
+-- label: string // display name in builder
+-- position: Position // { x, y, w, h } on grid
+-- responsive?: Responsive // { lg?, md?, sm? } overrides
+-- dataSource?: DataSource // { table, mode }
+-- fields?: FieldConfig[] // the ONE field spec
+-- inputs?: DataPort[] // received data
+-- outputs?: DataPort[] // emitted data
+-- config: TypeConfig // type-specific settings
Position
| Property | Type | Description |
x | number | Horizontal position (grid units) |
y | number | Vertical position (grid units) |
w | number | Width (grid units) |
h | number | Height (grid units) |
Responsive Override
Breakpoint-specific position/size overrides. Only changed properties need to be specified.
| Breakpoint | Condition | Type |
lg | ≥ 1200px | Partial<Position> |
md | ≥ 768px | Partial<Position> |
sm | < 768px | Partial<Position> |
DataSource
| Property | Type | Description |
table | string | Binding target table name |
mode | 'read' | 'write' | 'readwrite' | Access mode |
TypeConfig Details
Select a component type to see its specific configuration.
DataPortType (4 Types)
DataPort standardizes all inter-component communication.
■
row
Single record: Record<string, any>
E.g., a selected table row passed to a form.
Record<string, any>
■■
rows
Multiple records: Record<string, any>[]
E.g., selected rows for bulk delete.
Record<string, any>[]
◆
value
Single value: any
E.g., a button click event or total count.
any
★
params
Search parameters: Record<string, any>
E.g., search filters passed to a table.
Record<string, any>
DataPort Interface
TypeScriptinterface DataPort {
name: string; // port name (e.g. 'selectedRow')
type: DataPortType; // row | rows | value | params
connectedTo?: string; // connected component ID
}
Connection Interface
TypeScriptinterface Connection {
id: string;
from: { componentId: string; port: string; };
to: { componentId: string; port: string; };
}
Default Ports by Component Type
Each component type has predefined input and output ports.
| Component |
Default Outputs |
Default Inputs |
DataPort Flow Diagram
Hover over components to highlight connected ports. Data dots animate along connections.
How it works: The builder visually sets connections. At runtime, an event bus automatically routes data through output → input matching. All data is Record<string, any> — no conversion needed.
Template Structure
One screen = one Template. List + create popup + edit popup in a single bundle.
Template
+-- templateId: string
+-- name: string
+-- category: string // sales, production, purchase...
+-- description?: string
+-- primaryTable: string // main binding table
+-- fields: FieldConfig[] // *** single source of truth ***
+-- views
| +-- list: ViewConfig // list screen
| +-- create: ViewConfig // create popup
| +-- edit: ViewConfig // edit popup (can extend create)
+-- connections: Connection[] // DataPort wiring
+-- companyCode: string
+-- version: number
+-- status: 'draft' | 'published'
+-- createdAt: string
+-- updatedAt: string
ViewConfig
| Property | Type | Description |
components | Component[] | Components placed in this view |
extends? | 'create' | Edit view inherits from create view |
size? | { w: number; h: number } | Popup dimensions (for create/edit) |
Key insight: Template.fields is the single source of truth. All three views (list, create, edit) share the same field definitions. Each view's components reference these shared fields — no duplication.
3 Views Visualization
Switch between list, create, and edit views to see their typical component layout.
List View
Create View
Edit View
vex vs invyone Comparison
Side-by-side comparison of the two approaches.
| Aspect |
vex |
invyone |
| Field Spec |
ColumnConfig (354 lines) + FilterConfig + FormField — all different |
FieldConfig (~30 lines) — one spec for everything |
| Inter-component Communication |
Custom implementation per component, all different |
DataPort standard protocol — output → input matching |
| Component Types |
33 types (v2-*), each with unique spec |
11 types, shared base + concise TypeConfig |
| Table Config |
TableListConfig — 354 lines |
TableConfig — ~15 lines |
| Screen Structure |
List / Create / Edit = 3 separate screens |
1 Template with 3 embedded views |
| Field Sharing |
Defined separately per screen |
Template.fields shared by all views |
| Data Transfer |
Custom events per component — conversion required |
output → input auto-matching — no conversion |
Code Comparison: Field Definition
// vex: ColumnConfig has 354 lines in types.ts
// Same "order_date" field needs 3 different definitions:
// 1. For table (ColumnConfig):
{ columnName: 'order_date',
header: 'Order Date',
dataType: 'date',
sortable: true,
filterable: true,
width: 120,
format: 'YYYY-MM-DD',
align: 'center',
// ... 20+ more properties }
// 2. For form (FormField) - different structure!
{ name: 'order_date',
label: 'Order Date',
inputType: 'datepicker',
required: true,
// ... different props }
// 3. For search (FilterConfig) - yet another!
{ field: 'order_date',
filterType: 'dateRange',
// ... different again }
// invyone: ONE definition for everything
{
column: 'order_date',
label: 'Order Date',
type: 'date',
visible: true,
order: 3,
required: true,
editable: true,
format: 'YYYY-MM-DD',
searchable: true,
sortable: true
}
// Table: renders as <td>2026-04-08</td>
// Form: renders as <DatePicker />
// Search: renders as <DateRangePicker />
//
// Same object. No conversion.
// The renderer decides how to display it.
Data Transfer Comparison
// Table row click → form load
// Need to convert ColumnConfig row to FormField format
const formData = convertTableRowToFormData(
selectedRow,
columnConfigs, // table format
formFields // form format
);
// Conversion code = error-prone
// What if a field name doesn't match?
// Table row click → form load
// DataPort sends the row as-is
// table.output(selectedRow) → form.input(loadRow)
// Both use the same FieldConfig
// Same column names, same types
// The renderer just switches mode:
// table mode → display cell
// form mode → input widget