Skip to content

Commit

Permalink
feat: basic setup and implementation for validator-cvapi
Browse files Browse the repository at this point in the history
  • Loading branch information
benbender committed Aug 2, 2021
1 parent 8ff2d89 commit 196af46
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 0 deletions.
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 <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"
}
}
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: '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');
});
});
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"]
}
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 196af46

Please sign in to comment.