Skip to content

Commit

Permalink
feat(yupFieldRule): sync yup rule resolvers (#30)
Browse files Browse the repository at this point in the history
* feat(yupFieldRule): Supports yup validateSync

* test: yupFieldRule

* chore: add one ToDo

* chore(isPromise): Determine if it is a `Promise` object
  • Loading branch information
LittleSound authored Jun 3, 2022
1 parent 64dbc86 commit a0ea9c7
Show file tree
Hide file tree
Showing 14 changed files with 421 additions and 92 deletions.
1 change: 1 addition & 0 deletions example/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare module '@vue/runtime-core' {
Footer: typeof import('./src/components/Footer.vue')['default']
Header: typeof import('./src/components/Header.vue')['default']
NativeInput: typeof import('./src/components/NativeInput.vue')['default']
RouteNav: typeof import('./src/components/RouteNav.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
Expand Down
13 changes: 13 additions & 0 deletions example/src/components/RouteNav.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts" setup>
</script>

<template>
<div>
<router-link to="/">
Home
</router-link>
<router-link to="/yup">
Yup
</router-link>
</div>
</template>
2 changes: 2 additions & 0 deletions example/src/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script setup lang="ts">
import Header from '~/components/Header.vue'
import RouteNav from '~/components/RouteNav.vue'
</script>

<template>
<div space-y-7>
<Header />
<RouteNav />
<Demo />
<NativeInput />
</div>
Expand Down
121 changes: 121 additions & 0 deletions example/src/pages/yup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<script lang="ts" setup>
import { useForm } from 'slimeform'
import { yupFieldRule } from 'slimeform/resolvers'
import * as yup from 'yup'
import RouteNav from '~/components/RouteNav.vue'
const local = ref('en')
/** mock i18n `t` function */
const mockT = (_: string) => local.value === 'en' ? 'Valid age up to 120 years old' : '有效年龄至 120 岁'
yup.object({
})
const { form, status } = useForm({
form: () => ({
age: '',
// ToDo: 支持 yup 的异步验证
asyncTest: '',
}),
rule: {
age: [
yupFieldRule(yup.string()
.required(),
),
yupFieldRule(yup.number()
.max(120, () => mockT('xxx_i18n_key'))
.integer()
.nullable(),
),
],
},
defaultMessage: 'none',
})
</script>

<template>
<div>
<RouteNav />
<div
space-y-5
text-left
max-w-400px
w-full
mx-auto
rounded-xl
border="1"
p="4"
>
<h2 text-2xl mb-2>
Yup Rule Demo
</h2>
<div>
<h3 text-xl mb-1>
Input Age
</h3>
<label>
<input
v-model="form.age"
type="text"
placeholder="edit me"
autocomplete="false"

p="x-4 y-2"
w="250px"
text="center"
bg="transparent"
border="~ rounded gray-200 dark:gray-700"
outline="none active:none"
>
</label>
<div>
<p>Value: {{ form.age }}</p>
<p>isDirty: {{ status.age.isDirty }}</p>
<p>isError: {{ status.age.isError }}</p>
<p>message: {{ status.age.message }}</p>
</div>
</div>
<div>
<h3 text-xl mb-1>
Async Rule
</h3>
<label>
<input
v-model="form.asyncTest"
type="text"
placeholder="edit me"
autocomplete="false"

p="x-4 y-2"
w="250px"
text="center"
bg="transparent"
border="~ rounded gray-200 dark:gray-700"
outline="none active:none"
>
</label>
<div>
<p>Value: {{ form.asyncTest }}</p>
<p>isDirty: {{ status.asyncTest.isDirty }}</p>
<p>isError: {{ status.asyncTest.isError }}</p>
<p>message: {{ status.asyncTest.message }}</p>
</div>
</div>
<div>
<h4 text-lg mb-1>
Local
</h4>
<div space-x-4>
<label>
<input v-model="local" type="radio" value="en">
<span ml-1>en</span>
</label>
<label>
<input v-model="local" type="radio" value="zh-CN">
<span ml-1>zh-CN</span>
</label>
</div>
</div>
</div>
</div>
</template>
26 changes: 18 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"version": "0.3.2",
"type": "module",
"description": "Form state management and validation for Vue3",
"main": "dist/index.cjs",
"author": {
"name": "Ayaka Rizumu",
"url": "https://github.com/LittleSound"
Expand All @@ -18,12 +17,21 @@
"./package.json": "./package.json",
".": {
"import": "./dist/index.js",
"default": "./dist/index.cjs"
"default": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./resolvers": {
"import": "./dist/resolvers.js",
"default": "./dist/resolvers.cjs",
"types": "./dist/resolvers.d.ts"
}
},
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
"dist",
"*.d.ts"
],
"scripts": {
"dev": "run-p dev:*",
Expand Down Expand Up @@ -65,14 +73,15 @@
"pnpm": "^7.1.3",
"tsup": "5.12.6",
"typescript": "4.6.3",
"unocss": "^0.34.0",
"unocss": "^0.37.2",
"unplugin-auto-import": "^0.7.2",
"unplugin-vue-components": "^0.19.5",
"vite": "2.9.6",
"vite-plugin-pages": "^0.23.0",
"vitest": "^0.12.9",
"vue": "^3.2.33",
"vue-tsc": "^0.34.15"
"vue-tsc": "^0.34.15",
"yup": "^0.32.11"
},
"peerDependencies": {
"vue": ">=3"
Expand All @@ -85,9 +94,10 @@
"*.{js,ts,md,json,yaml}": "eslint . --fix"
},
"tsup": {
"entry": [
"src/index.ts"
],
"entry": {
"index": "src/index.ts",
"resolvers": "packages/resolvers/index.ts"
},
"format": [
"esm",
"cjs"
Expand Down
1 change: 1 addition & 0 deletions packages/resolvers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './yup'
67 changes: 67 additions & 0 deletions packages/resolvers/yup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect, test } from 'vitest'
import { nextTick, ref } from 'vue'
import * as yup from 'yup'
import { useForm } from '../../src'
import { useSetup } from '../.test'
import { yupFieldRule } from './yup'

test('yupFieldRule', async () => {
const wr = useSetup(() => {
const local = ref('en')
/** mock i18n `t` function */
const mockT = () => local.value === 'en' ? 'A' : '文'

const { form, status, isError } = useForm({
form: () => ({
age: '',
}),
rule: {
age: [
yupFieldRule(yup.string()
.required(),
),
yupFieldRule(yup.number()
.max(120, () => mockT())
.integer()
.nullable(),
),
],
},
})
return { form, status, isError, local }
})
wr.form.age = 'abc'

await nextTick()
expect(wr.status.age.isError).true
expect(wr.isError).true
expect(wr.status.age.message).includes('must be a `number`')

wr.form.age = '18'

await nextTick()
expect(wr.status.age.isError).false
expect(wr.isError).false
expect(wr.status.age.message).toBe('')

wr.form.age = '18.55'
await nextTick()
expect(wr.status.age.isError).true
expect(wr.status.age.message).includes('integer')

wr.form.age = ''
await nextTick()
expect(wr.status.age.isError).true
expect(wr.status.age.message).includes('required')

wr.form.age = '121'
await nextTick()
expect(wr.status.age.isError).true
expect(wr.status.age.message).toBe('A')

wr.local = 'zh-CN'
await nextTick()
expect(wr.status.age.message).toBe('文')

wr.unmount()
})
31 changes: 31 additions & 0 deletions packages/resolvers/yup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { BaseSchema, ValidationError } from 'yup'
import type { ValidateOptions } from 'yup/lib/types'

export interface ResolverOptions {
model?: 'validateSync' | 'validate'
}

/** yup field rule resolver */
export const yupFieldRule = <SchemaT extends BaseSchema, TContext = {}>(
fieldSchema: SchemaT,
schemaOptions: ValidateOptions<TContext> = {},
) => {
return (val: unknown) => {
try {
fieldSchema.validateSync(
val,
Object.assign({ abortEarly: false }, schemaOptions),
)
return true
}
catch (error: any) {
if (!error?.inner)
throw error
return parseYupError(error)
}
}
}

function parseYupError(error: ValidationError) {
return error.errors[0]
}
Loading

0 comments on commit a0ea9c7

Please sign in to comment.