Skip to content

Commit

Permalink
Support server-only validations (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Jan 16, 2023
1 parent ffcaf2e commit 2f0bfd2
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 12 deletions.
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Input>` 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<FormSchema> = {
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 `<Input>` 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:
Expand Down
37 changes: 25 additions & 12 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ export interface InputInfo {
errorMessages?: Record<string, string>;
}

export type ServerOnlyCustomValidations<T extends FormDefinition> = Partial<{
[key in KeyOf<T["inputs"]>]: CustomValidations;
}>;

// Server-side only (currently) - validate all specified inputs in the formData
export type ServerFormInfo<T extends FormDefinition> = {
submittedValues: Record<KeyOf<T["inputs"]>, string | string[] | null>;
Expand Down Expand Up @@ -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
Expand All @@ -381,16 +386,16 @@ 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<BuiltInValidationAttrs>;
// Ignoring this "error" since the type narrowing to accomplish this
// would be nasty due to the differences in attribute values per input
// type. We're going to rely on the *ValidationAttrs types to ensure
// 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
Expand All @@ -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;
Expand All @@ -428,7 +433,8 @@ async function validateInput(
// Perform all validations for a submitted form on the server
export async function validateServerFormData<T extends FormDefinition>(
formData: FormData,
formDefinition: T
formDefinition: T,
serverCustomValidations?: ServerOnlyCustomValidations<T>
): Promise<ServerFormInfo<T>> {
// 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
Expand All @@ -454,7 +460,11 @@ export async function validateServerFormData<T extends FormDefinition>(
state: "done",
validity: await validateInput(
inputName,
inputDef,
inputDef.validationAttrs,
{
...inputDef.customValidations,
...serverCustomValidations?.[inputName],
},
value,
undefined,
formData
Expand Down Expand Up @@ -482,7 +492,8 @@ export async function validateServerFormData<T extends FormDefinition>(
state: "done",
validity: await validateInput(
inputName,
inputDef,
inputDef.validationAttrs,
inputDef.customValidations,
"",
undefined,
formData
Expand Down Expand Up @@ -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(
Expand All @@ -893,7 +905,8 @@ function useValidatedControl<
} else {
validity = await validateInput(
name,
inputDef,
inputDef.validationAttrs,
inputDef.customValidations,
value,
inputRef.current || undefined
);
Expand Down

0 comments on commit 2f0bfd2

Please sign in to comment.