Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic implementation of validator-cvapi #48

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@
"tslib": "^2.1.0",
"typedoc": "^0.20.20",
"typescript": "^4.1.3",
"vite": "^2.1.2",
"yup": "^0.32.9",
"zod": "^1.11.13"
},
Expand All @@ -77,7 +76,6 @@
"@solid-reach/rollup-plugin-jsx": "^0.0.2",
"babel-plugin-annotate-pure-calls": "^0.4.0",
"babel-plugin-dev-expression": "^0.2.2",
"package-build-stats": "^7.2.4",
"rollup-plugin-svelte": "^7.1.0",
"svelte-preprocess": "^4.6.9"
}
Expand Down
1 change: 1 addition & 0 deletions packages/validator-cvapi/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @felte/validator-cvapi
40 changes: 40 additions & 0 deletions packages/validator-cvapi/README.md
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>(/* ... */);
```
11 changes: 11 additions & 0 deletions packages/validator-cvapi/jest.config.js
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',
};
48 changes: 48 additions & 0 deletions packages/validator-cvapi/package.json
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 <[email protected]>",
"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"
}
}
41 changes: 41 additions & 0 deletions packages/validator-cvapi/rollup.config.js
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(),
],
};
81 changes: 81 additions & 0 deletions packages/validator-cvapi/src/index.ts
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 {};
};
36 changes: 36 additions & 0 deletions packages/validator-cvapi/tests/Form.svelte
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>
56 changes: 56 additions & 0 deletions packages/validator-cvapi/tests/validator.test.ts
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: '[email protected]',
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: '[email protected]' } });
fireEvent.change(passwordInput, { target: { value: 'pass' } });
fireEvent.change(legalInput, { target: { value: true } });

await fireEvent.click(submitButton);
expect(submitButton).toHaveTextContent('Submit');
});
});
16 changes: 16 additions & 0 deletions packages/validator-cvapi/tsconfig.json
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"]
}
Loading