diff --git a/.eslintrc b/.eslintrc index f8e9646..6d7f6e3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,15 +15,19 @@ "node": true, "es6": true }, - "ignorePatterns": ["dist", "setupTests.ts", "babel.config.js"], + "ignorePatterns": ["dist", "setupTests.ts", "vite.config.ts"], "rules": { - "comma-dangle": "off", + "comma-dangle": ["error", "only-multiline"], "class-methods-use-this": "off", "import/prefer-default-export": "off", "import/no-dynamic-require": "off", "global-require": "off", "quotes": ["error", "single", { "allowTemplateLiterals": true }], "@typescript-eslint/indent": ["error", 4], - "@typescript-eslint/no-non-null-assertion": ["off"] + "@typescript-eslint/no-non-null-assertion": ["off"], + "@typescript-eslint/no-explicit-any": ["off"], + "react/react-in-jsx-scope": "off", + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/ban-ts-comment": "off" } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c180e0..de24492 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,32 +4,42 @@ name: CI on: [push, pull_request] jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [ 20, latest ] + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v2 + with: + version: 8 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: pnpm install + - name: Run tests + run: pnpm test test-coverage: - name: Test on Node.js Latest + name: Test on latest NodeJS runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Use Node.js latest - uses: actions/setup-node@v2.4.1 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v2 with: - node-version: "15" + version: 8 + - name: Use NodeJS + uses: actions/setup-node@v4 + with: + node-version: latest - name: Install dependencies - run: npm install + run: pnpm install - name: Generate coverage report - run: npm run test-coverage + run: pnpm run test-coverage - name: Upload coverage report - uses: codecov/codecov-action@v2.1.0 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV }} - test-node-12: - name: Test on Node.js v12 - runs-on: ubuntu-latest - steps: - - name: Checkout codd - uses: actions/checkout@v2 - - name: Use Node.js v12 - uses: actions/setup-node@v2.4.1 - with: - node-version: "12" - + diff --git a/README.md b/README.md index 126189d..142cc2c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ The smart React element validator [![example workflow name](https://github.com/coderan-io/validator/workflows/CI/badge.svg)](https://github.com/coderan-io/validator/actions?query=workflow%3ACI) [![codecov](https://codecov.io/gh/coderan-io/validator/branch/develop/graph/badge.svg?token=OX5CACK0K0)](https://codecov.io/gh/coderan-io/validator) +> [!WARNING] +> Full V3 docs coming soon! The following docs are updated for V3, but not complete yet. + ### Introduction The goal of this package, is to simplify the struggle of validating elements in React, with a simple system which allows users to add their own rules. @@ -11,163 +14,114 @@ The system communicates directly with the elements in the DOM, and is therefore like [Bootstrap](https://react-bootstrap.github.io/). ### The concept -Validator consists of two main elements, an `Area` and a `Provider`. Areas are a sort of wrappers having elements that -need validation as their children. An area scans the underlying components and elements and indexes validatable elements. +Validator consists of two main elements, an `Area` and a `Field`. Fields are some sort of containers having elements that +need validation as their children. A field scans the underlying components and elements and indexes validatable elements. -Providers on the other hand are wrappers around areas, and allow them to communicate between each other. This communication -is needed in order to match with values in other areas. It can also be used to validate all areas at once, and preventing -actions to happen while not all areas are valid. +Areas on the other hand are containers around fields, and allow them to communicate between each other. This communication +is needed in order to match with values in other fields. It can also be used to validate all areas at once, and preventing +actions to happen while not all areas are valid. There should always be an area defined around the fields in your form. ### How to use -First, start with adding rules to the validator in order to use them. There are some rules pre-made, but more specific -rules you have to create yourself. - -```javascript -import { Validator } from '@coderan/validator'; -import { min } from '@coderan/rules/min'; - -Validator.extend('min', min); -``` -#### Area +#### Field Basic usage: ```jsx -import { ValidatorArea } from '@coderan/validator'; +import { ValidationField } from '@coderan/validator'; +import { required } from '@coderan/validator'; - + - + ``` -When the input is focused and blurred, the `required` rule is called. +When the input is blurred, the `required` rule is called. -Every area needs a name. This name is used to index areas in the provider, and make meaningful error messages. When using -multiple inputs within an area, i.e. when validating a multi-input date of birth, `name` prop is required when defining -the `ValidatorArea` component. Like so: +Every field needs a name. This name is used to index fields in the area, and make meaningful error messages. When using +multiple inputs within an field, i.e. when validating a multi-input date of birth, `name` prop is required when defining +the `ValidationField` component. Like so: ```jsx -import { ValidatorArea } from '@coderan/validator'; +import { ValidationField, min } from '@coderan/validator'; - + - + ``` Showing errors: ```jsx -import { ValidatorArea } from '@coderan/validator'; +import { ValidationField, min } from '@coderan/validator'; - + {({ errors }) => ( <> { errors.length && {errors[0]} } )} - + ``` #### Provider Basic usage: ```jsx -import { ValidatorProvider, ValidatorArea } from '@coderan/validator'; +import { ValidationArea, ValidationField, min } from '@coderan/validator'; - + {({ validate }) => ( <> - + - - + + - + )} - + ``` It is possible to give the validator a `rules` prop as well, whose rules apply to all underlying areas: ```jsx -import { ValidatorProvider, ValidatorArea } from '@coderan/validator'; +import { ValidationArea, ValidationField, required, min } from '@coderan/validator'; - - + + {/* on blur, both required and min rules are applied */} - - -``` - -#### Adding rules - -With access to validator -```javascript -import { Validator } from '@coderan/validator' -Validator.extend('test_types', (validator: Validator) => ({ - passed(): boolean { - return validator.refs(undefined, HTMLInputElement).length === 1 - && validator.refs('test1', HTMLTextAreaElement).length === 1 - && validator.refs('test1').length === 1 - && validator.refs('test1', HTMLProgressElement).length === 0; - }, - message(): string { - return 'test'; - } -})); -``` - -or without -```javascript -import { getValue, isInputElement, isSelectElement } from '@/utils/dom'; - -export default { - passed(elements: HTMLElement[]): boolean { - return elements.every((element: HTMLElement) => { - if (isInputElement(element) || isSelectElement(element)) { - const value = getValue(element); - - return value && value.length; - } - - return true; - }) - }, - - message(): string { - return `{name} is required`; - } -}; + + ``` You can create your own rules, as long as it follows this interface: ```typescript -import { Validator } from '@coderan/validator'; - +import { FieldManager } from '@coderan/validator'; /** * Function to access validator using the rule */ -declare type RuleFunction = (validator: Validator) => RuleObject; +export type RuleFunction = (fieldManager: FieldManager) => RuleObject; /** * Object structure rules must implement */ -declare type RuleObject = { +export type RuleObject = { + name: string; /** * Returns whether the rule passed with the given element(s) */ - passed(elements: HTMLElement[], ...args: string[]): boolean; + passed(elements: HTMLElement[], ...args: string[]): boolean | Promise; /** - * Message shown when the rule doesn't pass + * Message shown when the rule doesn't pass. This returns a tuple with the translation key and the parameters */ - message(): string; + message(): [string, Record?]; } export type Rule = RuleObject | RuleFunction; @@ -175,16 +129,16 @@ export type Rule = RuleObject | RuleFunction; Perhaps you would like to use a different name for the message than the `name`-attribute. That's perfectly fine! ```tsx -import { ValidatorArea } from '@coderan/validator'; +import { ValidationField, required } from '@coderan/validator'; - + {({ errors }) => ( <> { errors.length && {errors[0]} } )} - + ``` and when no value is present in the input, a message like "Surname is required" will appear. diff --git a/__tests__/suits/Area.test.tsx b/__tests__/suits/Area.test.tsx deleted file mode 100644 index 237972a..0000000 --- a/__tests__/suits/Area.test.tsx +++ /dev/null @@ -1,455 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import { - ValidatorArea, - ValidatorAreaProps, - Validator, - ValidatorProvider, - ValidatorProviderProps, - ProviderScope, - required -} from '../../src'; -import tick from '../common/tick'; - -describe('test ValidatorProvider', () => { - beforeEach(() => { - Validator.extend('passes_not', { - passed(): boolean { - return false; - }, - message(): string { - return 'not passed'; - } - }); - Validator.extend('required', required); - - Validator.extend('long_wait', { - async passed(): Promise { - return new Promise((resolve: (value: boolean) => void): void => { - setTimeout(() => { - resolve(true); - }, 100); - }) - }, - message(): string { - return 'test'; - } - }); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - - it('should render input', () => { - const area = mount( - - - - ); - - expect(area.find('input')).toBeDefined(); - }); - - it('should render inputs with callback as child', () => { - const area = mount( - - {() => ( - - )} - - ); - - expect(area.find('input')).toBeDefined(); - }) - - it('should throw an exception when no name provided', () => { - const area = () => { - mount( - - {() => ( - <> - - - - )} - - ); - } - expect(() => area()).toThrow(); - }); - - it('should index (nested) inputs', () => { - const area = mount( - - {() => ( - <> - <> -
- - - <> -
- - )} -
- ); - - expect(area.instance().getInputRefs().length).toBe(4); - }); - - it('should apply rules on blur', async () => { - const area = mount( - - - - ); - - area.find('input').at(0).simulate('blur'); - await tick(); - expect(area.state().errors[0]).toBe('Not passed'); - }); - - it('should not apply rules on blur when non-blurrable element', () => { - const area = mount( - - - - ); - - area.find('canvas').at(0).simulate('blur'); - expect(area.state().errors.length).toBe(0); - }); - - it('should render error when area dirty', async () => { - const area = mount( - - {({errors}) => { - return ( - <> - - {!!errors.length &&
{errors[0]}
} - - ); - }} -
- ); - - area.find('input').simulate('blur'); - await tick(); - area.update(); - expect(area.find('div').text()).toBe('Not passed'); - }) - - it('should call element\'s provided blur along validator blur', () => { - const mockFn = jest.fn(); - - const area = mount( - - - - ); - - area.find('input').simulate('blur'); - expect(mockFn).toBeCalled(); - }); - - it('should call element\'s provided onChange along validator onChange', () => { - const mockFn = jest.fn(); - - const area = mount( - - - - ); - - area.find('input').simulate('change'); - expect(mockFn).toBeCalled(); - }); - - it('should get all input refs from the provider', async () => { - Validator.extend('test_all', (validator: Validator) => ({ - passed(): boolean { - return validator.refs().length === 2; - }, - message(): string { - return ''; - } - })) - const mockFn = jest.fn(); - - const provider = mount( - - {({validate}: ProviderScope) => ( - <> - - - - - - -