@behivetech/forms.form-field-managed 0.0.0-stage → 0.1.5

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
@@ -1,3 +1,127 @@
1
- # Temporary Holding Version
1
+ # FormFieldManaged
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `FormField` with its react-hook-form wiring already done — the control's
4
+ value/onChange/onBlur/ref, the label association, and the error message.
5
+
6
+ Use plain [`FormField`](../form-field) when you are managing state yourself;
7
+ use this when the field belongs to a react-hook-form form.
8
+
9
+ ## Usage
10
+
11
+ ```jsx
12
+ import { FormFieldManaged } from "@behivetech/forms.form-field-managed";
13
+ import { Form, useForm } from "@behivetech/forms.form-provider";
14
+ import { TextField } from "@behivetech/atoms.text-field";
15
+
16
+ const methods = useForm({ defaultValues: { email: "" } });
17
+ const save = (values) => alert(JSON.stringify(values));
18
+
19
+ <Form methods={methods} onSubmit={save}>
20
+ <FormFieldManaged
21
+ name="email"
22
+ label="Email"
23
+ rules={{ required: "Email is required" }}
24
+ >
25
+ {(field) => <TextField {...field} />}
26
+ </FormFieldManaged>
27
+ </Form>;
28
+ ```
29
+
30
+ The error message comes from the field's own validation state — there is no
31
+ `error` prop to pass, and no need to read `formState.errors`. `label`, `hint`,
32
+ `required` and `className` pass through to `FormField`.
33
+
34
+ `control` is optional; the field resolves it from the surrounding `Form` /
35
+ `FormProvider` context.
36
+
37
+ ## Typing in TypeScript
38
+
39
+ `field.value` is typed as the field `name` points at, so it spreads onto a
40
+ control without a cast. Getting that narrowing depends on how the generics are
41
+ supplied.
42
+
43
+ Passing `control` infers both type parameters and needs no annotation:
44
+
45
+ ```tsx
46
+ function Example() {
47
+ const methods = useForm({ defaultValues: { email: "" } });
48
+ return (
49
+ <Form
50
+ methods={methods}
51
+ onSubmit={(values) => alert(JSON.stringify(values))}
52
+ >
53
+ <FormFieldManaged control={methods.control} name="email" label="Email">
54
+ {(field) => <TextField {...field} />}
55
+ </FormFieldManaged>
56
+ </Form>
57
+ );
58
+ }
59
+ ```
60
+
61
+ Taking `control` from context instead means naming the field type too:
62
+
63
+ ```tsx
64
+ type Values = { email: string };
65
+
66
+ function Example() {
67
+ const methods = useForm<Values>({ defaultValues: { email: "" } });
68
+ return (
69
+ <Form
70
+ methods={methods}
71
+ onSubmit={(values) => alert(JSON.stringify(values))}
72
+ >
73
+ <FormFieldManaged<Values, "email"> name="email" label="Email">
74
+ {(field) => <TextField {...field} />}
75
+ </FormFieldManaged>
76
+ </Form>
77
+ );
78
+ }
79
+ ```
80
+
81
+ Supplying only the first type argument — `<FormFieldManaged<Values> name="email">`
82
+ — does **not** narrow. TypeScript stops inferring the remaining parameters once
83
+ any is given explicitly, so `TName` falls back to its default and `field.value`
84
+ widens to a union of every field in the form. Prefer one of the two forms above.
85
+
86
+ ## Why a render prop
87
+
88
+ The one thing that genuinely differs between controls is the prop they report
89
+ changes on. Native inputs take the field as-is:
90
+
91
+ ```ts
92
+ (field) => <TextField {...field} />
93
+ ```
94
+
95
+ Radix-backed controls (`Select`, `Checkbox`, `Toggle`) report through
96
+ `onValueChange` / `onCheckedChange`, so the caller maps that one prop:
97
+
98
+ ```ts
99
+ (field) => (
100
+ <Select
101
+ id={field.id}
102
+ name={field.name}
103
+ options={roles}
104
+ value={field.value}
105
+ onValueChange={field.onChange}
106
+ />
107
+ )
108
+ ```
109
+
110
+ Making that visible is deliberate — it is the difference that matters, and it
111
+ cannot be typed if hidden behind a clone.
112
+
113
+ ## Why not `register`
114
+
115
+ `register()` returns an uncontrolled _native input_ contract
116
+ (`{name, onChange, onBlur, ref}`). It works on `TextField` and `Textarea`,
117
+ which render real `<input>` / `<textarea>` elements.
118
+
119
+ It does not work on `Checkbox`, `Toggle` or `Select`. Those are Radix-backed:
120
+ the ref would land on the trigger `<button>`, whose `value` is always `""`, and
121
+ Radix reports changes through `onCheckedChange` / `onValueChange`, never
122
+ `onChange`. The field would silently submit its default value with no type or
123
+ runtime error. `FormFieldManaged` uses `useController`, which works for all of
124
+ them.
125
+
126
+ The render prop also receives the field's state as a second argument
127
+ (`(field, fieldState) => …`) if you need `invalid`, `isDirty` or `isTouched`.
package/dist/index.cjs ADDED
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ FormFieldManaged: () => FormFieldManaged
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // form-field-managed.tsx
28
+ var import_react = require("react");
29
+ var import_forms = require("@behivetech/forms.form-field");
30
+ var import_forms2 = require("@behivetech/forms.form-provider");
31
+ var import_jsx_runtime = require("react/jsx-runtime");
32
+ var FormFieldManaged = ({
33
+ name,
34
+ control,
35
+ rules,
36
+ children,
37
+ ...fieldProps
38
+ }) => {
39
+ const id = (0, import_react.useId)();
40
+ const { field, fieldState } = (0, import_forms2.useController)({
41
+ name,
42
+ control,
43
+ rules
44
+ });
45
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_forms.FormField, { ...fieldProps, htmlFor: id, error: fieldState.error?.message, children: children({ ...field, id }, fieldState) });
46
+ };
47
+ // Annotate the CommonJS export names for ESM import in node:
48
+ 0 && (module.exports = {
49
+ FormFieldManaged
50
+ });
@@ -0,0 +1,67 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { FormFieldProps } from '@behivetech/forms.form-field';
4
+ import { FieldValues, FieldPath, Control, RegisterOptions, ControllerRenderProps, ControllerFieldState } from '@behivetech/forms.form-provider';
5
+
6
+ /**
7
+ * What the render prop receives: the wired field plus its `id`.
8
+ *
9
+ * `TName` is threaded through to `ControllerRenderProps` rather than left at its
10
+ * default. Without it `value` widens to the union of every field type in the
11
+ * form, so a call site spreading onto a `TextField` has to assert
12
+ * `value as string` — an assertion that keeps compiling if the field later
13
+ * changes type, which is the mistake the generic exists to catch.
14
+ */
15
+ type ManagedField<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = ControllerRenderProps<TFieldValues, TName> & {
16
+ /** Matches the label's `htmlFor`; spread onto the control or pass explicitly */
17
+ id: string;
18
+ };
19
+ interface FormFieldManagedProps<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> extends Omit<FormFieldProps, "children" | "error" | "htmlFor"> {
20
+ /** Field path in the form values; infers the field's value type */
21
+ name: TName;
22
+ /** Form control; falls back to the surrounding `useFormContext()` when omitted */
23
+ control?: Control<TFieldValues>;
24
+ /** Validation rules passed through to react-hook-form */
25
+ rules?: RegisterOptions<TFieldValues, TName>;
26
+ /** Renders the control from the wired field */
27
+ children: (field: ManagedField<TFieldValues, TName>, fieldState: ControllerFieldState) => ReactNode;
28
+ }
29
+ /**
30
+ * `FormField` with its react-hook-form wiring already done: the control's
31
+ * value/onChange/onBlur/ref, the label association, and the error message.
32
+ *
33
+ * Uses `useController` rather than `register` because `register` only works on
34
+ * controls that render a native input. `TextField` and `Textarea` do;
35
+ * `Checkbox`, `Toggle` and `Select` are Radix-backed, where the ref would land
36
+ * on the trigger button and the change callback is `onCheckedChange` /
37
+ * `onValueChange`, so `register` binds nothing and the field silently keeps its
38
+ * default. `useController` works for all of them.
39
+ *
40
+ * The control is supplied through a render prop rather than cloned, because the
41
+ * prop a control listens on is the one thing that genuinely differs between
42
+ * them — worth seeing at the call site, and impossible to type if hidden.
43
+ *
44
+ * Pass `control` and both type parameters infer, so `field.value` is the type
45
+ * of that one field and spreads onto a control without a cast:
46
+ *
47
+ * ```tsx
48
+ * <FormFieldManaged control={methods.control} name="email" label="Email">
49
+ * {(field) => <TextField {...field} />}
50
+ * </FormFieldManaged>
51
+ * ```
52
+ *
53
+ * Taking `control` from context instead means naming both parameters. Supplying
54
+ * only the first — `<FormFieldManaged<Values> name="email">` — leaves `TName` at
55
+ * its default and widens `field.value` to a union of every field in the form,
56
+ * because TypeScript stops inferring the rest once any type argument is given
57
+ * explicitly:
58
+ *
59
+ * ```tsx
60
+ * <FormFieldManaged<Values, "email"> name="email" label="Email">
61
+ * {(field) => <TextField {...field} />}
62
+ * </FormFieldManaged>
63
+ * ```
64
+ */
65
+ declare const FormFieldManaged: <TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>({ name, control, rules, children, ...fieldProps }: FormFieldManagedProps<TFieldValues, TName>) => react.JSX.Element;
66
+
67
+ export { FormFieldManaged, type FormFieldManagedProps, type ManagedField };
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ // form-field-managed.tsx
2
+ import { useId } from "react";
3
+ import { FormField } from "@behivetech/forms.form-field";
4
+ import {
5
+ useController
6
+ } from "@behivetech/forms.form-provider";
7
+ import { jsx } from "react/jsx-runtime";
8
+ var FormFieldManaged = ({
9
+ name,
10
+ control,
11
+ rules,
12
+ children,
13
+ ...fieldProps
14
+ }) => {
15
+ const id = useId();
16
+ const { field, fieldState } = useController({
17
+ name,
18
+ control,
19
+ rules
20
+ });
21
+ return /* @__PURE__ */ jsx(FormField, { ...fieldProps, htmlFor: id, error: fieldState.error?.message, children: children({ ...field, id }, fieldState) });
22
+ };
23
+ export {
24
+ FormFieldManaged
25
+ };
@@ -0,0 +1,83 @@
1
+ import { Button } from "@behivetech/atoms.button";
2
+ import { Select } from "@behivetech/atoms.select";
3
+ import { TextField } from "@behivetech/atoms.text-field";
4
+ import { Toggle } from "@behivetech/atoms.toggle";
5
+ import { Form, useForm } from "@behivetech/forms.form-provider";
6
+ import { FormFieldManaged } from "./form-field-managed.js";
7
+
8
+ interface DemoValues {
9
+ email: string;
10
+ role: string;
11
+ active: boolean;
12
+ }
13
+
14
+ const ROLES = [
15
+ { label: "Admin", value: "admin" },
16
+ { label: "Editor", value: "editor" },
17
+ ];
18
+
19
+ /**
20
+ * One managed form covering both integration shapes: a native control, where
21
+ * the field spreads straight on, and two Radix controls, where the caller maps
22
+ * the one prop that differs.
23
+ *
24
+ * Each field passes `control` rather than naming type parameters. That is the
25
+ * shape worth copying: it infers both parameters, so `field.value` is the type
26
+ * of that one field and needs no cast. Reading `control` from context instead
27
+ * works too, but then the field type has to be named — `<FormFieldManaged<
28
+ * DemoValues, "email">` — because supplying one type argument stops TypeScript
29
+ * inferring the other.
30
+ */
31
+ export const ManagedForm = () => {
32
+ const methods = useForm<DemoValues>({
33
+ defaultValues: { email: "", role: "", active: false },
34
+ });
35
+
36
+ return (
37
+ <Form methods={methods} onSubmit={(values) => console.log(values)}>
38
+ <FormFieldManaged
39
+ control={methods.control}
40
+ name="email"
41
+ label="Email"
42
+ hint="We only use this for sign-in"
43
+ required
44
+ rules={{ required: "Email is required" }}
45
+ >
46
+ {(field) => <TextField {...field} type="email" />}
47
+ </FormFieldManaged>
48
+
49
+ <FormFieldManaged
50
+ control={methods.control}
51
+ name="role"
52
+ label="Role"
53
+ rules={{ required: "Pick a role" }}
54
+ >
55
+ {(field) => (
56
+ <Select
57
+ id={field.id}
58
+ name={field.name}
59
+ options={ROLES}
60
+ value={field.value}
61
+ onValueChange={field.onChange}
62
+ placeholder="Pick one"
63
+ />
64
+ )}
65
+ </FormFieldManaged>
66
+
67
+ <FormFieldManaged control={methods.control} name="active" label="Active">
68
+ {(field) => (
69
+ <Toggle
70
+ id={field.id}
71
+ name={field.name}
72
+ checked={field.value}
73
+ onCheckedChange={field.onChange}
74
+ />
75
+ )}
76
+ </FormFieldManaged>
77
+
78
+ <Button type="submit" variant="primary" size="sm">
79
+ Save
80
+ </Button>
81
+ </Form>
82
+ );
83
+ };
@@ -0,0 +1,105 @@
1
+ import { type ReactNode, useId } from "react";
2
+ import { FormField, type FormFieldProps } from "@behivetech/forms.form-field";
3
+ import {
4
+ useController,
5
+ type Control,
6
+ type ControllerFieldState,
7
+ type ControllerRenderProps,
8
+ type FieldPath,
9
+ type FieldValues,
10
+ type RegisterOptions,
11
+ } from "@behivetech/forms.form-provider";
12
+
13
+ /**
14
+ * What the render prop receives: the wired field plus its `id`.
15
+ *
16
+ * `TName` is threaded through to `ControllerRenderProps` rather than left at its
17
+ * default. Without it `value` widens to the union of every field type in the
18
+ * form, so a call site spreading onto a `TextField` has to assert
19
+ * `value as string` — an assertion that keeps compiling if the field later
20
+ * changes type, which is the mistake the generic exists to catch.
21
+ */
22
+ export type ManagedField<
23
+ TFieldValues extends FieldValues = FieldValues,
24
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
25
+ > = ControllerRenderProps<TFieldValues, TName> & {
26
+ /** Matches the label's `htmlFor`; spread onto the control or pass explicitly */
27
+ id: string;
28
+ };
29
+
30
+ export interface FormFieldManagedProps<
31
+ TFieldValues extends FieldValues = FieldValues,
32
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
33
+ > extends Omit<FormFieldProps, "children" | "error" | "htmlFor"> {
34
+ /** Field path in the form values; infers the field's value type */
35
+ name: TName;
36
+ /** Form control; falls back to the surrounding `useFormContext()` when omitted */
37
+ control?: Control<TFieldValues>;
38
+ /** Validation rules passed through to react-hook-form */
39
+ rules?: RegisterOptions<TFieldValues, TName>;
40
+ /** Renders the control from the wired field */
41
+ children: (
42
+ field: ManagedField<TFieldValues, TName>,
43
+ fieldState: ControllerFieldState,
44
+ ) => ReactNode;
45
+ }
46
+
47
+ /**
48
+ * `FormField` with its react-hook-form wiring already done: the control's
49
+ * value/onChange/onBlur/ref, the label association, and the error message.
50
+ *
51
+ * Uses `useController` rather than `register` because `register` only works on
52
+ * controls that render a native input. `TextField` and `Textarea` do;
53
+ * `Checkbox`, `Toggle` and `Select` are Radix-backed, where the ref would land
54
+ * on the trigger button and the change callback is `onCheckedChange` /
55
+ * `onValueChange`, so `register` binds nothing and the field silently keeps its
56
+ * default. `useController` works for all of them.
57
+ *
58
+ * The control is supplied through a render prop rather than cloned, because the
59
+ * prop a control listens on is the one thing that genuinely differs between
60
+ * them — worth seeing at the call site, and impossible to type if hidden.
61
+ *
62
+ * Pass `control` and both type parameters infer, so `field.value` is the type
63
+ * of that one field and spreads onto a control without a cast:
64
+ *
65
+ * ```tsx
66
+ * <FormFieldManaged control={methods.control} name="email" label="Email">
67
+ * {(field) => <TextField {...field} />}
68
+ * </FormFieldManaged>
69
+ * ```
70
+ *
71
+ * Taking `control` from context instead means naming both parameters. Supplying
72
+ * only the first — `<FormFieldManaged<Values> name="email">` — leaves `TName` at
73
+ * its default and widens `field.value` to a union of every field in the form,
74
+ * because TypeScript stops inferring the rest once any type argument is given
75
+ * explicitly:
76
+ *
77
+ * ```tsx
78
+ * <FormFieldManaged<Values, "email"> name="email" label="Email">
79
+ * {(field) => <TextField {...field} />}
80
+ * </FormFieldManaged>
81
+ * ```
82
+ */
83
+ export const FormFieldManaged = <
84
+ TFieldValues extends FieldValues = FieldValues,
85
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
86
+ >({
87
+ name,
88
+ control,
89
+ rules,
90
+ children,
91
+ ...fieldProps
92
+ }: FormFieldManagedProps<TFieldValues, TName>) => {
93
+ const id = useId();
94
+ const { field, fieldState } = useController<TFieldValues, TName>({
95
+ name,
96
+ control,
97
+ rules,
98
+ });
99
+
100
+ return (
101
+ <FormField {...fieldProps} htmlFor={id} error={fieldState.error?.message}>
102
+ {children({ ...field, id }, fieldState)}
103
+ </FormField>
104
+ );
105
+ };
package/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { FormFieldManaged } from "./form-field-managed.js";
2
+ export type {
3
+ FormFieldManagedProps,
4
+ ManagedField,
5
+ } from "./form-field-managed.js";
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@behivetech/forms.form-field-managed",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.5",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "private": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "index.ts",
18
+ "form-field-managed.tsx",
19
+ "form-field-managed.module.scss",
20
+ "*.example.tsx",
21
+ "docs.json"
22
+ ],
23
+ "registry": {
24
+ "status": "stable",
25
+ "tags": []
26
+ },
27
+ "dependencies": {
28
+ "@behivetech/forms.form-field": "0.3.3",
29
+ "@behivetech/forms.form-provider": "0.1.5"
30
+ },
31
+ "peerDependencies": {
32
+ "react": "^19.2.7",
33
+ "react-dom": "^19.2.7"
34
+ },
35
+ "devDependencies": {
36
+ "@types/react": "19.2.17",
37
+ "@types/react-dom": "19.2.3",
38
+ "eslint": "^9.39.5",
39
+ "tsup": "^8.5.1",
40
+ "typescript": "^6.0.3",
41
+ "vitest": "^4.1.10",
42
+ "@behivetech/atoms.select": "0.4.3",
43
+ "@behivetech/atoms.text-field": "0.3.3",
44
+ "@behivetech/atoms.toggle": "0.4.3",
45
+ "@bht-component-registry/env": "0.1.0"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/behivetech/bht-galaxy.git",
50
+ "directory": "registry/behivetech/forms/form-field-managed"
51
+ },
52
+ "publishConfig": {
53
+ "registry": "https://registry.npmjs.org",
54
+ "access": "public",
55
+ "provenance": true
56
+ },
57
+ "scripts": {
58
+ "build": "tsup && bht-registry-docgen",
59
+ "dev": "tsup --watch",
60
+ "lint": "eslint . --max-warnings 0",
61
+ "check-types": "tsc --noEmit",
62
+ "test": "vitest run"
63
+ }
6
64
  }