@asnewyla/form 0.2.0 → 0.2.2

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
@@ -10,15 +10,14 @@ npm install @asnewyla/form @asnewyla/input
10
10
  ```
11
11
 
12
12
  `FormFieldInput` renders `@asnewyla/input`'s `Input` under the hood, so its
13
- stylesheet also needs importing once, anywhere in your app:
13
+ stylesheet also needs importing once, alongside `@asnewyla/form`'s own
14
+ (which styles the error summary):
14
15
 
15
16
  ```ts
17
+ import '@asnewyla/form/styles.css';
16
18
  import '@asnewyla/input/styles.css';
17
19
  ```
18
20
 
19
- `@asnewyla/form` ships no CSS of its own — every field's appearance comes
20
- from `@asnewyla/input`.
21
-
22
21
  ## Usage
23
22
 
24
23
  ```tsx
package/dist/index.css ADDED
@@ -0,0 +1,26 @@
1
+ /* src/components/FormErrorSummary.css */
2
+ .xd-form-error-summary {
3
+ border: var(--xd-border-width-thin, 1px) solid var(--xd-color-destructive, #dc2626);
4
+ border-radius: var(--xd-radius-md, 0.375rem);
5
+ padding: var(--xd-space-lg, 0.75rem) var(--xd-space-xl, 1rem);
6
+ margin-block-end: var(--xd-space-lg, 0.75rem);
7
+ }
8
+ .xd-form-error-summary:focus {
9
+ outline: var(--xd-focus-ring-width, 2px) solid var(--xd-color-destructive, #dc2626);
10
+ outline-offset: var(--xd-focus-ring-offset, 2px);
11
+ }
12
+ .xd-form-error-summary-heading {
13
+ margin: 0 0 var(--xd-space-sm, 0.375rem);
14
+ font-size: var(--xd-font-size-md, 0.875rem);
15
+ font-weight: var(--xd-font-weight-semibold, 600);
16
+ color: var(--xd-color-destructive, #dc2626);
17
+ }
18
+ .xd-form-error-summary ul {
19
+ margin: 0;
20
+ padding-inline-start: var(--xd-space-xl, 1rem);
21
+ }
22
+ .xd-form-error-summary a {
23
+ color: var(--xd-color-destructive, #dc2626);
24
+ font-size: var(--xd-font-size-md, 0.875rem);
25
+ }
26
+ /*# sourceMappingURL=index.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/FormErrorSummary.css"],"sourcesContent":[".xd-form-error-summary {\n border: var(--xd-border-width-thin, 1px) solid\n var(--xd-color-destructive, #dc2626);\n border-radius: var(--xd-radius-md, 0.375rem);\n padding: var(--xd-space-lg, 0.75rem) var(--xd-space-xl, 1rem);\n margin-block-end: var(--xd-space-lg, 0.75rem);\n}\n\n/* Focus (not focus-visible): the summary is only ever reachable via a\n programmatic .focus() call after a failed submit, never by tabbing to\n it — the visible ring should show regardless of what triggered the\n submit (mouse click on a Save button counts too). */\n.xd-form-error-summary:focus {\n outline: var(--xd-focus-ring-width, 2px) solid\n var(--xd-color-destructive, #dc2626);\n outline-offset: var(--xd-focus-ring-offset, 2px);\n}\n\n.xd-form-error-summary-heading {\n margin: 0 0 var(--xd-space-sm, 0.375rem);\n font-size: var(--xd-font-size-md, 0.875rem);\n font-weight: var(--xd-font-weight-semibold, 600);\n color: var(--xd-color-destructive, #dc2626);\n}\n\n.xd-form-error-summary ul {\n margin: 0;\n padding-inline-start: var(--xd-space-xl, 1rem);\n}\n\n.xd-form-error-summary a {\n color: var(--xd-color-destructive, #dc2626);\n font-size: var(--xd-font-size-md, 0.875rem);\n}\n"],"mappings":";AAAA,CAAC;AACC,UAAQ,IAAI,sBAAsB,EAAE,KAAK,MACvC,IAAI,sBAAsB,EAAE;AAC9B,iBAAe,IAAI,cAAc,EAAE;AACnC,WAAS,IAAI,aAAa,EAAE,SAAS,IAAI,aAAa,EAAE;AACxD,oBAAkB,IAAI,aAAa,EAAE;AACvC;AAMA,CAZC,qBAYqB;AACpB,WAAS,IAAI,qBAAqB,EAAE,KAAK,MACvC,IAAI,sBAAsB,EAAE;AAC9B,kBAAgB,IAAI,sBAAsB,EAAE;AAC9C;AAEA,CAAC;AACC,UAAQ,EAAE,EAAE,IAAI,aAAa,EAAE;AAC/B,aAAW,IAAI,iBAAiB,EAAE;AAClC,eAAa,IAAI,yBAAyB,EAAE;AAC5C,SAAO,IAAI,sBAAsB,EAAE;AACrC;AAEA,CAzBC,sBAyBsB;AACrB,UAAQ;AACR,wBAAsB,IAAI,aAAa,EAAE;AAC3C;AAEA,CA9BC,sBA8BsB;AACrB,SAAO,IAAI,sBAAsB,EAAE;AACnC,aAAW,IAAI,iBAAiB,EAAE;AACpC;","names":[]}
package/dist/index.js CHANGED
@@ -6,6 +6,22 @@ var reactDom = require('react-dom');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
 
8
8
  // src/components/Form.tsx
9
+ var FormErrorSummary = react.forwardRef(function FormErrorSummary2({ invalidFields }, ref) {
10
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "xd-form-error-summary", role: "alert", tabIndex: -1, ref, children: [
11
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "xd-form-error-summary-heading", children: "There is a problem" }),
12
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { children: invalidFields.map(({ name, id, message }) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(
13
+ "a",
14
+ {
15
+ href: `#${id}`,
16
+ onClick: (event) => {
17
+ event.preventDefault();
18
+ document.getElementById(id)?.focus();
19
+ },
20
+ children: message
21
+ }
22
+ ) }, name)) })
23
+ ] });
24
+ });
9
25
 
10
26
  // src/components/validation.ts
11
27
  function validateValue(value, rules) {
@@ -129,37 +145,10 @@ function Form({ children, onSubmit }) {
129
145
  [registerField, unregisterField, validateField, errors]
130
146
  );
131
147
  return /* @__PURE__ */ jsxRuntime.jsxs("form", { ref: formRef, onSubmit: handleSubmit, noValidate: true, children: [
132
- invalid.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ErrorSummary, { ref: summaryRef, invalidFields: invalid }),
148
+ invalid.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(FormErrorSummary, { ref: summaryRef, invalidFields: invalid }),
133
149
  /* @__PURE__ */ jsxRuntime.jsx(input.FormFieldProvider, { value: contextValue, children })
134
150
  ] });
135
151
  }
136
- var ErrorSummary = react.forwardRef(
137
- function ErrorSummary2({ invalidFields }, ref) {
138
- return /* @__PURE__ */ jsxRuntime.jsxs(
139
- "div",
140
- {
141
- className: "xd-form-error-summary",
142
- role: "alert",
143
- tabIndex: -1,
144
- ref,
145
- children: [
146
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "xd-form-error-summary-heading", children: "There is a problem" }),
147
- /* @__PURE__ */ jsxRuntime.jsx("ul", { children: invalidFields.map(({ name, id, message }) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(
148
- "a",
149
- {
150
- href: `#${id}`,
151
- onClick: (event) => {
152
- event.preventDefault();
153
- document.getElementById(id)?.focus();
154
- },
155
- children: message
156
- }
157
- ) }, name)) })
158
- ]
159
- }
160
- );
161
- }
162
- );
163
152
  function FormFieldInputInner({
164
153
  name,
165
154
  required,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/validation.ts","../src/components/useFieldRegistry.ts","../src/components/Form.tsx","../src/components/FormFieldInput.tsx"],"names":["useRef","useState","useCallback","flushSync","useMemo","jsx","FormFieldProvider","forwardRef","ErrorSummary","jsxs","useFormFieldContext","useEffect","Input"],"mappings":"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","file":"index.js","sourcesContent":["import type { ValidationRules } from '@asnewyla/input';\r\n\r\nexport function validateValue(\r\n value: string,\r\n rules?: ValidationRules,\r\n): string | undefined {\r\n if (!rules) return undefined;\r\n\r\n if (rules.required && value.trim() === '') {\r\n return typeof rules.required === 'string'\r\n ? rules.required\r\n : 'This field is required';\r\n }\r\n if (rules.pattern && value !== '' && !new RegExp(rules.pattern).test(value)) {\r\n return 'Invalid format';\r\n }\r\n if (rules.minLength !== undefined && value.length < rules.minLength) {\r\n return `Must be at least ${rules.minLength} characters`;\r\n }\r\n if (rules.maxLength !== undefined && value.length > rules.maxLength) {\r\n return `Must be at most ${rules.maxLength} characters`;\r\n }\r\n if (\r\n rules.min !== undefined &&\r\n value !== '' &&\r\n Number(value) < Number(rules.min)\r\n ) {\r\n return `Must be at least ${rules.min}`;\r\n }\r\n if (\r\n rules.max !== undefined &&\r\n value !== '' &&\r\n Number(value) > Number(rules.max)\r\n ) {\r\n return `Must be at most ${rules.max}`;\r\n }\r\n if (rules.validate) {\r\n return rules.validate(value);\r\n }\r\n return undefined;\r\n}\r\n","import type { FieldRegistration } from '@asnewyla/input';\r\nimport { type RefObject, useCallback, useRef, useState } from 'react';\r\nimport { validateValue } from './validation';\r\n\r\nexport interface InvalidField {\r\n name: string;\r\n id: string;\r\n message: string;\r\n}\r\n\r\nexport interface FieldRegistry {\r\n errors: Record<string, string | undefined>;\r\n registerField: (name: string, registration: FieldRegistration) => void;\r\n unregisterField: (name: string) => void;\r\n validateField: (name: string) => string | undefined;\r\n fieldNames: () => string[];\r\n invalidFields: () => InvalidField[];\r\n}\r\n\r\nexport function useFieldRegistry(\r\n formRef: RefObject<HTMLFormElement>,\r\n): FieldRegistry {\r\n const fieldsRef = useRef<Map<string, FieldRegistration>>(new Map());\r\n const [errors, setErrors] = useState<Record<string, string | undefined>>({});\r\n\r\n const registerField = useCallback(\r\n (name: string, registration: FieldRegistration) => {\r\n fieldsRef.current.set(name, registration);\r\n },\r\n [],\r\n );\r\n\r\n const unregisterField = useCallback((name: string) => {\r\n fieldsRef.current.delete(name);\r\n setErrors((prev) => {\r\n if (!(name in prev)) return prev;\r\n const next = { ...prev };\r\n delete next[name];\r\n return next;\r\n });\r\n }, []);\r\n\r\n const getFieldValue = useCallback(\r\n (name: string): string => {\r\n if (!formRef.current) return '';\r\n return String(new FormData(formRef.current).get(name) ?? '');\r\n },\r\n [formRef],\r\n );\r\n\r\n const validateField = useCallback(\r\n (name: string) => {\r\n const field = fieldsRef.current.get(name);\r\n const error = validateValue(getFieldValue(name), field?.rules);\r\n setErrors((prev) => ({ ...prev, [name]: error }));\r\n return error;\r\n },\r\n [getFieldValue],\r\n );\r\n\r\n const fieldNames = useCallback(\r\n () => Array.from(fieldsRef.current.keys()),\r\n [],\r\n );\r\n\r\n const invalidFields = useCallback((): InvalidField[] => {\r\n const result: InvalidField[] = [];\r\n for (const [name, message] of Object.entries(errors)) {\r\n if (!message) continue;\r\n const id = fieldsRef.current.get(name)?.ref.current?.id;\r\n if (!id) continue;\r\n result.push({ name, id, message });\r\n }\r\n return result;\r\n }, [errors]);\r\n\r\n return {\r\n errors,\r\n registerField,\r\n unregisterField,\r\n validateField,\r\n fieldNames,\r\n invalidFields,\r\n };\r\n}\r\n","import { FormFieldProvider } from '@asnewyla/input';\r\nimport { forwardRef, useMemo, useRef, useState } from 'react';\r\nimport { flushSync } from 'react-dom';\r\nimport { type InvalidField, useFieldRegistry } from './useFieldRegistry';\r\n\r\nexport type FormProps = {\r\n children: React.ReactNode;\r\n onSubmit: (values: Record<string, string>) => void;\r\n};\r\n\r\nexport function Form({ children, onSubmit }: FormProps) {\r\n const formRef = useRef<HTMLFormElement>(null);\r\n const summaryRef = useRef<HTMLDivElement>(null);\r\n const {\r\n errors,\r\n registerField,\r\n unregisterField,\r\n validateField,\r\n fieldNames,\r\n invalidFields,\r\n } = useFieldRegistry(formRef);\r\n\r\n const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);\r\n\r\n function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\r\n event.preventDefault();\r\n\r\n let hasError = false;\r\n // flushSync commits the validation state synchronously, so the\r\n // summary is already in the DOM by the time we call .focus() below.\r\n flushSync(() => {\r\n for (const name of fieldNames()) {\r\n if (validateField(name)) hasError = true;\r\n }\r\n setHasAttemptedSubmit(true);\r\n });\r\n if (hasError) {\r\n summaryRef.current?.focus();\r\n return;\r\n }\r\n\r\n const values: Record<string, string> = {};\r\n for (const [key, value] of new FormData(event.currentTarget).entries()) {\r\n values[key] = String(value);\r\n }\r\n onSubmit(values);\r\n }\r\n\r\n const invalid: InvalidField[] = hasAttemptedSubmit ? invalidFields() : [];\r\n\r\n const contextValue = useMemo(\r\n () => ({ registerField, unregisterField, validateField, errors }),\r\n [registerField, unregisterField, validateField, errors],\r\n );\r\n\r\n return (\r\n <form ref={formRef} onSubmit={handleSubmit} noValidate>\r\n {invalid.length > 0 && (\r\n <ErrorSummary ref={summaryRef} invalidFields={invalid} />\r\n )}\r\n <FormFieldProvider value={contextValue}>{children}</FormFieldProvider>\r\n </form>\r\n );\r\n}\r\n\r\ntype ErrorSummaryProps = {\r\n invalidFields: InvalidField[];\r\n};\r\n\r\nconst ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(\r\n function ErrorSummary({ invalidFields }, ref) {\r\n return (\r\n <div\r\n className=\"xd-form-error-summary\"\r\n role=\"alert\"\r\n tabIndex={-1}\r\n ref={ref}\r\n >\r\n <p className=\"xd-form-error-summary-heading\">There is a problem</p>\r\n <ul>\r\n {invalidFields.map(({ name, id, message }) => (\r\n <li key={name}>\r\n <a\r\n href={`#${id}`}\r\n onClick={(event) => {\r\n event.preventDefault();\r\n document.getElementById(id)?.focus();\r\n }}\r\n >\r\n {message}\r\n </a>\r\n </li>\r\n ))}\r\n </ul>\r\n </div>\r\n );\r\n },\r\n);\r\n","import {\r\n Input,\r\n type InputProps,\r\n type ValidationRules,\r\n useFormFieldContext,\r\n} from '@asnewyla/input';\r\nimport { type ForwardedRef, forwardRef, useEffect, useRef } from 'react';\r\n\r\nexport type FormFieldInputProps = Omit<InputProps, 'name'> & {\r\n name: string;\r\n validate?: (value: string) => string | undefined;\r\n};\r\n\r\nfunction FormFieldInputInner(\r\n {\r\n name,\r\n required,\r\n pattern,\r\n minLength,\r\n maxLength,\r\n min,\r\n max,\r\n validate,\r\n error,\r\n onBlur,\r\n ...rest\r\n }: FormFieldInputProps,\r\n ref: ForwardedRef<HTMLInputElement>,\r\n) {\r\n // useFormFieldContext() returns null when there's no ancestor\r\n // FormFieldProvider — that's what makes FormFieldInput a no-op outside a\r\n // Form (still a perfectly usable Input, just without registration).\r\n const formField = useFormFieldContext();\r\n\r\n // A stable ref we always own, so registerField always gets a real node —\r\n // separate from `ref`, which is whatever (or nothing) the consumer passed.\r\n const inputRef = useRef<HTMLInputElement | null>(null);\r\n\r\n const rules: ValidationRules = {\r\n required,\r\n pattern,\r\n minLength,\r\n maxLength,\r\n min,\r\n max,\r\n validate,\r\n };\r\n\r\n // Deliberately depends on formField?.registerField/unregisterField, not\r\n // on `formField` itself — Form's context value changes identity whenever\r\n // its `errors` state updates (that's correct: this component needs to\r\n // re-render to read the new error). But if this effect depended on the\r\n // whole object, every validation would also re-trigger registration:\r\n // cleanup would call the *previous* render's unregisterField, deleting\r\n // the field and wiping the error validateField just set, in the same\r\n // tick it was written. Depending on the two functions directly (stable\r\n // via Form's useCallback) keeps registration decoupled from errors\r\n // changing.\r\n // Also deliberately not depending on `rules` — it's a fresh object every\r\n // render, so registration happens once per mount (or when formField/name\r\n // change), not on every rule-prop tweak. Rule props are effectively fixed\r\n // at mount for v0.x; no test or use case yet needs them reactive.\r\n // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally narrowed deps — see comment above\r\n useEffect(() => {\r\n if (!formField) return;\r\n formField.registerField(name, { ref: inputRef, rules });\r\n return () => formField.unregisterField(name);\r\n }, [formField?.registerField, formField?.unregisterField, name]);\r\n\r\n // Merges `inputRef` (ours, for registration) with `ref` (the consumer's\r\n // forwarded one, if any) so both end up pointing at the same DOM node —\r\n // neither one silently loses the element to the other.\r\n const setRefs = (node: HTMLInputElement | null) => {\r\n inputRef.current = node;\r\n if (typeof ref === 'function') {\r\n ref(node);\r\n } else if (ref) {\r\n ref.current = node;\r\n }\r\n };\r\n\r\n function handleBlur(event: React.FocusEvent<HTMLInputElement>) {\r\n if (formField) {\r\n formField.validateField(name);\r\n }\r\n onBlur?.(event);\r\n }\r\n\r\n const formContextError = formField ? formField.errors[name] : undefined;\r\n const formFieldError = error ?? formContextError;\r\n\r\n return (\r\n <Input\r\n name={name}\r\n required={required}\r\n pattern={pattern}\r\n minLength={minLength}\r\n maxLength={maxLength}\r\n min={min}\r\n max={max}\r\n {...rest}\r\n error={formFieldError}\r\n onBlur={handleBlur}\r\n ref={setRefs}\r\n />\r\n );\r\n}\r\n\r\nexport const FormFieldInput = forwardRef<HTMLInputElement, FormFieldInputProps>(\r\n FormFieldInputInner,\r\n);\r\n"]}
1
+ {"version":3,"sources":["../src/components/FormErrorSummary.tsx","../src/components/validation.ts","../src/components/useFieldRegistry.ts","../src/components/Form.tsx","../src/components/FormFieldInput.tsx"],"names":["forwardRef","FormErrorSummary","jsxs","jsx","useRef","useState","useCallback","flushSync","useMemo","FormFieldProvider","useFormFieldContext","useEffect","Input"],"mappings":"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","file":"index.js","sourcesContent":["import { forwardRef } from 'react';\nimport type { InvalidField } from './useFieldRegistry';\nimport './FormErrorSummary.css';\n\nexport type FormErrorSummaryProps = {\n invalidFields: InvalidField[];\n};\n\nexport const FormErrorSummary = forwardRef<\n HTMLDivElement,\n FormErrorSummaryProps\n>(function FormErrorSummary({ invalidFields }, ref) {\n return (\n <div className=\"xd-form-error-summary\" role=\"alert\" tabIndex={-1} ref={ref}>\n <p className=\"xd-form-error-summary-heading\">There is a problem</p>\n <ul>\n {invalidFields.map(({ name, id, message }) => (\n <li key={name}>\n <a\n href={`#${id}`}\n onClick={(event) => {\n event.preventDefault();\n document.getElementById(id)?.focus();\n }}\n >\n {message}\n </a>\n </li>\n ))}\n </ul>\n </div>\n );\n});\n","import type { ValidationRules } from '@asnewyla/input';\n\nexport function validateValue(\n value: string,\n rules?: ValidationRules,\n): string | undefined {\n if (!rules) return undefined;\n\n if (rules.required && value.trim() === '') {\n return typeof rules.required === 'string'\n ? rules.required\n : 'This field is required';\n }\n if (rules.pattern && value !== '' && !new RegExp(rules.pattern).test(value)) {\n return 'Invalid format';\n }\n if (rules.minLength !== undefined && value.length < rules.minLength) {\n return `Must be at least ${rules.minLength} characters`;\n }\n if (rules.maxLength !== undefined && value.length > rules.maxLength) {\n return `Must be at most ${rules.maxLength} characters`;\n }\n if (\n rules.min !== undefined &&\n value !== '' &&\n Number(value) < Number(rules.min)\n ) {\n return `Must be at least ${rules.min}`;\n }\n if (\n rules.max !== undefined &&\n value !== '' &&\n Number(value) > Number(rules.max)\n ) {\n return `Must be at most ${rules.max}`;\n }\n if (rules.validate) {\n return rules.validate(value);\n }\n return undefined;\n}\n","import type { FieldRegistration } from '@asnewyla/input';\nimport { type RefObject, useCallback, useRef, useState } from 'react';\nimport { validateValue } from './validation';\n\nexport interface InvalidField {\n name: string;\n id: string;\n message: string;\n}\n\nexport interface FieldRegistry {\n errors: Record<string, string | undefined>;\n registerField: (name: string, registration: FieldRegistration) => void;\n unregisterField: (name: string) => void;\n validateField: (name: string) => string | undefined;\n fieldNames: () => string[];\n invalidFields: () => InvalidField[];\n}\n\nexport function useFieldRegistry(\n formRef: RefObject<HTMLFormElement>,\n): FieldRegistry {\n const fieldsRef = useRef<Map<string, FieldRegistration>>(new Map());\n const [errors, setErrors] = useState<Record<string, string | undefined>>({});\n\n const registerField = useCallback(\n (name: string, registration: FieldRegistration) => {\n fieldsRef.current.set(name, registration);\n },\n [],\n );\n\n const unregisterField = useCallback((name: string) => {\n fieldsRef.current.delete(name);\n setErrors((prev) => {\n if (!(name in prev)) return prev;\n const next = { ...prev };\n delete next[name];\n return next;\n });\n }, []);\n\n const getFieldValue = useCallback(\n (name: string): string => {\n if (!formRef.current) return '';\n return String(new FormData(formRef.current).get(name) ?? '');\n },\n [formRef],\n );\n\n const validateField = useCallback(\n (name: string) => {\n const field = fieldsRef.current.get(name);\n const error = validateValue(getFieldValue(name), field?.rules);\n setErrors((prev) => ({ ...prev, [name]: error }));\n return error;\n },\n [getFieldValue],\n );\n\n const fieldNames = useCallback(\n () => Array.from(fieldsRef.current.keys()),\n [],\n );\n\n const invalidFields = useCallback((): InvalidField[] => {\n const result: InvalidField[] = [];\n for (const [name, message] of Object.entries(errors)) {\n if (!message) continue;\n const id = fieldsRef.current.get(name)?.ref.current?.id;\n if (!id) continue;\n result.push({ name, id, message });\n }\n return result;\n }, [errors]);\n\n return {\n errors,\n registerField,\n unregisterField,\n validateField,\n fieldNames,\n invalidFields,\n };\n}\n","import { FormFieldProvider } from '@asnewyla/input';\nimport { useMemo, useRef, useState } from 'react';\nimport { flushSync } from 'react-dom';\nimport { FormErrorSummary } from './FormErrorSummary';\nimport { type InvalidField, useFieldRegistry } from './useFieldRegistry';\n\nexport type FormProps = {\n children: React.ReactNode;\n onSubmit: (values: Record<string, string>) => void;\n};\n\nexport function Form({ children, onSubmit }: FormProps) {\n const formRef = useRef<HTMLFormElement>(null);\n const summaryRef = useRef<HTMLDivElement>(null);\n const {\n errors,\n registerField,\n unregisterField,\n validateField,\n fieldNames,\n invalidFields,\n } = useFieldRegistry(formRef);\n\n const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);\n\n function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n event.preventDefault();\n\n let hasError = false;\n // flushSync commits the validation state synchronously, so the\n // summary is already in the DOM by the time we call .focus() below.\n flushSync(() => {\n for (const name of fieldNames()) {\n if (validateField(name)) hasError = true;\n }\n setHasAttemptedSubmit(true);\n });\n if (hasError) {\n summaryRef.current?.focus();\n return;\n }\n\n const values: Record<string, string> = {};\n for (const [key, value] of new FormData(event.currentTarget).entries()) {\n values[key] = String(value);\n }\n onSubmit(values);\n }\n\n const invalid: InvalidField[] = hasAttemptedSubmit ? invalidFields() : [];\n\n const contextValue = useMemo(\n () => ({ registerField, unregisterField, validateField, errors }),\n [registerField, unregisterField, validateField, errors],\n );\n\n return (\n <form ref={formRef} onSubmit={handleSubmit} noValidate>\n {invalid.length > 0 && (\n <FormErrorSummary ref={summaryRef} invalidFields={invalid} />\n )}\n <FormFieldProvider value={contextValue}>{children}</FormFieldProvider>\n </form>\n );\n}\n","import {\n Input,\n type InputProps,\n type ValidationRules,\n useFormFieldContext,\n} from '@asnewyla/input';\nimport { type ForwardedRef, forwardRef, useEffect, useRef } from 'react';\n\nexport type FormFieldInputProps = Omit<InputProps, 'name'> & {\n name: string;\n validate?: (value: string) => string | undefined;\n};\n\nfunction FormFieldInputInner(\n {\n name,\n required,\n pattern,\n minLength,\n maxLength,\n min,\n max,\n validate,\n error,\n onBlur,\n ...rest\n }: FormFieldInputProps,\n ref: ForwardedRef<HTMLInputElement>,\n) {\n // useFormFieldContext() returns null when there's no ancestor\n // FormFieldProvider — that's what makes FormFieldInput a no-op outside a\n // Form (still a perfectly usable Input, just without registration).\n const formField = useFormFieldContext();\n\n // A stable ref we always own, so registerField always gets a real node —\n // separate from `ref`, which is whatever (or nothing) the consumer passed.\n const inputRef = useRef<HTMLInputElement | null>(null);\n\n const rules: ValidationRules = {\n required,\n pattern,\n minLength,\n maxLength,\n min,\n max,\n validate,\n };\n\n // Deliberately depends on formField?.registerField/unregisterField, not\n // on `formField` itself — Form's context value changes identity whenever\n // its `errors` state updates (that's correct: this component needs to\n // re-render to read the new error). But if this effect depended on the\n // whole object, every validation would also re-trigger registration:\n // cleanup would call the *previous* render's unregisterField, deleting\n // the field and wiping the error validateField just set, in the same\n // tick it was written. Depending on the two functions directly (stable\n // via Form's useCallback) keeps registration decoupled from errors\n // changing.\n // Also deliberately not depending on `rules` — it's a fresh object every\n // render, so registration happens once per mount (or when formField/name\n // change), not on every rule-prop tweak. Rule props are effectively fixed\n // at mount for v0.x; no test or use case yet needs them reactive.\n // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally narrowed deps — see comment above\n useEffect(() => {\n if (!formField) return;\n formField.registerField(name, { ref: inputRef, rules });\n return () => formField.unregisterField(name);\n }, [formField?.registerField, formField?.unregisterField, name]);\n\n // Merges `inputRef` (ours, for registration) with `ref` (the consumer's\n // forwarded one, if any) so both end up pointing at the same DOM node —\n // neither one silently loses the element to the other.\n const setRefs = (node: HTMLInputElement | null) => {\n inputRef.current = node;\n if (typeof ref === 'function') {\n ref(node);\n } else if (ref) {\n ref.current = node;\n }\n };\n\n function handleBlur(event: React.FocusEvent<HTMLInputElement>) {\n if (formField) {\n formField.validateField(name);\n }\n onBlur?.(event);\n }\n\n const formContextError = formField ? formField.errors[name] : undefined;\n const formFieldError = error ?? formContextError;\n\n return (\n <Input\n name={name}\n required={required}\n pattern={pattern}\n minLength={minLength}\n maxLength={maxLength}\n min={min}\n max={max}\n {...rest}\n error={formFieldError}\n onBlur={handleBlur}\n ref={setRefs}\n />\n );\n}\n\nexport const FormFieldInput = forwardRef<HTMLInputElement, FormFieldInputProps>(\n FormFieldInputInner,\n);\n"]}
package/dist/index.mjs CHANGED
@@ -4,6 +4,22 @@ import { flushSync } from 'react-dom';
4
4
  import { jsxs, jsx } from 'react/jsx-runtime';
5
5
 
6
6
  // src/components/Form.tsx
7
+ var FormErrorSummary = forwardRef(function FormErrorSummary2({ invalidFields }, ref) {
8
+ return /* @__PURE__ */ jsxs("div", { className: "xd-form-error-summary", role: "alert", tabIndex: -1, ref, children: [
9
+ /* @__PURE__ */ jsx("p", { className: "xd-form-error-summary-heading", children: "There is a problem" }),
10
+ /* @__PURE__ */ jsx("ul", { children: invalidFields.map(({ name, id, message }) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
11
+ "a",
12
+ {
13
+ href: `#${id}`,
14
+ onClick: (event) => {
15
+ event.preventDefault();
16
+ document.getElementById(id)?.focus();
17
+ },
18
+ children: message
19
+ }
20
+ ) }, name)) })
21
+ ] });
22
+ });
7
23
 
8
24
  // src/components/validation.ts
9
25
  function validateValue(value, rules) {
@@ -127,37 +143,10 @@ function Form({ children, onSubmit }) {
127
143
  [registerField, unregisterField, validateField, errors]
128
144
  );
129
145
  return /* @__PURE__ */ jsxs("form", { ref: formRef, onSubmit: handleSubmit, noValidate: true, children: [
130
- invalid.length > 0 && /* @__PURE__ */ jsx(ErrorSummary, { ref: summaryRef, invalidFields: invalid }),
146
+ invalid.length > 0 && /* @__PURE__ */ jsx(FormErrorSummary, { ref: summaryRef, invalidFields: invalid }),
131
147
  /* @__PURE__ */ jsx(FormFieldProvider, { value: contextValue, children })
132
148
  ] });
133
149
  }
134
- var ErrorSummary = forwardRef(
135
- function ErrorSummary2({ invalidFields }, ref) {
136
- return /* @__PURE__ */ jsxs(
137
- "div",
138
- {
139
- className: "xd-form-error-summary",
140
- role: "alert",
141
- tabIndex: -1,
142
- ref,
143
- children: [
144
- /* @__PURE__ */ jsx("p", { className: "xd-form-error-summary-heading", children: "There is a problem" }),
145
- /* @__PURE__ */ jsx("ul", { children: invalidFields.map(({ name, id, message }) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
146
- "a",
147
- {
148
- href: `#${id}`,
149
- onClick: (event) => {
150
- event.preventDefault();
151
- document.getElementById(id)?.focus();
152
- },
153
- children: message
154
- }
155
- ) }, name)) })
156
- ]
157
- }
158
- );
159
- }
160
- );
161
150
  function FormFieldInputInner({
162
151
  name,
163
152
  required,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/validation.ts","../src/components/useFieldRegistry.ts","../src/components/Form.tsx","../src/components/FormFieldInput.tsx"],"names":["useRef","useState","ErrorSummary","jsx","forwardRef"],"mappings":"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","file":"index.mjs","sourcesContent":["import type { ValidationRules } from '@asnewyla/input';\r\n\r\nexport function validateValue(\r\n value: string,\r\n rules?: ValidationRules,\r\n): string | undefined {\r\n if (!rules) return undefined;\r\n\r\n if (rules.required && value.trim() === '') {\r\n return typeof rules.required === 'string'\r\n ? rules.required\r\n : 'This field is required';\r\n }\r\n if (rules.pattern && value !== '' && !new RegExp(rules.pattern).test(value)) {\r\n return 'Invalid format';\r\n }\r\n if (rules.minLength !== undefined && value.length < rules.minLength) {\r\n return `Must be at least ${rules.minLength} characters`;\r\n }\r\n if (rules.maxLength !== undefined && value.length > rules.maxLength) {\r\n return `Must be at most ${rules.maxLength} characters`;\r\n }\r\n if (\r\n rules.min !== undefined &&\r\n value !== '' &&\r\n Number(value) < Number(rules.min)\r\n ) {\r\n return `Must be at least ${rules.min}`;\r\n }\r\n if (\r\n rules.max !== undefined &&\r\n value !== '' &&\r\n Number(value) > Number(rules.max)\r\n ) {\r\n return `Must be at most ${rules.max}`;\r\n }\r\n if (rules.validate) {\r\n return rules.validate(value);\r\n }\r\n return undefined;\r\n}\r\n","import type { FieldRegistration } from '@asnewyla/input';\r\nimport { type RefObject, useCallback, useRef, useState } from 'react';\r\nimport { validateValue } from './validation';\r\n\r\nexport interface InvalidField {\r\n name: string;\r\n id: string;\r\n message: string;\r\n}\r\n\r\nexport interface FieldRegistry {\r\n errors: Record<string, string | undefined>;\r\n registerField: (name: string, registration: FieldRegistration) => void;\r\n unregisterField: (name: string) => void;\r\n validateField: (name: string) => string | undefined;\r\n fieldNames: () => string[];\r\n invalidFields: () => InvalidField[];\r\n}\r\n\r\nexport function useFieldRegistry(\r\n formRef: RefObject<HTMLFormElement>,\r\n): FieldRegistry {\r\n const fieldsRef = useRef<Map<string, FieldRegistration>>(new Map());\r\n const [errors, setErrors] = useState<Record<string, string | undefined>>({});\r\n\r\n const registerField = useCallback(\r\n (name: string, registration: FieldRegistration) => {\r\n fieldsRef.current.set(name, registration);\r\n },\r\n [],\r\n );\r\n\r\n const unregisterField = useCallback((name: string) => {\r\n fieldsRef.current.delete(name);\r\n setErrors((prev) => {\r\n if (!(name in prev)) return prev;\r\n const next = { ...prev };\r\n delete next[name];\r\n return next;\r\n });\r\n }, []);\r\n\r\n const getFieldValue = useCallback(\r\n (name: string): string => {\r\n if (!formRef.current) return '';\r\n return String(new FormData(formRef.current).get(name) ?? '');\r\n },\r\n [formRef],\r\n );\r\n\r\n const validateField = useCallback(\r\n (name: string) => {\r\n const field = fieldsRef.current.get(name);\r\n const error = validateValue(getFieldValue(name), field?.rules);\r\n setErrors((prev) => ({ ...prev, [name]: error }));\r\n return error;\r\n },\r\n [getFieldValue],\r\n );\r\n\r\n const fieldNames = useCallback(\r\n () => Array.from(fieldsRef.current.keys()),\r\n [],\r\n );\r\n\r\n const invalidFields = useCallback((): InvalidField[] => {\r\n const result: InvalidField[] = [];\r\n for (const [name, message] of Object.entries(errors)) {\r\n if (!message) continue;\r\n const id = fieldsRef.current.get(name)?.ref.current?.id;\r\n if (!id) continue;\r\n result.push({ name, id, message });\r\n }\r\n return result;\r\n }, [errors]);\r\n\r\n return {\r\n errors,\r\n registerField,\r\n unregisterField,\r\n validateField,\r\n fieldNames,\r\n invalidFields,\r\n };\r\n}\r\n","import { FormFieldProvider } from '@asnewyla/input';\r\nimport { forwardRef, useMemo, useRef, useState } from 'react';\r\nimport { flushSync } from 'react-dom';\r\nimport { type InvalidField, useFieldRegistry } from './useFieldRegistry';\r\n\r\nexport type FormProps = {\r\n children: React.ReactNode;\r\n onSubmit: (values: Record<string, string>) => void;\r\n};\r\n\r\nexport function Form({ children, onSubmit }: FormProps) {\r\n const formRef = useRef<HTMLFormElement>(null);\r\n const summaryRef = useRef<HTMLDivElement>(null);\r\n const {\r\n errors,\r\n registerField,\r\n unregisterField,\r\n validateField,\r\n fieldNames,\r\n invalidFields,\r\n } = useFieldRegistry(formRef);\r\n\r\n const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);\r\n\r\n function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\r\n event.preventDefault();\r\n\r\n let hasError = false;\r\n // flushSync commits the validation state synchronously, so the\r\n // summary is already in the DOM by the time we call .focus() below.\r\n flushSync(() => {\r\n for (const name of fieldNames()) {\r\n if (validateField(name)) hasError = true;\r\n }\r\n setHasAttemptedSubmit(true);\r\n });\r\n if (hasError) {\r\n summaryRef.current?.focus();\r\n return;\r\n }\r\n\r\n const values: Record<string, string> = {};\r\n for (const [key, value] of new FormData(event.currentTarget).entries()) {\r\n values[key] = String(value);\r\n }\r\n onSubmit(values);\r\n }\r\n\r\n const invalid: InvalidField[] = hasAttemptedSubmit ? invalidFields() : [];\r\n\r\n const contextValue = useMemo(\r\n () => ({ registerField, unregisterField, validateField, errors }),\r\n [registerField, unregisterField, validateField, errors],\r\n );\r\n\r\n return (\r\n <form ref={formRef} onSubmit={handleSubmit} noValidate>\r\n {invalid.length > 0 && (\r\n <ErrorSummary ref={summaryRef} invalidFields={invalid} />\r\n )}\r\n <FormFieldProvider value={contextValue}>{children}</FormFieldProvider>\r\n </form>\r\n );\r\n}\r\n\r\ntype ErrorSummaryProps = {\r\n invalidFields: InvalidField[];\r\n};\r\n\r\nconst ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(\r\n function ErrorSummary({ invalidFields }, ref) {\r\n return (\r\n <div\r\n className=\"xd-form-error-summary\"\r\n role=\"alert\"\r\n tabIndex={-1}\r\n ref={ref}\r\n >\r\n <p className=\"xd-form-error-summary-heading\">There is a problem</p>\r\n <ul>\r\n {invalidFields.map(({ name, id, message }) => (\r\n <li key={name}>\r\n <a\r\n href={`#${id}`}\r\n onClick={(event) => {\r\n event.preventDefault();\r\n document.getElementById(id)?.focus();\r\n }}\r\n >\r\n {message}\r\n </a>\r\n </li>\r\n ))}\r\n </ul>\r\n </div>\r\n );\r\n },\r\n);\r\n","import {\r\n Input,\r\n type InputProps,\r\n type ValidationRules,\r\n useFormFieldContext,\r\n} from '@asnewyla/input';\r\nimport { type ForwardedRef, forwardRef, useEffect, useRef } from 'react';\r\n\r\nexport type FormFieldInputProps = Omit<InputProps, 'name'> & {\r\n name: string;\r\n validate?: (value: string) => string | undefined;\r\n};\r\n\r\nfunction FormFieldInputInner(\r\n {\r\n name,\r\n required,\r\n pattern,\r\n minLength,\r\n maxLength,\r\n min,\r\n max,\r\n validate,\r\n error,\r\n onBlur,\r\n ...rest\r\n }: FormFieldInputProps,\r\n ref: ForwardedRef<HTMLInputElement>,\r\n) {\r\n // useFormFieldContext() returns null when there's no ancestor\r\n // FormFieldProvider — that's what makes FormFieldInput a no-op outside a\r\n // Form (still a perfectly usable Input, just without registration).\r\n const formField = useFormFieldContext();\r\n\r\n // A stable ref we always own, so registerField always gets a real node —\r\n // separate from `ref`, which is whatever (or nothing) the consumer passed.\r\n const inputRef = useRef<HTMLInputElement | null>(null);\r\n\r\n const rules: ValidationRules = {\r\n required,\r\n pattern,\r\n minLength,\r\n maxLength,\r\n min,\r\n max,\r\n validate,\r\n };\r\n\r\n // Deliberately depends on formField?.registerField/unregisterField, not\r\n // on `formField` itself — Form's context value changes identity whenever\r\n // its `errors` state updates (that's correct: this component needs to\r\n // re-render to read the new error). But if this effect depended on the\r\n // whole object, every validation would also re-trigger registration:\r\n // cleanup would call the *previous* render's unregisterField, deleting\r\n // the field and wiping the error validateField just set, in the same\r\n // tick it was written. Depending on the two functions directly (stable\r\n // via Form's useCallback) keeps registration decoupled from errors\r\n // changing.\r\n // Also deliberately not depending on `rules` — it's a fresh object every\r\n // render, so registration happens once per mount (or when formField/name\r\n // change), not on every rule-prop tweak. Rule props are effectively fixed\r\n // at mount for v0.x; no test or use case yet needs them reactive.\r\n // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally narrowed deps — see comment above\r\n useEffect(() => {\r\n if (!formField) return;\r\n formField.registerField(name, { ref: inputRef, rules });\r\n return () => formField.unregisterField(name);\r\n }, [formField?.registerField, formField?.unregisterField, name]);\r\n\r\n // Merges `inputRef` (ours, for registration) with `ref` (the consumer's\r\n // forwarded one, if any) so both end up pointing at the same DOM node —\r\n // neither one silently loses the element to the other.\r\n const setRefs = (node: HTMLInputElement | null) => {\r\n inputRef.current = node;\r\n if (typeof ref === 'function') {\r\n ref(node);\r\n } else if (ref) {\r\n ref.current = node;\r\n }\r\n };\r\n\r\n function handleBlur(event: React.FocusEvent<HTMLInputElement>) {\r\n if (formField) {\r\n formField.validateField(name);\r\n }\r\n onBlur?.(event);\r\n }\r\n\r\n const formContextError = formField ? formField.errors[name] : undefined;\r\n const formFieldError = error ?? formContextError;\r\n\r\n return (\r\n <Input\r\n name={name}\r\n required={required}\r\n pattern={pattern}\r\n minLength={minLength}\r\n maxLength={maxLength}\r\n min={min}\r\n max={max}\r\n {...rest}\r\n error={formFieldError}\r\n onBlur={handleBlur}\r\n ref={setRefs}\r\n />\r\n );\r\n}\r\n\r\nexport const FormFieldInput = forwardRef<HTMLInputElement, FormFieldInputProps>(\r\n FormFieldInputInner,\r\n);\r\n"]}
1
+ {"version":3,"sources":["../src/components/FormErrorSummary.tsx","../src/components/validation.ts","../src/components/useFieldRegistry.ts","../src/components/Form.tsx","../src/components/FormFieldInput.tsx"],"names":["FormErrorSummary","useRef","useState","jsxs","jsx","forwardRef"],"mappings":"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","file":"index.mjs","sourcesContent":["import { forwardRef } from 'react';\nimport type { InvalidField } from './useFieldRegistry';\nimport './FormErrorSummary.css';\n\nexport type FormErrorSummaryProps = {\n invalidFields: InvalidField[];\n};\n\nexport const FormErrorSummary = forwardRef<\n HTMLDivElement,\n FormErrorSummaryProps\n>(function FormErrorSummary({ invalidFields }, ref) {\n return (\n <div className=\"xd-form-error-summary\" role=\"alert\" tabIndex={-1} ref={ref}>\n <p className=\"xd-form-error-summary-heading\">There is a problem</p>\n <ul>\n {invalidFields.map(({ name, id, message }) => (\n <li key={name}>\n <a\n href={`#${id}`}\n onClick={(event) => {\n event.preventDefault();\n document.getElementById(id)?.focus();\n }}\n >\n {message}\n </a>\n </li>\n ))}\n </ul>\n </div>\n );\n});\n","import type { ValidationRules } from '@asnewyla/input';\n\nexport function validateValue(\n value: string,\n rules?: ValidationRules,\n): string | undefined {\n if (!rules) return undefined;\n\n if (rules.required && value.trim() === '') {\n return typeof rules.required === 'string'\n ? rules.required\n : 'This field is required';\n }\n if (rules.pattern && value !== '' && !new RegExp(rules.pattern).test(value)) {\n return 'Invalid format';\n }\n if (rules.minLength !== undefined && value.length < rules.minLength) {\n return `Must be at least ${rules.minLength} characters`;\n }\n if (rules.maxLength !== undefined && value.length > rules.maxLength) {\n return `Must be at most ${rules.maxLength} characters`;\n }\n if (\n rules.min !== undefined &&\n value !== '' &&\n Number(value) < Number(rules.min)\n ) {\n return `Must be at least ${rules.min}`;\n }\n if (\n rules.max !== undefined &&\n value !== '' &&\n Number(value) > Number(rules.max)\n ) {\n return `Must be at most ${rules.max}`;\n }\n if (rules.validate) {\n return rules.validate(value);\n }\n return undefined;\n}\n","import type { FieldRegistration } from '@asnewyla/input';\nimport { type RefObject, useCallback, useRef, useState } from 'react';\nimport { validateValue } from './validation';\n\nexport interface InvalidField {\n name: string;\n id: string;\n message: string;\n}\n\nexport interface FieldRegistry {\n errors: Record<string, string | undefined>;\n registerField: (name: string, registration: FieldRegistration) => void;\n unregisterField: (name: string) => void;\n validateField: (name: string) => string | undefined;\n fieldNames: () => string[];\n invalidFields: () => InvalidField[];\n}\n\nexport function useFieldRegistry(\n formRef: RefObject<HTMLFormElement>,\n): FieldRegistry {\n const fieldsRef = useRef<Map<string, FieldRegistration>>(new Map());\n const [errors, setErrors] = useState<Record<string, string | undefined>>({});\n\n const registerField = useCallback(\n (name: string, registration: FieldRegistration) => {\n fieldsRef.current.set(name, registration);\n },\n [],\n );\n\n const unregisterField = useCallback((name: string) => {\n fieldsRef.current.delete(name);\n setErrors((prev) => {\n if (!(name in prev)) return prev;\n const next = { ...prev };\n delete next[name];\n return next;\n });\n }, []);\n\n const getFieldValue = useCallback(\n (name: string): string => {\n if (!formRef.current) return '';\n return String(new FormData(formRef.current).get(name) ?? '');\n },\n [formRef],\n );\n\n const validateField = useCallback(\n (name: string) => {\n const field = fieldsRef.current.get(name);\n const error = validateValue(getFieldValue(name), field?.rules);\n setErrors((prev) => ({ ...prev, [name]: error }));\n return error;\n },\n [getFieldValue],\n );\n\n const fieldNames = useCallback(\n () => Array.from(fieldsRef.current.keys()),\n [],\n );\n\n const invalidFields = useCallback((): InvalidField[] => {\n const result: InvalidField[] = [];\n for (const [name, message] of Object.entries(errors)) {\n if (!message) continue;\n const id = fieldsRef.current.get(name)?.ref.current?.id;\n if (!id) continue;\n result.push({ name, id, message });\n }\n return result;\n }, [errors]);\n\n return {\n errors,\n registerField,\n unregisterField,\n validateField,\n fieldNames,\n invalidFields,\n };\n}\n","import { FormFieldProvider } from '@asnewyla/input';\nimport { useMemo, useRef, useState } from 'react';\nimport { flushSync } from 'react-dom';\nimport { FormErrorSummary } from './FormErrorSummary';\nimport { type InvalidField, useFieldRegistry } from './useFieldRegistry';\n\nexport type FormProps = {\n children: React.ReactNode;\n onSubmit: (values: Record<string, string>) => void;\n};\n\nexport function Form({ children, onSubmit }: FormProps) {\n const formRef = useRef<HTMLFormElement>(null);\n const summaryRef = useRef<HTMLDivElement>(null);\n const {\n errors,\n registerField,\n unregisterField,\n validateField,\n fieldNames,\n invalidFields,\n } = useFieldRegistry(formRef);\n\n const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);\n\n function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n event.preventDefault();\n\n let hasError = false;\n // flushSync commits the validation state synchronously, so the\n // summary is already in the DOM by the time we call .focus() below.\n flushSync(() => {\n for (const name of fieldNames()) {\n if (validateField(name)) hasError = true;\n }\n setHasAttemptedSubmit(true);\n });\n if (hasError) {\n summaryRef.current?.focus();\n return;\n }\n\n const values: Record<string, string> = {};\n for (const [key, value] of new FormData(event.currentTarget).entries()) {\n values[key] = String(value);\n }\n onSubmit(values);\n }\n\n const invalid: InvalidField[] = hasAttemptedSubmit ? invalidFields() : [];\n\n const contextValue = useMemo(\n () => ({ registerField, unregisterField, validateField, errors }),\n [registerField, unregisterField, validateField, errors],\n );\n\n return (\n <form ref={formRef} onSubmit={handleSubmit} noValidate>\n {invalid.length > 0 && (\n <FormErrorSummary ref={summaryRef} invalidFields={invalid} />\n )}\n <FormFieldProvider value={contextValue}>{children}</FormFieldProvider>\n </form>\n );\n}\n","import {\n Input,\n type InputProps,\n type ValidationRules,\n useFormFieldContext,\n} from '@asnewyla/input';\nimport { type ForwardedRef, forwardRef, useEffect, useRef } from 'react';\n\nexport type FormFieldInputProps = Omit<InputProps, 'name'> & {\n name: string;\n validate?: (value: string) => string | undefined;\n};\n\nfunction FormFieldInputInner(\n {\n name,\n required,\n pattern,\n minLength,\n maxLength,\n min,\n max,\n validate,\n error,\n onBlur,\n ...rest\n }: FormFieldInputProps,\n ref: ForwardedRef<HTMLInputElement>,\n) {\n // useFormFieldContext() returns null when there's no ancestor\n // FormFieldProvider — that's what makes FormFieldInput a no-op outside a\n // Form (still a perfectly usable Input, just without registration).\n const formField = useFormFieldContext();\n\n // A stable ref we always own, so registerField always gets a real node —\n // separate from `ref`, which is whatever (or nothing) the consumer passed.\n const inputRef = useRef<HTMLInputElement | null>(null);\n\n const rules: ValidationRules = {\n required,\n pattern,\n minLength,\n maxLength,\n min,\n max,\n validate,\n };\n\n // Deliberately depends on formField?.registerField/unregisterField, not\n // on `formField` itself — Form's context value changes identity whenever\n // its `errors` state updates (that's correct: this component needs to\n // re-render to read the new error). But if this effect depended on the\n // whole object, every validation would also re-trigger registration:\n // cleanup would call the *previous* render's unregisterField, deleting\n // the field and wiping the error validateField just set, in the same\n // tick it was written. Depending on the two functions directly (stable\n // via Form's useCallback) keeps registration decoupled from errors\n // changing.\n // Also deliberately not depending on `rules` — it's a fresh object every\n // render, so registration happens once per mount (or when formField/name\n // change), not on every rule-prop tweak. Rule props are effectively fixed\n // at mount for v0.x; no test or use case yet needs them reactive.\n // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally narrowed deps — see comment above\n useEffect(() => {\n if (!formField) return;\n formField.registerField(name, { ref: inputRef, rules });\n return () => formField.unregisterField(name);\n }, [formField?.registerField, formField?.unregisterField, name]);\n\n // Merges `inputRef` (ours, for registration) with `ref` (the consumer's\n // forwarded one, if any) so both end up pointing at the same DOM node —\n // neither one silently loses the element to the other.\n const setRefs = (node: HTMLInputElement | null) => {\n inputRef.current = node;\n if (typeof ref === 'function') {\n ref(node);\n } else if (ref) {\n ref.current = node;\n }\n };\n\n function handleBlur(event: React.FocusEvent<HTMLInputElement>) {\n if (formField) {\n formField.validateField(name);\n }\n onBlur?.(event);\n }\n\n const formContextError = formField ? formField.errors[name] : undefined;\n const formFieldError = error ?? formContextError;\n\n return (\n <Input\n name={name}\n required={required}\n pattern={pattern}\n minLength={minLength}\n maxLength={maxLength}\n min={min}\n max={max}\n {...rest}\n error={formFieldError}\n onBlur={handleBlur}\n ref={setRefs}\n />\n );\n}\n\nexport const FormFieldInput = forwardRef<HTMLInputElement, FormFieldInputProps>(\n FormFieldInputInner,\n);\n"]}
@@ -0,0 +1,10 @@
1
+ import { UserEvent } from '@testing-library/user-event';
2
+
3
+ type FieldRole = 'textbox' | 'spinbutton';
4
+ declare function getFieldInput(name: string | RegExp, role?: FieldRole): HTMLElement;
5
+ declare function getSubmitButton(name?: string | RegExp): HTMLElement;
6
+ declare function submitForm(user: UserEvent, name?: string | RegExp): Promise<void>;
7
+ declare function getErrorSummary(): HTMLElement;
8
+ declare function queryErrorSummary(): HTMLElement | null;
9
+
10
+ export { getErrorSummary, getFieldInput, getSubmitButton, queryErrorSummary, submitForm };
@@ -0,0 +1,10 @@
1
+ import { UserEvent } from '@testing-library/user-event';
2
+
3
+ type FieldRole = 'textbox' | 'spinbutton';
4
+ declare function getFieldInput(name: string | RegExp, role?: FieldRole): HTMLElement;
5
+ declare function getSubmitButton(name?: string | RegExp): HTMLElement;
6
+ declare function submitForm(user: UserEvent, name?: string | RegExp): Promise<void>;
7
+ declare function getErrorSummary(): HTMLElement;
8
+ declare function queryErrorSummary(): HTMLElement | null;
9
+
10
+ export { getErrorSummary, getFieldInput, getSubmitButton, queryErrorSummary, submitForm };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ var react = require('@testing-library/react');
4
+
5
+ // src/test-utils.ts
6
+ function getFieldInput(name, role = "textbox") {
7
+ return react.screen.getByRole(role, { name });
8
+ }
9
+ function getSubmitButton(name = "Save") {
10
+ return react.screen.getByRole("button", { name });
11
+ }
12
+ function submitForm(user, name = "Save") {
13
+ return user.click(getSubmitButton(name));
14
+ }
15
+ function getErrorSummary() {
16
+ return react.screen.getByRole("alert");
17
+ }
18
+ function queryErrorSummary() {
19
+ return react.screen.queryByRole("alert");
20
+ }
21
+
22
+ exports.getErrorSummary = getErrorSummary;
23
+ exports.getFieldInput = getFieldInput;
24
+ exports.getSubmitButton = getSubmitButton;
25
+ exports.queryErrorSummary = queryErrorSummary;
26
+ exports.submitForm = submitForm;
27
+ //# sourceMappingURL=test-utils.js.map
28
+ //# sourceMappingURL=test-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/test-utils.ts"],"names":["screen"],"mappings":";;;;;AAKO,SAAS,aAAA,CACd,IAAA,EACA,IAAA,GAAkB,SAAA,EACL;AACb,EAAA,OAAOA,YAAA,CAAO,SAAA,CAAU,IAAA,EAAM,EAAE,MAAM,CAAA;AACxC;AAEO,SAAS,eAAA,CAAgB,OAAwB,MAAA,EAAqB;AAC3E,EAAA,OAAOA,YAAA,CAAO,SAAA,CAAU,QAAA,EAAU,EAAE,MAAM,CAAA;AAC5C;AAEO,SAAS,UAAA,CACd,IAAA,EACA,IAAA,GAAwB,MAAA,EACT;AACf,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,eAAA,CAAgB,IAAI,CAAC,CAAA;AACzC;AAEO,SAAS,eAAA,GAA+B;AAC7C,EAAA,OAAOA,YAAA,CAAO,UAAU,OAAO,CAAA;AACjC;AAEO,SAAS,iBAAA,GAAwC;AACtD,EAAA,OAAOA,YAAA,CAAO,YAAY,OAAO,CAAA;AACnC","file":"test-utils.js","sourcesContent":["import { screen } from '@testing-library/react';\nimport type { UserEvent } from '@testing-library/user-event';\n\ntype FieldRole = 'textbox' | 'spinbutton';\n\nexport function getFieldInput(\n name: string | RegExp,\n role: FieldRole = 'textbox',\n): HTMLElement {\n return screen.getByRole(role, { name });\n}\n\nexport function getSubmitButton(name: string | RegExp = 'Save'): HTMLElement {\n return screen.getByRole('button', { name });\n}\n\nexport function submitForm(\n user: UserEvent,\n name: string | RegExp = 'Save',\n): Promise<void> {\n return user.click(getSubmitButton(name));\n}\n\nexport function getErrorSummary(): HTMLElement {\n return screen.getByRole('alert');\n}\n\nexport function queryErrorSummary(): HTMLElement | null {\n return screen.queryByRole('alert');\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import { screen } from '@testing-library/react';
2
+
3
+ // src/test-utils.ts
4
+ function getFieldInput(name, role = "textbox") {
5
+ return screen.getByRole(role, { name });
6
+ }
7
+ function getSubmitButton(name = "Save") {
8
+ return screen.getByRole("button", { name });
9
+ }
10
+ function submitForm(user, name = "Save") {
11
+ return user.click(getSubmitButton(name));
12
+ }
13
+ function getErrorSummary() {
14
+ return screen.getByRole("alert");
15
+ }
16
+ function queryErrorSummary() {
17
+ return screen.queryByRole("alert");
18
+ }
19
+
20
+ export { getErrorSummary, getFieldInput, getSubmitButton, queryErrorSummary, submitForm };
21
+ //# sourceMappingURL=test-utils.mjs.map
22
+ //# sourceMappingURL=test-utils.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/test-utils.ts"],"names":[],"mappings":";;;AAKO,SAAS,aAAA,CACd,IAAA,EACA,IAAA,GAAkB,SAAA,EACL;AACb,EAAA,OAAO,MAAA,CAAO,SAAA,CAAU,IAAA,EAAM,EAAE,MAAM,CAAA;AACxC;AAEO,SAAS,eAAA,CAAgB,OAAwB,MAAA,EAAqB;AAC3E,EAAA,OAAO,MAAA,CAAO,SAAA,CAAU,QAAA,EAAU,EAAE,MAAM,CAAA;AAC5C;AAEO,SAAS,UAAA,CACd,IAAA,EACA,IAAA,GAAwB,MAAA,EACT;AACf,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,eAAA,CAAgB,IAAI,CAAC,CAAA;AACzC;AAEO,SAAS,eAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,UAAU,OAAO,CAAA;AACjC;AAEO,SAAS,iBAAA,GAAwC;AACtD,EAAA,OAAO,MAAA,CAAO,YAAY,OAAO,CAAA;AACnC","file":"test-utils.mjs","sourcesContent":["import { screen } from '@testing-library/react';\nimport type { UserEvent } from '@testing-library/user-event';\n\ntype FieldRole = 'textbox' | 'spinbutton';\n\nexport function getFieldInput(\n name: string | RegExp,\n role: FieldRole = 'textbox',\n): HTMLElement {\n return screen.getByRole(role, { name });\n}\n\nexport function getSubmitButton(name: string | RegExp = 'Save'): HTMLElement {\n return screen.getByRole('button', { name });\n}\n\nexport function submitForm(\n user: UserEvent,\n name: string | RegExp = 'Save',\n): Promise<void> {\n return user.click(getSubmitButton(name));\n}\n\nexport function getErrorSummary(): HTMLElement {\n return screen.getByRole('alert');\n}\n\nexport function queryErrorSummary(): HTMLElement | null {\n return screen.queryByRole('alert');\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asnewyla/form",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Form wrapper with real validation and an accessible error summary",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,14 +16,23 @@
16
16
  "types": "dist/index.d.ts",
17
17
  "exports": {
18
18
  ".": {
19
+ "types": "./dist/index.d.ts",
19
20
  "import": "./dist/index.mjs",
20
- "require": "./dist/index.js",
21
- "types": "./dist/index.d.ts"
21
+ "require": "./dist/index.js"
22
+ },
23
+ "./styles.css": "./dist/index.css",
24
+ "./test-utils": {
25
+ "types": "./dist/test-utils.d.ts",
26
+ "import": "./dist/test-utils.mjs",
27
+ "require": "./dist/test-utils.js"
22
28
  }
23
29
  },
24
30
  "files": [
25
31
  "dist"
26
32
  ],
33
+ "sideEffects": [
34
+ "*.css"
35
+ ],
27
36
  "keywords": [
28
37
  "react",
29
38
  "form",
@@ -32,10 +41,20 @@
32
41
  ],
33
42
  "peerDependencies": {
34
43
  "react": "^18.0.0 || ^19.0.0",
35
- "react-dom": "^18.0.0 || ^19.0.0"
44
+ "react-dom": "^18.0.0 || ^19.0.0",
45
+ "@testing-library/react": "^16.0.0",
46
+ "@testing-library/user-event": "^14.5.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "@testing-library/react": {
50
+ "optional": true
51
+ },
52
+ "@testing-library/user-event": {
53
+ "optional": true
54
+ }
36
55
  },
37
56
  "dependencies": {
38
- "@asnewyla/input": "0.2.0"
57
+ "@asnewyla/input": "0.2.1"
39
58
  },
40
59
  "devDependencies": {
41
60
  "@testing-library/jest-dom": "^6.4.0",