Skip to content
Merged
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
29 changes: 0 additions & 29 deletions .github/workflows/playwright.yml

This file was deleted.

27 changes: 26 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,31 @@ const myConfig = {

---

#### `validateWhenHidden`

A boolean flag to determine if a field should be validated even when it's not visible. This is particularly useful for hidden input fields that should always be included in validation.

**Default Value:** `false`

**Usage**

```js
const myConfig = {
...
fields: {
'hiddenField': {
validateWhenHidden: true,
rules: ['required'],
...
},
...
}
...
}
```

---

#### `normalizer`

This function serves the purpose of adjusting the value in any way before it's validated. This function is destructive so it will change the value of the input. If the function is present, it will be added to the `onKeyDown` event to writeable inputs (`text`, `password`, `email`, etc.), or `onChange` to all other inputs before being validated.
Expand Down Expand Up @@ -977,7 +1002,7 @@ This function adds a validation rule to a specific field. If the field already h
| ----------- | -------- | -------------------------------------------------------- |
| `fieldName` | `string` | _[Required]_ The name of the field. |
| `ruleName` | `string` | _[Required]_ The name of the validation rule. |
| `message` | `string` | _[Required]_ The custom message for the validation rule. |
| `message` | `string` | _[Optional]_ The custom message for the validation rule. |

**Usage**

Expand Down
16 changes: 8 additions & 8 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ export default defineConfig({
use: { ...devices['Desktop Chrome'] },
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },

/* Test against mobile viewports. */
// {
Expand Down
137 changes: 75 additions & 62 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class Validation implements FormValidation {
'onChange',
'onKeyUpAfterChange',
] as Array<Flag>,
submitCallback: this.defaultSubmit,
submitCallback: undefined,
invalidHandler: () => {
/* Do nothing */
},
Expand Down Expand Up @@ -91,8 +91,8 @@ export class Validation implements FormValidation {
Object.keys(rules).forEach((rule) => {
if (typeof rules[rule]?.validator !== 'function')
throw new Error(`${rule} must be a function.`);
if (typeof rules[rule]?.message !== 'string')
throw new Error(`${rule} message must be a string.`);
if (typeof rules[rule]?.message !== 'string' && typeof rules[rule]?.message !== 'function')
throw new Error(`${rule} message must be a string or a function.`);

this.addMethod(rule, rules[rule].validator, rules[rule].message);
});
Expand All @@ -107,6 +107,36 @@ export class Validation implements FormValidation {

/*********************** Private Methods ***********************/

/**
* Clones an object deeply.
* We need this method to clone the configuration object and allow us to use the same configuration object in different instances.
* @param {T} obj - Object to clone.
* @returns {T} - Cloned object.
*/
private cloneDeep<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return obj;
}

if (obj instanceof Node) {
return obj;
}

if (Array.isArray(obj)) {
const copy: any = [];
obj.forEach((elem, index) => {
copy[index] = this.cloneDeep(elem);
});
return copy;
}

const copy: any = {};
Object.keys(obj).forEach((key) => {
copy[key] = this.cloneDeep((obj as any)[key]);
});
return copy;
}

/**
* Normalizes a field's value.
* @param {Event} event - Event object sent by the 'keyup' trigger.
Expand Down Expand Up @@ -195,7 +225,6 @@ export class Validation implements FormValidation {
message: string,
fieldConfig: FieldConfig
) {
this.errors.push([field, message]);
const {
optional,
errorClass,
Expand Down Expand Up @@ -333,6 +362,7 @@ export class Validation implements FormValidation {

if (errorRule) {
const errorMessage = messages[errorRule];
this.errors.push([field, errorMessage as string]);

if (!silently)
this.onError(
Expand All @@ -359,25 +389,31 @@ export class Validation implements FormValidation {
const errors: Array<[ValidatorInput, string]> = [];
const errorSelector: Array<string> = [];

this.fieldsToValidate.filter(this.isFieldVisible).map((field) => {
const error = this.validateField(field, silently);
const hasOnKeyUp = field.validator.hasOnKeyUp;
this.fieldsToValidate
.filter(
(field) =>
this.isFieldVisible(field) ||
this.config.fields[field.name].validateWhenHidden
)
.map((field) => {
const error = this.validateField(field, silently);
const hasOnKeyUp = field.validator.hasOnKeyUp;

if (error) {
errors.push(error);
errorSelector.push(`[name="${field.name}"]`);
}
if (error) {
errors.push(error);
errorSelector.push(`[name="${field.name}"]`);
}

// Add "keyup" event only if field doesn't have it yet.
if (
!hasOnKeyUp &&
this.config.validationFlags.includes('onKeyUpAfterChange') &&
WRITEABLE_INPUTS.includes(field.type)
) {
field.addEventListener('keyup', this.onChange.bind(this));
field.validator.hasOnKeyUp = true;
}
});
// Add "keyup" event only if field doesn't have it yet.
if (
!hasOnKeyUp &&
this.config.validationFlags.includes('onKeyUpAfterChange') &&
WRITEABLE_INPUTS.includes(field.type)
) {
field.addEventListener('keyup', this.onChange.bind(this));
field.validator.hasOnKeyUp = true;
}
});

return { errors, errorSelector };
}
Expand Down Expand Up @@ -411,26 +447,24 @@ export class Validation implements FormValidation {
const data: FormDataObject = {};
const formData = new FormData(this.form);

for (const key of formData.keys()) {
const field = this.form.querySelector(
`[name="${key}"]`
) as ValidatorInput;

if (this.isFieldVisible(field))
data[key] = this.sanitizeInput(
formData.get(key)?.toString().trim() as string
this.fieldsToValidate.forEach((field) => {
const { validateWhenHidden } = this.config.fields[field.name];
if (this.isFieldVisible(field) || validateWhenHidden) {
data[field.name] = this.sanitizeInput(
formData.get(field.name)?.toString().trim() as string
);
}
}
});

this.config.submitCallback(data, this.form);
this.config.submitCallback ? this.config.submitCallback(data, this.form) : this.defaultSubmit();
}

/**
* If no submit callback is provided, this method will be called. It will
* sanitize all inputs and submit the form.
*/
private defaultSubmit() {
this.fieldsToValidate.filter(this.isFieldVisible).map((field) => {
this.fieldsToValidate.filter(field => this.isFieldVisible(field) || this.config.fields[field.name].validateWhenHidden).map((field) => {
field.value = this.sanitizeInput(field.value);
});

Expand Down Expand Up @@ -486,14 +520,14 @@ export class Validation implements FormValidation {
*/
private setupFieldConfig(
fieldName: string,
rules: FieldConfig['rules'],
rules?: FieldConfig['rules'],
messages?: FieldConfig['messages']
) {
const field = this.form.querySelector(
`[name="${fieldName}"]`
) as ValidatorInput;
if (!field) throw new Error(`Field ${fieldName} was not found in the form`);
if (!rules) throw new Error('Rules cannot be empty');
if (!rules) rules = [];
if (!Array.isArray(rules)) throw new Error('Rules must be an array');
if (messages && typeof messages !== 'object')
throw new Error('Messages must be an object');
Expand Down Expand Up @@ -526,7 +560,7 @@ export class Validation implements FormValidation {
hasOnKeyUp: false,
};

const { inputContainer, optional } = this.config.fields[fieldName];
const { inputContainer, optional, validateWhenHidden } = this.config.fields[fieldName];
if (inputContainer && typeof inputContainer === 'string') {
const inputContainerElement = field.closest(
inputContainer
Expand All @@ -547,6 +581,10 @@ export class Validation implements FormValidation {

if (!optional && !rules.includes('required'))
this.addFieldRule(fieldName, 'required');

if ((validateWhenHidden || optional) && !this.fieldsToValidate.includes(field)) {
this.fieldsToValidate.push(field);
}
}

/**
Expand Down Expand Up @@ -581,6 +619,7 @@ export class Validation implements FormValidation {
* @returns True if all fields are valid, false otherwise.
*/
isValid(): boolean {
this.validateAllVisibleFields(true);
return this.errors.length === 0;
}

Expand All @@ -591,7 +630,7 @@ export class Validation implements FormValidation {
*/
validateForm(silently = true): boolean {
this.validateAllVisibleFields(silently);
return this.isValid();
return this.errors.length === 0;
}

/**
Expand Down Expand Up @@ -771,32 +810,6 @@ export class Validation implements FormValidation {
this.config.fields[fieldName] = { ...config };
this.setupFieldConfig(fieldName, config.rules, config.messages);
}

/**
* Clones an object deeply.
* We need this method to clone the configuration object and allow us to use the same configuration object in different instances.
* @param {T} obj - Object to clone.
* @returns {T} - Cloned object.
*/
cloneDeep<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return obj;
}

if (Array.isArray(obj)) {
const copy: any = [];
obj.forEach((elem, index) => {
copy[index] = this.cloneDeep(elem);
});
return copy;
}

const copy: any = {};
Object.keys(obj).forEach((key) => {
copy[key] = this.cloneDeep((obj as any)[key]);
});
return copy;
}
}

export { validatorRules as rules }
1 change: 1 addition & 0 deletions src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const rules: Rules = {
},
message: 'Empty spaces are not allowed',
},
// The name might be a bit confusing, but it's used to validate if the field is empty or contains at least 1 letter
emptyOrLetters: {
validator: function (element: ValidatorInput, value: string) {
return (value !== '' && /[a-z]+/i.test(value)) || value === '';
Expand Down
7 changes: 4 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface FieldConfig {
errorClass?: string;
errorTag?: string;
validClass?: string;
validateWhenHidden?: boolean;
normalizer?: (
value: string,
element?: ValidatorInput,
Expand All @@ -74,10 +75,10 @@ export interface Config {
[key: string]: FieldConfig;
};
validationFlags: Array<Flag>;
submitCallback: (
submitCallback?: ((
formDataObj: { [key: string]: string },
form?: HTMLFormElement
) => void;
) => void);
invalidHandler: (
errors: Array<[ValidatorInput, Message] | boolean>,
form?: HTMLFormElement
Expand All @@ -100,6 +101,6 @@ export interface FormValidation {
rules?: Array<string>,
messages?: PreprocessedMessages
) => void;
addFieldRule: (fieldName: string, ruleName: string, message: Message) => void;
addFieldRule: (fieldName: string, ruleName: string, message?: Message) => void;
removeFieldRule: (fieldName: string, ruleName: string) => void;
}
3 changes: 3 additions & 0 deletions tests/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ test.describe('Form Validation Config Options Tests', () => {
fields: {
name: { rules: ['required'] },
email: { rules: ['required', 'validEmail'] },
password: {
validateWhenHidden: true,
},
},
});

Expand Down
Loading
Loading