@burgantech/pseudo-ui
v0.1.1
Published
Server-Driven UI rendering engine with framework adapters. JSON Schema + Pseudo UI → platform widgets.
Readme
@burgantech/pseudo-ui
A Server-Driven UI (SDUI) rendering engine for TypeScript. Define your UI once with JSON — render it natively on any platform.
The SDK pairs a JSON Schema data contract with a View JSON component tree to produce fully interactive forms, summaries, and multi-step workflows without shipping new client code.
Features
- 30+ Material Design 3 components — TextField, Dropdown, DatePicker, TabView, Dialog, Stepper, Carousel, and more
- Delegate-driven architecture — the SDK never makes HTTP calls; your app provides data via a simple interface
- Expression engine —
$form,$instance,$param,$ui,$lov,$lookup,$schema,$item,$contextnamespaces for dynamic value resolution - Conditional engine —
showIf/hideIf/enableIf/disableIfwithallOf,anyOf,notcompound rules and 13 operators - Validation engine — JSON Schema validation (
pattern,format,minLength,min/max) plus async custom validation via delegate - LOV & Lookup — List-of-Values dropdowns with cascade filtering and real-time data enrichment
- Nested components — reusable sub-components with isolated contexts, input contracts (
x-binding), and two-way data flow $uistate — transient UI state (dialog visibility, active tab) that never pollutes form data- Accessibility —
aria-required,aria-invalid,aria-labelon all inputs;role="alert"on errors - Error boundaries —
ComponentandForEachnodes are wrapped to prevent child crashes from taking down the app - Multi-language — all labels, errors, and enum values support
{ "en": "...", "tr": "...", "ar": "..." } - View & ViewModel vocabularies — included JSON Schema definitions for tooling and IDE auto-complete
Installation
npm install @burgantech/pseudo-uiPeer dependencies
The Vue adapter requires these in your project:
npm install vue@^3.5 primevue@^4.5 @primeuix/themes@^2 primeicons@^7Quick Start
Two steps: first a minimal form, then we enrich it with a nested component and a dropdown. Each step is self-contained.
Step 1: Minimal form
Name, surname, birth date, and a Submit button. No dropdowns, no nested components — just schema, view, and a minimal delegate.
Schema:
{
"$schema": "https://amorphie.io/meta/view-model-vocabulary",
"$id": "urn:amorphie:res:schema:demo:quick-start",
"type": "object",
"required": ["name", "surname"],
"properties": {
"name": { "type": "string", "minLength": 1, "x-labels": { "en": "First Name", "tr": "Ad" } },
"surname": { "type": "string", "minLength": 1, "x-labels": { "en": "Surname", "tr": "Soyad" } },
"birthDate": { "type": "string", "format": "date", "x-labels": { "en": "Date of Birth", "tr": "Doğum Tarihi" } }
}
}View:
{
"$schema": "https://amorphie.io/meta/view-vocabulary/1.0",
"dataSchema": "urn:amorphie:res:schema:demo:quick-start",
"view": {
"type": "Column",
"gap": "md",
"children": [
{ "type": "TextField", "bind": "name" },
{ "type": "TextField", "bind": "surname" },
{ "type": "DatePicker", "bind": "birthDate" },
{ "type": "Button", "label": { "en": "Submit", "tr": "Gönder" }, "variant": "filled", "action": "submit" }
]
}
}Delegate + render: requestData and loadComponent are never called here, so we stub them to throw. Only onAction matters.
<script setup lang="ts">
import { provideDelegate } from '@burgantech/pseudo-ui/vue'
import { PseudoView } from '@burgantech/pseudo-ui/vue'
import '@burgantech/pseudo-ui/vue/style.css'
import type { PseudoViewDelegate, DataSchema, ViewDefinition } from '@burgantech/pseudo-ui'
const schema: DataSchema = {
$schema: 'https://amorphie.io/meta/view-model-vocabulary',
$id: 'urn:amorphie:res:schema:demo:quick-start',
type: 'object',
required: ['name', 'surname'],
properties: {
name: { type: 'string', minLength: 1, 'x-labels': { en: 'First Name', tr: 'Ad' } },
surname: { type: 'string', minLength: 1, 'x-labels': { en: 'Surname', tr: 'Soyad' } },
birthDate: { type: 'string', format: 'date', 'x-labels': { en: 'Date of Birth', tr: 'Doğum Tarihi' } },
},
}
const view: ViewDefinition = {
$schema: 'https://amorphie.io/meta/view-vocabulary/1.0',
dataSchema: 'urn:amorphie:res:schema:demo:quick-start',
view: {
type: 'Column',
gap: 'md',
children: [
{ type: 'TextField', bind: 'name' },
{ type: 'TextField', bind: 'surname' },
{ type: 'DatePicker', bind: 'birthDate' },
{ type: 'Button', label: { en: 'Submit', tr: 'Gönder' }, variant: 'filled', action: 'submit' },
],
},
}
const delegate: PseudoViewDelegate = {
async requestData() { throw new Error('No LOV in this example — add a dropdown to trigger this') },
async loadComponent() { throw new Error('No nested components — add type: "Component" to trigger this') },
async onAction(action, formData) {
if (action === 'submit') console.log('Form submitted:', formData)
},
}
</script>
<template>
<PseudoView :schema="schema" :view="view" lang="en" :delegate="delegate" />
</template>Step 2: Add a continent dropdown (nested component + LOV)
We extend the form with a continent field. The dropdown lives in a nested sub-component and gets its options via requestData — so you see both loadComponent (nested UI) and requestData (LOV data) in action. All data is inline.
Add to main schema:
"continent": { "type": "string", "x-labels": { "en": "Continent you live in", "tr": "Yaşadığınız kıta" } }Add to main view (between DatePicker and Button):
{
"type": "Component",
"ref": "continent-selector",
"bind": { "continent": "$form.continent" }
}Sub-component schema (continent-selector — has x-lov, so the SDK calls requestData("get-continents")):
{
"$schema": "https://amorphie.io/meta/view-model-vocabulary",
"$id": "urn:amorphie:res:schema:demo:continent-selector",
"type": "object",
"required": ["continent"],
"properties": {
"continent": {
"type": "string",
"x-labels": { "en": "Continent you live in", "tr": "Yaşadığınız kıta" },
"x-lov": {
"source": "get-continents",
"valueField": "$.response.data.code",
"displayField": "$.response.data.name"
}
}
}
}Sub-component view:
{
"$schema": "https://amorphie.io/meta/view-vocabulary/1.0",
"dataSchema": "urn:amorphie:res:schema:demo:continent-selector",
"view": { "type": "Dropdown", "bind": "continent" }
}Full delegate (replace the stubs with real implementations):
<script setup lang="ts">
import { provideDelegate } from '@burgantech/pseudo-ui/vue'
import { PseudoView } from '@burgantech/pseudo-ui/vue'
import '@burgantech/pseudo-ui/vue/style.css'
import type { PseudoViewDelegate, DataSchema, ViewDefinition } from '@burgantech/pseudo-ui'
const CONTINENTS = [
{ code: 'eu', name: 'Europe' },
{ code: 'as', name: 'Asia' },
{ code: 'na', name: 'North America' },
{ code: 'sa', name: 'South America' },
{ code: 'af', name: 'Africa' },
{ code: 'oc', name: 'Oceania' },
{ code: 'an', name: 'Antarctica' },
]
const mainSchema: DataSchema = {
$schema: 'https://amorphie.io/meta/view-model-vocabulary',
$id: 'urn:amorphie:res:schema:demo:quick-start',
type: 'object',
required: ['name', 'surname'],
properties: {
name: { type: 'string', minLength: 1, 'x-labels': { en: 'First Name', tr: 'Ad' } },
surname: { type: 'string', minLength: 1, 'x-labels': { en: 'Surname', tr: 'Soyad' } },
birthDate: { type: 'string', format: 'date', 'x-labels': { en: 'Date of Birth', tr: 'Doğum Tarihi' } },
continent: { type: 'string', 'x-labels': { en: 'Continent you live in', tr: 'Yaşadığınız kıta' } },
},
}
const continentSchema: DataSchema = {
$schema: 'https://amorphie.io/meta/view-model-vocabulary',
$id: 'urn:amorphie:res:schema:demo:continent-selector',
type: 'object',
required: ['continent'],
properties: {
continent: {
type: 'string',
'x-labels': { en: 'Continent you live in', tr: 'Yaşadığınız kıta' },
'x-lov': { source: 'get-continents', valueField: '$.response.data.code', displayField: '$.response.data.name' },
},
},
}
const mainView: ViewDefinition = {
$schema: 'https://amorphie.io/meta/view-vocabulary/1.0',
dataSchema: 'urn:amorphie:res:schema:demo:quick-start',
view: {
type: 'Column',
gap: 'md',
children: [
{ type: 'TextField', bind: 'name' },
{ type: 'TextField', bind: 'surname' },
{ type: 'DatePicker', bind: 'birthDate' },
{ type: 'Component', ref: 'continent-selector', bind: { continent: '$form.continent' } },
{ type: 'Button', label: { en: 'Submit', tr: 'Gönder' }, variant: 'filled', action: 'submit' },
],
},
}
const delegate: PseudoViewDelegate = {
async requestData(ref, params) {
if (ref === 'get-continents') return { response: { data: CONTINENTS } }
throw new Error(`Unknown data source: ${ref}`)
},
async loadComponent(ref) {
if (ref === 'continent-selector') {
return {
schema: continentSchema,
view: { $schema: 'https://amorphie.io/meta/view-vocabulary/1.0', dataSchema: 'urn:amorphie:res:schema:demo:continent-selector', view: { type: 'Dropdown', bind: 'continent' } },
}
}
throw new Error(`Unknown component: ${ref}`)
},
async onAction(action, formData) {
if (action === 'submit') console.log('Form submitted:', formData)
},
}
</script>
<template>
<PseudoView :schema="mainSchema" :view="mainView" lang="en" :delegate="delegate" />
</template>Step 1 gives you the basics. Step 2 shows how to add a nested component and LOV — requestData serves dropdown options, loadComponent serves the sub-component.
Initial data (optional)
You can pass initial values when the view first renders:
| Prop | Purpose |
|---|---|
| formData | Pre-fill editable fields (e.g. user draft, edit mode). Fields are bound to $form and can be changed by the user. |
| instanceData | Backend/persisted data (e.g. read-only display, lookup filters). Used by $instance expressions and summary views. |
<PseudoView
:schema="schema"
:view="view"
:form-data="{ name: 'Jane', surname: 'Doe', birthDate: '1990-05-15' }"
:instance-data="{ status: 'active', createdAt: '2024-01-01' }"
lang="en"
:delegate="delegate"
/>formData is for user-editable data. instanceData is for backend state that drives display and lookups — both are optional and merged when the view mounts.
Lookups (enrichment)
When a schema property has x-lookup, the SDK fetches enrichment data via requestData. You must activate the lookup by listing it in the view's lookups array — otherwise it won't run.
Schema (defines the lookup):
{
"branchDetail": {
"type": "object",
"x-lookup": {
"source": "get-branch-details",
"resultField": "$.response.data",
"filter": [{ "param": "branchCode", "value": "$param.selectedBranchCode", "required": true }]
}
}
}View (activates it):
{
"$schema": "https://amorphie.io/meta/view-vocabulary/1.0",
"dataSchema": "urn:amorphie:res:schema:shared:branch-info",
"lookups": ["branchDetail"],
"view": { ... }
}Then use $lookup.branchDetail.address, $lookup.branchDetail.phone, etc. in Text or other components. The SDK calls requestData(source, filterParams) when the view mounts; the delegate returns the enrichment payload.
Package Exports
| Import path | Content |
|---|---|
| @burgantech/pseudo-ui | Core engine: types, expression resolver, schema resolver, conditional engine |
| @burgantech/pseudo-ui/vue | Vue 3 adapter: PseudoView, DynamicRenderer, provideDelegate |
| @burgantech/pseudo-ui/vue/style.css | Component styles (Material Design 3 tokens) |
| @burgantech/pseudo-ui/vocabularies/view-vocabulary.json | View meta-schema (UI component tree definition) |
| @burgantech/pseudo-ui/vocabularies/view-model-vocabulary.json | ViewModel meta-schema (JSON Schema x- extensions) |
Architecture
┌─────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ ┌──────────────┐ implements ┌────────────────┐ │
│ │ App.vue │ ──────────────▶│ Delegate │ │
│ │ │ │ - requestData │ │
│ │ <PseudoView │ │ - loadComponent│ │
│ │ :schema │ │ - onAction │ │
│ │ :view │ │ - onLog │ │
│ │ lang="en"/>│ └────────────────┘ │
│ └──────┬───────┘ ▲ │
│ │ provides │ │
├─────────┼────────────────────────────────┼──────────┤
│ SDK │ │ │
│ ▼ │ │
│ ┌─────────────────┐ ┌──────────────┐ │ │
│ │ DynamicRenderer │ │ Expression │ │ │
│ │ (recursive) │ │ Resolver │ │ │
│ │ │ ├──────────────┤ │ │
│ │ 30+ MD3 widgets │ │ Conditional │ │ │
│ │ via PrimeVue 4 │ │ Engine │ │ │
│ │ │ ├──────────────┤ │ │
│ │ ErrorBoundary │ │ Schema │ │ │
│ │ for Component & │ │ Resolver │◀──┘ │
│ │ ForEach │ │ (validation) │ │
│ └─────────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────┘Data Model (MVVM)
| Layer | File | Purpose |
|---|---|---|
| ViewModel | schema.json | Data contract — field types, validation, LOV sources, conditionals, multi-lang labels |
| View | view.json | UI component tree — layout, binding, actions, transient UI state |
| Model | Backend | Persisted data, served via delegate's requestData |
Delegate Interface
interface PseudoViewDelegate {
/** Fetch data from backend (LOV items, lookup enrichment). ref = source (required); params = resolved filter from x-lookup (optional when no filter). */
requestData(ref: string, params?: Record<string, string>): Promise<unknown>
/** Load a nested component's schema + view by reference */
loadComponent(ref: string): Promise<{ schema: DataSchema; view: ViewDefinition }>
/** Handle user actions (submit, cancel, back, custom commands) */
onAction(action: string, formData: Record<string, unknown>, command?: string): Promise<void>
/** Optional: custom async validation after built-in checks pass */
onValidationRequest?(field: string, value: unknown, formData: Record<string, unknown>): Promise<string | null>
/** Optional: capture SDK logs (debug, info, warn, error) */
onLog?(level: LogLevel, message: string, error?: unknown, context?: Record<string, unknown>): void
}Supported Components
Layout
Column · Row · Stack · Grid · Expanded · SizedBox · Divider · Spacer
Input
TextField · TextArea · NumberField · Dropdown · Checkbox · RadioGroup · DatePicker · TimePicker · Switch · Slider · SegmentedButton · SearchField · AutoComplete
Display
Text · Image · Chip · ListTile · Avatar · RichText · LoadingIndicator
Surface & Overlay
Card · Dialog · BottomSheet · SideSheet · Snackbar · Tooltip
Navigation
TabView · AppBar · NavigationBar · NavigationDrawer
Container
ExpansionPanel · Stepper
Action
Button · IconButton · FAB · Menu · Toolbar · Carousel
Control
ForEach · Component (nested)
Expression Namespaces
| Namespace | Source | Example |
|---|---|---|
| $form.field | User input data | $form.firstName |
| $instance.field | Backend persisted data | $instance.status |
| $param.field | Parent-bound data (nested components) | $param.cityCode |
| $ui.key | Transient UI state (not submitted) | $ui.showDialog |
| $schema.field.label | Schema label for current language | $schema.city.label |
| $lov.field | LOV items array | $lov.city |
| $lov.field.display | Localized display name for current value | $lov.city.display |
| $lookup.prop.field | Enrichment data | $lookup.branch.address |
| $item.field | ForEach iteration item | $item.name |
| $context.lang | Runtime context | $context.lang |
Conditional Operators
equals · notEquals · in · notIn · greaterThan · lessThan · greaterThanOrEquals · lessThanOrEquals · contains · startsWith · endsWith · isEmpty · isNotEmpty
Compound rules: allOf (AND), anyOf (OR), not (negate) — recursive nesting supported.
Validation Formats
Built-in format validators: email · uri / url · date · date-time · time · phone / tel · iban
Vocabularies
The package includes JSON Schema vocabulary definitions for IDE auto-complete and tooling:
// Import as JSON modules
import viewVocab from '@burgantech/pseudo-ui/vocabularies/view-vocabulary.json'
import viewModelVocab from '@burgantech/pseudo-ui/vocabularies/view-model-vocabulary.json'- View Vocabulary — defines all valid component types, properties, and their constraints
- ViewModel Vocabulary — defines all
x-*extensions (x-labels,x-lov,x-conditional, etc.)
Cross-Platform
This package provides the TypeScript/Vue implementation. A Dart/Flutter package is planned and will render the same JSON schemas and views using Material 3 widgets. Both will share the same vocabulary definitions, ensuring consistent behavior across platforms.
License
MIT
