feat(validator): server-side rendered validation errors (#123)

* feat(validator): server-side rendered validation errors

* test(form): field should render error

* refactor: remove comment

* fix: incorrect imports

* chore: update deps

* chore: update build commands
This commit is contained in:
Ayo Ayco 2022-10-22 04:35:16 +02:00 committed by GitHub
parent 82c121a85d
commit d128747240
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1138 additions and 1129 deletions

View file

@ -3,6 +3,6 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true "source.fixAll": true
}, },
"cSpell.words": ["Astro"], "cSpell.words": ["Astro", "maxlength"],
"prettier.documentSelectors": ["**/*.astro"] "prettier.documentSelectors": ["**/*.astro"]
} }

View file

@ -1,10 +0,0 @@
const form = document.querySelector('form');
form.addEventListener('submit', (e) => {
e.preventDefault();
console.log('build script');
});

View file

@ -1,100 +0,0 @@
---
import Form, {
ControlConfig,
FormGroup,
FormControl,
} from "@astro-reactive/form";
const baseForm = new FormGroup([
{
name: "crust",
label: "Crust",
type: "radio",
value: [{ label: "Garlic", value: "garlic" },]
},
{
name: "size",
label: "Size",
type: "radio",
value: ["Small", "Medium", "Large"]
},
{
name: "sauce",
label: "Sauce",
type: "radio",
value: ["Tomato", "Barbeque"]
}
]);
const toppingsForm = new FormGroup([
{
name: "mushrooms",
label: "Mushrooms",
type: "checkbox"
},
{
name: "extraCheese",
label: "Extra Cheese",
type: "checkbox"
},
{
name: "onions",
label: "Onions",
type: "checkbox"
},
{
name: "peppers",
label: "Peppers",
type: "checkbox"
},
{
name: "pepperoni",
label: "Pepperoni",
type: "checkbox"
},
{
name: "sausage",
label: "Sausage",
type: "checkbox"
},
{
name: "chicken",
label: "Chicken",
type: "checkbox"
},
{
name: "pineapple",
label: "Pineapple",
type: "checkbox"
},
]);
baseForm.name = "Base"
toppingsForm.name = "Toppings";
// const config: ControlConfig = {
// type: "checkbox",
// name: "is-awesome",
// label: "is Awesome?",
// labelPosition: "right",
// };
// insert a control
// form.controls.push(new FormControl(config));
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<h1>Pizza Form Demo</h1>
<Form showValidationHints={true} formGroups={[baseForm, toppingsForm]} />
</body>
</html>

View file

@ -27,7 +27,7 @@ const form = new FormGroup([
name: "rating", name: "rating",
label: "Rating", label: "Rating",
type: "radio", type: "radio",
value: ["1", "2", "3", "4", "5"] value: ["1", "2", "3", "4", "5"],
}, },
{ {
name: "agreement", name: "agreement",
@ -35,9 +35,9 @@ const form = new FormGroup([
type: "radio", type: "radio",
value: [ value: [
{ label: "Agree", value: "yes" }, { label: "Agree", value: "yes" },
{ label: "Disagree", value: "no", labelPosition: "right" } { label: "Disagree", value: "no", labelPosition: "right" },
] ],
} },
]); ]);
form.name = "Simple Form"; form.name = "Simple Form";
@ -58,6 +58,9 @@ const userNameControl = form.get("username");
// set values dynamically // set values dynamically
userNameControl?.setValue("RAMOOOON"); userNameControl?.setValue("RAMOOOON");
form.get("is-awesome")?.setValue("checked"); form.get("is-awesome")?.setValue("checked");
// setting an invalid value will cause errors as server-rendered
form.get("email")?.setValue("invalid-email");
--- ---
<html lang="en"> <html lang="en">

View file

@ -1,9 +1,5 @@
--- ---
import Form, { import Form, { FormGroup } from "@astro-reactive/form";
ControlConfig,
FormGroup,
FormControl,
} from "@astro-reactive/form";
import { Validators } from "@astro-reactive/validator"; import { Validators } from "@astro-reactive/validator";
const baseForm = new FormGroup([ const baseForm = new FormGroup([
@ -11,62 +7,62 @@ const baseForm = new FormGroup([
name: "crust", name: "crust",
label: "Crust", label: "Crust",
type: "radio", type: "radio",
value: [{ label: "Garlic", value: "garlic" },] value: [{ label: "Garlic", value: "garlic" }],
}, },
{ {
name: "size", name: "size",
label: "Size", label: "Size",
type: "radio", type: "radio",
value: ["Small", "Medium", "Large"] value: ["Small", "Medium", "Large"],
}, },
{ {
name: "sauce", name: "sauce",
label: "Sauce", label: "Sauce",
type: "radio", type: "radio",
value: ["Tomato", "Barbeque"] value: ["Tomato", "Barbeque"],
} },
]); ]);
const toppingsForm = new FormGroup([ const toppingsForm = new FormGroup([
{ {
name: "mushrooms", name: "mushrooms",
label: "Mushrooms", label: "Mushrooms",
type: "checkbox" type: "checkbox",
}, },
{ {
name: "extraCheese", name: "extraCheese",
label: "Extra Cheese", label: "Extra Cheese",
type: "checkbox" type: "checkbox",
}, },
{ {
name: "onions", name: "onions",
label: "Onions", label: "Onions",
type: "checkbox" type: "checkbox",
}, },
{ {
name: "peppers", name: "peppers",
label: "Peppers", label: "Peppers",
type: "checkbox" type: "checkbox",
}, },
{ {
name: "pepperoni", name: "pepperoni",
label: "Pepperoni", label: "Pepperoni",
type: "checkbox" type: "checkbox",
}, },
{ {
name: "sausage", name: "sausage",
label: "Sausage", label: "Sausage",
type: "checkbox" type: "checkbox",
}, },
{ {
name: "chicken", name: "chicken",
label: "Chicken", label: "Chicken",
type: "checkbox" type: "checkbox",
}, },
{ {
name: "pineapple", name: "pineapple",
label: "Pineapple", label: "Pineapple",
type: "checkbox" type: "checkbox",
}, },
]); ]);
@ -88,21 +84,9 @@ const infoForm = new FormGroup([
}, },
]); ]);
baseForm.name = "Base" baseForm.name = "Base";
toppingsForm.name = "Toppings"; toppingsForm.name = "Toppings";
infoForm.name = "Customer Info"; infoForm.name = "Customer Info";
// const config: ControlConfig = {
// type: "checkbox",
// name: "is-awesome",
// label: "is Awesome?",
// labelPosition: "right",
// };
// insert a control
// form.controls.push(new FormControl(config));
--- ---
<html lang="en"> <html lang="en">
@ -115,6 +99,9 @@ infoForm.name = "Customer Info";
</head> </head>
<body> <body>
<h1>Pizza Form Demo</h1> <h1>Pizza Form Demo</h1>
<Form showValidationHints={true} formGroups={[baseForm, toppingsForm, infoForm]} /> <Form
showValidationHints={true}
formGroups={[baseForm, toppingsForm, infoForm]}
/>
</body> </body>
</html> </html>

View file

@ -19,7 +19,7 @@
"astro-eleventy-img": "^0.5.0", "astro-eleventy-img": "^0.5.0",
"astro-icon": "^0.7.3", "astro-icon": "^0.7.3",
"prettier": "2.7.1", "prettier": "2.7.1",
"prettier-plugin-astro": "^0.5.5", "prettier-plugin-astro": "^0.6.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"tailwindcss-fluid-type": "^1.3.3" "tailwindcss-fluid-type": "^1.3.3"
}, },

1710
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -37,5 +37,8 @@
"apps/docs", "apps/docs",
"apps/landing-page", "apps/landing-page",
"packages/common" "packages/common"
] ],
"dependencies": {
"prettier-plugin-astro": "^0.6.0"
}
} }

View file

@ -3,63 +3,61 @@
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types
*/ */
export type ControlType = export type ControlType =
| 'text' | "text"
| 'checkbox' | "checkbox"
| 'radio' | "radio"
| 'password' | "password"
| 'button' | "button"
| 'color' | "color"
| 'date' | "date"
| 'datetime-local' | "datetime-local"
| 'email' | "email"
| 'file' | "file"
| 'hidden' | "hidden"
| 'image' | "image"
| 'month' | "month"
| 'number' | "number"
| 'range' | "range"
| 'search' | "search"
| 'submit' | "submit"
| 'tel' | "tel"
| 'time' | "time"
| 'url' | "url"
| 'week'; | "week";
export type ControlConfig = ControlBase | Checkbox | Radio | Submit | Button;
export interface ControlBase { export interface ControlBase {
name: string; name: string;
id?: string; id?: string;
type?: ControlType; type?: ControlType;
value?: string | number | string[]; value?: string | number | string[];
label?: string; label?: string;
labelPosition?: 'right' | 'left'; labelPosition?: "right" | "left";
placeholder?: string; placeholder?: string;
validators?: string[]; // TODO: implement validator type validators?: string[]; // TODO: implement validator type
} }
export interface Checkbox extends ControlBase { export interface Checkbox extends ControlBase {
type: 'checkbox'; type: "checkbox";
checked: boolean; checked: boolean;
} }
export interface Radio extends Omit<ControlBase, 'value'> { export interface Radio extends Omit<ControlBase, "value"> {
type: 'radio'; type: "radio";
value: string[] | RadioOption[]; value: string[] | RadioOption[];
} }
export interface RadioOption extends Omit<ControlBase, 'name'> { export interface RadioOption extends Omit<ControlBase, "name"> {
label: string; label: string;
value: string; value: string;
checked?: boolean; checked?: boolean;
} }
export interface Submit extends ControlBase { export interface Submit extends ControlBase {
type: 'submit'; type: "submit";
callBack?: () => void; callBack?: () => void;
} }
export interface Button extends ControlBase { export interface Button extends ControlBase {
type: 'button'; type: "button";
callBack?: () => void; callBack?: () => void;
} }

View file

@ -1 +1,2 @@
export * from "./control.types"; export * from "./control.types";
export * from "./validator.types";

View file

@ -0,0 +1,9 @@
export type HookType = "onSubmit" | "onControlBlur" | "all";
export type ValidationResult = true | ValidationError;
export type ValidationError = {
error: string;
value: string;
limit?: number;
};

View file

@ -4,16 +4,19 @@ import type { FormControl } from '../core/form-control';
export interface Props { export interface Props {
control: FormControl; control: FormControl;
showValidationHints: boolean; showValidationHints: boolean;
showErrors?: boolean;
showOnlyLabel?: boolean; showOnlyLabel?: boolean;
} }
const { control, showValidationHints, showOnlyLabel = false } = Astro.props; const { control, showValidationHints, showErrors = false, showOnlyLabel = false } = Astro.props;
const { validators = [] } = control; const { validators = [] } = control;
const isRequired = showValidationHints && validators.includes('validator-required'); const isRequired: boolean = showValidationHints && validators.includes('validator-required');
const validatorAttirbutes: Record<string, string> = validators?.reduce( const hasErrors: boolean = showValidationHints && !!control.errors?.length;
const validatorAttributes: Record<string, string> = validators?.reduce(
(prev, validator) => { (prev, validator) => {
const split: string[] = validator.split(':'); const split: string[] = validator.split(':');
const label: string = `data-${split[0]}` || 'invalid'; const label: string = `data-${split[0]}` || 'invalid';
@ -29,7 +32,11 @@ const validatorAttirbutes: Record<string, string> = validators?.reduce(
); );
--- ---
<div class="field" data-validator-hints={showValidationHints.toString()}> <div
class="field"
data-validator-hints={showValidationHints?.toString()}
data-validator-haserrors={hasErrors ? hasErrors?.toString() : null}
>
{ {
control.label && control.labelPosition === 'left' && ( control.label && control.labelPosition === 'left' && (
<label for={control?.id ?? control.name}> <label for={control?.id ?? control.name}>
@ -50,11 +57,23 @@ const validatorAttirbutes: Record<string, string> = validators?.reduce(
placeholder={control.placeholder} placeholder={control.placeholder}
data-label={control.label} data-label={control.label}
data-label-position={control.labelPosition} data-label-position={control.labelPosition}
{...validatorAttirbutes} data-validator-haserrors={hasErrors ? hasErrors?.toString() : null}
class:list={[{ 'has-errors': hasErrors }]}
{...validatorAttributes}
/> />
) )
} }
{
showErrors &&
hasErrors &&
control.errors.map((error) => (
<li>
{error.error} {error.limit}
</li>
))
}
{ {
control.label && control.labelPosition === 'right' && ( control.label && control.labelPosition === 'right' && (
<label for={control?.id ?? control.name}> <label for={control?.id ?? control.name}>
@ -66,8 +85,13 @@ const validatorAttirbutes: Record<string, string> = validators?.reduce(
</div> </div>
<style> <style>
/**
TODO: remove usage of data-validator-haserrors,
class has-errors is sufficient
*/
[data-validator-hints='true'][data-validator-haserrors='true'], [data-validator-hints='true'][data-validator-haserrors='true'],
[data-validator-hints='true'] [data-validator-haserrors='true'] { [data-validator-hints='true'] [data-validator-haserrors='true'],
.has-errors {
color: red; color: red;
border-color: red; border-color: red;
} }

View file

@ -1,4 +1,15 @@
import type { ControlConfig, ControlType, RadioOption } from 'common/types'; import type {
Button,
Checkbox,
ControlBase,
ControlType,
Radio,
RadioOption,
Submit,
ValidationError,
} from 'common/types';
export type ControlConfig = ControlBase | Checkbox | Radio | Submit | Button;
export class FormControl { export class FormControl {
private _name = ''; private _name = '';
@ -11,9 +22,19 @@ export class FormControl {
private _isPristine = true; private _isPristine = true;
private _placeholder?: string; private _placeholder?: string;
private _validators?: string[]; private _validators?: string[];
private _errors: ValidationError[] = [];
constructor(config: ControlConfig) { private validate: (value: string, validators: string[]) => ValidationError[] = (
const { name, id, type, value, label, labelPosition, placeholder, validators } = config; value: string,
validators: string[]
) => {
value;
validators;
return [];
};
constructor(private config: ControlConfig) {
const { name, id, type, value, label, labelPosition, placeholder, validators = [] } = config;
this._name = name; this._name = name;
this._id = id ?? name; this._id = id ?? name;
this._type = type ?? 'text'; this._type = type ?? 'text';
@ -21,7 +42,20 @@ export class FormControl {
this._label = label ?? ''; this._label = label ?? '';
this._labelPosition = labelPosition ?? 'left'; this._labelPosition = labelPosition ?? 'left';
this._placeholder = placeholder ?? ''; this._placeholder = placeholder ?? '';
this._validators = validators ?? []; this._validators = validators;
// dynamic import of the validator package
// if user did not install the validator, then errors should be empty
import('@astro-reactive/validator').then((validator) => {
if (validator) {
this.validate = validator.validate;
const valueStr: string = this._value?.toString() || '';
this._errors = this.validate(valueStr, validators);
} else {
this._errors = [];
}
});
} }
get name() { get name() {
@ -56,10 +90,6 @@ export class FormControl {
return this._isPristine; return this._isPristine;
} }
set isPristine(value: boolean) {
this._isPristine = value;
}
get isValid() { get isValid() {
return this._isValid; return this._isValid;
} }
@ -68,12 +98,21 @@ export class FormControl {
return this._validators; return this._validators;
} }
get errors() {
return this._errors;
}
setValue(value: string) { setValue(value: string) {
this._value = value; this._value = value;
this._isPristine = false; this._isPristine = false;
this._errors = this.validate(this._value, this.config.validators || []);
} }
setIsPristine(value: boolean) { clearErrors() {
this._isPristine = value; this._errors = [];
}
setError(error: ValidationError) {
this._errors = [...(this._errors || []), error];
} }
} }

View file

@ -1,5 +1,4 @@
import type { ControlConfig } from 'common/types'; import { ControlConfig, FormControl } from './form-control';
import { FormControl } from './form-control';
export class FormGroup { export class FormGroup {
controls: FormControl[]; controls: FormControl[];

View file

@ -1,3 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import Form from './Form.astro'; import Form from './Form.astro';
export default Form; export default Form;
export * from './core'; export * from './core';

View file

@ -30,7 +30,7 @@
"format": "prettier -w .", "format": "prettier -w .",
"lint": "eslint . --ext .ts,.js", "lint": "eslint . --ext .ts,.js",
"lint:fix": "eslint --fix . --ext .ts,.js", "lint:fix": "eslint --fix . --ext .ts,.js",
"build": "tsc --noEmit" "build": "astro check && tsc --noEmit"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.3.3", "@types/chai": "^4.3.3",
@ -43,19 +43,17 @@
"astro": "^1.5.0", "astro": "^1.5.0",
"astro-component-tester": "^0.6.0", "astro-component-tester": "^0.6.0",
"chai": "^4.3.6", "chai": "^4.3.6",
"common": "file:packages/common",
"eslint": "^8.23.1", "eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"mocha": "^10.0.0", "mocha": "^10.0.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prettier-plugin-astro": "^0.5.4", "prettier-plugin-astro": "^0.6.0",
"typescript": "^4.8.3" "typescript": "^4.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "^1.5.0" "astro": "^1.5.0"
}, },
"dependencies": {
"common": "file:packages/common"
},
"license": "MIT" "license": "MIT"
} }

View file

@ -72,6 +72,33 @@ describe('Field.astro test', () => {
expect(actualResult).to.contain(expectedLabel); expect(actualResult).to.contain(expectedLabel);
expect(actualResult).to.contain('*'); expect(actualResult).to.contain('*');
}); });
it('Should server-render validation error attributes', async () => {
// arrange
const expectedClass = 'has-errors';
const props = {
control: {
label: 'FAKE LABEL',
name: 'username',
labelPosition: 'left',
validators: ['validator-required'],
errors: [
{
error: 'required',
},
],
value: '',
},
showValidationHints: true,
};
// act
component = await getComponentOutput('./components/Field.astro', props);
const actualResult = cleanString(component.html);
// assert
expect(actualResult).to.contain(expectedClass);
});
}); });
function cleanString(str) { function cleanString(str) {

View file

@ -1,4 +1,3 @@
{ {
"extends": "astro/tsconfigs/strictest", "extends": "astro/tsconfigs/strictest",
"exclude": ["index.ts"]
} }

View file

@ -1,5 +1,5 @@
--- ---
import type { HookType } from './core'; import type { HookType } from 'common/types';
export interface Props { export interface Props {
hook?: HookType; hook?: HookType;
@ -28,8 +28,34 @@ const { hook = 'all', displayErrorMessages = false } = Astro.props;
// const hook: HookType = (document.getElementById('hook') as HTMLInputElement).value as HookType; // const hook: HookType = (document.getElementById('hook') as HTMLInputElement).value as HookType;
const inputs = [...document.querySelectorAll('form input')] as HTMLInputElement[]; const inputs = [...document.querySelectorAll('form input')] as HTMLInputElement[];
inputs?.forEach((input) => { inputs?.forEach((input) => {
input.addEventListener('blur', validate); input.addEventListener('blur', (e: Event) => {
// NOTE: event target attribute names are converted to lowercase
const element = e.target as HTMLInputElement;
const attributeNames = element?.getAttributeNames() || [];
const validators = attributeNames
.filter((attribute) => attribute.includes('data-validator-'))
.map((attribute) => {
const limit = element.getAttribute(attribute);
return `${attribute}:${limit}`;
});
const value = element.value;
const errors = validate(value, validators);
// set element hasErrors
if (errors.length) {
element.parentElement?.setAttribute('data-validator-haserrors', 'true');
element.setAttribute('data-validator-haserrors', 'true');
element.classList.add('has-errors');
// TODO: display error messages
} else {
element.parentElement?.removeAttribute('data-validator-haserrors');
element.removeAttribute('data-validator-haserrors');
element.classList.remove('has-errors');
}
});
input.addEventListener('input', clearErrors); input.addEventListener('input', clearErrors);
}); });
</script> </script>

View file

@ -1,3 +1,2 @@
export * from './validator.functions'; export * from './validator-functions';
export * from './validator.types'; export * from './validator-names';
export * from './validators';

View file

@ -1,28 +1,27 @@
import type { ValidationResult } from './validator.types'; import type { ValidationError } from 'common/types';
import { Validators } from './validators'; import { Validators } from './validator-names';
export function validate(event: FocusEvent) { /**
// NOTE: event target attribute names are converted to lowercase * given the value and validators, `validate()` returns an array of errors
const element = event.target as HTMLInputElement; * @param value - value to be validated
const attributeNames = element?.getAttributeNames() || []; * @param validators - names of validation logic to be applied
const validatorAttirbutes = attributeNames.filter((attribute) => * @returns errors - array of errors `ValidationError`
attribute.includes('data-validator-') */
); export function validate(value: string, validators: string[]): ValidationError[] {
const value = element.value; return validators
const validityArray: ValidationResult[] = validatorAttirbutes
.map((validator) => validator.replace('data-', '')) .map((validator) => validator.replace('data-', ''))
.map((validator): ValidationResult => { .map((attribute): ValidationError | null => {
// insert logic for each validator // TODO: implement dynamic import of function depending on validators
// TODO: implement a map of functions,validator const split = attribute.split(':');
const validator = split[0];
const limitStr = split[1];
const limit = parseInt(limitStr || '0', 10);
if (validator === Validators.min()) { if (validator === Validators.min()) {
const limit = parseInt(element.getAttribute('data-validator-min') || '0', 10);
return validateMin(value, limit); return validateMin(value, limit);
} }
if (validator === Validators.max()) { if (validator === Validators.max()) {
const limit = parseInt(element.getAttribute('data-validator-min') || '0', 10);
return validateMax(value, limit); return validateMax(value, limit);
} }
@ -39,26 +38,16 @@ export function validate(event: FocusEvent) {
} }
if (validator === Validators.minLength()) { if (validator === Validators.minLength()) {
const limit = parseInt(element.getAttribute('data-validator-minlength') || '0', 10);
return validateMinLength(value, limit); return validateMinLength(value, limit);
} }
if (validator === Validators.maxLength()) { if (validator === Validators.maxLength()) {
const limit = parseInt(element.getAttribute('data-validator-maxlength') || '0', 10);
return validateMaxLength(value, limit); return validateMaxLength(value, limit);
} }
return true; return null;
}); })
.filter((result) => result !== null) as ValidationError[];
const errors = validityArray.filter((result) => result !== true);
// set element hasErrors
if (errors.length) {
element.parentElement?.setAttribute('data-validator-haserrors', 'true');
element.setAttribute('data-validator-haserrors', 'true');
// TODO: display error messages
}
} }
export function clearErrors(event: Event) { export function clearErrors(event: Event) {
@ -67,52 +56,62 @@ export function clearErrors(event: Event) {
element.setAttribute('data-validator-haserrors', 'false'); element.setAttribute('data-validator-haserrors', 'false');
} }
function validateMin(value: string, limit: number): ValidationResult { function validateMin(value: string, limit: number): ValidationError | null {
const isValid = parseInt(value, 10) >= limit; const isValid = parseInt(value, 10) >= limit;
if (!isValid) { if (!isValid) {
return { return {
value,
error: 'min', error: 'min',
limit: limit, limit: limit,
}; };
} }
return true;
return null;
} }
function validateMax(value: string, limit: number): ValidationResult { function validateMax(value: string, limit: number): ValidationError | null {
const isValid = parseInt(value, 10) <= limit; const isValid = parseInt(value, 10) <= limit;
if (!isValid) { if (!isValid) {
return { return {
value,
error: 'max', error: 'max',
limit: limit, limit: limit,
}; };
} }
return true;
return null;
} }
function validateRequired(value: string): ValidationResult { function validateRequired(value: string): ValidationError | null {
const isValid = !!value; const isValid = !!value;
if (!isValid) { if (!isValid) {
return { return {
value,
error: 'required', error: 'required',
}; };
} }
return true;
return null;
} }
function validateRequiredChecked(value: string): ValidationResult { function validateRequiredChecked(value: string): ValidationError | null {
const isValid = value === 'checked'; const isValid = value === 'checked';
if (!isValid) { if (!isValid) {
return { return {
value,
error: 'requiredChecked', error: 'requiredChecked',
}; };
} }
return true;
return null;
} }
// TODO: review regexp vulnerability // TODO: review regexp vulnerability
function validateEmail(value: string): ValidationResult { function validateEmail(value: string): ValidationError | null {
const isValid = String(value) const isValid = String(value)
.toLowerCase() .toLowerCase()
.match( .match(
@ -121,32 +120,38 @@ function validateEmail(value: string): ValidationResult {
if (!isValid) { if (!isValid) {
return { return {
value,
error: 'email', error: 'email',
}; };
} }
return true;
return null;
} }
function validateMinLength(value: string, limit: number): ValidationResult { function validateMinLength(value: string, limit: number): ValidationError | null {
const isValid = value.length >= limit; const isValid = value.length >= limit;
if (!isValid) { if (!isValid) {
return { return {
value,
error: 'minLength', error: 'minLength',
limit: limit, limit: limit,
}; };
} }
return true;
return null;
} }
function validateMaxLength(value: string, limit: number): ValidationResult { function validateMaxLength(value: string, limit: number): ValidationError | null {
const isValid = value.length <= limit; const isValid = value.length <= limit;
if (!isValid) { if (!isValid) {
return { return {
value,
error: 'minLength', error: 'minLength',
limit: limit, limit: limit,
}; };
} }
return true;
return null;
} }

View file

@ -1,8 +0,0 @@
export type HookType = 'onSubmit' | 'onControlBlur' | 'all';
export type ValidationResult = true | ValidationError;
export type ValidationError = {
error: string;
limit?: number;
};

View file

@ -1,3 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import Validator from './Validator.astro'; import Validator from './Validator.astro';
export default Validator; export default Validator;
export * from './core'; export * from './core';

View file

@ -27,7 +27,7 @@
"format": "prettier -w .", "format": "prettier -w .",
"lint": "eslint . --ext .ts,.js", "lint": "eslint . --ext .ts,.js",
"lint:fix": "eslint --fix . --ext .ts,.js", "lint:fix": "eslint --fix . --ext .ts,.js",
"build": "tsc --noEmit" "build": "astro check && tsc --noEmit"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.3.3", "@types/chai": "^4.3.3",
@ -40,12 +40,13 @@
"astro": "^1.5.0", "astro": "^1.5.0",
"astro-component-tester": "^0.6.0", "astro-component-tester": "^0.6.0",
"chai": "^4.3.6", "chai": "^4.3.6",
"common": "file:packages/common",
"eslint": "^8.23.1", "eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"mocha": "^10.0.0", "mocha": "^10.0.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prettier-plugin-astro": "^0.5.4", "prettier-plugin-astro": "^0.6.0",
"typescript": "^4.8.3" "typescript": "^4.8.3"
}, },
"peerDependencies": { "peerDependencies": {

View file

@ -1,4 +1,3 @@
{ {
"extends": "astro/tsconfigs/strictest", "extends": "astro/tsconfigs/strictest",
"exclude": ["index.ts"]
} }