Skip to content

Latest commit

 

History

History
883 lines (729 loc) · 21.4 KB

File metadata and controls

883 lines (729 loc) · 21.4 KB
title Widget Contract
description Standard props, events, and lifecycle for ObjectUI components

import { Component, Zap, Code, Check, AlertCircle, Globe } from 'lucide-react';

Widget Contract: Component Specification

The Widget Contract defines the standard interface that all ObjectUI components must implement. This contract ensures consistency across renderers and enables components to be composed, configured, and extended predictably.

Philosophy: Props Down, Events Up

ObjectUI follows React's unidirectional data flow pattern:

┌─────────────────────────────────────┐
│         Parent Component            │
│  (Form, Section, Container)         │
└─────────────┬───────────────────────┘
              │
              │ Props ↓ (data, config, callbacks)
              │
┌─────────────▼───────────────────────┐
│       Child Component               │
│  (TextField, Select, Button)        │
└─────────────┬───────────────────────┘
              │
              │ Events ↑ (onChange, onClick, onBlur)
              │
┌─────────────▼───────────────────────┐
│         Parent Component            │
│  (Updates state, triggers actions)  │
└─────────────────────────────────────┘

Key Principles:

  • Props are immutable: Components receive props, never modify them
  • Events are declarative: Components emit events, parent handles them
  • State is lifted: Form state lives in parent, not in field components

Standard Component Props

Every ObjectUI component receives a standard set of base props.

BaseComponentProps

interface BaseComponentProps {
  // Identification
  id: string;                    // Unique component ID
  name: string;                  // Field name (for form submission)
  
  // Display
  label?: string;                // Display label
  placeholder?: string;          // Placeholder text
  helpText?: string;             // Helper text below field
  tooltip?: string;              // Tooltip on hover
  icon?: string;                 // Lucide icon name
  
  // State
  value?: any;                   // Current value
  defaultValue?: any;            // Initial value
  disabled?: boolean;            // Disabled state
  readonly?: boolean;            // Read-only state
  required?: boolean;            // Required field marker
  
  // Validation
  error?: string;                // Error message
  warning?: string;              // Warning message
  valid?: boolean;               // Validation state
  
  // Layout
  className?: string;            // CSS classes
  style?: CSSProperties;         // Inline styles
  span?: number;                 // Grid column span (1-12)
  hidden?: boolean;              // Visibility
  
  // Accessibility
  ariaLabel?: string;            // ARIA label
  ariaDescribedBy?: string;      // ARIA description
  tabIndex?: number;             // Tab order
  
  // Events
  onChange?: (value: any) => void;
  onBlur?: () => void;
  onFocus?: () => void;
  onClick?: () => void;
}

Example: TextField Component

# YAML Configuration
field:
  name: customer_email
  type: email
  label: Email Address
  placeholder: you@company.com
  helpText: We'll never share your email
  required: true
  icon: mail

Rendered as (React):

<TextField
  id="field_customer_email"
  name="customer_email"
  type="email"
  label="Email Address"
  placeholder="you@company.com"
  helpText="We'll never share your email"
  required={true}
  icon="mail"
  value={formData.customer_email}
  error={errors.customer_email}
  onChange={(value) => setFormData({ ...formData, customer_email: value })}
  onBlur={() => validateField('customer_email')}
/>

Field-Specific Props

Different field types have specialized props.

TextField Props

interface TextFieldProps extends BaseComponentProps {
  type: 'text' | 'email' | 'tel' | 'url' | 'password';
  maxLength?: number;           // Character limit
  minLength?: number;           // Minimum characters
  pattern?: string;             // Regex validation pattern
  autocomplete?: string;        // Browser autocomplete hint
  spellcheck?: boolean;         // Enable spell check
}

Example:

field:
  name: company_name
  type: text
  maxLength: 100
  autocomplete: organization
  spellcheck: true

TextArea Props

interface TextAreaProps extends BaseComponentProps {
  rows?: number;                // Visible rows
  maxLength?: number;           // Character limit
  resize?: 'none' | 'vertical' | 'horizontal' | 'both';
  autoGrow?: boolean;           // Auto-expand on content
}

Example:

field:
  name: description
  type: textarea
  rows: 5
  maxLength: 1000
  autoGrow: true
  resize: vertical

Select Props

interface SelectProps extends BaseComponentProps {
  options: Option[];            // Available options
  multiple?: boolean;           // Multi-select
  searchable?: boolean;         // Filterable options
  clearable?: boolean;          // Show clear button
  placeholder?: string;         // Placeholder when empty
  
  // Async options
  optionsSource?: string;       // URL to fetch options
  optionsDependsOn?: string[];  // Refresh when these fields change
}

interface Option {
  value: string | number;
  label: string;
  icon?: string;
  disabled?: boolean;
  group?: string;               // Option group
}

Example:

field:
  name: country
  type: select
  options:
    - { value: US, label: United States, icon: flag-us }
    - { value: CA, label: Canada, icon: flag-ca }
    - { value: UK, label: United Kingdom, icon: flag-gb }
  searchable: true
  clearable: true

Lookup Props (Reference Field)

interface LookupProps extends BaseComponentProps {
  object: string;               // Target object name
  displayField: string;         // Field to display (e.g., 'name')
  searchFields: string[];       // Fields to search
  filters?: FilterExpression;   // Pre-filter options
  multiple?: boolean;           // Multi-select
  createNew?: boolean;          // Allow creating new records
  
  // Modal configuration
  modalTitle?: string;
  modalColumns?: string[];      // Columns to show in picker modal
}

Example:

field:
  name: account_id
  type: lookup
  object: account
  displayField: name
  searchFields: [name, email, phone]
  filters:
    status: active
  createNew: true
  modalTitle: Select Account
  modalColumns: [name, industry, city]

Boolean Props (Checkbox/Toggle)

interface BooleanProps extends BaseComponentProps {
  variant: 'checkbox' | 'toggle' | 'switch';
  labelPosition?: 'left' | 'right';
  trueLabel?: string;           // Label when true (for toggles)
  falseLabel?: string;          // Label when false
}

Example:

field:
  name: email_notifications
  type: boolean
  variant: toggle
  trueLabel: Enabled
  falseLabel: Disabled
  labelPosition: right

Date/DateTime Props

interface DateProps extends BaseComponentProps {
  type: 'date' | 'datetime' | 'time';
  format?: string;              // Display format (e.g., 'MM/DD/YYYY')
  min?: string;                 // Minimum date
  max?: string;                 // Maximum date
  disabledDates?: string[];     // Specific disabled dates
  showTime?: boolean;           // Include time picker (for datetime)
  timeFormat?: '12h' | '24h';   // Time format
}

Example:

field:
  name: meeting_date
  type: datetime
  format: MM/DD/YYYY hh:mm A
  min: today
  showTime: true
  timeFormat: 12h

Number/Currency Props

interface NumberProps extends BaseComponentProps {
  type: 'number' | 'currency' | 'percent';
  min?: number;                 // Minimum value
  max?: number;                 // Maximum value
  step?: number;                // Increment step
  precision?: number;           // Decimal places
  
  // Currency-specific
  currency?: string;            // ISO currency code (USD, EUR)
  currencyDisplay?: 'symbol' | 'code' | 'name';
  
  // Percent-specific
  percentPrecision?: number;    // Decimal places for percent
}

Example:

field:
  name: unit_price
  type: currency
  currency: USD
  min: 0
  precision: 2
  currencyDisplay: symbol

Standard Events

All components emit standard lifecycle and interaction events.

Core Events

interface ComponentEvents {
  // Value changes
  onChange: (value: any, context: EventContext) => void;
  onInput: (value: any) => void;  // Real-time input (every keystroke)
  
  // Focus events
  onFocus: (context: EventContext) => void;
  onBlur: (context: EventContext) => void;
  
  // Interaction events
  onClick: (context: EventContext) => void;
  onDoubleClick: (context: EventContext) => void;
  
  // Validation events
  onValidate: (value: any) => ValidationResult;
  onError: (error: ValidationError) => void;
  
  // Lifecycle events
  onMount: () => void;
  onUnmount: () => void;
}

interface EventContext {
  fieldName: string;
  value: any;
  previousValue: any;
  formData: Record<string, any>;
  metadata: Record<string, any>;
}

Event Examples

onChange: Field Value Changed

field:
  name: quantity
  type: number
  onChange:
    - action: calculate_total
      formula: quantity * unit_price
      targetField: total_price

onBlur: Field Loses Focus

field:
  name: email
  type: email
  onBlur:
    - action: validate_email
    - action: check_duplicates
      endpoint: /api/customers/check-email

Conditional Logic on Change

field:
  name: shipping_required
  type: boolean
  onChange:
    - when: { value: true }
      show: [shipping_address, shipping_method]
    - when: { value: false }
      hide: [shipping_address, shipping_method]
      clear: [shipping_address, shipping_method]

Validation Contract

Components support declarative validation rules.

Built-in Validators

interface FieldValidation {
  // Presence
  required?: boolean | string;   // true or custom error message
  
  // String validation
  minLength?: number;
  maxLength?: number;
  pattern?: RegExp;
  email?: boolean;
  url?: boolean;
  
  // Number validation
  min?: number;
  max?: number;
  integer?: boolean;
  positive?: boolean;
  
  // Async validation
  asyncValidator?: {
    endpoint: string;
    debounce?: number;          // Debounce delay (ms)
  };
  
  // Custom validation
  custom?: (value: any, formData: any) => ValidationResult;
}

interface ValidationResult {
  valid: boolean;
  error?: string;
  warning?: string;
}

Validation Examples

Required Field

field:
  name: customer_name
  type: text
  required: Customer name is required

Pattern Validation

field:
  name: phone
  type: tel
  pattern: ^\+?[1-9]\d{1,14}$
  patternMessage: Please enter a valid phone number (E.164 format)

Async Validation (Check Uniqueness)

field:
  name: username
  type: text
  asyncValidator:
    endpoint: /api/users/check-username
    debounce: 500  # Wait 500ms after typing stops
    errorMessage: Username already taken

Cross-Field Validation

field:
  name: password_confirm
  type: password
  custom:
    validate: password_confirm === password
    errorMessage: Passwords must match

Rendering Contract

Renderers must implement specific methods to paint components.

Renderer Interface

interface ObjectUIRenderer {
  // Core rendering
  renderField(field: FieldDefinition): ReactNode;
  renderSection(section: SectionDefinition): ReactNode;
  renderForm(form: FormDefinition): ReactNode;
  
  // Component registry
  registerComponent(type: string, component: ComponentClass): void;
  getComponent(type: string): ComponentClass | undefined;
  
  // Lifecycle hooks
  beforeRender(definition: ComponentDefinition): void;
  afterRender(element: ReactNode): void;
  
  // Error handling
  renderError(error: Error, definition: ComponentDefinition): ReactNode;
  renderFallback(type: string): ReactNode;  // Unknown component type
}

Component Registration

Custom components can be registered with the renderer:

// Define custom component
const CustomRatingField: React.FC<BaseComponentProps> = ({ value, onChange }) => {
  return (
    <div className="rating-field">
      {[1, 2, 3, 4, 5].map(star => (
        <Star
          key={star}
          filled={value >= star}
          onClick={() => onChange(star)}
        />
      ))}
    </div>
  );
};

// Register with renderer
renderer.registerComponent('rating', CustomRatingField);

Use in Layout:

field:
  name: customer_satisfaction
  type: rating  # ← Custom type
  label: How satisfied are you?
  min: 1
  max: 5

Theme Contract

Components must support theme customization.

Theme Props

interface ThemeProps {
  // Colors
  colorScheme?: 'light' | 'dark' | 'auto';
  primaryColor?: string;
  secondaryColor?: string;
  errorColor?: string;
  warningColor?: string;
  successColor?: string;
  
  // Typography
  fontFamily?: string;
  fontSize?: 'small' | 'medium' | 'large';
  
  // Spacing
  density?: 'compact' | 'default' | 'spacious';
  
  // Borders
  borderRadius?: number;        // px
  borderStyle?: 'solid' | 'dashed' | 'none';
  
  // Variants
  variant?: 'outlined' | 'filled' | 'standard';
}

Theme Example

theme:
  colorScheme: light
  primaryColor: '#3B82F6'
  errorColor: '#EF4444'
  fontFamily: Inter, sans-serif
  fontSize: medium
  density: default
  borderRadius: 8
  variant: outlined

fields:
  - name: email
    type: email
    # Inherits theme automatically

Accessibility Contract

All components must meet WCAG 2.1 AA standards.

Required Accessibility Features

interface A11yProps {
  // ARIA attributes
  role?: string;                // ARIA role
  ariaLabel?: string;           // Accessible label
  ariaDescribedBy?: string;     // ID of description element
  ariaLive?: 'polite' | 'assertive' | 'off';
  ariaAtomic?: boolean;
  
  // Keyboard navigation
  tabIndex?: number;            // Tab order (-1 = not tabbable)
  onKeyDown?: (e: KeyboardEvent) => void;
  onKeyUp?: (e: KeyboardEvent) => void;
  
  // Focus management
  autoFocus?: boolean;          // Auto-focus on mount
  focusOnError?: boolean;       // Focus when error occurs
}

Accessibility Example

field:
  name: credit_card
  type: text
  label: Credit Card Number
  ariaLabel: Credit card number, 16 digits
  ariaDescribedBy: credit_card_help
  helpText:
    id: credit_card_help
    text: Enter 16-digit card number without spaces
  autocomplete: cc-number
  inputMode: numeric

Keyboard Support (Required):

  • Tab: Move to next field
  • Shift+Tab: Move to previous field
  • Enter: Submit form (on last field)
  • Escape: Cancel/clear (if applicable)
  • Arrow keys: Navigate options (Select, Radio)
  • Space: Toggle (Checkbox, Toggle)

Performance Contract

Components should optimize rendering and minimize re-renders.

Optimization Guidelines

interface PerformanceProps {
  // Lazy loading
  lazy?: boolean;               // Defer rendering until visible
  lazyPlaceholder?: ReactNode;  // Show while loading
  
  // Memoization
  memoize?: boolean;            // Cache rendered output
  memoizeKey?: string[];        // Re-render only when these props change
  
  // Virtualization
  virtualScroll?: boolean;      // Use virtual scrolling for long lists
  itemHeight?: number;          // Fixed item height (for virtual scroll)
  
  // Debouncing
  debounce?: number;            // Debounce onChange events (ms)
  throttle?: number;            // Throttle onChange events (ms)
}

Example: Large Select with Virtual Scrolling

field:
  name: product_id
  type: select
  optionsSource: /api/products  # 10,000 products
  virtualScroll: true
  itemHeight: 40
  searchable: true
  debounce: 300  # Debounce search input

Error Handling Contract

Components must gracefully handle errors and edge cases.

Error States

interface ErrorHandling {
  // Validation errors
  error?: string;               // Error message
  errorPosition?: 'top' | 'bottom' | 'inline';
  
  // Loading states
  loading?: boolean;            // Show loading indicator
  loadingText?: string;         // Loading message
  
  // Empty states
  empty?: boolean;              // No data available
  emptyText?: string;           // Empty state message
  emptyAction?: Action;         // Call-to-action when empty
  
  // Error recovery
  retry?: () => void;           // Retry failed action
  fallback?: any;               // Fallback value on error
}

Example: Lookup with Error Handling

field:
  name: manager_id
  type: lookup
  object: user
  displayField: name
  optionsSource: /api/users/managers
  loading: true
  loadingText: Loading managers...
  emptyText: No managers found
  emptyAction:
    type: standard_new
    label: Create Manager
  retry: true  # Show retry button on fetch error

Component Lifecycle

Components go through a standard lifecycle.

Lifecycle Phases

1. Initialize
   ↓
2. Mount (render for first time)
   ↓
3. Update (props/state change)
   ↓
4. Validate (on blur or submit)
   ↓
5. Unmount (cleanup)

Lifecycle Hooks

interface LifecycleHooks {
  onInit?: () => void;          // Before first render
  onMount?: () => void;         // After first render
  onUpdate?: (prevProps: Props) => void;  // After props change
  onValidate?: () => ValidationResult;    // On validation
  onUnmount?: () => void;       // Before component removed
}

Example:

field:
  name: email
  type: email
  lifecycle:
    onMount:
      - action: fetch_user_email
        endpoint: /api/user/email
    onBlur:
      - action: validate_email
      - action: check_duplicates
    onUnmount:
      - action: save_draft

Widget Types Reference

Input Widgets

Type Props Events Example
text maxLength, pattern onChange, onBlur Name, Title
textarea rows, maxLength onChange, onInput Description
email pattern onChange, onBlur Email Address
tel pattern onChange Phone Number
url pattern onChange Website
password minLength onChange Password

Selection Widgets

Type Props Events Example
select options, multiple onChange Country, Status
radio options onChange Gender, Size
checkbox - onChange Accept Terms
toggle trueLabel, falseLabel onChange Enable Feature
lookup object, displayField onChange, onSearch Account, Contact

Date/Time Widgets

Type Props Events Example
date min, max onChange Birth Date
datetime showTime onChange Appointment
time format onChange Meeting Time
daterange - onChange Start/End Date

Numeric Widgets

Type Props Events Example
number min, max, step onChange Quantity, Age
currency currency, precision onChange Price, Revenue
percent precision onChange Discount, Tax
slider min, max, step onChange Volume, Rating

Rich Content Widgets

Type Props Events Example
richtext toolbar, plugins onChange Description
markdown preview onChange Documentation
code language, theme onChange API Key, JSON
file accept, maxSize onUpload Attachment

Real-World Example: Complete Form Field

field:
  # Identification
  name: customer_email
  type: email
  
  # Display
  label: Email Address
  placeholder: you@company.com
  helpText: We'll use this for account notifications
  icon: mail
  
  # Validation
  required: Email is required
  pattern: ^[^\s@]+@[^\s@]+\.[^\s@]+$
  patternMessage: Please enter a valid email address
  asyncValidator:
    endpoint: /api/customers/check-email
    debounce: 500
    errorMessage: This email is already registered
  
  # Layout
  span: 6
  
  # Behavior
  autocomplete: email
  spellcheck: false
  
  # Events
  onChange:
    - action: validate_email
  onBlur:
    - action: check_duplicates
    - action: format_email
      transform: toLowerCase
  
  # Accessibility
  ariaLabel: Customer email address
  tabIndex: 1
  
  # Performance
  debounce: 300

What's Next?

} title="Action Protocol" href="/docs/protocols/objectui/actions" description="Define buttons, triggers, and navigation flows" /> } title="Custom Components" href="/docs/guides/custom-components" description="Build and register your own widgets" />

Related Resources