@asnewyla/form 0.2.0 → 0.2.1
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 +3 -4
- package/dist/index.css +26 -0
- package/dist/index.css.map +1 -0
- package/dist/index.js +17 -28
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +17 -28
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -2
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,
|
|
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(
|
|
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(
|
|
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,
|
package/dist/index.mjs.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","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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asnewyla/form",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Form wrapper with real validation and an accessible error summary",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,11 +19,15 @@
|
|
|
19
19
|
"import": "./dist/index.mjs",
|
|
20
20
|
"require": "./dist/index.js",
|
|
21
21
|
"types": "./dist/index.d.ts"
|
|
22
|
-
}
|
|
22
|
+
},
|
|
23
|
+
"./styles.css": "./dist/index.css"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"dist"
|
|
26
27
|
],
|
|
28
|
+
"sideEffects": [
|
|
29
|
+
"*.css"
|
|
30
|
+
],
|
|
27
31
|
"keywords": [
|
|
28
32
|
"react",
|
|
29
33
|
"form",
|