diff --git a/README.md b/README.md
index 9f44062..5a0b526 100644
--- a/README.md
+++ b/README.md
@@ -423,6 +423,90 @@ function FavoriteSkill() {
}
```
+#### Server-only Validations
+
+While this library tries to lean-into shared validations between client and server, there are also good reasons not to share validations entirely. Most of the time, this comes down to keeping client-bundles small and/or needing direct server or DB access for certain validations.
+
+One approach is to just perform server-only validations manually _after_ calling `validateServerFormInfo`:
+
+```js
+import { validateServerFormData } from "remix-validity-state";
+
+export async function action({ request }: ActionArgs) {
+ const formData = await request.formData();
+ const serverFormInfo = await validateServerFormData(
+ formData,
+ formDefinitions
+ );
+ if (!serverFormInfo.valid) {
+ return json({ serverFormInfo });
+ }
+
+ // Now that we know our shared validations passed, we can perform more complex validations
+ let isEmailUnique = await checkEmail(
+ serverFormInfo.submittedValues.emailAddress
+ );
+ if (!isEmailUnique) {
+ return json({
+ serverFormInfo,
+ errors: { email: "Email address is not unique " },
+ });
+ }
+ // ...
+}
+```
+
+This may be sufficient in some cases, but also now requires you to support a new error messaging UI separate from the one already handled via `` and/or provided by `useValidatedInput().info.errorMessages`.
+
+To support this common use-case, you can pass a set of `customValidations` server-only implementations to `validateServerFormData`, which will be used instead of the validations you define in the shared `formDefinition`. Usually, you'll just put a stub `() => true` in your shared validations so the client is aware of the validation.
+
+```ts
+import type { ServerOnlyCustomValidations } from 'remix-validity-state'
+import { validateServerFormData } from 'remix-validity-state'
+
+let formDefinition: FormSchema = {
+ inputs: {
+ emailAddress: {
+ validationAttrs: {
+ type: "email",
+ required: true,
+ },
+ customValidations: {
+ // always "valid" in shared validations
+ uniqueEmail: () => true,
+ },
+ errorMessages: {
+ uniqueEmail: () => 'Email address already in use!'',
+ },
+ },
+ }
+};
+
+const serverCustomValidations: ServerOnlyCustomValidations = {
+ emailAddress: {
+ async uniqueEmail(value) {
+ let isUnique = await checkEmail(value);
+ return isUnique;
+ },
+ },
+};
+
+export async function action({ request }: ActionArgs) {
+ const formData = await request.formData();
+ const serverFormInfo = await validateServerFormData(
+ formData,
+ formDefinitions,
+ serverCustomValidations
+ );
+ // serverFormInfo.valid here will be reflective of the custom server-only
+ // validation and will leverage your shared `errorMessages`
+ if (!serverFormInfo.valid) {
+ return json({ serverFormInfo });
+ }
+ // ...
+}
+```
+
#### Error Messages
Basic error messaging is handled out of the box by `` for built-in HTML validations. If you are using custom validations, or if you want to override the built-in messaging, you can provide custom error messages in our `formDefinition`. Custom error messages can either be a static string, or a function that receives the attribute value (built-in validations only), the input name, and the input value:
diff --git a/src/index.tsx b/src/index.tsx
index 828fd84..52cdd89 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -176,6 +176,10 @@ export interface InputInfo {
errorMessages?: Record;
}
+export type ServerOnlyCustomValidations = Partial<{
+ [key in KeyOf]: CustomValidations;
+}>;
+
// Server-side only (currently) - validate all specified inputs in the formData
export type ServerFormInfo = {
submittedValues: Record, string | string[] | null>;
@@ -365,7 +369,8 @@ function IsInputDefinition(
// Called in a useEffect client side and from validateServerFormIno server-side
async function validateInput(
inputName: string,
- inputDef: ControlDefinition,
+ validationAttrs: ControlDefinition["validationAttrs"],
+ customValidations: ControlDefinition["customValidations"],
value: string,
inputEl?: SupportedHTMLElements | SupportedHTMLElements[], // CSR
formData?: FormData // SSR
@@ -381,8 +386,8 @@ async function validateInput(
formData = new FormData(formEl);
}
- if (inputDef.validationAttrs) {
- for (let _attr of Object.keys(inputDef.validationAttrs)) {
+ if (validationAttrs) {
+ for (let _attr of Object.keys(validationAttrs)) {
let attr = _attr as KeyOf;
// Ignoring this "error" since the type narrowing to accomplish this
// would be nasty due to the differences in attribute values per input
@@ -390,7 +395,7 @@ async function validateInput(
// users are specifying valid attributes up front in their schemas and
// just yolo this lookup
// @ts-expect-error
- let _attrValue = inputDef.validationAttrs[attr] || null;
+ let _attrValue = validationAttrs[attr] || null;
let attrValue = calculateValidationAttr(_attrValue, formData);
// Undefined attr values means the attribute doesn't exist and there's
// nothing to validate
@@ -413,9 +418,9 @@ async function validateInput(
}
}
- if (inputDef.customValidations) {
- for (let name of Object.keys(inputDef.customValidations)) {
- let validate = inputDef.customValidations[name];
+ if (customValidations) {
+ for (let name of Object.keys(customValidations)) {
+ let validate = customValidations[name];
let isInvalid = !(await validate(value, formData));
validity[name] = isInvalid;
validity.valid = validity.valid && !isInvalid;
@@ -428,7 +433,8 @@ async function validateInput(
// Perform all validations for a submitted form on the server
export async function validateServerFormData(
formData: FormData,
- formDefinition: T
+ formDefinition: T,
+ serverCustomValidations?: ServerOnlyCustomValidations
): Promise> {
// Unsure if there's a better way to do this type of object mapping while
// keeping the keys strongly typed - but this currently complains since we
@@ -454,7 +460,11 @@ export async function validateServerFormData(
state: "done",
validity: await validateInput(
inputName,
- inputDef,
+ inputDef.validationAttrs,
+ {
+ ...inputDef.customValidations,
+ ...serverCustomValidations?.[inputName],
+ },
value,
undefined,
formData
@@ -482,7 +492,8 @@ export async function validateServerFormData(
state: "done",
validity: await validateInput(
inputName,
- inputDef,
+ inputDef.validationAttrs,
+ inputDef.customValidations,
"",
undefined,
formData
@@ -882,7 +893,8 @@ function useValidatedControl<
if (inputType === "radio" || inputType === "checkbox") {
validity = await validateInput(
name,
- inputDef,
+ inputDef.validationAttrs,
+ inputDef.customValidations,
value,
Array.from(
inputRef.current?.form?.querySelectorAll(
@@ -893,7 +905,8 @@ function useValidatedControl<
} else {
validity = await validateInput(
name,
- inputDef,
+ inputDef.validationAttrs,
+ inputDef.customValidations,
value,
inputRef.current || undefined
);