-
Notifications
You must be signed in to change notification settings - Fork 108
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e0ebcc5
commit 627d5a6
Showing
11 changed files
with
499 additions
and
22,523 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# ignore all .ts, .tsx files except .d.ts | ||
*.ts | ||
*.tsx | ||
!*.d.ts | ||
|
||
# config / build result | ||
tsconfig.json | ||
*.tsbuildinfo |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# @conform-to/typebox | ||
|
||
> [Conform](https://github.com/edmundhung/conform) helpers for integrating with [typebox](https://github.com/sinclairzx81/typebox) | ||
<!-- aside --> | ||
|
||
## API Reference | ||
|
||
- [getFieldsetConstraint](#getfieldsetconstraint) | ||
- [parse](#parse) | ||
|
||
<!-- /aside --> | ||
|
||
### getFieldsetConstraint | ||
|
||
This tries to infer constraint of each field based on the typebox schema. This is useful for: | ||
|
||
1. Making it easy to style input using CSS, e.g. `:required` | ||
2. Having some basic validation working before/without JS. | ||
|
||
```tsx | ||
import { useForm } from '@conform-to/react'; | ||
import { getFieldsetConstraint } from '@conform-to/typebox'; | ||
import { Type } from '@sinclairzx81/typebox'; | ||
|
||
const schema = Type.Object({ | ||
email: Type.String(), | ||
password: Type.String(), | ||
}); | ||
|
||
function Example() { | ||
const [form, { email, password }] = useForm({ | ||
constraint: getFieldsetConstraint(schema), | ||
}); | ||
|
||
// ... | ||
} | ||
``` | ||
|
||
### parse | ||
|
||
It parses the formData and returns a submission result with the validation error. If no error is found, the parsed data will also be populated as `submission.value`. | ||
|
||
```tsx | ||
import { useForm } from '@conform-to/react'; | ||
import { parse } from '@conform-to/typebox'; | ||
import { Type } from '@sinclairzx81/typebox'; | ||
|
||
const schema = Type.Object({ | ||
email: Type.String(), | ||
password: Type.String(), | ||
}); | ||
|
||
function ExampleForm() { | ||
const [form] = useForm({ | ||
onValidate({ formData }) { | ||
return parse(formData, { schema }); | ||
}, | ||
}); | ||
|
||
// ... | ||
} | ||
``` | ||
|
||
Or when parsing the formData on server side (e.g. Remix): | ||
|
||
```tsx | ||
import { useForm } from '@conform-to/react'; | ||
import { parse } from '@conform-to/typebox'; | ||
import { Type } from '@sinclairzx81/typebox'; | ||
|
||
const schema = Type.Object({ | ||
// Define the schema with typebox | ||
}); | ||
|
||
export async function action({ request }) { | ||
const formData = await request.formData(); | ||
const submission = parse(formData, { | ||
schema, | ||
}); | ||
|
||
if (submission.intent !== 'submit' || !submission.value) { | ||
return submission; | ||
} | ||
|
||
// ... | ||
} | ||
``` |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { | ||
type FieldConstraint, | ||
type FieldsetConstraint, | ||
type Submission, | ||
parse as baseParse, | ||
} from '@conform-to/dom'; | ||
import type { StaticDecode, TObject, TSchema } from '@sinclair/typebox'; | ||
import { OptionalKind, TypeGuard } from '@sinclair/typebox'; | ||
import { Value, ValueErrorIterator } from '@sinclair/typebox/value'; | ||
|
||
function transformPath(path: string): string { | ||
const parts = path.split('/').filter(Boolean); // Split the string and remove empty parts | ||
return parts | ||
.map((part, index) => { | ||
// If the part is a number, format it as an array index, otherwise use a dot or nothing for the first part | ||
return isNaN(+part) ? (index === 0 ? part : `.${part}`) : `[${part}]`; | ||
}) | ||
.join(''); | ||
} | ||
|
||
export function getFieldsetConstraint< | ||
T extends TObject, | ||
R extends Record<string, any> = StaticDecode<T>, | ||
>(schema: T): FieldsetConstraint<R> { | ||
function discardKey(value: Record<PropertyKey, any>, key: PropertyKey) { | ||
const { [key]: _, ...rest } = value; | ||
return rest; | ||
} | ||
function inferConstraint<T extends TSchema>(schema: T): FieldConstraint<T> { | ||
let constraint: FieldConstraint = {}; | ||
if (TypeGuard.IsOptional(schema)) { | ||
const unwrapped = discardKey(schema, OptionalKind) as TSchema; | ||
constraint = { | ||
...inferConstraint(unwrapped), | ||
required: false, | ||
}; | ||
} else if (TypeGuard.IsArray(schema)) { | ||
constraint = { | ||
...inferConstraint(schema.items), | ||
multiple: true, | ||
}; | ||
} else if (TypeGuard.IsString(schema)) { | ||
if (schema.minLength) { | ||
constraint.minLength = schema.minLength; | ||
} | ||
if (schema.maxLength) { | ||
constraint.maxLength = schema.maxLength; | ||
} | ||
if (schema.pattern) { | ||
constraint.pattern = schema.pattern; | ||
} | ||
} else if (TypeGuard.IsNumber(schema) || TypeGuard.IsInteger(schema)) { | ||
if (schema.minimum) { | ||
constraint.min = schema.minimum; | ||
} | ||
if (schema.maximum) { | ||
constraint.max = schema.maximum; | ||
} | ||
if (schema.multipleOf) { | ||
constraint.step = schema.multipleOf; | ||
} | ||
} else if (TypeGuard.IsUnionLiteral(schema)) { | ||
constraint.pattern = schema.anyOf | ||
.map((literal) => { | ||
const option = literal.const.toString(); | ||
// To escape unsafe characters on regex | ||
return option | ||
.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') | ||
.replace(/-/g, '\\x2d'); | ||
}) | ||
.join('|'); | ||
} | ||
|
||
if (typeof constraint.required === 'undefined') { | ||
constraint.required = true; | ||
} | ||
|
||
return constraint; | ||
} | ||
function resolveFieldsetConstraint< | ||
T extends TObject, | ||
R extends Record<string, any> = StaticDecode<T>, | ||
>(schema: T): FieldsetConstraint<R> { | ||
return Object.getOwnPropertyNames(schema.properties).reduce((acc, key) => { | ||
return { | ||
...acc, | ||
[key]: inferConstraint( | ||
schema.properties[key as keyof FieldsetConstraint<R>], | ||
), | ||
}; | ||
}, {} as FieldsetConstraint<R>); | ||
} | ||
|
||
return resolveFieldsetConstraint(schema); | ||
} | ||
|
||
export function parse<Schema extends TObject, R = StaticDecode<Schema>>( | ||
payload: FormData | URLSearchParams, | ||
config: { | ||
schema: Schema | ((intent: string) => Schema); | ||
}, | ||
): Submission<R>; | ||
|
||
export function parse<Schema extends TObject, R = StaticDecode<Schema>>( | ||
payload: FormData | URLSearchParams, | ||
config: { | ||
schema: Schema | ((intent: string) => Schema); | ||
}, | ||
): Submission<R> { | ||
return baseParse<R>(payload, { | ||
resolve(input, intent) { | ||
const schema = | ||
typeof config.schema === 'function' | ||
? config.schema(intent) | ||
: config.schema; | ||
const resolveData = (value: R) => ({ value }); | ||
const resolveError = (error: unknown) => { | ||
if (error instanceof ValueErrorIterator) { | ||
return { | ||
error: Array.from(error).reduce((error, valueError) => { | ||
const path = transformPath(valueError.path); | ||
const innerError = (error[path] ??= []); | ||
innerError.push(valueError.message); | ||
return error; | ||
}, {} as Record<string, string[]>), | ||
}; | ||
} | ||
|
||
throw error; | ||
}; | ||
|
||
// coerce the input to the schema | ||
const payload = Value.Convert(schema, input); | ||
try { | ||
return resolveData(Value.Decode(schema, payload)); | ||
} catch (error) { | ||
return resolveError(Value.Errors(schema, payload)); | ||
} | ||
}, | ||
}); | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
{ | ||
"name": "@conform-to/typebox", | ||
"description": "Conform helpers for integrating with typebox", | ||
"homepage": "https://conform.guide", | ||
"license": "MIT", | ||
"version": "0.9.1", | ||
"main": "index.js", | ||
"module": "index.mjs", | ||
"types": "index.d.ts", | ||
"exports": { | ||
".": { | ||
"types": "./index.d.ts", | ||
"module": "./index.mjs", | ||
"import": "./index.mjs", | ||
"require": "./index.js", | ||
"default": "./index.mjs" | ||
} | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/edmundhung/conform", | ||
"directory": "packages/conform-typebox" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/edmundhung/conform/issues" | ||
}, | ||
"peerDependencies": { | ||
"@conform-to/dom": "0.9.1", | ||
"@sinclair/typebox": ">=0.32.0" | ||
}, | ||
"devDependencies": { | ||
"@sinclair/typebox": "^0.32.3" | ||
}, | ||
"keywords": [ | ||
"constraint-validation", | ||
"form", | ||
"form-validation", | ||
"html", | ||
"progressive-enhancement", | ||
"validation", | ||
"typebox" | ||
], | ||
"sideEffects": false | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"compilerOptions": { | ||
"lib": ["ESNext", "DOM", "DOM.Iterable"], | ||
"target": "ES2020", | ||
"moduleResolution": "node16", | ||
"allowSyntheticDefaultImports": false, | ||
"noUncheckedIndexedAccess": true, | ||
"strict": true, | ||
"declaration": true, | ||
"emitDeclarationOnly": true, | ||
"composite": true, | ||
"skipLibCheck": true | ||
} | ||
} |
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
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
Oops, something went wrong.