-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: basic setup and implementation for validator-cvapi
- Loading branch information
Showing
10 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
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 @@ | ||
# @felte/validator-cvapi |
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,40 @@ | ||
# @felte/validator-cvapi | ||
|
||
[![Bundle size](https://img.shields.io/bundlephobia/min/@felte/validator-cvapi)](https://bundlephobia.com/result?p=@felte/validator-cvapi) | ||
[![NPM Version](https://img.shields.io/npm/v/@felte/validator-cvapi)](https://www.npmjs.com/package/@felte/validator-cvapi) | ||
|
||
A package to help you handle validation with the native validation-api in Felte. | ||
|
||
## Installation | ||
|
||
```sh | ||
npm install --save @felte/validator-cvapi | ||
|
||
# Or, if you use yarn | ||
|
||
yarn add @felte/validator-cvapi | ||
``` | ||
|
||
## Usage | ||
|
||
Extend Felte with the `validator` extender. | ||
|
||
```javascript | ||
import { validator } from '@felte/validator-cvapi'; | ||
|
||
const { form } = createForm({ | ||
// ... | ||
extend: validator(), // or `extend: [validator()],` | ||
// ... | ||
}); | ||
``` | ||
|
||
## Typescript | ||
|
||
For typechecking add the exported type `ValidatorConfig` as a second argument to `createForm` generic. | ||
|
||
```typescript | ||
import type { ValidatorConfig } from '@felte/validator-cvapi'; | ||
|
||
const { form } = createForm<z.infer<typeof schema>, ValidatorConfig>(/* ... */); | ||
``` |
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,11 @@ | ||
module.exports = { | ||
testEnvironment: 'jsdom', | ||
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], | ||
transform: { | ||
'^.+\\.svelte$': 'svelte-jester', | ||
'^.+\\.(js|ts)$': 'ts-jest', | ||
}, | ||
moduleFileExtensions: ['js', 'ts', 'svelte'], | ||
collectCoverageFrom: ['./src/**'], | ||
preset: 'ts-jest', | ||
}; |
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,48 @@ | ||
{ | ||
"name": "@felte/validator-cvapi", | ||
"version": "0.1.0", | ||
"description": "A package to use native validation-api with Felte", | ||
"main": "dist/index.js", | ||
"browser": "dist/index.js", | ||
"module": "dist/index.mjs", | ||
"types": "dist/index.d.ts", | ||
"sideEffects": false, | ||
"author": "Pablo Berganza <pablo@berganza.dev>", | ||
"repository": "github:pablo-abc/felte", | ||
"homepage": "https://github.com/pablo-abc/felte/tree/main/packages/validator-zod", | ||
"keywords": [ | ||
"svelte", | ||
"forms", | ||
"validation", | ||
"felte", | ||
"cvapi" | ||
], | ||
"scripts": { | ||
"prebuild": "rimraf ./dist", | ||
"build": "cross-env NODE_ENV=production rollup -c", | ||
"dev": "rollup -cw", | ||
"prepublishOnly": "pnpm build && pnpm test", | ||
"test": "jest", | ||
"test:ci": "jest --ci --coverage" | ||
}, | ||
"license": "MIT", | ||
"dependencies": { | ||
"@felte/common": "^0.4.9" | ||
}, | ||
"devDependencies": { | ||
"@testing-library/svelte": "^3.0.3", | ||
"felte": "^0.7.11", | ||
"svelte-jester": "^1.3.0" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"exports": { | ||
".": { | ||
"import": "./dist/index.mjs", | ||
"require": "./dist/index.js", | ||
"default": "./dist/index.mjs" | ||
}, | ||
"./package.json": "./package.json" | ||
} | ||
} |
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,41 @@ | ||
import typescript from 'rollup-plugin-ts'; | ||
import commonjs from '@rollup/plugin-commonjs'; | ||
import resolve from '@rollup/plugin-node-resolve'; | ||
import replace from '@rollup/plugin-replace'; | ||
import { terser } from 'rollup-plugin-terser'; | ||
import bundleSize from 'rollup-plugin-bundle-size'; | ||
import pkg from './package.json'; | ||
|
||
const prod = process.env.NODE_ENV === 'production'; | ||
const name = pkg.name | ||
.replace(/^(@\S+\/)?(svelte-)?(\S+)/, '$3') | ||
.replace(/^\w/, (m) => m.toUpperCase()) | ||
.replace(/-\w/g, (m) => m[1].toUpperCase()); | ||
|
||
export default { | ||
input: './src/index.ts', | ||
external: ['zod'], | ||
output: [ | ||
{ | ||
file: pkg.browser, | ||
format: 'umd', | ||
sourcemap: prod, | ||
exports: 'named', | ||
name, | ||
}, | ||
{ file: pkg.module, format: 'esm', sourcemap: prod, exports: 'named' }, | ||
], | ||
plugins: [ | ||
replace({ | ||
'process.env.NODE_ENV': JSON.stringify( | ||
prod ? 'production' : 'development' | ||
), | ||
preventAssignment: true, | ||
}), | ||
resolve({ browser: true }), | ||
commonjs(), | ||
typescript(), | ||
prod && terser(), | ||
prod && bundleSize(), | ||
], | ||
}; |
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,81 @@ | ||
import type { | ||
Errors, | ||
ValidationFunction, | ||
ExtenderHandler, | ||
CurrentForm, | ||
} from '@felte/common'; | ||
import { getPath } from '@felte/common'; | ||
|
||
export type ValidatorConfig = { | ||
controls?: Record<string, (state: ValidityState) => string | string> | ||
defaults?: Record<keyof ValidityState, string>, | ||
}; | ||
|
||
export const validator = (options?: ValidatorConfig) => ( | ||
currentForm: CurrentForm<Record<string, string>> | ||
): ExtenderHandler<Record<string, string>> => { | ||
// Check if the current HTMLFormElement is supplied and if the validator isn't set up yet | ||
if (currentForm.form && currentForm.config.cvapivalidation !== true) { | ||
const cvapiValidatorFn: ValidationFunction<Record<string, string>> = () => { | ||
const cvErrors: Errors<Record<string, string>> = {}; | ||
|
||
if (currentForm.form && currentForm.controls) { | ||
// enable native form-validation | ||
currentForm.form.novalidate = false; | ||
|
||
// iterate over each field | ||
currentForm.controls.forEach((control) => { | ||
// get its path | ||
const path = getPath(control); | ||
|
||
// if the field is invalid | ||
if (control.validity.valid === false) { | ||
// check if there is an error-message for that control in the supplied config | ||
if (options?.controls?.[path]) { | ||
// if yes, check if its a function | ||
if (typeof options.controls[path] === 'function') { | ||
// if yes, call the function and set the error-msg | ||
cvErrors[path] = options.controls[path](control.validity); | ||
} else { | ||
// if not, it has to be a string, set the error-msg | ||
// @ts-expect-error TS is yelling at me besides of the type-guard. Not sure why. Somethings fishy with the type-inference. | ||
cvErrors[path] = options.controls[path]; | ||
} | ||
// if not, check if default error-msgs are supplied. | ||
} else if (options?.defaults) { | ||
// if yes, try to find the first matching supplied msg for an error-category which is set to true in validity-state | ||
cvErrors[path] = Object.keys(control.validity).find(key => options?.defaults?.[key as keyof ValidityState]); | ||
} | ||
|
||
// if no supplied error-msg could be found, fall back to the browser-supplied default | ||
if (!cvErrors[path]) { | ||
cvErrors[path] = control.validationMessage; | ||
} | ||
} else { | ||
// if the field is valid, use the default, browser-supplied msg, which should be empty and reset the error-renderer | ||
cvErrors[path] = control.validationMessage; | ||
} | ||
}); | ||
|
||
// disable native form-validation to suppress the native "error-bubbles". | ||
currentForm.form.novalidate = true; | ||
} | ||
|
||
return cvErrors; | ||
}; | ||
|
||
const validate = currentForm.config.validate; | ||
|
||
if (validate && Array.isArray(validate)) { | ||
currentForm.config.validate = [...validate, cvapiValidatorFn]; | ||
} else if (validate) { | ||
currentForm.config.validate = [validate, cvapiValidatorFn]; | ||
} else { | ||
currentForm.config.validate = [cvapiValidatorFn]; | ||
} | ||
|
||
currentForm.config.cvapivalidation = true; | ||
} | ||
|
||
return {}; | ||
}; |
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,36 @@ | ||
<script> | ||
import { createForm } from 'felte'; | ||
import { validator } from '../src'; | ||
const { form } = createForm({ | ||
extend: validator({ | ||
legal: 'You have to confirm!', | ||
password: (state) => { | ||
if (state.tooShort) { | ||
return 'Yoda once said: Your password strong has to be!'; | ||
} | ||
if (state.valueMissing) { | ||
return 'No entry without password!'; | ||
} | ||
return 'Nice try, buddy!'; | ||
}, | ||
}), | ||
onSubmit: async (values) => {}, | ||
}); | ||
</script> | ||
|
||
<form use:form> | ||
<input type="email" required name="email" data-testid="email" /> | ||
<input | ||
type="password" | ||
required | ||
name="password" | ||
minlength={8} | ||
data-testid="password" | ||
/> | ||
<input type="checkbox" required name="legal" data-testid="legal" /> | ||
<button type="submit" data-testid="submit">Submit</button> | ||
</form> |
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,56 @@ | ||
import { render, fireEvent } from '@testing-library/svelte'; | ||
import { createForm } from 'felte'; | ||
import { validator } from '../src'; | ||
import { get } from 'svelte/store'; | ||
|
||
import Comp from './Form.svelte'; | ||
|
||
describe('Validator cvapi', () => { | ||
test('correctly validates', async () => { | ||
const mockData = { | ||
email: '', | ||
password: '', | ||
}; | ||
const { validate, errors, data } = createForm({ | ||
initialValues: mockData, | ||
onSubmit: jest.fn(), | ||
extend: validator(), | ||
}); | ||
|
||
await validate(); | ||
|
||
expect(get(data)).toEqual(mockData); | ||
expect(get(errors)).toEqual({ | ||
email: null, | ||
password: null, | ||
}); | ||
|
||
data.set({ | ||
email: 'test@email.com', | ||
password: 'test', | ||
}); | ||
|
||
await validate(); | ||
|
||
expect(get(errors)).toEqual({ | ||
email: null, | ||
password: null, | ||
}); | ||
}); | ||
|
||
test('shows proper heading when rendered', async () => { | ||
const { findByRole, getByTestId } = render(Comp); | ||
|
||
const emailInput = getByTestId('email'); | ||
const passwordInput = getByTestId('password'); | ||
const legalInput = getByTestId('legal'); | ||
const submitButton = await findByRole('button'); | ||
|
||
fireEvent.change(emailInput, { target: { value: 'foo@bar.com' } }); | ||
fireEvent.change(passwordInput, { target: { value: 'pass' } }); | ||
fireEvent.change(legalInput, { target: { value: true } }); | ||
|
||
await fireEvent.click(submitButton); | ||
expect(submitButton).toHaveTextContent('Submit'); | ||
}); | ||
}); |
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,16 @@ | ||
{ | ||
"compilerOptions": { | ||
"moduleResolution": "node", | ||
"target": "es2017", | ||
"importsNotUsedAsValues": "error", | ||
"isolatedModules": true, | ||
"strict": true, | ||
"esModuleInterop": true, | ||
"skipLibCheck": true, | ||
"forceConsistentCasingInFileNames": true, | ||
"declaration": true, | ||
"declarationMap": true, | ||
"declarationDir": "./dist" | ||
}, | ||
"include": ["src"] | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.