@formos/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,499 @@
1
+ # @formos/react
2
+
3
+ React bindings for the Formos form engine.
4
+
5
+ ## Purpose
6
+
7
+ `@formos/react` provides **React hooks and components** to integrate the headless `@formos/kernel` with React applications. It bridges the gap between the framework-agnostic form engine and React's component model.
8
+
9
+ ## Responsibilities
10
+
11
+ This package is responsible for:
12
+
13
+ - **React Context Management**: Providing form engine instance via React context
14
+ - **Hook API**: Exposing form functionality through idiomatic React hooks
15
+ - **State Synchronization**: Keeping React components in sync with form state
16
+ - **SSR Compatibility**: Safe server-side rendering support
17
+ - **Performance Optimization**: Minimizing unnecessary re-renders
18
+
19
+ ## What This Package Does NOT Do
20
+
21
+ ❌ **NO UI Components**
22
+ - Does not provide input components, buttons, or form elements
23
+ - Does not include styling or CSS
24
+ - Does not use shadcn/ui or any UI library
25
+ - Headless - bring your own UI
26
+
27
+ ❌ **NO Form Logic**
28
+ - Does not contain validation logic (use kernel adapters)
29
+ - Does not define schemas (use `@formos/schema`)
30
+ - Does not execute effects or validators
31
+ - Pure React bindings only
32
+
33
+ ❌ **NO Styling or Theming**
34
+ - No CSS or style objects
35
+ - No theme providers
36
+ - Style your components however you want
37
+
38
+ **Think of it as:** React hooks for the form engine, not a complete form library.
39
+
40
+ ## Stability Guarantee
41
+
42
+ ### Hooks API is STABLE ✅
43
+
44
+ The public hooks and components are committed to backward compatibility:
45
+ - Hook signatures won't change
46
+ - Return types remain consistent
47
+ - Context API is stable
48
+
49
+ This enables:
50
+ - **Safe updates**: Upgrade without breaking UI
51
+ - **Custom UI flexibility**: Build any UI on these hooks
52
+ - **Long-term reliability**: Code written today works tomorrow
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pnpm add @formos/react @formos/kernel @formos/schema react
58
+ ```
59
+
60
+ ## Basic Usage
61
+
62
+ ### 1. Setup FormProvider
63
+
64
+ Wrap your application with `FormProvider`:
65
+
66
+ ```tsx
67
+ import { FormProvider } from '@formos/react';
68
+ import { normalizeSchema, type FormSchemaV1 } from '@formos/schema';
69
+
70
+ const schema: FormSchemaV1 = {
71
+ version: 'v1',
72
+ fields: [
73
+ {
74
+ name: 'email',
75
+ type: 'email',
76
+ required: true
77
+ },
78
+ {
79
+ name: 'password',
80
+ type: 'password',
81
+ required: true
82
+ }
83
+ ]
84
+ };
85
+
86
+ const normalized = normalizeSchema(schema);
87
+
88
+ function App() {
89
+ return (
90
+ <FormProvider
91
+ schema={normalized}
92
+ validators={{
93
+ required: (value) => value ? null : 'Required',
94
+ email: (value) => {
95
+ const regex = /\S+@\S+\.\S+/;
96
+ return regex.test(String(value)) ? null : 'Invalid email';
97
+ }
98
+ }}
99
+ onSubmit={async (values) => {
100
+ console.log('Form submitted:', values);
101
+ await fetch('/api/submit', {
102
+ method: 'POST',
103
+ body: JSON.stringify(values)
104
+ });
105
+ }}
106
+ >
107
+ <LoginForm />
108
+ </FormProvider>
109
+ );
110
+ }
111
+ ```
112
+
113
+ ### 2. Use Form Hooks
114
+
115
+ #### useForm - Form-level operations
116
+
117
+ ```tsx
118
+ import { useForm } from '@formos/react';
119
+
120
+ function LoginForm() {
121
+ const { submit, reset, isValid, isDirty } = useForm();
122
+
123
+ const handleSubmit = async (e: React.FormEvent) => {
124
+ e.preventDefault();
125
+ const success = await submit();
126
+
127
+ if (success) {
128
+ console.log('Login successful');
129
+ } else {
130
+ console.log('Validation failed');
131
+ }
132
+ };
133
+
134
+ return (
135
+ <form onSubmit={handleSubmit}>
136
+ <EmailField />
137
+ <PasswordField />
138
+
139
+ <button type="submit" disabled={!isValid() || !isDirty()}>
140
+ Login
141
+ </button>
142
+ <button type="button" onClick={reset}>
143
+ Reset
144
+ </button>
145
+ </form>
146
+ );
147
+ }
148
+ ```
149
+
150
+ #### useField - Field-level state and actions
151
+
152
+ ```tsx
153
+ import { useField } from '@formos/react';
154
+
155
+ function EmailField() {
156
+ const {
157
+ value,
158
+ error,
159
+ touched,
160
+ dirty,
161
+ setValue,
162
+ markTouched
163
+ } = useField('email', {
164
+ validateOnChange: true,
165
+ validateOnBlur: true
166
+ });
167
+
168
+ return (
169
+ <div>
170
+ <label htmlFor="email">Email</label>
171
+ <input
172
+ id="email"
173
+ type="email"
174
+ value={String(value || '')}
175
+ onChange={(e) => setValue(e.target.value)}
176
+ onBlur={() => markTouched()}
177
+ className={touched && error ? 'error' : ''}
178
+ />
179
+ {touched && error && (
180
+ <span className="error-message">{error}</span>
181
+ )}
182
+ {dirty && <span className="dirty-indicator">*</span>}
183
+ </div>
184
+ );
185
+ }
186
+ ```
187
+
188
+ #### useFormState - Observe form state
189
+
190
+ ```tsx
191
+ import { useFormState } from '@formos/react';
192
+
193
+ function SubmitButton() {
194
+ const { isValid, isDirty, isSubmitting } = useFormState();
195
+
196
+ return (
197
+ <button
198
+ type="submit"
199
+ disabled={!isValid || !isDirty || isSubmitting}
200
+ >
201
+ {isSubmitting ? 'Submitting...' : 'Submit Form'}
202
+ </button>
203
+ );
204
+ }
205
+
206
+ function ErrorSummary() {
207
+ const { errors } = useFormState();
208
+ const errorList = Object.entries(errors).filter(([_, error]) => error);
209
+
210
+ if (errorList.length === 0) return null;
211
+
212
+ return (
213
+ <div className="error-summary">
214
+ <h3>Please fix the following errors:</h3>
215
+ <ul>
216
+ {errorList.map(([field, error]) => (
217
+ <li key={field}>{field}: {error}</li>
218
+ ))}
219
+ </ul>
220
+ </div>
221
+ );
222
+ }
223
+ ```
224
+
225
+ ## Multi-Step Forms
226
+
227
+ Use `useStep` for multi-step form navigation:
228
+
229
+ ```tsx
230
+ import { useStep } from '@formos/react';
231
+
232
+ const multiStepSchema: FormSchemaV1 = {
233
+ version: 'v1',
234
+ fields: [
235
+ { name: 'firstName', type: 'text' },
236
+ { name: 'lastName', type: 'text' },
237
+ { name: 'email', type: 'email' },
238
+ { name: 'phone', type: 'tel' }
239
+ ],
240
+ steps: [
241
+ { id: 'personal', title: 'Personal Info', fields: ['firstName', 'lastName'] },
242
+ { id: 'contact', title: 'Contact', fields: ['email', 'phone'] }
243
+ ]
244
+ };
245
+
246
+ function StepNavigator() {
247
+ const {
248
+ currentStep,
249
+ totalSteps,
250
+ nextStep,
251
+ prevStep,
252
+ canGoToStep
253
+ } = useStep();
254
+
255
+ const handleNext = async () => {
256
+ const moved = await nextStep();
257
+ if (!moved) {
258
+ alert('Please fix errors before proceeding');
259
+ }
260
+ };
261
+
262
+ return (
263
+ <div>
264
+ <div className="step-indicator">
265
+ Step {currentStep + 1} of {totalSteps}
266
+ </div>
267
+
268
+ <div className="step-content">
269
+ {currentStep === 0 ? <PersonalInfoStep /> : <ContactStep />}
270
+ </div>
271
+
272
+ <div className="step-navigation">
273
+ <button
274
+ onClick={prevStep}
275
+ disabled={currentStep === 0}
276
+ >
277
+ Previous
278
+ </button>
279
+
280
+ <button onClick={handleNext}>
281
+ {currentStep === totalSteps - 1 ? 'Submit' : 'Next'}
282
+ </button>
283
+ </div>
284
+ </div>
285
+ );
286
+ }
287
+ ```
288
+
289
+ ## Advanced Patterns
290
+
291
+ ### Custom Field Components
292
+
293
+ Create reusable field components:
294
+
295
+ ```tsx
296
+ interface TextFieldProps {
297
+ name: string;
298
+ label: string;
299
+ type?: 'text' | 'email' | 'password' | 'tel';
300
+ }
301
+
302
+ function TextField({ name, label, type = 'text' }: TextFieldProps) {
303
+ const { value, error, touched, setValue, markTouched } = useField(name);
304
+
305
+ return (
306
+ <div className="field">
307
+ <label htmlFor={name}>{label}</label>
308
+ <input
309
+ id={name}
310
+ type={type}
311
+ value={String(value || '')}
312
+ onChange={(e) => setValue(e.target.value)}
313
+ onBlur={() => markTouched()}
314
+ aria-invalid={touched && !!error}
315
+ aria-describedby={error ? `${name}-error` : undefined}
316
+ />
317
+ {touched && error && (
318
+ <span id={`${name}-error`} className="error" role="alert">
319
+ {error}
320
+ </span>
321
+ )}
322
+ </div>
323
+ );
324
+ }
325
+
326
+ // Usage
327
+ <TextField name="email" label="Email Address" type="email" />
328
+ ```
329
+
330
+ ### Conditional Fields
331
+
332
+ Show/hide fields based on conditions:
333
+
334
+ ```tsx
335
+ import { isFieldVisible } from '@formos/kernel';
336
+
337
+ function ConditionalField({ fieldName }: { fieldName: string }) {
338
+ const { schema, engine } = useForm();
339
+ const field = schema.fieldMap?.get(fieldName);
340
+
341
+ if (!field || !isFieldVisible(field, (name) => engine.getValue(name))) {
342
+ return null;
343
+ }
344
+
345
+ return <TextField name={fieldName} label={field.label || fieldName} />;
346
+ }
347
+ ```
348
+
349
+ ### Form-level Validation
350
+
351
+ Trigger validation at form level:
352
+
353
+ ```tsx
354
+ function ValidateAllButton() {
355
+ const { validate, getErrors } = useForm();
356
+
357
+ const handleValidate = async () => {
358
+ const isValid = await validate('manual');
359
+
360
+ if (isValid) {
361
+ alert('Form is valid!');
362
+ } else {
363
+ const errors = getErrors();
364
+ console.log('Validation errors:', errors);
365
+ }
366
+ };
367
+
368
+ return (
369
+ <button type="button" onClick={handleValidate}>
370
+ Validate All Fields
371
+ </button>
372
+ );
373
+ }
374
+ ```
375
+
376
+ ## SSR (Server-Side Rendering)
377
+
378
+ The package is SSR-safe. Use it with Next.js, Remix, or other SSR frameworks:
379
+
380
+ ```tsx
381
+ // app/page.tsx (Next.js App Router)
382
+ 'use client';
383
+
384
+ import { FormProvider, useField } from '@formos/react';
385
+ import { normalizeSchema } from '@formos/schema';
386
+
387
+ const schema = normalizeSchema({
388
+ version: 'v1',
389
+ fields: [{ name: 'email', type: 'email' }]
390
+ });
391
+
392
+ export default function ContactForm() {
393
+ return (
394
+ <FormProvider schema={schema} validators={{/* ... */}}>
395
+ <EmailField />
396
+ </FormProvider>
397
+ );
398
+ }
399
+ ```
400
+
401
+ **Note:** FormProvider must be used in a Client Component (`'use client'`) because it manages React state.
402
+
403
+ ## Performance Optimization
404
+
405
+ ### Minimize Re-renders
406
+
407
+ The hooks use React context with a version counter system. Components only re-render when:
408
+ 1. Their specific field value changes (for `useField`)
409
+ 2. Form state changes (for `useFormState`)
410
+ 3. Step changes (for `useStep`)
411
+
412
+ ### Memoization
413
+
414
+ Form engine instance is memoized and only recreated if schema or validators change:
415
+
416
+ ```tsx
417
+ // Engine is stable across re-renders
418
+ const engine = useMemo(() => createFormEngine(schema, options), [schema, options]);
419
+ ```
420
+
421
+ ### Selective Subscriptions
422
+
423
+ Only subscribe to the state you need:
424
+
425
+ ```tsx
426
+ // ❌ Bad - subscribes to all form state
427
+ function SubmitButton() {
428
+ const { values, errors, isDirty } = useFormState();
429
+ return <button disabled={!isDirty}>Submit</button>;
430
+ }
431
+
432
+ // ✅ Good - only checks dirty state when needed
433
+ function SubmitButton() {
434
+ const { isDirty } = useForm();
435
+ return <button disabled={!isDirty()}>Submit</button>;
436
+ }
437
+ ```
438
+
439
+ ## API Reference
440
+
441
+ ### Components
442
+
443
+ - **`FormProvider`**: Context provider for form engine
444
+
445
+ ### Hooks
446
+
447
+ - **`useForm()`**: Form-level operations and engine access
448
+ - **`useField(name, options?)`**: Field-level state and actions
449
+ - **`useFormState()`**: Reactive form state observation
450
+ - **`useStep()`**: Multi-step navigation (multi-step forms only)
451
+
452
+ ### Types
453
+
454
+ - **`FormProviderProps`**: Props for FormProvider
455
+ - **`UseFieldOptions`**: Options for useField
456
+ - **`UseFieldResult`**: Return type of useField
457
+ - **`UseFormStateResult`**: Return type of useFormState
458
+ - **`UseStepResult`**: Return type of useStep
459
+
460
+ ## TypeScript Support
461
+
462
+ Full TypeScript support with strict typing:
463
+
464
+ ```tsx
465
+ import type { UseFieldResult, UseFormStateResult } from '@formos/react';
466
+
467
+ // Field result is fully typed
468
+ const field: UseFieldResult = useField('email');
469
+
470
+ // Form state is fully typed
471
+ const state: UseFormStateResult = useFormState();
472
+ ```
473
+
474
+ ## Package Structure
475
+
476
+ ```
477
+ @formos/react/
478
+ ├── src/
479
+ │ ├── index.ts # Public API exports
480
+ │ ├── FormContext.tsx # React context
481
+ │ ├── FormProvider.tsx # Provider component
482
+ │ ├── useForm.ts # Form hook
483
+ │ ├── useField.ts # Field hook
484
+ │ ├── useFormState.ts # State observation hook
485
+ │ ├── useStep.ts # Multi-step hook
486
+ │ └── types.ts # Type definitions
487
+ ├── package.json
488
+ ├── tsconfig.json
489
+ ├── tsup.config.ts
490
+ └── README.md
491
+ ```
492
+
493
+ ## Contributing
494
+
495
+ This package is part of the Formos monorepo. See the root README for contribution guidelines.
496
+
497
+ ## License
498
+
499
+ MIT