@formality-ui/react 0.0.0 → 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 +250 -32
- package/dist/index.cjs +342 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +250 -19
- package/dist/index.d.ts +250 -19
- package/dist/index.js +342 -78
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
|
24
|
-
import type { InputConfig, FormFieldsConfig } from
|
|
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
|
|
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
|
|
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:
|
|
52
|
-
email: { type:
|
|
53
|
-
subscribed: { type:
|
|
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:
|
|
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:
|
|
233
|
+
signed: { type: "switch" },
|
|
225
234
|
creditApp: {
|
|
226
|
-
type:
|
|
235
|
+
type: "switch",
|
|
227
236
|
conditions: [
|
|
228
|
-
{ when:
|
|
229
|
-
{ when:
|
|
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:
|
|
267
|
+
client: { type: "autocomplete" },
|
|
259
268
|
clientContact: {
|
|
260
|
-
type:
|
|
269
|
+
type: "autocomplete",
|
|
261
270
|
selectProps: {
|
|
262
|
-
queryParams:
|
|
263
|
-
disabled:
|
|
264
|
-
placeholder:
|
|
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
|
|
303
|
+
import { useFormContext } from "@formality-ui/react";
|
|
295
304
|
|
|
296
305
|
function CustomComponent() {
|
|
297
|
-
const { config, methods, record, unusedFields, submitImmediate } =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
371
|
+
import { useConfigContext } from "@formality-ui/react";
|
|
362
372
|
|
|
363
|
-
const { inputs, validators, formatters, parsers, errorMessages } =
|
|
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
|
|
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
|
|
392
|
+
import { useGroupContext } from "@formality-ui/react";
|
|
382
393
|
|
|
383
394
|
const groupState = useGroupContext();
|
|
384
395
|
```
|
|
@@ -420,9 +431,189 @@ import type {
|
|
|
420
431
|
UseFormStateOptions,
|
|
421
432
|
WatcherSetterFn,
|
|
422
433
|
DebouncedFunction,
|
|
423
|
-
|
|
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 caveat (important).** Today `Field` delivers the RHF ref via the
|
|
610
|
+
React-special `ref` key, **not** a top-level `forwardRef` prop. To receive it
|
|
611
|
+
as `forwardRef` on a bare function component, either wrap your component with
|
|
612
|
+
`React.forwardRef`, or target React 19's ref-as-prop. Making `Field` deliver a
|
|
613
|
+
top-level `forwardRef` key for bare components is a future runtime task; the
|
|
614
|
+
type ships the **intended contract now** so consumers can stop hand-rolling
|
|
615
|
+
`WithFormality`.
|
|
616
|
+
|
|
426
617
|
## Utilities
|
|
427
618
|
|
|
428
619
|
### makeProxyState
|
|
@@ -430,12 +621,39 @@ import type {
|
|
|
430
621
|
Create proxy state for efficient subscriptions:
|
|
431
622
|
|
|
432
623
|
```typescript
|
|
433
|
-
import { makeProxyState, makeDeepProxyState } from
|
|
624
|
+
import { makeProxyState, makeDeepProxyState } from "@formality-ui/react";
|
|
434
625
|
|
|
435
626
|
const proxy = makeProxyState(initialState);
|
|
436
627
|
const deepProxy = makeDeepProxyState(initialState);
|
|
437
628
|
```
|
|
438
629
|
|
|
630
|
+
## Testing & Coverage
|
|
631
|
+
|
|
632
|
+
Run the test suite with coverage from the repo root:
|
|
633
|
+
|
|
634
|
+
```bash
|
|
635
|
+
pnpm test:coverage
|
|
636
|
+
# equivalent to: vitest run --coverage
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
Coverage is enforced as a **hard gate**: the run exits non-zero if **any** of
|
|
640
|
+
statements, branches, functions, or lines drop below **90%**
|
|
641
|
+
([vitest coverage thresholds](https://vitest.dev/guide/coverage.html#coverage-thresholds)).
|
|
642
|
+
|
|
643
|
+
Coverage is computed **repo-wide** (merged across `packages/core` and
|
|
644
|
+
`packages/react`), excluding only the directories below:
|
|
645
|
+
|
|
646
|
+
| Glob | Reason |
|
|
647
|
+
| -------------------- | ---------------------- |
|
|
648
|
+
| `examples/**` | Demo apps; not shipped |
|
|
649
|
+
| `packages/svelte/**` | Stubbed adapter |
|
|
650
|
+
| `packages/vue/**` | Stubbed adapter |
|
|
651
|
+
| `**/dist/**` | Build output |
|
|
652
|
+
|
|
653
|
+
All other code — `packages/core/**`, `packages/react/**`, and any future
|
|
654
|
+
adapter with a real implementation — is in scope and must clear 90%. See
|
|
655
|
+
`vitest.config.ts` for the exact configuration.
|
|
656
|
+
|
|
439
657
|
## License
|
|
440
658
|
|
|
441
659
|
MIT
|