diff --git a/.gitignore b/.gitignore index 40b878d..ec26ac4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -node_modules/ \ No newline at end of file +node_modules/ +yarn.lock +package-lock.json +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 9c2cb0f..3553ffa 100644 --- a/README.md +++ b/README.md @@ -72,30 +72,37 @@ Usage ## Custom validations -Open `src/config.js` and add: -``` -export const validations = { - ... - nameOfValidation: { - message: (params, value, values) => `The value is ${value} and the param is ${params[0]} and the form values is ${JSON.stringify(values)}`, - handle: ({ value, params }) => { - // value is field value | params is array of validation params: "nameOfValidation:param1:param2 ...." - return true // true if is valid | false if is invalid - } - } +Inside `` tag, import `Config` as module before import `index.js` and add: +```html + .... + + + ... ``` Use your validation on html: ``` - - + ``` ## Custom field types -- First: Create a class that implements the template and store `formitem` public variable of form element to receive event handlers +- First: Create a class that implements the template and store `formitem` as public variable of form element to receive event handlers - Second: Create a method to trigger error message on the template -```` +```js export class FormCurrency { constructor({ el, shadow, internals }) { this.label = el.getAttribute('label') @@ -113,24 +120,25 @@ export class FormCurrency { ` // REQUIRED - this.formitem = shadow.querySelector('input'); + this.formitem = shadow.querySelector('input'); } setError(error) { // false or `string of errors separatelly of
` - console.log('Do anything with form error', error) + console.log('Do anything with error message', error) } } -```` -Next, open `src/config.js`, import your class and add on inputs list your new input type: ``` -import { FormCurrency } from 'path/of/file/FormCurrency.js' -... -export const inputs = { +Next, import `Config` store and import your class. register your new input type: +```html + + ... - currency: { - source: FormCurrency - } ``` Use your new input on html: diff --git a/index.html b/index.html index e2b7b5e..f33d2e2 100644 --- a/index.html +++ b/index.html @@ -4,9 +4,19 @@ - replit + Web Components Forms + - + @@ -37,13 +47,13 @@

Custom label slot

- + - + diff --git a/package.json b/package.json new file mode 100644 index 0000000..7df02cc --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "wc-forms", + "version": "1.0.0", + "author": "Ulisses Mantovani", + "description": "Is a modular, 100% vanilla js, form inputs group, based on [Vue FormKit](https://formkit.com/getting-started/what-is-formkit) library.", + "repository": "https://github.com/minasvisual/wc-forms.git", + "license": "ISC", + "main": "src/index.js", + "type": "module", + "scripts": { + "test": "vitest" + }, + "keywords": [], + "devDependencies": { + "vitest": "^2.1.3" + }, + "dependencies": {} +} diff --git a/src/config.js b/src/config.js index a4f262b..1c5e982 100644 --- a/src/config.js +++ b/src/config.js @@ -2,97 +2,153 @@ import { FormText } from './inputs/forminput.js' import { FormSelect } from './inputs/formselect.js' import { FormTextarea, } from './inputs/formtext.js' import { FormChecksBox, FormChecksRadio } from './inputs/formchecks.js' -import { splitValues, dateRegex, emailRegex } from './helpers.js' -import { FormCurrency } from './inputs/custominput.js' +import { splitValues, get, dateRegex, emailRegex, isValidNumber } from './helpers.js' +/** + * Validations object + * @namespace validations + * @prop {Object} required - required validation + * @prop {Object} email - email validation + * @prop {Object} isdate - check if is a date + * @prop {Object} isafter - check if is after a date + * @prop {Object} isbefore - check if is before a date + * @prop {Object} isnumber - check if is a number + * @prop {Object} minlen - check if is greater than a min length + * @prop {Object} maxlen - check if is lesser than a max length + * @prop {Object} confirm - check if is equal to other field + * @prop {Object} in - check if is a value in an array + * + * @example + * { + * required: { + * message: () => 'This field is required', + * handle: ({ value, params }) => { + * return ![null, undefined, NaN, ''].includes(value) + * } + * }, + * email: { + * message: () => 'This should be an valid email', + * handle: ({ value, params }) => { + * return emailRegex.test(value) + * } + * } + * } + */ export const validations = { + required: { message: () => 'This field is required', - handle: ({ value, params }) => { - return value && value.length > 0 + /** + * @function handle + * @description Check if the value is not empty (not null, undefined, NaN, or empty string) + * @param {Object} - object with the value to check and the parameters of the validation + * @param {any} value - the value to check + * @param {string[]} params - the parameters of the validation + * @returns {boolean} - true if the value is not empty, false otherwise + */ + handle: ({ value }) => { + return ![null, undefined, NaN, ''].includes(value) } }, email: { - message: () => 'This should be an valid email', - handle: ({ value, params }) => { + message: () => 'This should be an valid email', + handle: ({ value }) => { return value && typeof value === 'string' && emailRegex.test(value) } }, minlen: { - message: (params, value, values) => `This field must be at least ${params[0]} characters`, + message: (params) => `This field must be at least ${get(params, '[0]', 1)} characters`, handle: ({ value, params }) => { + if(!get(params, '[0]')) throw new Error('Parameter 1 not found') return value && (value.length >= parseInt(params[0] || 1)) } }, maxlen: { - message: (params, value, values) => `This field must be maximum ${params[0]} characters`, - handle: ({ value, params }) => { - return value && (value.length <= parseInt(params[0] || 255)) + message: (params, value, values) => `This field must be maximum ${get(params, '[0]', 255)} characters`, + handle: ({ value, params }) => { + return value && ( value.length <= parseInt(get(params, '[0]', '255')) ) } }, confirm: { - message: (params, value, values) => `This field must be equal to ${params[0]} field`, - handle: ({ value, params, values }) => { - return value && params[0] && (value === values[params[0]]) + message: (params) => `This field must be equal to ${get(params, '[0]')} field`, + handle: ({ value, params = [], values }) => { + if(!get(params, '[0]')) throw new Error('Parameter to match not found') + return value && get(params, '[0]') && (value === values[get(params, '[0]')]) } }, isdate: { message: () => 'This field must be a valid date', - handle: ({ value, params }) => { + handle: ({ value }) => { return value && typeof value === 'string' && dateRegex.test(value) } }, isafter: { - message: (params, value, values) => `This field must be after ${params[0]}`, - handle: ({ value, params, values }) => { + message: (params = [], value, values) => `This field must be after ${get(params, '[0]')}`, + handle: ({ value, params = [], values }) => { + if(!get(params, '[0]')) throw new Error('Parameter 1 not found') return value && typeof value === 'string' && dateRegex.test(value) && new Date(value) > new Date(params[0]) } }, isbefore: { - message: (params, value, values) => `This field must be before ${params[0]}`, - handle: ({ value, params, values }) => { + message: (params = [], value, values) => `This field must be before ${get(params, '[0]')}`, + handle: ({ value, params = [], values }) => { + if(!get(params, '[0]')) throw new Error('Parameter 1 not found') return value && typeof value === 'string' && dateRegex.test(value) && new Date(value) < new Date(params[0]) } }, isnumber: { message: () => 'This field must be a valid number', handle: ({ value, params }) => { - return value && typeof Number(value) === 'number' + console.log(value, Number.isNaN(value)) + return value && typeof Number(value) === 'number' && !Number.isNaN(Number(value)) } }, startwith: { message: (params, value) => `This field must start with ${value}`, handle: ({ value, params }) => { - return value && value.startsWith(params[0]) + if(!get(params, '[0]')) throw new Error('Parameter 1 not found') + return value && value.toLowerCase().startsWith(params[0].toLowerCase()) } }, endswith: { - message: (params, value) => `This field must ends with ${value}`, + message: (params = [], value) => `This field must ends with ${value}`, handle: ({ value, params }) => { - return value && value.endsWith(params[0]) + return value && value.toLowerCase().endsWith(params[0].toLowerCase()) } }, in: { - message: (params, value) => `This field must contains ${params.join(',')}`, + message: (params = [], value) => `This field must contains ${params?.join(',')}`, handle: ({ value, params }) => { - return value && (splitValues(params).includes(value) || splitValues(value).includes(params)) + if(params.length === 0) throw new Error('Parameters is required') + if(value && value.includes(',')) + return splitValues(value).some(v => splitValues(params).includes(v)) + else + return value && splitValues(params).includes(value) } }, notin: { - message: (params, value) => `This field must not contains ${params.join(',')}`, + message: (params = [], value) => `This field must not contains ${params.join(',')}`, handle: ({ value, params }) => { - return value && !params.includes(value) + if(params.length === 0) throw new Error('Parameters is required') + if(value && value.includes(',')) + return !splitValues(value).some(v => splitValues(params).includes(v)) + else + return value && !splitValues(params).includes(value) } }, max: { - message: (params, value) => `This field must be less than ${params[0]}`, + message: (params = [], value) => `This field must be less than ${get(params, '[0]')}`, handle: ({ value, params }) => { + if( !get(params, '[0]') || !isValidNumber(value) ) throw new Error('Parameter 1 not found') + if( !isValidNumber(value) ) throw new Error('Value must be a number') return value && Number(value) <= Number(params[0]) } }, min: { - message: (params, value) => `This field must be greater than ${params[0]}`, + message: (params = [], value) => `This field must be greater than ${get(params, '[0]')}`, handle: ({ value, params }) => { + if( !get(params, '[0]') || !isValidNumber(value) ) throw new Error('Parameter 1 not found') + if( !isValidNumber(value) ) throw new Error('Value must be a number') return value && Number(value) >= Number(params[0]) } } @@ -137,8 +193,24 @@ export const inputs = { }, textarea: { source: FormTextarea, - }, - currency: { - source: FormCurrency, + }, +} + +export const Config = { + basePath: '/src', + validations, + inputs, + registerValidation(name, rule) { + if(!name) throw new Error('Name is required') + if(!rule || typeof rule !== 'object') throw new Error('Rule is required as object') + if(!rule.message || typeof rule.message !== 'function') throw new Error('Rule message must be a function') + if(!rule.handle || typeof rule.handle !== 'function') throw new Error('Rule handle must be a function') + this.validations[name] = rule + }, + registerInput(name, classObj) { + console.log(name, typeof classObj) + if(!name) throw new Error('Name is required') + if(!classObj || typeof classObj !== 'function') throw new Error('Rule is required as object') + this.inputs[name] = { source: classObj } } } \ No newline at end of file diff --git a/src/helpers.js b/src/helpers.js index 09d17d5..8ebd59a 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -28,6 +28,21 @@ export function splitValues(value) { if (value.includes(',')) return value.split(',') } -export const dateRegex = /^[1-2]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])(T([01]\d|2[0-3]):[0-5]\d)?$/; +export function get (obj, path, defaultValue = undefined) { + const travel = regexp => + String.prototype.split + .call(path, regexp) + .filter(Boolean) + .reduce((res, key) => (res !== null && res !== undefined ? res[key] : res), obj); + const result = travel(/[,[\]]+?/) || travel(/[,[\].]+?/); + return result === undefined || result === obj ? defaultValue : result; +} + + +export function isValidNumber(value) { + return typeof Number(value) === 'number' && !Number.isNaN(Number(value)) +} + +export const dateRegex = /^[1-2]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])(T([01]\d|2[0-3]):[0-5]\d(:[01]\d)?)?$/; export const emailRegex = /^([a-z]){1,}([a-z0-9._-]){1,}([@]){1}([a-z]){2,}([.]){1}([a-z]){2,}([.]?){1}([a-z]?){2,}$/i; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 6124ba7..fafc516 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ import { renderAttributes, extractValidations } from './helpers.js' import { Validate } from './validation.js' -import { inputs } from './config.js' +import { Config } from './config.js' class FormComponent extends HTMLElement { static formAssociated = true; @@ -12,7 +12,8 @@ class FormComponent extends HTMLElement { connectedCallback() { this.itype = this.getAttribute('type') - // todo: create adapters + // todo: create adapters + const inputs = Config.inputs const InputSource = inputs[this.itype] ?? inputs.text if (!InputSource) throw new Error(`Input type not found: ${this.itype}`) this.instance = new InputSource.source({ @@ -24,7 +25,6 @@ class FormComponent extends HTMLElement { this.shadowRoot.appendChild(this.addStyles()) this.formitem = this.instance.formitem this.formitem.addEventListener('change', (e) => { - console.log('check changed', e.target.value) this.internals.setFormValue(this.formitem.value) this.emitEvent('change', this.formitem.value) this.validate() @@ -32,6 +32,9 @@ class FormComponent extends HTMLElement { if (this.instance?.onMounted) this.instance?.onMounted() + + if (this.instance?.onDestroy) + this.instance?.onDestroy() } disconnectedCallback() { @@ -43,7 +46,9 @@ class FormComponent extends HTMLElement { type: 'text/css', }) style.appendChild( - document.createTextNode(`@import "/src/style.css";`) + document.createTextNode( + `@import "${Config.basePath}/style.css";` + ) ); return style } diff --git a/src/validation.js b/src/validation.js index ae69634..ddde2c3 100644 --- a/src/validation.js +++ b/src/validation.js @@ -1,4 +1,4 @@ -import { validations } from './config.js' +import { Config } from './config.js' export class Validate { constructor(ruleString) { this.rule = '' @@ -24,6 +24,7 @@ export class Validate { } getSourceRule() { + let validations = Config.validations if (!validations[this.rule]) throw new Error(`Rule not found: ${this.rule}`) return validations[this.rule] diff --git a/tests/helpers.test.js b/tests/helpers.test.js new file mode 100644 index 0000000..db69a11 --- /dev/null +++ b/tests/helpers.test.js @@ -0,0 +1,14 @@ + +import { test, expect, describe } from 'vitest' +import { dateRegex } from '../src/helpers.js' + +describe('Helpers test - dateRegex', () => { + test('should return a dateRegex function', async () => { + const sut = dateRegex + expect(dateRegex.test('2024-01-01')).toBe(true) + expect(dateRegex.test('2024-01-01T12:00')).toBe(true) + expect(dateRegex.test('2024-01-01T12:00:00')).toBe(true) + expect(dateRegex.test('2024-01-01T12:00:00.000Z')).toBe(false) + }) + +}) diff --git a/tests/validations.test.js b/tests/validations.test.js new file mode 100644 index 0000000..6c88621 --- /dev/null +++ b/tests/validations.test.js @@ -0,0 +1,226 @@ + +import { test, expect, describe } from 'vitest' +import { Config } from '../src/config.js' + +describe('Config validations - Required', () => { + test('should return a validation function', async () => { + const sut = Config.validations.required.handle + const message = Config.validations.required.message + expect(typeof sut).toBe('function') + expect(sut({ value: 'value' })).toBe(true) + expect(sut({ value: '' })).toBe(false) + expect(message({})).toBe('This field is required') + }) + +}) + +describe('Config validations - Email', () => { + test('should test validation email', async () => { + const sut = Config.validations.email.handle + const message = Config.validations.email.message + expect(typeof sut).toBe('function') + expect(sut({ value: 'test@domain.com' })).toBe(true) + expect(sut({ value: 'test@subdomain.domain.com' })).toBe(true) + expect(sut({ value: 'test@domain.com.br' })).toBe(true) + expect(sut({ value: 'test@subdomain.subdomain.domain.com' })).toBe(false) + expect(sut({ value: 'test' })).toBe(false) + expect(message({})).toBe('This should be an valid email') + }) + +}) + +describe('Config validations - Min Length', () => { + test('should test max length validation', async () => { + const sut = Config.validations.minlen.handle + const message = Config.validations.minlen.message + expect(typeof sut).toBe('function') + expect(sut({ value: '12345', params: ['5'] })).toBe(true) + expect(sut({ value: '12345', params: ['6'] })).toBe(false) + expect(message([])).toBe( `This field must be at least 1 characters`) + expect(message(['5'])).toBe( `This field must be at least 5 characters`) + }) +}) + +describe('Config validations - Max Length', () => { + test('should test max length validation', async () => { + const sut = Config.validations.maxlen.handle + const message = Config.validations.maxlen.message + expect(typeof sut).toBe('function') + expect(sut({ value: '12345', params: ['5'] })).toBe(true) + expect(sut({ value: '12345', params: ['4'] })).toBe(false) + expect(message([])).toBe( `This field must be maximum 255 characters`) + expect(message(['5'])).toBe( `This field must be maximum 5 characters`) + }) +}) + +describe('Config validations - Confirm', () => { + test('should test confirm validation', async () => { + const sut = Config.validations.confirm.handle + const message = Config.validations.confirm.message + expect(typeof sut).toBe('function') + expect(sut({ value: '123mudar', params: ['password2'], values: { password2: '123mudar' } })).toBe(true) + expect(sut({ value: '12345', params: ['4'], values: { password2: '123MUDAR'} })).toBe(false) + expect(message([])).toBe( `This field must be equal to undefined field`) + expect(message(['password2'])).toBe( `This field must be equal to password2 field`) + }) +}) + +describe('Config validations - Is date', () => { + test('should test isdate validation', async () => { + const sut = Config.validations.isdate.handle + const message = Config.validations.isdate.message + expect(typeof sut).toBe('function') + expect(sut({ value: '2024-01-01' })).toBe(true) + expect(sut({ value: '2024-01-01T12:00' })).toBe(true) + expect(sut({ value: '2024-01-01T12:00:00' })).toBe(true) + expect(sut({ value: '12345abc' })).toBe(false) + expect(sut({ value: 'abc' })).toBe(false) + expect(message({})).toBe('This field must be a valid date') + }) + +}) + +describe('Config validations - Is After', () => { + test('should test isafter validation', async () => { + const sut = Config.validations.isafter.handle + const message = Config.validations.isafter.message + expect(typeof sut).toBe('function') + expect(sut({ value: '2020-01-01', params: ['2020-01-02'] })).toBe(false) + expect(sut({ value: '2020-01-02', params: ['2020-01-02'] })).toBe(false) + expect(sut({ value: '2020-01-03', params: ['2020-01-02'] })).toBe(true) + expect(message([])).toBe( `This field must be after undefined`) + expect(message(['2020-01-01'])).toBe( `This field must be after 2020-01-01`) + }) +}) + +describe('Config validations - Is Before', () => { + test('should test isbefore validation', async () => { + const sut = Config.validations.isbefore.handle + const message = Config.validations.isbefore.message + expect(typeof sut).toBe('function') + expect(sut({ value: '2020-01-01', params: ['2020-01-02'] })).toBe(true) + expect(sut({ value: '2020-01-02', params: ['2020-01-02'] })).toBe(false) + expect(message([])).toBe( `This field must be before undefined`) + expect(message(['2020-01-01'])).toBe( `This field must be before 2020-01-01`) + }) +}) + +describe('Config validations - Is Number', () => { + test('should test isnumber validation', async () => { + const sut = Config.validations.isnumber.handle + const message = Config.validations.isnumber.message + expect(typeof sut).toBe('function') + expect(sut({ value: '12345' })).toBe(true) + expect(sut({ value: '12345.12' })).toBe(true) + expect(sut({ value: '12345abc' })).toBe(false) + expect(sut({ value: 'abc' })).toBe(false) + expect(message({})).toBe('This field must be a valid number') + }) + +}) + +describe('Config validations - Starts With', () => { + test('should test startswith validation', async () => { + const sut = Config.validations.startwith.handle + const message = Config.validations.startwith.message + expect(typeof sut).toBe('function') + expect(sut({ value: 'hello world', params: ['hello'] })).toBe(true) + expect(sut({ value: 'Hello world', params: ['hello'] })).toBe(true) + expect(sut({ value: 'world hello', params: ['hello'] })).toBe(false) + expect(message([])).toBe(`This field must start with undefined`) + expect(message([], 'hello')).toBe(`This field must start with hello`) + }) +}) + +describe('Config validations - Ends With', () => { + test('should test endswith validation', async () => { + const sut = Config.validations.endswith.handle + const message = Config.validations.endswith.message + expect(typeof sut).toBe('function') + expect(sut({ value: 'hello world', params: ['world'] })).toBe(true) + expect(sut({ value: 'Hello World', params: ['world'] })).toBe(true) + expect(sut({ value: 'world hello', params: ['world'] })).toBe(false) + expect(message([])).toBe(`This field must ends with undefined`) + expect(message([], 'hello')).toBe(`This field must ends with hello`) + }) +}) + +describe('Config validations - IN', () => { + test('should test In validation', async () => { + const sut = Config.validations.in.handle + const message = Config.validations.in.message + expect(typeof sut).toBe('function') + expect(sut({ value: 'apple', params: ['apple','banana','orange'] })).toBe(true) + expect(sut({ value: 'apple,banana', params: ['apple','banana','orange'] })).toBe(true) + expect(sut({ value: 'watermelon,apple', params: ['apple','banana','orange'] })).toBe(true) + expect(sut({ value: 'watermelon', params: ['apple','banana','orange'] })).toBe(false) + expect(sut({ value: 'watermelon,lemon', params: ['apple','banana','orange'] })).toBe(false) + expect(message([])).toBe(`This field must contains `) + expect(message(['hello'])).toBe(`This field must contains hello`) + expect(message(['lemon','watermelon'])).toBe(`This field must contains lemon,watermelon`) + }) +}) + +describe('Config validations - NotIn', () => { + test('should test NotIn validation', async () => { + const sut = Config.validations.notin.handle + const message = Config.validations.notin.message + expect(typeof sut).toBe('function') + expect(sut({ value: 'lemon', params: ['apple','banana','orange'] })).toBe(true) + expect(sut({ value: 'lemon,watermelon', params: ['apple','banana','orange'] })).toBe(true) + expect(sut({ value: 'apple', params: ['apple','banana','orange'] })).toBe(false) + expect(sut({ value: 'apple,orange', params: ['apple','banana','orange'] })).toBe(false) + expect(message([])).toBe(`This field must not contains `) + expect(message(['lemon','watermelon'])).toBe(`This field must not contains lemon,watermelon`) + }) +}) + +describe('Config validations - Max and Min', () => { + test('should test max validation', async () => { + const sut = Config.validations.max.handle + const message = Config.validations.max.message + expect(typeof sut).toBe('function') + expect(sut({ value: '5', params: ['10'] })).toBe(true) + expect(sut({ value: '15', params: ['10'] })).toBe(false) + expect(message([])).toBe('This field must be less than undefined') + expect(message(['10'])).toBe('This field must be less than 10') + }) + + test('should test min validation', async () => { + const sut = Config.validations.min.handle + const message = Config.validations.min.message + expect(typeof sut).toBe('function') + expect(sut({ value: '15', params: ['10'] })).toBe(true) + expect(sut({ value: '5', params: ['10'] })).toBe(false) + expect(message([])).toBe('This field must be greater than undefined') + expect(message(['10'])).toBe('This field must be greater than 10') + }) +}) + +// describe('registerValidation test', () => { +// it('should register new validation', () => { +// const name = 'test' +// const rule = { +// message: (params, value, values) => `This field must quals ${value}`, +// handle: ({ value, params }) => value == params[0] +// } +// Config.registerValidation(name, rule) +// expect(Config.validations[name]).toEqual(rule) +// }) + +// it('should throw an error if no name is provided', () => { +// expect(() => Config.registerValidation(undefined, {})).toThrowError('Name is required') +// }) + +// it('should throw an error if no rule is provided', () => { +// expect(() => Config.registerValidation('name', undefined)).toThrowError('Rule is required as object') +// }) + +// it('should throw an error if rule message is not a function', () => { +// expect(() => Config.registerValidation('name', { message: 'message' })).toThrowError('Rule message must be a function') +// }) + +// it('should throw an error if rule handle is not a function', () => { +// expect(() => Config.registerValidation('name', { message: () => 'message', handle: 'handle' })).toThrowError('Rule handle must be a function') +// }) +// }) \ No newline at end of file