@formality-ui/react 0.0.0 → 0.2.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 CHANGED
@@ -13,6 +13,7 @@ yarn add @formality-ui/react react-hook-form
13
13
  ```
14
14
 
15
15
  **Peer Dependencies:**
16
+
16
17
  - `react` >= 18.0.0
17
18
  - `react-dom` >= 18.0.0
18
19
  - `react-hook-form` >= 7.0.0
@@ -20,8 +21,8 @@ yarn add @formality-ui/react react-hook-form
20
21
  ## Quick Start
21
22
 
22
23
  ```tsx
23
- import { FormalityProvider, Form, Field } from '@formality-ui/react';
24
- import type { InputConfig, FormFieldsConfig } from '@formality-ui/react';
24
+ import { FormalityProvider, Form, Field } from "@formality-ui/react";
25
+ import type { InputConfig, FormFieldsConfig } from "@formality-ui/react";
25
26
 
26
27
  // Define your input types
27
28
  const inputs: Record<string, InputConfig> = {
@@ -29,16 +30,24 @@ const inputs: Record<string, InputConfig> = {
29
30
  component: ({ value, onChange, label, error, ...props }) => (
30
31
  <div>
31
32
  <label>{label}</label>
32
- <input value={value ?? ''} onChange={(e) => onChange(e.target.value)} {...props} />
33
+ <input
34
+ value={value ?? ""}
35
+ onChange={(e) => onChange(e.target.value)}
36
+ {...props}
37
+ />
33
38
  {error && <span>{error}</span>}
34
39
  </div>
35
40
  ),
36
- defaultValue: '',
41
+ defaultValue: "",
37
42
  },
38
43
  switch: {
39
44
  component: ({ value, onChange, label }) => (
40
45
  <label>
41
- <input type="checkbox" checked={value ?? false} onChange={(e) => onChange(e.target.checked)} />
46
+ <input
47
+ type="checkbox"
48
+ checked={value ?? false}
49
+ onChange={(e) => onChange(e.target.checked)}
50
+ />
42
51
  {label}
43
52
  </label>
44
53
  ),
@@ -48,9 +57,9 @@ const inputs: Record<string, InputConfig> = {
48
57
 
49
58
  // Define your form fields
50
59
  const config: FormFieldsConfig = {
51
- name: { type: 'textField', label: 'Full Name' },
52
- email: { type: 'textField', label: 'Email Address' },
53
- subscribed: { type: 'switch', label: 'Subscribe to newsletter' },
60
+ name: { type: "textField", label: "Full Name" },
61
+ email: { type: "textField", label: "Email Address" },
62
+ subscribed: { type: "switch", label: "Subscribe to newsletter" },
54
63
  };
55
64
 
56
65
  // Use in your app
@@ -181,7 +190,7 @@ Apply conditions to multiple fields.
181
190
  const formConfig = {
182
191
  groups: {
183
192
  signedFields: {
184
- conditions: [{ when: 'signed', is: true, disabled: false }],
193
+ conditions: [{ when: "signed", is: true, disabled: false }],
185
194
  },
186
195
  },
187
196
  };
@@ -189,7 +198,7 @@ const formConfig = {
189
198
  <FieldGroup name="signedFields">
190
199
  <Field name="creditApp" />
191
200
  <Field name="inCarvin" />
192
- </FieldGroup>
201
+ </FieldGroup>;
193
202
  ```
194
203
 
195
204
  **Props:**
@@ -221,12 +230,12 @@ Add conditional logic to fields:
221
230
 
222
231
  ```typescript
223
232
  const config: FormFieldsConfig = {
224
- signed: { type: 'switch' },
233
+ signed: { type: "switch" },
225
234
  creditApp: {
226
- type: 'switch',
235
+ type: "switch",
227
236
  conditions: [
228
- { when: 'signed', is: false, disabled: true },
229
- { when: 'signed', is: true, visible: true },
237
+ { when: "signed", is: false, disabled: true },
238
+ { when: "signed", is: true, visible: true },
230
239
  ],
231
240
  },
232
241
  };
@@ -255,13 +264,13 @@ Evaluate props dynamically based on form state:
255
264
 
256
265
  ```typescript
257
266
  const config: FormFieldsConfig = {
258
- client: { type: 'autocomplete' },
267
+ client: { type: "autocomplete" },
259
268
  clientContact: {
260
- type: 'autocomplete',
269
+ type: "autocomplete",
261
270
  selectProps: {
262
- queryParams: 'client.id',
263
- disabled: '!client',
264
- placeholder: 'client.name',
271
+ queryParams: "client.id",
272
+ disabled: "!client",
273
+ placeholder: "client.name",
265
274
  },
266
275
  },
267
276
  };
@@ -291,10 +300,11 @@ Enable automatic form submission on changes:
291
300
  Access form state and methods from any child component:
292
301
 
293
302
  ```typescript
294
- import { useFormContext } from '@formality-ui/react';
303
+ import { useFormContext } from "@formality-ui/react";
295
304
 
296
305
  function CustomComponent() {
297
- const { config, methods, record, unusedFields, submitImmediate } = useFormContext();
306
+ const { config, methods, record, unusedFields, submitImmediate } =
307
+ useFormContext();
298
308
  // ...
299
309
  }
300
310
  ```
@@ -304,7 +314,7 @@ function CustomComponent() {
304
314
  Evaluate conditions manually:
305
315
 
306
316
  ```typescript
307
- import { useConditions } from '@formality-ui/react';
317
+ import { useConditions } from "@formality-ui/react";
308
318
 
309
319
  const { disabled, visible, setValue } = useConditions({
310
320
  conditions: fieldConfig.conditions,
@@ -316,7 +326,7 @@ const { disabled, visible, setValue } = useConditions({
316
326
  Evaluate dynamic props:
317
327
 
318
328
  ```typescript
319
- import { usePropsEvaluation } from '@formality-ui/react';
329
+ import { usePropsEvaluation } from "@formality-ui/react";
320
330
 
321
331
  const evaluatedProps = usePropsEvaluation(selectProps, watchedValues);
322
332
  ```
@@ -326,7 +336,7 @@ const evaluatedProps = usePropsEvaluation(selectProps, watchedValues);
326
336
  Subscribe to form state changes:
327
337
 
328
338
  ```typescript
329
- import { useFormState } from '@formality-ui/react';
339
+ import { useFormState } from "@formality-ui/react";
330
340
 
331
341
  const { methods, formState } = useFormState(options);
332
342
  ```
@@ -336,7 +346,7 @@ const { methods, formState } = useFormState(options);
336
346
  Subscribe to field value changes:
337
347
 
338
348
  ```typescript
339
- import { useSubscriptions } from '@formality-ui/react';
349
+ import { useSubscriptions } from "@formality-ui/react";
340
350
 
341
351
  const watchedValues = useSubscriptions(fieldNames);
342
352
  ```
@@ -346,7 +356,7 @@ const watchedValues = useSubscriptions(fieldNames);
346
356
  Infer input configurations:
347
357
 
348
358
  ```typescript
349
- import { useInferredInputs } from '@formality-ui/react';
359
+ import { useInferredInputs } from "@formality-ui/react";
350
360
 
351
361
  const inputs = useInferredInputs(config);
352
362
  ```
@@ -358,9 +368,10 @@ const inputs = useInferredInputs(config);
358
368
  Global configuration context:
359
369
 
360
370
  ```typescript
361
- import { useConfigContext } from '@formality-ui/react';
371
+ import { useConfigContext } from "@formality-ui/react";
362
372
 
363
- const { inputs, validators, formatters, parsers, errorMessages } = useConfigContext();
373
+ const { inputs, validators, formatters, parsers, errorMessages } =
374
+ useConfigContext();
364
375
  ```
365
376
 
366
377
  ### FormContext
@@ -368,7 +379,7 @@ const { inputs, validators, formatters, parsers, errorMessages } = useConfigCont
368
379
  Form-level context:
369
380
 
370
381
  ```typescript
371
- import { useFormContext } from '@formality-ui/react';
382
+ import { useFormContext } from "@formality-ui/react";
372
383
 
373
384
  const { config, methods, record, formConfig, unusedFields } = useFormContext();
374
385
  ```
@@ -378,7 +389,7 @@ const { config, methods, record, formConfig, unusedFields } = useFormContext();
378
389
  Group-level context for nested conditions:
379
390
 
380
391
  ```typescript
381
- import { useGroupContext } from '@formality-ui/react';
392
+ import { useGroupContext } from "@formality-ui/react";
382
393
 
383
394
  const groupState = useGroupContext();
384
395
  ```
@@ -420,9 +431,190 @@ import type {
420
431
  UseFormStateOptions,
421
432
  WatcherSetterFn,
422
433
  DebouncedFunction,
423
- } from '@formality-ui/react';
434
+
435
+ // React type overlays — precise React/RHF types layered over core's loose
436
+ // `unknown` types. Prefer these in React code (see Type Safety below).
437
+ ReactInputConfig,
438
+ ReactFieldConfig,
439
+ ReactFormFieldsConfig,
440
+ FormalityFieldComponentProps,
441
+
442
+ // Re-exported react-hook-form types — so consumers need no direct RHF import.
443
+ RefCallBack,
444
+ UseFormStateReturn,
445
+ FieldValues,
446
+ } from "@formality-ui/react";
447
+
448
+ // `defineInputs` is a VALUE export (an identity helper), not a type — import
449
+ // it separately, not inside an `import type { ... }` block.
450
+ import { defineInputs } from "@formality-ui/react";
451
+ ```
452
+
453
+ ## Type Safety
454
+
455
+ Formality ships opt-in, compile-time checking for the three places typos hurt
456
+ most: **Form config keys**, **Field names**, and **input `type` strings**. It
457
+ also ships a precise type for the props Formality injects onto your field
458
+ components, so you can stop hand-rolling a lossy `WithFormality<P>` helper.
459
+
460
+ All of the checks below are **opt-in and non-breaking** — the non-generic
461
+ `<Form>`, `<Field>`, and `InputConfig`/`FormFieldsConfig` patterns shown in
462
+ [Quick Start](#quick-start) keep working byte-for-byte. The overlays below are
463
+ the recommended pattern for new React code.
464
+
465
+ ### Checked Form config keys (`<Form<TFieldValues>>`)
466
+
467
+ `<Form>` is generic over your form's field-values type. With the default
468
+ generic, any string key is accepted (unchanged behavior). Narrow the generic to
469
+ your values type and unknown `config` keys become a **compile error** —
470
+ catching typos like `ofice` at compile time instead of silently rendering
471
+ nothing.
472
+
473
+ ```tsx
474
+ import { Form } from "@formality-ui/react";
475
+ import type { ReactFormFieldsConfig } from "@formality-ui/react";
476
+
477
+ type ClientValues = { name: string; email: string; subscribed: boolean };
478
+
479
+ // ✅ Narrowed — only known field names are accepted.
480
+ const config: ReactFormFieldsConfig<ClientValues> = {
481
+ name: { type: "textField", label: "Full Name" },
482
+ email: { type: "textField", label: "Email" },
483
+ subscribed: { type: "switch", label: "Subscribe" },
484
+ };
485
+
486
+ // @ts-expect-error — typo `ofice` is rejected when the generic is narrowed.
487
+ const bad: ReactFormFieldsConfig<ClientValues> = {
488
+ ofice: { type: "textField" },
489
+ };
490
+
491
+ <Form<ClientValues> config={config}>{/* ... */}</Form>;
492
+ ```
493
+
494
+ The default `<Form>` (no generic) still accepts any string key, so existing
495
+ consumers migrate at their own pace.
496
+
497
+ ### Checked Field names (`FieldProps<TName>`)
498
+
499
+ By default `<Field name="..." />` accepts **any string** — this is backwards
500
+ compatible and matches the Quick Start. Name-checking engages **only when
501
+ `FieldProps` is explicitly narrowed**.
502
+
503
+ > React generics do **not** thread from `<Form<T>>` into its children, so a
504
+ > `<Form<ClientValues>>` does **not** automatically narrow the `name` on a
505
+ > child `<Field>`. To check field names you narrow `FieldProps` explicitly
506
+ > (the honest pattern below), typically via a thin typed wrapper.
507
+
508
+ ```tsx
509
+ import { Field } from "@formality-ui/react";
510
+ import type { FieldProps } from "@formality-ui/react";
511
+
512
+ type ClientValues = { name: string; email: string; subscribed: boolean };
513
+ type Names = keyof ClientValues; // "name" | "email" | "subscribed"
514
+
515
+ // Default usage — any string name compiles (unchanged):
516
+ <Field name="anything" />;
517
+
518
+ // Opt-in strict usage — a typed wrapper that narrows FieldProps:
519
+ function TypedField(props: FieldProps<Names>) {
520
+ return <Field {...props} />;
521
+ }
522
+
523
+ <TypedField name="email" />; // ✅
524
+ // @ts-expect-error — typo `ofice` is rejected once FieldProps is narrowed.
525
+ const _bad: FieldProps<Names> = { name: "ofice" };
526
+ ```
527
+
528
+ Automatic per-form narrowing — where a `<Field>` auto-narrows against the
529
+ enclosing `<Form<TFieldValues>>`'s key set — is a planned follow-up.
530
+
531
+ ### Checking input types with `defineInputs` (opt-in)
532
+
533
+ `type: "textField"` typos (e.g. `type: "texField"`) are invisible by default
534
+ because `FieldConfig.type` / `FieldProps.type` default to `string`.
535
+ `defineInputs` is an **identity helper** that lets you derive a checked union
536
+ of your input-type keys, which you can then thread into `type`.
537
+
538
+ `defineInputs` is a **value** export (it returns `inputs` unchanged with zero
539
+ runtime effect — bundlers tree-shake it to nothing). Import it as a value, not
540
+ `import type`:
541
+
542
+ ```tsx
543
+ import { defineInputs } from "@formality-ui/react";
544
+
545
+ const inputs = defineInputs({
546
+ textField: { component: TextField, defaultValue: "" },
547
+ switch: { component: Switch, defaultValue: false },
548
+ });
549
+
550
+ // "textField" | "switch" — a checked union of your input-type keys.
551
+ export type InputType = keyof typeof inputs;
552
+ ```
553
+
554
+ This is purely additive — the existing non-generic `Field` and
555
+ `FieldConfig.type` still work unchanged. End-to-end wiring of `InputType` into
556
+ those types is a follow-up; `defineInputs` is the opt-in entry point.
557
+
558
+ ### Field component props: `FormalityFieldComponentProps`
559
+
560
+ `<Field>` renders your input component via React Hook Form's `<Controller>` and
561
+ injects a bundle of props onto it. `FormalityFieldComponentProps<P>` is the
562
+ **precise** type for that contract — replacing the lossy `WithFormality<P>`
563
+ helper consumers (e.g. `sellario-ui`) hand-roll today.
564
+
565
+ **Before — the lossy hand-rolled helper:**
566
+
567
+ ```tsx
568
+ // ❌ Lossy: state/formState are `unknown`, and forwardRef is the wrong type.
569
+ type WithFormality<P> = P & {
570
+ state?: unknown;
571
+ formState?: unknown;
572
+ forwardRef?: React.Ref<HTMLInputElement>; // wrong: RHF hands a RefCallBack
573
+ };
574
+ ```
575
+
576
+ **After — the shipped precise type:**
577
+
578
+ ```tsx
579
+ import type { FormalityFieldComponentProps } from "@formality-ui/react";
580
+
581
+ // FormalityFieldComponentProps<P = unknown> = P & {
582
+ // state?: CustomFieldState | Record<string, CustomFieldState>;
583
+ // formState?: UseFormStateReturn<FieldValues>;
584
+ // forwardRef?: RefCallBack;
585
+ // }
586
+
587
+ type TextFieldProps = { label?: string };
588
+
589
+ const TextField: React.ComponentType<
590
+ FormalityFieldComponentProps<TextFieldProps>
591
+ > = ({ state, formState, forwardRef, ...domProps }) => (
592
+ <input ref={forwardRef} {...domProps} />
593
+ );
424
594
  ```
425
595
 
596
+ **Destructure before forwarding.** Always pull `state`, `formState`, and
597
+ `forwardRef` **out** of props before spreading the rest onto the underlying DOM
598
+ node — otherwise these non-DOM props leak to the DOM and React warns.
599
+
600
+ **Wiring `forwardRef` to the inner input.** `forwardRef` is RHF's `RefCallBack`
601
+ (`(instance: any) => void`), **not** `React.Ref<HTMLInputElement>`. For a
602
+ plain `<input>` use `ref={forwardRef}`. For **MUI v9** components (e.g.
603
+ `Checkbox`) that no longer accept a top-level `inputRef`, wire it via slots:
604
+
605
+ ```tsx
606
+ slotProps={{ input: { ref: forwardRef } }}
607
+ ```
608
+
609
+ **Runtime delivery (important).** `<Field>` delivers the RHF ref as a regular,
610
+ top-level `forwardRef` prop — no `React.forwardRef` wrap is required for a
611
+ plain function component that destructures `forwardRef` and wires it to the
612
+ inner input (`ref={forwardRef}`). Consumers migrating off the old
613
+ React-special `ref` key: a `React.forwardRef`-wrapped component should consume
614
+ `props.forwardRef` (PRD §20.4), and under React 19 ref-as-prop use `forwardRef`
615
+ directly. The type ships the **intended contract** so consumers can stop
616
+ hand-rolling `WithFormality`.
617
+
426
618
  ## Utilities
427
619
 
428
620
  ### makeProxyState
@@ -430,12 +622,39 @@ import type {
430
622
  Create proxy state for efficient subscriptions:
431
623
 
432
624
  ```typescript
433
- import { makeProxyState, makeDeepProxyState } from '@formality-ui/react';
625
+ import { makeProxyState, makeDeepProxyState } from "@formality-ui/react";
434
626
 
435
627
  const proxy = makeProxyState(initialState);
436
628
  const deepProxy = makeDeepProxyState(initialState);
437
629
  ```
438
630
 
631
+ ## Testing & Coverage
632
+
633
+ Run the test suite with coverage from the repo root:
634
+
635
+ ```bash
636
+ pnpm test:coverage
637
+ # equivalent to: vitest run --coverage
638
+ ```
639
+
640
+ Coverage is enforced as a **hard gate**: the run exits non-zero if **any** of
641
+ statements, branches, functions, or lines drop below **90%**
642
+ ([vitest coverage thresholds](https://vitest.dev/guide/coverage.html#coverage-thresholds)).
643
+
644
+ Coverage is computed **repo-wide** (merged across `packages/core` and
645
+ `packages/react`), excluding only the directories below:
646
+
647
+ | Glob | Reason |
648
+ | -------------------- | ---------------------- |
649
+ | `examples/**` | Demo apps; not shipped |
650
+ | `packages/svelte/**` | Stubbed adapter |
651
+ | `packages/vue/**` | Stubbed adapter |
652
+ | `**/dist/**` | Build output |
653
+
654
+ All other code — `packages/core/**`, `packages/react/**`, and any future
655
+ adapter with a real implementation — is in scope and must clear 90%. See
656
+ `vitest.config.ts` for the exact configuration.
657
+
439
658
  ## License
440
659
 
441
660
  MIT