Skip to content

Instantly share code, notes, and snippets.

@musou1500
Last active February 22, 2022 08:47
Show Gist options
  • Save musou1500/a6a8304ebd78dd0e5523ee36bcf88573 to your computer and use it in GitHub Desktop.
Save musou1500/a6a8304ebd78dd0e5523ee36bcf88573 to your computer and use it in GitHub Desktop.
const formGroup = new FormGroup({
username: new FormControl([
Validators.minLength({
len: 1,
message: "user name must longer than or equal to 1",
}),
]),
});
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const result = formGroup.validate(formData);
return json({
...result,
username: [
...result.username,
{ message: "You can add server validation errors" },
],
});
};
const Admin = () => {
const { formProps, isDirty, hasError, getError } = useFormValidation({
formGroup,
results: useActionData(),
});
return (
<div className="admin">
<Form method="post" {...formProps}>
<p>
<label htmlFor="username">username</label>
<input type="text" name="username" id="username" />
{getError("username")?.message}
</p>
<p>
<button type="submit" disabled={hasError() || !isDirty()}>
submit
</button>
</p>
</Form>
</div>
);
};
export default Admin;
import { ComponentPropsWithRef, useRef, useState } from "react";
import { ActionFunction, Form, json, useActionData } from "remix";
type Validator = (v: unknown) => string | undefined;
type ValidatorFactoryOptions<T = {}> = {
message: string;
} & T;
type ValidationResultField = { message: string };
type ValidationResult = Record<string, ValidationResultField[]>;
class Validators {
static string(options: ValidatorFactoryOptions) {
return (v: unknown) =>
typeof v === "string" ? undefined : options.message;
}
static numeric(options: ValidatorFactoryOptions) {
return (v: unknown) =>
typeof v === "string" && !isNaN(parseInt(v, 10))
? undefined
: options.message;
}
static maxLength(options: ValidatorFactoryOptions<{ len: number }>) {
return (v: unknown) =>
typeof v === "string" && v.length <= options.len
? undefined
: options.message;
}
static minLength(options: ValidatorFactoryOptions<{ len: number }>) {
return (v: unknown) =>
typeof v === "string" && v.length >= options.len
? undefined
: options.message;
}
}
class FormGroup {
constructor(private fields: Record<string, FormControl>) {}
validate(formData: FormData): ValidationResult {
const results: ValidationResult = {};
for (const [k, v] of Object.entries(this.fields)) {
results[k] = v
.validate(formData.get(k))
.map((result) => ({ message: result }));
}
return results;
}
}
class FormControl {
constructor(private validators: Validator[]) {}
validate(v: unknown): string[] {
const results: string[] = [];
for (const validator of this.validators) {
const result = validator(v);
if (result !== undefined) {
results.push(result);
}
}
return results;
}
}
function isHtmlElement(object: any): object is HTMLElement {
return object != null && typeof object.tagName === "string";
}
function isFormElement(object: any): object is HTMLFormElement {
return isHtmlElement(object) && object.tagName.toLowerCase() === "form";
}
const useFormValidation = ({
formGroup,
results: serverResults = {},
}: {
formGroup: FormGroup;
results?: ValidationResult;
}): {
formProps: ComponentPropsWithRef<typeof Form>;
isDirty: (name?: string) => boolean;
hasError: (name?: string) => boolean;
getError: (name: string) => ValidationResultField | undefined;
getErrors: (name: string) => ValidationResultField[];
} => {
const ref = useRef<HTMLFormElement | null>(null);
const [dirtyMap, setDirtyMap] = useState<Record<string, boolean>>({});
const [localResults, setLocalResults] = useState<ValidationResult>({});
const getError = (name: string) => {
if (localResults?.[name]?.length > 0) {
return localResults[name][0];
} else if (serverResults?.[name]?.length > 0) {
return serverResults[name][0];
} else {
return undefined;
}
};
const hasError = (name?: string) => {
if (name) {
return getError(name) !== undefined;
} else {
return [localResults, serverResults].some((results) => {
return Object.entries(results).some(([_k, v]) => v.length > 0);
});
}
};
return {
isDirty: (name?: string) => {
if (name) {
return !!dirtyMap[name];
} else {
return Object.entries(dirtyMap).some(([k, v]) => v);
}
},
hasError,
getError,
getErrors: (name: string) => {
return localResults?.[name] || serverResults?.[name] || [];
},
formProps: {
ref,
onBlur: (e) => {
setDirtyMap((oldDirtyMap) => ({
...oldDirtyMap,
[(e.target as any).name as string]: true,
}));
if (ref.current) {
setLocalResults(formGroup.validate(new FormData(ref.current)));
}
},
onSubmit: (e) => {
const target = e.target;
if (!isFormElement(target)) {
e.preventDefault();
return;
}
const formData = new FormData(target);
const result = formGroup.validate(formData);
const passed = Array.from(formData.keys()).every(
(k) => !result[k] || result[k].length <= 0
);
if (!passed) {
e.preventDefault();
return;
}
},
},
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment