Last active
February 22, 2022 08:47
-
-
Save musou1500/a6a8304ebd78dd0e5523ee36bcf88573 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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