npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@inubeapislive/render-control

v1.0.0

Published

iNube render control utilities for conditional and dynamic rendering

Readme

@inubeapislive/render-control

A modular, schema-driven form rendering engine for the iNube platform. Define your forms as JSON schemas and the engine handles rendering, validation, state management, visibility rules, and field lifecycle — all with zero business logic coupling.

Installation

npm install @inubeapislive/render-control

Peer Dependencies

npm install react react-dom @mui/material @emotion/react @emotion/styled

Required Companion Packages

npm install @inubeapislive/components @inubeapislive/validations

Architecture Overview

The package is split into independent, tree-shakeable modules:

@inubeapislive/render-control
├── engine        → RenderEngine component (the main entry point)
├── renderers     → Built-in UI renderers (text, select, checkbox, etc.)
├── schema        → JSON schema parser and validator
├── state         → Immutable form state (reducer + React context + hooks)
├── validation    → Validation pipeline (auto-resolves from @inubeapislive/validations)
├── visibility    → Conditional visibility/required rule evaluator
├── path-resolver → Arbitrary-depth dot/bracket path traversal
├── adapters      → Plugin registry for custom UI adapters
├── api-client    → Generic HTTP client with URL placeholder substitution
└── controls      → RenderWhen, RenderList, RenderSwitch utilities

Each module can be imported independently via subpath exports:

import { get, set } from '@inubeapislive/render-control/path-resolver';
import { evaluateCondition } from '@inubeapislive/render-control/visibility';
import { formReducer } from '@inubeapislive/render-control/state';

Quick Start

import { RenderEngine } from '@inubeapislive/render-control';

const schema = {
  fields: [
    { path: 'name', type: 'text', label: 'Full Name', required: true },
    { path: 'email', type: 'text', label: 'Email', blurValidators: [{ name: 'isEmail' }] },
    { path: 'gender', type: 'radio', label: 'Gender', options: [
      { label: 'Male', value: 'male' },
      { label: 'Female', value: 'female' },
    ]},
    { path: 'premium', type: 'currency', label: 'Premium Amount' },
    { path: 'dob', type: 'date', label: 'Date of Birth', props: { maxDate: '2010-01-01' } },
  ],
};

function App() {
  return (
    <RenderEngine
      schema={schema}
      data={{ name: 'John Doe', gender: 'male' }}
      onWarning={(path, type) => console.warn(`No renderer for ${type} at ${path}`)}
    />
  );
}

RenderEngine

The main orchestrating component. Accepts a schema, renders fields using the built-in renderers map, manages form state, evaluates visibility rules, and runs validation pipelines.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | schema | FormSchema | — | JSON schema defining all form fields | | data | FormState | {} | Initial form data (keyed by field path) | | onValidateAll | (errors: {path, message}[]) => void | — | Called after validateAll with all validation errors | | onWarning | (path: string, type: string) => void | — | Called when a field type has no registered renderer | | customRenderers | Record<string, (props: RendererProps) => ReactElement> | — | Extend or override built-in renderers | | customValidators | Record<string, (...args) => unknown> | — | Extend or override built-in validators |

How It Works

  1. Schema Parsing — Validates the schema structure and checks all type values against known renderers
  2. State Initialization — Wraps children in FormProvider with initial data
  3. Visibility Resolution — For each field, evaluates visible and required conditions against current state
  4. Renderer Lookup — Finds the matching renderer from the built-in map (or customRenderers)
  5. Memoized Rendering — Each field is wrapped in React.memo keyed on (value, error, visible, required) — only re-renders when these change
  6. Validation — On blur, runs field-specific validators. On validateAll, iterates all visible+required fields
  7. Error Boundary — Each field has an isolated error boundary; a crashing renderer won't take down the whole form

Schema Change Handling

When the schema prop changes at runtime:

  • Fields in both old and new schema retain their existing values
  • Fields removed from the new schema are dropped from state
  • New fields are initialized with defaultValue or from data

Schema Format

FormSchema

interface FormSchema {
  fields: FieldDescriptor[];
  version?: string;
  metadata?: Record<string, unknown>;
}

FieldDescriptor

interface FieldDescriptor {
  path: string;          // Dot-notation path into form state (e.g., "applicant.name")
  type: string;          // Renderer type (text, select, radio, checkbox, etc.)
  label?: string;        // Display label
  defaultValue?: unknown;// Initial value if not provided in data
  required?: boolean | Condition;    // Static or conditional required rule
  visible?: boolean | Condition;     // Static or conditional visibility rule
  disabled?: boolean | Condition;    // Static or conditional disabled rule
  validators?: ValidatorRef[];       // Change-time validators
  blurValidators?: ValidatorRef[];   // Blur-time validators
  options?: OptionItem[];            // For select/radio/checkbox types
  props?: Record<string, unknown>;   // Extra props passed to the renderer
  layout?: LayoutHints;              // Grid layout hints (columns, order, group)
}

Full Schema Example

{
  "fields": [
    {
      "path": "applicant.firstName",
      "type": "text",
      "label": "First Name",
      "required": true,
      "validators": [
        { "name": "isAlphaSpace" },
        { "name": "isMinLength", "params": [2] },
        { "name": "isMaxLength", "params": [50] }
      ]
    },
    {
      "path": "applicant.email",
      "type": "text",
      "label": "Email Address",
      "blurValidators": [{ "name": "isEmail" }]
    },
    {
      "path": "applicant.type",
      "type": "select",
      "label": "Applicant Type",
      "options": [
        { "label": "Individual", "value": "individual" },
        { "label": "Corporate", "value": "corporate" }
      ]
    },
    {
      "path": "company.name",
      "type": "text",
      "label": "Company Name",
      "visible": {
        "type": "equals",
        "path": "applicant.type",
        "value": "corporate"
      },
      "required": {
        "type": "equals",
        "path": "applicant.type",
        "value": "corporate"
      }
    }
  ]
}

Built-in Renderers

The engine includes 11 pre-built renderers that use @inubeapislive/components:

| Type | Renderer | Component Used | Description | |------|----------|----------------|-------------| | text | TextRenderer | TextField | Standard text input (supports multiline, placeholder, maxLength) | | select | SelectRenderer | AutoComplete | Dropdown/autocomplete with options from schema | | button | ButtonRenderer | Button | Action button (variant, color, onClick from props) | | checkbox | CheckboxRenderer | Checkbox | Single or group checkbox (checkedVal/unCheckedVal or options) | | radio | RadioRenderer | Radio | Radio group with options from schema | | data-table | DataTableRenderer | DataTable | Read-only data table (columns/rows from props) | | typography | TypographyRenderer | Typography | Display-only text (variant, color from props) | | dialog | DialogRenderer | Dialog | Modal dialog (open state from value, title/content/actions from props) | | tabs | TabsRenderer | Tabs | Tab navigation (tabs config from props) | | date | DateRenderer | DatePicker | Native date picker (minDate/maxDate from props) | | currency | CurrencyRenderer | CurrencyInput | Formatted currency input (symbol, decimals, locale from props) |

Adding Custom Renderers

import { RenderEngine } from '@inubeapislive/render-control';
import type { RendererProps } from '@inubeapislive/render-control/renderers';

function RatingRenderer(props: RendererProps) {
  const { descriptor, value, onChange } = props;
  return (
    <div>
      <label>{descriptor.label}</label>
      {[1, 2, 3, 4, 5].map(star => (
        <span
          key={star}
          onClick={() => onChange(star)}
          style={{ cursor: 'pointer', color: (value as number) >= star ? 'gold' : 'gray' }}
        >
          ★
        </span>
      ))}
    </div>
  );
}

<RenderEngine
  schema={schema}
  customRenderers={{ rating: RatingRenderer }}
/>

RendererProps Interface

Every renderer receives these standardized props:

interface RendererProps {
  descriptor: FieldDescriptor;  // Full field definition from schema
  value: unknown;               // Current value from form state
  error: boolean;               // Whether field has validation error
  errorMessage: string;         // Error message (empty if no error)
  disabled: boolean;            // Whether field is disabled
  required: boolean;            // Whether field is required
  onChange: (value: unknown) => void;  // Emit value change
  onBlur: () => void;                  // Emit blur (triggers validation)
}

Validation

Validators are resolved by name from @inubeapislive/validations at runtime. No manual registration needed.

How It Works

  1. When a field blurs, the engine runs blurValidators in sequence
  2. Each ValidatorRef has a name (maps to a function in @inubeapislive/validations) and optional params
  3. The pipeline short-circuits on first failure, returning the error message
  4. Custom validators can be passed via customValidators prop (take priority over built-in)
  5. Unknown validator names are skipped with a console warning

ValidatorRef Format

interface ValidatorRef {
  name: string;       // e.g., "isEmail", "isMinLength", "IsRequired"
  params?: unknown[]; // e.g., [5] for isMinLength(value, 5)
}

Schema Examples

{
  "path": "phone",
  "type": "text",
  "label": "Mobile Number",
  "validators": [{ "name": "isNumeric" }],
  "blurValidators": [
    { "name": "IsRequired" },
    { "name": "isMobileNumber" }
  ]
}

Custom Validators

<RenderEngine
  schema={schema}
  customValidators={{
    isUniquePolicyNo: async (value) => {
      const exists = await checkPolicyExists(value as string);
      return exists ? 'Policy number already exists' : true;
    },
    isMinAge: (value, minAge) => {
      const age = calculateAge(value as string);
      return age >= (minAge as number) ? true : `Must be at least ${minAge} years old`;
    },
  }}
/>

Async Validators & Timeout

Async validators are awaited with a 5-second timeout. If a validator exceeds the timeout, it fails with "Validation timed out after 5000ms".

Direct Pipeline Usage

import { runValidationPipeline } from '@inubeapislive/render-control/validation';

const result = await runValidationPipeline(
  'abc',                                    // value to validate
  [{ name: 'isNumeric' }],                 // validators
  { myCustomValidator: (v) => true },      // optional custom validators
  { timeout: 3000 }                         // optional timeout override
);
// result: "Value is not numeric" or true

State Management

FormProvider & Hooks

The engine uses a React Context-based state system with undo support.

import {
  FormProvider,
  useFormState,
  useFormDispatch,
  useFieldValue,
} from '@inubeapislive/render-control/state';

Actions

| Action | Payload | Description | |--------|---------|-------------| | SET_FIELD | { path, value } | Immutably set a single field value | | BULK_UPDATE | { changes: [{path, value}] } | Atomically apply multiple changes (one history entry) | | UNDO | — | Revert to previous state (10-level history) | | RESET | { data } | Replace entire state, clear history | | SCHEMA_CHANGE | { newFields, data? } | Reconcile state with new schema |

useFieldValue (Selective Subscription)

import { useFieldValue } from '@inubeapislive/render-control/state';

function PremiumDisplay() {
  // Only re-renders when this specific path changes
  const premium = useFieldValue('policy.premium');
  return <div>Premium: ₹{premium}</div>;
}

Immutability & Undo

Every SET_FIELD creates a new state object (via the Path Resolver's immutable set). Previous states are stored in a bounded history stack (default 10 entries):

const dispatch = useFormDispatch();

dispatch({ type: 'SET_FIELD', path: 'name', value: 'John' });
dispatch({ type: 'SET_FIELD', path: 'name', value: 'Jane' });
dispatch({ type: 'UNDO' }); // state.name === 'John'

Visibility & Conditional Rules

Fields can have visible, required, and disabled rules that evaluate against current form state.

Condition Types

// Simple equality
{ "type": "equals", "path": "applicant.type", "value": "corporate" }

// Array includes
{ "type": "includes", "path": "selectedAddons", "values": ["roadside", "engine"] }

// Compound AND
{ "type": "and", "conditions": [
  { "type": "equals", "path": "country", "value": "IN" },
  { "type": "equals", "path": "state", "value": "KA" }
]}

// Compound OR
{ "type": "or", "conditions": [
  { "type": "equals", "path": "type", "value": "health" },
  { "type": "equals", "path": "type", "value": "life" }
]}

// NOT
{ "type": "not", "condition": { "type": "equals", "path": "age", "value": 0 } }

Nesting Depth

Conditions can be nested up to 5 levels deep. Beyond that, the evaluator returns false.

Direct Usage

import { evaluateCondition, resolveField } from '@inubeapislive/render-control/visibility';

const isVisible = evaluateCondition(
  { type: 'equals', path: 'country', value: 'IN' },
  { country: 'IN' }
); // true

const { visible, required } = resolveField(
  { visible: { type: 'equals', path: 'x', value: 1 }, required: true },
  { x: 1 }
); // { visible: true, required: true }

Path Resolver

Replaces hardcoded nested object access with iterative traversal at any depth.

import { parsePath, get, set } from '@inubeapislive/render-control/path-resolver';

// Parse path into segments
parsePath('items[0].name');          // ['items', '0', 'name']
parsePath('applicant.personal.dob'); // ['applicant', 'personal', 'dob']

// Get nested value (returns undefined for missing paths, never throws)
get({ a: { b: { c: 42 } } }, 'a.b.c');     // 42
get({ items: [{ name: 'X' }] }, 'items[0].name'); // 'X'
get(null, 'any.path');                       // undefined

// Immutably set (creates intermediates, never mutates original)
const obj = { user: { name: 'Alice' } };
const next = set(obj, 'user.name', 'Bob');
// next.user.name === 'Bob'
// obj.user.name === 'Alice' (unchanged)

API Client

Portable HTTP utility for dynamic data fetching in forms (dropdown options, autocomplete, etc.).

import { createApiClient } from '@inubeapislive/render-control/api-client';

const client = createApiClient({
  adapter: {
    get: (url) => fetch(url).then(r => ({ data: r.json() })),
    post: (url, body) => fetch(url, { method: 'POST', body: JSON.stringify(body) }).then(r => ({ data: r.json() })),
  },
});

// URL placeholder substitution: {P1}, {P2}, etc.
const data = await client.get('/api/policies/{P1}/riders', ['POL-12345']);
// Calls: /api/policies/POL-12345/riders

// Missing params return error without making request
const result = await client.get('/api/{P1}/{P2}', ['only-one']);
// { status: 0, message: "Missing parameter for placeholder {P2}" }

Controls (Backward-Compatible Utilities)

Three utility functions preserved from the legacy codebase for conditional rendering logic:

RenderWhen

import { RenderWhen } from '@inubeapislive/render-control/controls';

const result = RenderWhen({
  condition: () => user.isAdmin,
  content: <AdminPanel />,
  fallback: <AccessDenied />,
});

RenderList

import { RenderList } from '@inubeapislive/render-control/controls';

const visibleItems = RenderList({
  items: [
    { key: '1', data: 'Item 1', visible: true },
    { key: '2', data: 'Item 2', visible: false },
    { key: '3', data: 'Item 3', visible: true },
  ],
  filterHidden: true,  // default
});
// Returns items 1 and 3 only

RenderSwitch

import { RenderSwitch } from '@inubeapislive/render-control/controls';

const content = RenderSwitch({
  value: policyType,
  cases: [
    { match: 'health', content: <HealthForm /> },
    { match: 'motor', content: <MotorForm /> },
    { match: 'life', content: <LifeForm /> },
  ],
  defaultContent: <GenericForm />,
});

Adapter Registry (Public Utility)

The adapter registry is exported as a standalone utility. While the RenderEngine itself uses the static renderers map internally, you can use the registry for your own plugin patterns:

import { createAdapterRegistry } from '@inubeapislive/render-control/adapters';

const registry = createAdapterRegistry();
registry.register('custom-chart', MyChartAdapter);
registry.register('map-view', MapViewAdapter);

registry.get('custom-chart');  // MyChartAdapter
registry.has('map-view');      // true
registry.types();              // ['custom-chart', 'map-view']

// Throws if already registered — use replace() for intentional overwrites
registry.replace('custom-chart', MyBetterChartAdapter);

Subpath Exports

Every module is independently importable for tree-shaking:

| Subpath | What's exported | |---------|----------------| | @inubeapislive/render-control | Everything (main barrel) | | @inubeapislive/render-control/engine | RenderEngine, RenderEngineProps | | @inubeapislive/render-control/renderers | renderers map, RendererProps | | @inubeapislive/render-control/schema | parseSchema, printSchema, all types | | @inubeapislive/render-control/state | FormProvider, useFormState, useFormDispatch, useFieldValue, formReducer | | @inubeapislive/render-control/validation | runValidationPipeline | | @inubeapislive/render-control/visibility | evaluateCondition, resolveField | | @inubeapislive/render-control/path-resolver | parsePath, get, set | | @inubeapislive/render-control/adapters | createAdapterRegistry | | @inubeapislive/render-control/api-client | createApiClient | | @inubeapislive/render-control/controls | RenderWhen, RenderList, RenderSwitch |


TypeScript

Full TypeScript coverage with strict mode. All types are exported:

import type {
  FormSchema,
  FieldDescriptor,
  ValidatorRef,
  OptionItem,
  LayoutHints,
  ParseResult,
  SchemaValidationError,
  FormState,
  FormAction,
  FormStore,
  Condition,
  SimpleCondition,
  ArrayIncludesCondition,
  CompoundCondition,
  VisibilityResult,
  RendererProps,
  RenderEngineProps,
  HttpAdapter,
  ApiError,
  ApiClientOptions,
  AdapterProps,
  ControlAdapter,
  AdapterRegistry,
  RenderCondition,
  RenderItem,
  SwitchCase,
} from '@inubeapislive/render-control';

Build Output

The package ships as dual ESM/CJS with TypeScript declarations:

dist/
├── index.mjs / index.cjs / index.d.ts
├── engine.mjs / engine.cjs / engine.d.ts
├── renderers.mjs / renderers.cjs / renderers.d.ts
├── schema.mjs / schema.cjs / schema.d.ts
├── state.mjs / state.cjs / state.d.ts
├── validation.mjs / validation.cjs / validation.d.ts
├── visibility.mjs / visibility.cjs / visibility.d.ts
├── path-resolver.mjs / path-resolver.cjs / path-resolver.d.ts
├── adapters.mjs / adapters.cjs / adapters.d.ts
├── api-client.mjs / api-client.cjs / api-client.d.ts
└── controls.mjs / controls.cjs / controls.d.ts

"sideEffects": false enables full tree-shaking — import only what you use.


Development

# Build
npm run build

# Run tests
npm run test

# Type check
npx tsc --noEmit