Documentation Index
Fetch the complete documentation index at: https://mintlify.com/UNOPS/UiMetadataFramework/llms.txt
Use this file to discover all available pages before exploring further.
UI Metadata Framework follows a client-server protocol where the backend generates metadata and the frontend consumes it to dynamically render forms. This guide explains how the protocol works and how to integrate it with your frontend application.
The Client-Server Protocol
UIML uses a simple JSON-based protocol:
- Metadata Request: Client requests metadata for a form
- Metadata Response: Server responds with form metadata (structure, fields, validation rules)
- Form Submission: Client submits form data
- Form Response: Server responds with results and output metadata
The framework provides TypeScript classes to work with metadata in a type-safe manner. These are available in the uimf-core package.
Represents the complete metadata for a form (source:uimf-core/src/FormMetadata.ts:8):
export class FormMetadata {
constructor(metadata: any) {
for (var property of Object.keys(metadata)) {
this[property] = metadata[property];
}
this.inputFields = metadata.inputFields.map(t => new InputFieldMetadata(t));
this.outputFields = metadata.outputFields.map(t => new OutputFieldMetadata(t));
}
// Form identifier
public id: string;
// Display label
public label: string;
// Input field definitions
public inputFields: InputFieldMetadata[];
// Output field definitions
public outputFields: OutputFieldMetadata[];
// Auto-post form on load
public postOnLoad: boolean;
// Validate before auto-posting
public postOnLoadValidation: boolean;
// Custom properties for client use
public customProperties: any;
// Event handlers
public eventHandlers: EventHandlerMetadata[];
public getCustomProperty(name: string): any {
if (this.customProperties != null && this.customProperties[name]) {
return this.customProperties[name];
}
return null;
}
}
Defines metadata for a single input field (source:uimf-core/src/InputFieldMetadata.ts:6):
export class InputFieldMetadata {
constructor(metadata: any) {
for (var property of Object.keys(metadata)) {
this[property] = metadata[property];
}
}
// Field identifier
public id: string;
// Display label
public label: string;
// Component type (e.g., "text", "dropdown", "date")
public type: string;
// Whether field is required
public required: boolean;
// Whether field should be hidden
public hidden: boolean;
// Rendering order
public orderIndex: number;
// Component-specific configuration
public customProperties: any;
// Event handlers
public eventHandlers: EventHandlerMetadata[];
public getCustomProperty(name: string): any {
if (this.customProperties != null && this.customProperties[name]) {
return this.customProperties[name];
}
return null;
}
}
Defines metadata for a single output field (source:uimf-core/src/OutputFieldMetadata.ts:6):
export class OutputFieldMetadata {
constructor(metadata: any) {
for (var property of Object.keys(metadata)) {
this[property] = metadata[property];
}
// Special case for paginated-data columns
if (this.customProperties != null && this.customProperties.columns != null) {
for (let columnPropertyName in this.customProperties.columns) {
let metadataAsJsonObject = this.customProperties.columns[columnPropertyName];
this.customProperties.columns[columnPropertyName] =
new OutputFieldMetadata(metadataAsJsonObject);
}
}
}
// Field identifier
public id: string;
// Display label
public label: string;
// Component type (e.g., "text", "table", "chart")
public type: string;
// Whether field should be hidden
public hidden: boolean;
// Rendering order
public orderIndex: number;
// Component-specific configuration
public customProperties: any;
// Event handlers
public eventHandlers: EventHandlerMetadata[];
public getCustomProperty(name: string): any {
if (this.customProperties != null && this.customProperties[name]) {
return this.customProperties[name];
}
return null;
}
}
Represents the server’s response after form submission (source:uimf-core/src/FormResponse.ts:6):
export class FormResponse extends Object {
// Additional metadata for rendering results
metadata: FormResponseMetadata;
}
Here’s what the metadata looks like when serialized (from README.md):
{
"id": "UiMetadataFramework.Web.Forms.AddNumbers",
"label": "Add 2 numbers",
"inputFields": [
{
"id": "Number1",
"label": "First number",
"type": "number",
"required": true,
"hidden": false,
"orderIndex": 0,
"customProperties": null,
"eventHandlers": []
},
{
"id": "Number2",
"label": "Second number",
"type": "number",
"required": true,
"hidden": false,
"orderIndex": 0,
"customProperties": null,
"eventHandlers": []
}
],
"outputFields": [
{
"id": "Result",
"label": "Result of your calculation",
"type": "number",
"hidden": false,
"orderIndex": 0,
"customProperties": null,
"eventHandlers": []
}
],
"postOnLoad": false,
"postOnLoadValidation": true,
"customProperties": null,
"eventHandlers": []
}
Implementing a Client
To create a UIMF client, you need to:
- Fetch metadata from the server
- Render input fields based on field type
- Handle form submission
- Render output fields based on field type
import { FormMetadata } from 'uimf-core';
async function getFormMetadata(formId: string): Promise<FormMetadata> {
const response = await fetch(`/api/forms/${formId}/metadata`);
const json = await response.json();
return new FormMetadata(json);
}
Create a component registry that maps component types to React/Vue/Svelte components:
// Component registry
const inputComponents = {
'text': TextInput,
'number': NumberInput,
'dropdown': DropdownInput,
'date': DateInput,
'datetime': DateTimeInput,
'boolean': CheckboxInput,
// ... more components
};
// Render function
function renderInputField(field: InputFieldMetadata) {
const Component = inputComponents[field.type];
if (!Component) {
throw new Error(`Unknown input component type: ${field.type}`);
}
return (
<Component
id={field.id}
label={field.label}
required={field.required}
hidden={field.hidden}
config={field.customProperties}
eventHandlers={field.eventHandlers}
/>
);
}
async function submitForm(formId: string, data: any): Promise<any> {
const response = await fetch(`/api/forms/${formId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return await response.json();
}
Step 4: Rendering Output Fields
const outputComponents = {
'text': TextOutput,
'number': NumberOutput,
'table': TableOutput,
'action-list': ActionListOutput,
'paginated-data': PaginatedDataOutput,
// ... more components
};
function renderOutputField(field: OutputFieldMetadata, value: any) {
const Component = outputComponents[field.type];
if (!Component) {
throw new Error(`Unknown output component type: ${field.type}`);
}
return (
<Component
id={field.id}
label={field.label}
value={value}
hidden={field.hidden}
config={field.customProperties}
eventHandlers={field.eventHandlers}
/>
);
}
Reference Implementation: uimf-svelte
The uimf-svelte project is a full-featured UIMF client built with Svelte. It demonstrates:
- Complete component library (inputs and outputs)
- Event handler system
- Form validation
- Dynamic form rendering
- Type-safe metadata handling
Key Features
Component Architecture:
// Each component receives metadata and emits events
export let field: InputFieldMetadata;
export let value: any;
export let form: FormInstance;
// Component-specific config from customProperties
const config = field.customProperties;
Event Handlers:
import { EventHandlerMetadata } from 'uimf-core';
// Run event handlers at specific lifecycle points
function runEventHandlers(runAt: string) {
field.eventHandlers
.filter(h => h.runAt === runAt)
.forEach(h => executeHandler(h));
}
Dynamic Forms:
{#each metadata.inputFields as field}
<Component
this={getInputComponent(field.type)}
{field}
bind:value={formData[field.id]}
/>
{/each}
Custom Properties Pattern
Components use customProperties to pass configuration from server to client:
// Server-side
public class DropdownAttribute : ComponentConfigurationAttribute
{
[ConfigurationProperty("Items")]
public List<DropdownItem> Items { get; set; }
[ConfigurationProperty("Source")]
public string Source { get; set; }
}
// Client-side
function DropdownInput({ field }) {
const items = field.customProperties.Items;
const source = field.customProperties.Source;
// Render dropdown with items or fetch from source
}
Event Handlers
Event handlers allow server-defined client-side behavior:
// Server-side
[BindToOutput("OutputField", "on-change")]
public DropdownValue<int> Category { get; set; }
// Client-side
function handleChange(value: any) {
field.eventHandlers
.filter(h => h.runAt === 'on-change')
.forEach(handler => {
// Execute handler logic
if (handler.customProperties.BindTo) {
bindFieldToOutput(handler.customProperties.BindTo);
}
});
}
Best Practices
- Use TypeScript classes: Import from
uimf-core for type safety
- Component registry pattern: Map component types to UI components
- Handle unknown types gracefully: Provide fallback components or error states
- Respect field metadata: Honor
hidden, required, orderIndex properties
- Implement event handlers: Support the event handler system for dynamic behavior
- Custom properties documentation: Document what custom properties your components expect
- Validate on client: Use
required and other metadata for client-side validation
- Preserve type information: The
type property determines which component to render
Testing Client Integration
import { FormMetadata, InputFieldMetadata } from 'uimf-core';
describe('Form Rendering', () => {
it('renders input fields', () => {
const metadata = new FormMetadata({
id: 'test-form',
label: 'Test',
inputFields: [
{
id: 'name',
label: 'Name',
type: 'text',
required: true,
hidden: false,
orderIndex: 0
}
],
outputFields: []
});
const component = render(<Form metadata={metadata} />);
expect(component.getByLabelText('Name')).toBeInTheDocument();
expect(component.getByLabelText('Name')).toBeRequired();
});
});
Next Steps