npm package: @robinpath/react
Description: RobinPath React components — render forms from RobinPath schemas
Author: Ryan Lee
Latest version: 0.1.1

# @robinpath/react

Enterprise-grade React form renderer for [RobinPath](https://robinpath.com) schemas. Render dynamic forms from JSON — works with React 18+, Next.js, Remix, Gatsby, Astro, and any React environment.

## Install

```bash
npm install @robinpath/react
```

## Quick Start

### From a schema (local)

```tsx
import { FormRenderer } from '@robinpath/react';
import type { FormSchema } from '@robinpath/react';

const schema: FormSchema = {
  config: { title: 'Contact Us', submitLabel: 'Send' },
  fields: [
    { name: 'name', type: 'text', label: 'Name', required: true, order: 0 },
    { name: 'email', type: 'email', label: 'Email', required: true, order: 1 },
    { name: 'message', type: 'textarea', label: 'Message', order: 2 },
  ],
};

function App() {
  return (
    <FormRenderer
      schema={schema}
      onSubmit={(values) => console.log(values)}
      theme="dark"
    />
  );
}
```

### From CDN (RobinPath snippet)

```tsx
import { RobinPathForm } from '@robinpath/react';

function App() {
  return (
    <RobinPathForm
      snippetId="your-snippet-id"
      onSubmit={(values) => console.log(values)}
      onResult={(result) => console.log(result)}
    />
  );
}
```

## Programmatic Control

Access the form imperatively via ref:

```tsx
import { useRef } from 'react';
import { FormRenderer } from '@robinpath/react';
import type { FormHandle } from '@robinpath/react';

function App() {
  const formRef = useRef<FormHandle>(null);

  return (
    <>
      <FormRenderer ref={formRef} schema={schema} onSubmit={handleSubmit} />
      <button onClick={() => formRef.current?.submit()}>Submit Externally</button>
      <button onClick={() => formRef.current?.reset()}>Reset</button>
      <button onClick={() => formRef.current?.setValue('name', 'John')}>Set Name</button>
    </>
  );
}
```

### FormHandle API

| Method | Description |
|--------|-------------|
| `submit()` | Submit the form programmatically |
| `reset()` | Reset to initial values |
| `validate()` | Validate all fields, returns `ValidationError[]` |
| `getValues()` | Get current form values |
| `setValue(name, value)` | Set a single field value |
| `setValues(values)` | Set multiple values |
| `setError(name, message)` | Set a field error |
| `clearErrors()` | Clear all errors |
| `focus(name)` | Focus a specific field |
| `getState()` | Get full form state |

## Props

### FormRenderer

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `schema` | `FormSchema` | required | Form schema from RobinPath |
| `initialValues` | `FormValues` | — | Override default values |
| `onChange` | `(values, fieldName) => void` | — | Called on every field change |
| `onSubmit` | `(values) => void \| Promise` | — | Called on valid submit |
| `onValidationError` | `(errors) => void` | — | Called when validation fails |
| `onBlur` | `(fieldName, value) => void` | — | Called on field blur |
| `theme` | `'dark' \| 'light'` | `'dark'` | Theme preset |
| `className` | `string` | — | Root element class |
| `style` | `CSSProperties` | — | Root element styles |
| `disabled` | `boolean` | `false` | Disable all fields |
| `hideSubmit` | `boolean` | `false` | Hide submit button |
| `submitLabel` | `string` | `'Submit'` | Custom submit text |
| `showBranding` | `boolean` | `false` | Show RobinPath footer |
| `validateOnBlur` | `boolean` | `true` | Validate on blur |
| `validateOnChange` | `boolean` | `false` | Validate on change |
| `focusOnError` | `boolean` | `true` | Focus first error on submit |
| `renderField` | `(field, props) => ReactNode` | — | Custom field renderer |
| `renderSubmit` | `(props) => ReactNode` | — | Custom submit button |
| `idPrefix` | `string` | `'rp'` | ID prefix for SSR |

### RobinPathForm

Extends all `FormRenderer` props, plus:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `snippetId` | `string` | required | RobinPath snippet ID |
| `cdnUrl` | `string` | `'https://cdn.robinpath.com'` | CDN base URL |
| `executeOnSubmit` | `boolean` | `true` | Execute script on CDN |
| `onResult` | `(result) => void` | — | Execution result callback |
| `renderLoading` | `() => ReactNode` | — | Custom loading UI |
| `renderError` | `(error) => ReactNode` | — | Custom error UI |

## Field Types

All 19 field types supported:

`text` · `textarea` · `password` · `email` · `url` · `phone` · `number` · `range` · `select` · `multiselect` · `radio` · `checkbox` · `date` · `time` · `datetime` · `file` · `hidden` · `color` · `json`

## Custom Field Renderer

```tsx
<FormRenderer
  schema={schema}
  renderField={(field, props) => (
    <div key={field.name}>
      <label>{field.label}</label>
      <input
        value={String(props.value ?? '')}
        onChange={(e) => props.onChange(e.target.value)}
        onBlur={props.onBlur}
      />
      {props.error && props.touched && <span>{props.error}</span>}
    </div>
  )}
  onSubmit={handleSubmit}
/>
```

## Conditional Fields

Fields can be conditionally visible based on other field values:

```tsx
const schema: FormSchema = {
  config: {},
  fields: [
    { name: 'hasCompany', type: 'checkbox', label: 'I have a company', order: 0 },
    { name: 'company', type: 'text', label: 'Company Name', order: 1, showWhen: 'hasCompany' },
    { name: 'role', type: 'select', label: 'Role', order: 2, options: [
      { value: 'dev', label: 'Developer' },
      { value: 'manager', label: 'Manager' },
    ]},
    { name: 'team_size', type: 'number', label: 'Team Size', order: 3, showWhen: 'role', showWhenValue: 'manager' },
  ],
};
```

## Context Hook

Access form state from nested components:

```tsx
import { useFormContext } from '@robinpath/react';

function StatusBar() {
  const { values, errors, isSubmitting } = useFormContext();
  return <div>Fields filled: {Object.values(values).filter(Boolean).length}</div>;
}
```

## Accessibility

WCAG 2.1 AA compliant:
- `aria-invalid` on error fields
- `aria-describedby` linking errors and descriptions
- `aria-required` on required fields
- `role="alert"` on error messages
- `role="radiogroup"` on radio groups
- `aria-live="polite"` on success state
- Keyboard navigable
- Auto-focus first error on submit

## Architecture

- **`useSyncExternalStore`** — React 18+ concurrent mode safe, no tearing
- **`memo` field renderer** — only rerenders when own value/error/touched changes
- **Error boundary** — graceful crash recovery
- **Zero CSS dependencies** — inline styles via CSS variables
- **29KB bundle** — tree-shakeable ES module

## License

MIT