Skip to content

Commit

Permalink
unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
minasvisual committed Oct 18, 2024
1 parent dc5c7d3 commit decf2ff
Show file tree
Hide file tree
Showing 10 changed files with 439 additions and 67 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
node_modules/
node_modules/
yarn.lock
package-lock.json
.vscode
58 changes: 33 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<head>` tag, import `Config` as module before import `index.js` and add:
```html
....
<script type="module" >
import { Config } from '/src/config.js'
Config.registerValidation('custom', {
// Build error message
message: (params, value, values)
=> `The value is ${value} and the param is ${params[0]} and the form values is ${JSON.stringify(values)}`,
// Validate changed input
handle: ({ value, params }) => {
// value is field value | params is array of validation params: "custom:param1:param2 ...."
return true // true if is valid | false if is invalid
}
})
</script>
<script type="module" src="/src/index.js" defer async></script>
...
```

Use your validation on html:
```
<form-input name="test" type="email" label="Email field" validations="required|nameOfValidation:param1">
</form-input>
<form-input name="test" type="email" label="Email field" validations="required|custom:param1"></form-input>
```


## 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')
Expand All @@ -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 <br/>`
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
<script type="module" >
import { Config } from '/src/config.js'
import { FormCurrency } from '/path/of/file/custominput.js'
Config.registerInput('currency', FormCurrency)
</script>
<script type="module" src="/src/index.js" defer async></script>
...
currency: {
source: FormCurrency
}
```

Use your new input on html:
Expand Down
18 changes: 14 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>replit</title>
<title>Web Components Forms</title>
<script type="module" >
import { Config } from '/src/config.js'
import { FormCurrency } from '/src/inputs/custominput.js'

Config.registerValidation('custom', {
message: (params, value) => `This field must quals ${params[0]}`,
handle: ({ value, params }) => value == params[0]
})
Config.registerInput('currency', FormCurrency)
</script>
<script type="module" src="https://cdn.skypack.dev/twind/shim"></script>
<script type="module" src="/src/index.js" defer></script>
<script type="module" src="/src/index.js" defer async></script>
</head>

<body >
Expand Down Expand Up @@ -37,13 +47,13 @@ <h1 style="color:red">Custom label slot</h1>
</form-input>
<form-input name="test10" type="search" label="Search field" validations="required|minlen:5">
</form-input>
<form-input name="test11" type="color" label="Color field" validations="required">
<form-input name="test11" type="color" label="Color field" validations="required|custom:#ffffff">
</form-input>
<form-input name="test12" type="datetime-local" label="Date time" validations="required|isdate|isafter:2020-01-01|isbefore:2020-12-31">
</form-input>

<form-input name="test13" type="currency" label="Custom field type" validations="required">
</form-input>
</form-input>
<button type="submit" class="border-2 border-black bg-zinc-300 p-2">
Enter
</button>
Expand Down
18 changes: 18 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {}
}
134 changes: 103 additions & 31 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
}
Expand Down Expand Up @@ -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 }
}
}
17 changes: 16 additions & 1 deletion src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading

0 comments on commit decf2ff

Please sign in to comment.