npm package: @00rez/form-runner
Description: A powerful, reusable React form rendering library built on [React JSON Schema Form (RJSF)](https://rjsf-team.github.io/react-jsonschema-form/). Render complex forms from JSON Schema with advanced features like dynamic options, event callbacks, conditional
Latest version: 1.0.6

# @00rez/form-runner

A powerful, reusable React form rendering library built on [React JSON Schema Form (RJSF)](https://rjsf-team.github.io/react-jsonschema-form/). Render complex forms from JSON Schema with advanced features like dynamic options, event callbacks, conditional logic, and custom widgets.

## Installation

```bash
npm install @00rez/form-runner react react-dom @rjsf/core @rjsf/utils @rjsf/validator-ajv8
```

## Quick Start

```tsx
import { Form } from '@00rez/form-runner';
import { createValidator } from '@rjsf/validator-ajv8';

const schema = {
  type: 'object',
  properties: {
    firstName: { type: 'string', title: 'First Name' },
    email: { type: 'string', format: 'email', title: 'Email' },
  },
  required: ['firstName', 'email'],
};

const uiSchema = {
  firstName: { 'ui:placeholder': 'Enter your first name' },
  email: { 'ui:widget': 'email' },
};

const validator = createValidator();

export default function App() {
  const [formData, setFormData] = React.useState({});

  return (
    <Form
      schema={schema}
      uiSchema={uiSchema}
      formData={formData}
      validator={validator}
      onChange={(e) => setFormData(e.formData)}
      onSubmit={(e) => console.log('Submitted:', e.formData)}
    />
  );
}
```

## Core Components

### Form

The main form rendering component. Wraps RJSF with custom widgets, templates, and theme.

**Props:**

```typescript
interface FormProps {
  id?: string;                          // Unique form ID (auto-normalized)
  schema: RJSFSchema;                   // JSON Schema defining form structure & validation
  uiSchema: UiSchema;                   // UI Schema controlling presentation
  formData: object;                     // Current form data
  validator: ValidatorType;             // JSON Schema validator (use @rjsf/validator-ajv8)
  onChange?: (data: IChangeEvent, id?: string) => void;  // Called when form data changes
  onSubmit?: (data: unknown) => void;   // Called when form is submitted
  omitExtraData?: boolean;              // Remove properties not in schema
  liveOmit?: boolean;                   // Remove extra data on change (not just submit)
  transformErrors?: ErrorTransformer | ((errors: RJSFValidationError[]) => RJSFValidationError[]);
  callbackRegistry?: EventCallbackRegistry;  // Registry for event callbacks
  formContext?: Record<string, unknown>; // Additional context passed to widgets
  children?: React.ReactNode;           // Custom buttons or elements
  theme?: ThemeProps;                   // Allow for custom theme override
}
```

**Example:**

```tsx
<Form
  id="contactForm"
  schema={schema}
  uiSchema={uiSchema}
  formData={data}
  validator={validator}
  onChange={(e) => setData(e.formData)}
  onSubmit={(e) => handleSubmit(e.formData)}
  callbackRegistry={myCallbacks}
/>
```

## Built-in Widgets

The form-runner includes custom widgets for enhanced UX:

| Widget | Usage | Description |
|--------|-------|-------------|
| `TextArea` | Text with longer content | Multi-line text input |
| `Email` | Email fields | Email validation |
| `Password` | Password fields | Masked input |
| `Toggle` | Boolean fields | Toggle switch component |
| `Checkbox` | Single boolean | Standard checkbox |
| `Checkboxes` | Multiple selections | Multi-select checkboxes |
| `Radio` | Single from multiple | Radio button group |
| `Select` | Dropdown selection | Standard select dropdown |
| `Autocomplete` | Searchable dropdown | Type-ahead autocomplete |
| `Date` | Date input | Date picker |
| `DateTime` | Date + time input | Combined date/time picker |
| `Time` | Time input | Time picker |
| `DateRange` | Date range selection | Two-date range picker |
| `DateTimeRange` | Date/time range | Combined range picker |
| `MaskedInput` | Formatted input | Phone, SSN, etc. |

**Using widgets in UI Schema:**

```tsx
const uiSchema = {
  email: { 'ui:widget': 'email' },
  password: { 'ui:widget': 'password' },
  comments: { 'ui:widget': 'textarea' },
  birthDate: { 'ui:widget': 'date' },
  agreedToTerms: { 'ui:widget': 'toggle' },
  phone: {
    'ui:widget': 'maskedInput',
    'ui:options': {
      mask: '(999) 999-9999',
    },
  },
};
```

## Advanced Features

### 1. Dynamic Options (Autocomplete/Select)

Fetch options dynamically from an API based on form data. Supports caching, debouncing, and variable interpolation.

**In UI Schema:**

```tsx
const uiSchema = {
  country: { 'ui:widget': 'select' },
  city: {
    'ui:widget': 'select',
    'ui:options': {
      apiConfig: {
        url: '/api/cities?country={{country}}',  // Interpolate form data
        method: 'GET',
        resultPath: 'data',           // Path to options array in response
        labelKey: 'name',             // Property to display
        valueKey: 'id',               // Property to use as value
        triggerOnLoad: false,         // Fetch on component load
        triggerOnInteraction: true,   // Fetch on user interaction
        cache: true,                  // Cache results
        debounce: 300,                // Debounce delay in ms
      },
    },
  },
};
```

**Hook Usage:**

```tsx
import { useDynamicOptions } from '@00rez/form-runner';

function MyComponent() {
  const [options, setOptions] = React.useState([]);
  const { options: dynamicOptions } = useDynamicOptions(
    apiConfig,
    options,
    formData
  );
  return <select>{/* render options */}</select>;
}
```

### 2. Event Callbacks

Trigger custom logic on field events: `onLoad`, `onFocus`, `onChange`, `onBlur`.

**Define a callback registry:**

```tsx
const callbackRegistry = {
  async fetchUserData(context) {
    const { fieldId, currentValue, formData } = context;
    const response = await fetch(`/api/users/${currentValue}`);
    return response.json();
  },
  
  formatPhoneNumber(context) {
    const { currentValue } = context;
    return currentValue.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
  },
};
```

**In UI Schema:**

```tsx
const uiSchema = {
  userId: {
    'ui:options': {
      eventConfig: {
        onChange: 'fetchUserData',    // Call by name from registry
        onBlur: (context) => {        // Or use inline function
          console.log('User left field:', context.fieldId);
        },
      },
    },
  },
  phone: {
    'ui:options': {
      eventConfig: {
        onChange: 'formatPhoneNumber',
      },
    },
  },
};
```

**Hook Usage:**

```tsx
import { useFieldEvents } from '@00rez/form-runner';

function MyField() {
  const { triggerEvent } = useFieldEvents(
    'fieldId',
    'fieldName',
    currentValue,
    formData,
    schema,
    eventConfig,
    callbackRegistry
  );

  return (
    <input
      onChange={async (e) => {
        const result = await triggerEvent('onChange', e.target.value);
        // Use result...
      }}
    />
  );
}
```

**Context Object Available to Callbacks:**

```typescript
interface FieldEventContext {
  fieldId: string;           // Unique field identifier
  fieldName: string;         // Field name/path
  currentValue: any;         // Current field value
  formData: any;             // Entire form data
  schema: RJSFSchema;        // Field's JSON Schema
  formContext?: any;         // Custom context passed to form
}
```

### 3. Conditional Logic

Show, hide, or require fields based on conditions using JSON Schema `if/then/else`.

**In FormBuilderElement (builder format):**

```tsx
const element = {
  id: 'fieldB',
  logic: {
    field: 'fieldA',
    operator: 'eq',
    value: 'yes',
    action: 'show',  // 'show', 'hide', or 'require'
  },
};
```

**Supported Operators:**
- `eq` - equals
- `neq` - not equals
- `gt` - greater than
- `lt` - less than
- `gte` - greater than or equal
- `lte` - less than or equal
- `contains` - string contains

The logic is compiled into the JSON Schema's `allOf` with `if/then/else` blocks.

### 4. Custom Error Transformation

Transform validation errors for better UX or to filter out specific errors.

```tsx
import { transformErrors } from '@00rez/form-runner';

const customTransformer = (errors) => {
  return errors
    .filter(err => err.property !== 'dynamicField')  // Hide certain fields
    .map(err => ({
      ...err,
      message: `Custom: ${err.message}`,
    }));
};

<Form
  {...props}
  transformErrors={customTransformer}
/>
```

## Types & Interfaces

### FormBuilderElement

Internal representation for form structure (used by form builder):

```typescript
interface FormBuilderElement {
  id: string;                    // Unique identifier
  label: string;                 // Display label
  description?: string;          // Help text
  placeholder?: string;          // Input placeholder
  required?: boolean;            // Required field
  icon: IconName;                // Icon identifier
  category: 'input' | 'layout';  // Element category
  isContainer?: boolean;         // Can contain children (layouts)
  elements?: FormBuilderElement[]; // Nested children
  schema: RJSFSchema;            // JSON Schema
  uiSchema: UiSchema;            // UI Schema
  logic?: LogicCondition;        // Conditional visibility
}
```

### LogicCondition

```typescript
interface LogicCondition {
  field: string;                // Reference field ID
  operator: 'eq' | 'neq' | 'gt' | 'lt' | 'gte' | 'lte' | 'contains';
  value: any;                   // Value to compare
  action: 'show' | 'hide' | 'require'; // Action to take
}
```

### ApiConfig

```typescript
interface ApiConfig {
  url: string;                    // API endpoint (supports {{variable}} interpolation)
  method: 'GET' | 'POST';         // HTTP method
  headers?: Record<string, string>; // Custom headers
  body?: string;                  // Request body template
  resultPath?: string;            // Path to options in response (e.g., 'data.items')
  labelKey?: string;              // Property name for display label
  valueKey?: string;              // Property name for value
  triggerOnLoad?: boolean;        // Fetch when component loads
  triggerOnInteraction?: boolean; // Fetch on user interaction
  cache?: boolean;                // Cache results
  debounce?: number;              // Debounce delay (ms)
}
```

### FieldEventConfig

```typescript
interface FieldEventConfig {
  onLoad?: string | FieldEventCallback;   // Called when field mounts
  onFocus?: string | FieldEventCallback;  // Called on focus
  onChange?: string | FieldEventCallback; // Called on value change
  onBlur?: string | FieldEventCallback;   // Called on blur
}

type FieldEventCallback = (context: FieldEventContext) => Promise<any> | any;
```

### EventCallbackRegistry

```typescript
interface EventCallbackRegistry {
  [callbackName: string]: FieldEventCallback;
}
```

## Utility Functions

### normalizeId

Sanitize field IDs by removing special characters:

```tsx
import { normalizeId } from '@00rez/form-runner';

const id = normalizeId('My Field!@#$%'); // 'MyField'
```

### transformErrors

Pre-built error transformer for common use cases:

```tsx
import { transformErrors } from '@00rez/form-runner';

const errors = transformErrors(rawErrors, schema, uiSchema);
```

### Interpolation Utilities

The library provides utilities for dynamic text interpolation with support for multiple interpolation styles:

#### interpolate

The original interpolation function supporting `{{key}}` patterns for language dictionaries and `[[key]]` patterns for global data:

```tsx
import { interpolate } from '@00rez/form-runner';

// Replace {{key}} with language data and [[key]] with global data
const result = interpolate(
  { label: 'Hello {{name}}', value: '[[globalVar]]' },
  { name: 'John' },           // Language/data dictionary
  { globalVar: 'Global!' }    // Global data (optional)
);
// Result: { label: 'Hello John', value: 'Global!' }
```

#### interpolateWithI18next

A string-focused function with i18next-compatible format. Supports `{{variable}}` patterns with fallback to global data:

```tsx
import { interpolateWithI18next } from '@00rez/form-runner';

const result = interpolateWithI18next(
  'Hello {{name}}, you have {{count}} messages',
  { name: 'John', count: 5 },
  { count: 10 }  // Fallback global data (optional)
);
// Result: 'Hello John, you have 5 messages'
```

**Features:**
- Supports nested object access with dot notation: `{{user.profile.name}}`
- Returns numeric values as strings
- Falls back to `globalData` if value not found in primary values
- Returns unmatched patterns unchanged

#### interpolateObjectWithI18next

Recursively interpolates all strings in objects and arrays using i18next format:

```tsx
import { interpolateObjectWithI18next } from '@00rez/form-runner';

const schema = {
  title: 'Welcome {{userName}}',
  properties: {
    message: { type: 'string', title: 'Message: {{itemCount}} items' },
  },
};

const result = interpolateObjectWithI18next(
  schema,
  { userName: 'Alice', itemCount: 42 }
);
// Result: { title: 'Welcome Alice', properties: { message: { ... title: 'Message: 42 items' } } }
```

**Use cases:**
- Interpolating entire form schemas
- Dynamic form configuration with i18next
- Maintaining structure while replacing text values

## Custom Validation

Use the validator's composition to add custom keywords:

```tsx
import { createValidator } from '@rjsf/validator-ajv8';

const validator = createValidator();

// Add custom validation keyword
validator.ajv.addKeyword({
  keyword: 'customKeyword',
  compile: (value) => {
    return (data) => value ? data > 10 : true;
  },
});
```

## Styling

The component uses **Tailwind CSS** for styling. Ensure Tailwind is installed and configured in your project:

```bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

Include styles in your app:

```tsx
import '@00rez/form-runner/form-runner.css';
```

## Examples

### Complete Form with All Features

```tsx
import React, { useState } from 'react';
import { Form, EventCallbackRegistry } from '@00rez/form-runner';
import { createValidator } from '@rjsf/validator-ajv8';

const schema = {
  type: 'object',
  properties: {
    country: {
      type: 'string',
      title: 'Country',
      enum: ['US', 'Canada', 'UK'],
    },
    city: {
      type: 'string',
      title: 'City',
    },
    productType: {
      type: 'string',
      title: 'Product Type',
    },
    message: {
      type: 'string',
      title: 'Additional Info',
    },
    agreeToTerms: {
      type: 'boolean',
      title: 'I agree to terms',
    },
  },
  required: ['country', 'city'],
};

const uiSchema = {
  country: {
    'ui:widget': 'select',
  },
  city: {
    'ui:widget': 'select',
    'ui:options': {
      apiConfig: {
        url: '/api/cities?country={{country}}',
        resultPath: 'cities',
        labelKey: 'name',
        valueKey: 'id',
        cache: true,
        debounce: 300,
      },
    },
  },
  productType: {
    'ui:widget': 'select',
    'ui:options': {
      apiConfig: {
        url: '/api/products?city={{city}}',
        resultPath: 'products',
        labelKey: 'name',
        valueKey: 'id',
      },
      eventConfig: {
        onChange: 'logSelection',
      },
    },
  },
  message: {
    'ui:widget': 'textarea',
  },
  agreeToTerms: {
    'ui:widget': 'toggle',
  },
};

const callbackRegistry: EventCallbackRegistry = {
  async logSelection(context) {
    console.log('Selected:', context.currentValue);
  },
};

export default function App() {
  const [data, setData] = useState({});
  const [submitted, setSubmitted] = useState(null);
  const validator = createValidator();

  return (
    <div className="p-8 max-w-2xl mx-auto">
      <h1 className="text-2xl font-bold mb-6">My Form</h1>
      {submitted ? (
        <div className="p-4 bg-green-50 border border-green-200 rounded">
          <p className="font-semibold">Submitted successfully!</p>
          <pre className="mt-2 text-sm">{JSON.stringify(submitted, null, 2)}</pre>
          <button
            onClick={() => setSubmitted(null)}
            className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
          >
            Edit
          </button>
        </div>
      ) : (
        <Form
          schema={schema}
          uiSchema={uiSchema}
          formData={data}
          validator={validator}
          onChange={(e) => setData(e.formData)}
          onSubmit={(e) => setSubmitted(e.formData)}
          callbackRegistry={callbackRegistry}
        />
      )}
    </div>
  );
}
```

## Troubleshooting

### Form doesn't render

- Ensure `schema`, `uiSchema`, and `validator` are provided
- Check browser console for validation errors
- Verify schema is valid JSON Schema format

### Dynamic options not loading

- Check network tab for API requests
- Verify URL interpolation with correct field names: `{{fieldName}}`
- Ensure `resultPath` matches your API response structure
- Check `labelKey` and `valueKey` match actual data properties

### Callbacks not firing

- Register callbacks in `callbackRegistry` by exact name
- Check `eventConfig` has correct callback name or function
- Verify field has focus/change event listeners
- Check browser console for callback execution

### Styling issues

- Ensure Tailwind CSS is properly configured
- Import styles: `import '@00rez/form-runner/styles.css'`
- Check conflicting CSS specificity

## Contributing

n/a

## License

MIT