feat: create astro reactive validator (#90)

* feat: initial validator component

* chore: fix eslint for validator

* chore: update package info for validator

* chore: remove vscode settings for docs

* chore: put docs and demo into apps

* chore: move package scope @astro-reactive

* test: update tests for validator

* feat: validator functions, hooks

* feat: validator sets haserrors attribute

* feat: use data-validator attributes

* feat: showValidationHints

* feature: add logic for all validators

* refactor: remove Validator component usage

* docs(validator): initial readme

* chore: comment out unsupported validator

* docs(validator): update installation

* chore: package docs and publish

* chore: update deps

* docs: update npm info on docs

* docs(validator): update docs for validator

* fix(form): handle undefined form

* test(validator): update tests

* chore: organize files; update deps

* chore: fix build scripts
This commit is contained in:
Ayo Ayco 2022-10-15 16:32:02 +02:00 committed by GitHub
parent e5d4e90805
commit 4dc020027f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 1615 additions and 1090 deletions

View file

@ -28,7 +28,7 @@ Currently, it is made up of [NPM workspaces](https://docs.npmjs.com/cli/v7/using
Packages:
1. [demo](https://github.com/ayoayco/astro-reactive-library/tree/main/demo#readme) - found in the directory `demo`
- the demo Astro app that we use to test and demonstrate the library features
2. [astro-reactive-form](https://github.com/ayoayco/astro-reactive-library/tree/main/packages/astro-reactive-form#readme) - found in the directory `packages/astro-reactive-form`
2. [form](https://github.com/ayoayco/astro-reactive-library/tree/main/packages/form#readme) - found in the directory `packages/form`
- allows developers to programatically build a Form for Astro
3. [astro-reactive-validator](https://github.com/ayoayco/astro-reactive-library/tree/main/packages/astro-reactive-validator) - found in the directory `packages/astro-reactive-validator`
@ -98,7 +98,7 @@ npm run lint:fix
As mentioned above, this project involves packages that are intened to be used in Astro applications. The demo app is our way to test and play with the packages.
If you plan to add features or fix bugs that are found in the packages, such as `astro-reactive-form`, you can find the source code for this inside the `packages` directory.
If you plan to add features or fix bugs that are found in the packages, such as `@astro-reactive/form`, you can find the source code for this inside the `packages` directory.
Thank you again for your interest in contributing!

View file

@ -17,8 +17,8 @@
| Packages | Version | Docs | Description |
| --- | --- | --- | --- |
| [astro-reactive-form](https://github.com/ayoayco/astro-reactive-library/blob/main/packages/astro-reactive-form/README.md) | [![npm](https://img.shields.io/npm/v/astro-reactive-form)](https://github.com/ayoayco/astro-reactive-library/blob/main/packages/astro-reactive-form/RELEASE.md) | 🛠 | generate a dynamic form which can be modified programatically |
| astro-reactive-validator | 🛠 | 🛠 | set of utilities for validating inputs |
| [@astro-reactive/form](https://github.com/ayoayco/astro-reactive-library/blob/main/packages/form/README.md) | [![npm](https://img.shields.io/npm/v/@astro-reactive/form)](https://www.npmjs.com/package/@astro-reactive/form) | 🛠 | generate a dynamic form which can be modified programatically |
| [@astro-reactive/validator](https://github.com/ayoayco/astro-reactive-library/blob/main/packages/validator/README.md)| ![npm](https://img.shields.io/npm/v/@astro-reactive/validator) | 🛠 | set up validators for your form easily |
| astro-reactive-datagrid | 🛠 | 🛠 | generate a dynamic datagrid or table of values |
# HACKTOBERFEST 2022

View file

@ -3,11 +3,7 @@
"description": "Demo App for Astro Reactive Library",
"type": "module",
"version": "0.0.1",
"author": {
"name": "Ayo Ayco",
"email": "ayo@ayco.io",
"url": "https://ayco.io"
},
"author": "Ayo Ayco <ayo@ayco.io> (https://ayco.io)",
"private": true,
"scripts": {
"dev": "astro dev",
@ -17,8 +13,9 @@
"astro": "astro"
},
"dependencies": {
"astro": "^1.4.4",
"astro-reactive-form": "file:packages/astro-reactive-form"
"@astro-reactive/form": "file:packages/form",
"@astro-reactive/validator": "file:packages/validator",
"astro": "^1.5.0"
},
"main": "index.js",
"repository": {

View file

Before

Width:  |  Height:  |  Size: 873 B

After

Width:  |  Height:  |  Size: 873 B

View file

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

View file

@ -1,21 +1,27 @@
---
import {
import Form, {
ControlConfig,
FormControl,
FormGroup,
} from "astro-reactive-form/core";
import Form from "astro-reactive-form";
FormControl,
} from "@astro-reactive/form";
import { Validators } from "@astro-reactive/validator";
const form = new FormGroup([
{
name: "username",
label: "Username",
placeholder: "astroIscool",
validators: [Validators.required],
},
{
name: "email",
label: "Email",
validators: [Validators.email, Validators.required],
},
{
name: "password",
label: "Password",
type: "password",
validators: [Validators.required, Validators.minLength(8)],
},
]);
@ -49,28 +55,6 @@ form.get("is-awesome")?.setValue("checked");
</head>
<body>
<h1>Astro Reactive Form</h1>
<Form
submitControl={{
type: "submit",
name: "what",
}}
formGroups={form}
/>
<script>
import { getFormGroup } from "astro-reactive-form/client";
const form = document.querySelector("form") as HTMLFormElement;
const simpleForm = getFormGroup("Simple Form");
form.addEventListener("submit", () => {
const username = simpleForm?.get("username")?.value;
const isAwesome = simpleForm?.get("is-awesome")?.value;
alert(
`Hi, My username is ${username}. This Library is ${
isAwesome === "checked" ? "awesome" : "not awesome"
}`
);
});
</script>
<Form showValidationHints={true} formGroups={form} />
</body>
</html>

View file

@ -36,5 +36,6 @@
"bugs": {
"url": "https://github.com/Rishav-12/astro-reactive-library/issues"
},
"homepage": "https://github.com/Rishav-12/astro-reactive-library#readme"
"homepage": "https://github.com/Rishav-12/astro-reactive-library#readme",
"devDependencies": {}
}

View file

Before

Width:  |  Height:  |  Size: 731 KiB

After

Width:  |  Height:  |  Size: 731 KiB

View file

Before

Width:  |  Height:  |  Size: 873 B

After

Width:  |  Height:  |  Size: 873 B

View file

@ -15,8 +15,8 @@ Let your data build your UI. Blazing-fast, reactive user interfaces with native
| Packages | Version | Docs | Description |
| --- | --- | --- | --- |
| [astro-reactive-form](https://github.com/ayoayco/astro-reactive-library/blob/main/packages/astro-reactive-form/README.md) | [![npm](https://img.shields.io/npm/v/astro-reactive-form)](https://www.npmjs.com/package/astro-reactive-form) | 🛠 | generate a dynamic form which can be modified programatically |
| astro-reactive-validator | 🛠 | 🛠 | set of utilities for validating inputs |
| [@astro-reactive/form](https://github.com/ayoayco/astro-reactive-library/blob/main/packages/form/README.md) | [![npm](https://img.shields.io/npm/v/@astro-reactive/form)](https://www.npmjs.com/package/@astro-reactive/form) | 🛠 | generate a dynamic form which can be modified programatically |
| [@astro-reactive/validator](https://github.com/ayoayco/astro-reactive-library/blob/main/packages/validator/README.md)| ![npm](https://img.shields.io/npm/v/@astro-reactive/validator) | 🛠 | set up validators for your form easily |
| astro-reactive-datagrid | 🛠 | 🛠 | generate a dynamic datagrid or table of values |
# Running locally

View file

@ -6,10 +6,10 @@ layout: ../../layouts/MainLayout.astro
![package-form-cover](https://user-images.githubusercontent.com/4262489/193812095-1cffa287-e2ac-4671-b604-1e2ff2e6f19e.png)
[![version](https://img.shields.io/npm/v/astro-reactive-form)](https://www.npmjs.com/package/astro-reactive-form)
[![license](https://img.shields.io/npm/l/astro-reactive-form)](https://www.npmjs.com/package/astro-reactive-form)
[![downloads](https://img.shields.io/npm/dt/astro-reactive-form)](https://www.npmjs.com/package/astro-reactive-form)
[![dependencies](https://img.shields.io/librariesio/release/npm/astro-reactive-form)](https://www.npmjs.com/package/astro-reactive-form)
[![version](https://img.shields.io/npm/v/@astro-reactive/form)](https://www.npmjs.com/package/@astro-reactive/form)
[![license](https://img.shields.io/npm/l/@astro-reactive/form)](https://www.npmjs.com/package/@astro-reactive/form)
[![downloads](https://img.shields.io/npm/dt/@astro-reactive/form)](https://www.npmjs.com/package/@astro-reactive/form)
[![dependencies](https://img.shields.io/librariesio/release/npm/@astro-reactive/form)](https://www.npmjs.com/package/@astro-reactive/form)
# Astro Reactive Form 🔥
@ -23,7 +23,7 @@ _[All contributions are welcome.](https://github.com/ayoayco/astro-reactive-libr
In your Astro project:
```
npm i astro-reactive-form
npm i @astro-reactive/form
```
## Usage
@ -31,8 +31,8 @@ Use in an Astro page:
```astro
---
import { FormControl, FormGroup } from "astro-reactive-form/core";
import Form from "astro-reactive-form";
import { FormControl, FormGroup } from "@astro-reactive/form/core";
import Form from "@astro-reactive/form";
// create a form group
const form = new FormGroup([

View file

@ -1,4 +0,0 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

View file

@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View file

@ -1,4 +0,0 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

View file

@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

1797
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -14,24 +14,24 @@
".": "./index.ts"
},
"scripts": {
"start": "npm run dev -w demo",
"start": "npm run dev -w apps/demo",
"dev": "npm run dev -w apps/demo",
"docs": "npm run dev -w apps/docs",
"test": "npm run test --workspaces --if-present",
"lint": "npm run lint --workspaces --if-present",
"lint:fix": "npm run lint:fix --workspaces --if-present",
"build": "npm run build --workspaces --if-present",
"test:watch": "npm run test:watch --workspaces --if-present",
"dev": "npm run dev -w demo",
"docs": "npm run dev -w docs",
"bump:patch": "npm version patch -w",
"bump:minor": "npm version minor -w",
"bump:major": "npm version major -w",
"publish": "npm publish --public -w"
"publish": "npm publish --access public -w"
},
"license": "ISC",
"workspaces": [
"demo",
"packages/astro-reactive-form",
"packages/astro-reactive-validator",
"docs"
"packages/form",
"packages/validator",
"apps/demo",
"apps/docs"
]
}

View file

@ -1,52 +0,0 @@
import { FormControl, FormGroup } from '../core';
import type { ControlType } from '../core/form-control-types';
export const getFormGroup = (formName: string) => {
const fieldSetEl = (document.getElementById(formName) as HTMLFieldSetElement) || null;
if (fieldSetEl === null) {
return undefined;
}
const formGroup = new FormGroup([], formName);
fieldSetEl.querySelectorAll('input').forEach((field) => {
const formControl = getFormControl(field.name);
if (!formControl) return;
formGroup.controls.push(formControl);
});
return formGroup;
};
const getFormControl = (name: string) => {
const inputEl = document.getElementById(name) as HTMLInputElement | null;
if (inputEl === null) {
return undefined;
}
const formControl = new FormControl({
name: inputEl.name,
value: inputEl.value,
type: inputEl.type as ControlType,
label: inputEl.dataset.label as string,
labelPosition: inputEl.dataset.labelPosition as 'right' | 'left',
});
inputEl.addEventListener('change', (e) => {
if (!(e.target instanceof HTMLInputElement)) return;
let value = e.target.value;
if (formControl.type === 'checkbox') {
value = formControl.value === 'checked' ? '' : 'checked';
}
formControl.setValue(value);
formControl.isPristine = false;
});
const controlProxy = new Proxy(formControl, {
set() {
return true;
},
});
return controlProxy;
};

View file

@ -1 +0,0 @@
export * from './controls';

View file

@ -1,34 +0,0 @@
---
import type { FormControl } from '../core/form-control';
export interface Props {
control: FormControl;
}
const { control } = Astro.props;
---
<div>
{
control.label && control.labelPosition === 'left' && (
<label for={control.name}>{control.label}</label>
)
}
<input
name={control.name}
id={control.name}
type={control.type}
value={control.value}
checked={control.value === 'checked'}
placeholder={control.placeholder}
data-label={control.label}
data-label-position={control.labelPosition}
/>
{
control.label && control.labelPosition === 'right' && (
<label for={control.name}>{control.label}</label>
)
}
</div>

View file

@ -1,29 +0,0 @@
{
"name": "astro-reactive-validator",
"version": "0.0.0",
"description": "Validation Library for Astro Reactive Form 🔥",
"author": {
"name": "Ayo Ayco",
"email": "ayo@ayco.io",
"url": "https://ayco.io"
},
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/ayoayco/astro-reactive-library.git"
},
"keywords": [
"astro-components",
"ui",
"form",
"validation"
],
"license": "ISC",
"bugs": {
"url": "https://github.com/ayoayco/astro-reactive-library/issues"
},
"homepage": "https://github.com/ayoayco/astro-reactive-library#readme",
"dependencies": {
"astro": "^1.4.4"
}
}

View file

@ -7,20 +7,28 @@ export interface Props {
formGroups: FormGroup | FormGroup[];
submitControl?: Submit;
theme?: 'light' | 'dark';
showValidationHints?: boolean;
}
const { submitControl, formGroups: form, theme } = Astro.props;
const { submitControl, formGroups: form, theme, showValidationHints = false } = Astro.props;
const formTheme = theme ?? 'light';
const formName = Array.isArray(form) ? null : form?.name || null;
---
<form class={formTheme}>
<form class={formTheme} name={formName} id={formName}>
{
Array.isArray(form)
? form?.map((group) => <FieldSet group={group} />)
: form?.controls.map((control) => <Field control={control} />)
? form?.map((group) => <FieldSet showValidationHints={showValidationHints} group={group} />)
: form?.controls.map((control) => (
<Field showValidationHints={showValidationHints} control={control} />
))
}
{
submitControl && (
<Field showValidationHints={showValidationHints} control={new FormControl(submitControl)} />
)
}
{submitControl && <Field control={new FormControl(submitControl)} />}
</form>
<style>

View file

@ -5,10 +5,10 @@
Generate a dynamic form based on your data, and modify programatically.
<br />
<br />
<img src="https://img.shields.io/npm/v/astro-reactive-form" />
<img src="https://img.shields.io/npm/l/astro-reactive-form" />
<img src="https://img.shields.io/npm/dt/astro-reactive-form" />
<img src="https://img.shields.io/librariesio/release/npm/astro-reactive-form" />
<img src="https://img.shields.io/npm/v/@astro-reactive/form" />
<img src="https://img.shields.io/npm/l/@astro-reactive/form" />
<img src="https://img.shields.io/npm/dt/@astro-reactive/form" />
<img src="https://img.shields.io/librariesio/release/npm/@astro-reactive/form" />
<br />
<br />
</p>
@ -17,7 +17,7 @@
In your [Astro](https://astro.build) project:
```
npm i astro-reactive-form
npm i @astro-reactive/form
```
## Usage
@ -25,8 +25,8 @@ Use in an Astro page:
```astro
---
import { FormControl, FormGroup } from "astro-reactive-form/core";
import Form from "astro-reactive-form";
import { FormControl, FormGroup } from "@astro-reactive/form/core";
import Form from "@astro-reactive/form";
// create a form group
const form = new FormGroup([
@ -85,14 +85,14 @@ Example of multiple form groups:
![Screen Shot 2022-09-27 at 10 44 03 PM](https://user-images.githubusercontent.com/4262489/192631524-3139ac60-8d84-4c12-9231-fe2d49962756.png)
# Validation
See our [package for setting up validators](https://www.npmjs.com/package/@astro-reactive/validator).
# Future Plans
Currently this only supports very basic form creation, but the goal of the project is ambitious:
1. Validator library for common validation scenarios
1. Client-side validation
1. Server-side validation
1. validation hooks - onFocus, onBlur, onSubmit
1. Themes - unstyled, light mode, dark mode, compact, large
1. FormGroup class
1. `statusChanges` - observable that emits the form status when it changes

View file

@ -1,3 +1,6 @@
# v0.4.1
- set `showValidationHints` to true to show validation hints
# v0.3.0
- new control configuration type `ControlConfig`

View file

@ -0,0 +1,69 @@
---
import type { FormControl } from '../core/form-control';
export interface Props {
control: FormControl;
showValidationHints: boolean;
}
const { control, showValidationHints } = Astro.props;
const { validators = [] } = control;
const isRequired = showValidationHints && validators.includes('validator-required');
const validatorAttirbutes: Record<string, string> = validators?.reduce(
(prev, validator) => {
const split: string[] = validator.split(':');
const label: string = `data-${split[0]}` || 'invalid';
const value: string | null = split.length > 1 ? split[1] ?? null : 'true';
return {
...prev,
[label]: value,
};
},
{}
);
---
<div class="field" data-validator-hints={showValidationHints.toString()}>
{
control.label && control.labelPosition === 'left' && (
<label for={control.name}>
{isRequired && <span>*</span>}
{control.label}
</label>
)
}
<input
name={control.name}
id={control.name}
type={control.type}
value={control.value}
checked={control.value === 'checked'}
placeholder={control.placeholder}
data-label={control.label}
data-label-position={control.labelPosition}
{...validatorAttirbutes}
/>
{
control.label && control.labelPosition === 'right' && (
<label for={control.name}>
{isRequired && <span>*</span>}
{control.label}
</label>
)
}
</div>
<style>
[data-validator-hints='true'][data-validator-haserrors='true'],
[data-validator-hints='true'] [data-validator-haserrors='true'] {
color: red;
border-color: red;
}
</style>

View file

@ -4,12 +4,17 @@ import Field from './Field.astro';
export interface Props {
group: FormGroup;
showValidationHints: boolean;
}
const { group } = Astro.props;
const { group, showValidationHints } = Astro.props;
---
<fieldset id={group.name} name={group.name}>
{group.name && <legend>{group.name}</legend>}
{group?.controls?.map((control) => <Field control={control} />)}
{
group?.controls?.map((control) => (
<Field showValidationHints={showValidationHints} control={control} />
))
}
</fieldset>

View file

@ -34,6 +34,7 @@ export interface ControlBase {
label?: string;
labelPosition?: 'right' | 'left';
placeholder?: string;
validators?: string[]; // TODO: implement validator type
}
export interface Checkbox extends ControlBase {

View file

@ -8,16 +8,18 @@ export class FormControl {
private _labelPosition?: 'right' | 'left' = 'left';
private _isValid = true;
private _isPristine = true;
private _placeholder?;
private _placeholder?: string;
private _validators?: string[];
constructor(config: ControlConfig) {
const { name, type, value, label, labelPosition, placeholder } = config;
const { name, type, value, label, labelPosition, placeholder, validators } = config;
this._name = name;
this._type = type || 'text';
this._value = value || null;
this._label = label || '';
this._labelPosition = labelPosition || 'left';
this._placeholder = placeholder || '';
this._type = type ?? 'text';
this._value = value ?? null;
this._label = label ?? '';
this._labelPosition = labelPosition ?? 'left';
this._placeholder = placeholder ?? '';
this._validators = validators ?? [];
}
get name() {
@ -56,8 +58,13 @@ export class FormControl {
return this._isValid;
}
get validators() {
return this._validators;
}
setValue(value: string) {
this._value = value;
this._isPristine = false;
}
setIsPristine(value: boolean) {

View file

@ -1,11 +1,11 @@
import type { ControlBase } from './form-control-types';
import type { ControlConfig } from './form-control-types';
import { FormControl } from './form-control';
export class FormGroup {
controls: FormControl[];
name?: string;
constructor(controls: ControlBase[], name = '') {
constructor(controls: ControlConfig[], name = '') {
this.name = name;
this.controls = controls
.filter((control) => control.type !== 'submit')

View file

@ -1,3 +1,3 @@
import Form from './Form.astro';
export default Form;
export * from './Form.astro';
export * from './core';

View file

@ -1,7 +1,7 @@
{
"name": "astro-reactive-form",
"name": "@astro-reactive/form",
"description": "The Reactive Form component for Astro 🔥",
"version": "0.3.0",
"version": "0.4.1",
"repository": "https://github.com/ayoayco/astro-reactive-library",
"homepage": "https://github.com/ayoayco/astro-reactive-library#readme",
"author": {
@ -11,14 +11,11 @@
},
"type": "module",
"exports": {
".": "./index.ts",
"./core": "./core/index.ts",
"./client": "./client/index.ts"
".": "./index.ts"
},
"files": [
"core/",
"components/",
"client/",
"Form.astro",
"index.ts"
],
@ -43,7 +40,7 @@
"@types/prettier": "^2.7.0",
"@typescript-eslint/eslint-plugin": "^5.37.0",
"@typescript-eslint/parser": "^5.37.0",
"astro": "^1.4.4",
"astro": "^1.5.0",
"astro-component-tester": "^0.6.0",
"chai": "^4.3.6",
"eslint": "^8.23.1",
@ -55,7 +52,7 @@
"typescript": "^4.8.3"
},
"peerDependencies": {
"astro": "^1.0.0"
"astro": "^1.5.0"
},
"license": "MIT"
}

View file

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

View file

@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = false
[{.*,*.md,*.json,*.toml,*.yml,}]
indent_style = space

View file

@ -0,0 +1 @@
test/**/*.js

View file

@ -0,0 +1,17 @@
/** @type {import("@types/eslint").Linter.Config} */
module.exports = {
env: {
node: true,
},
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'prettier'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
rules: {
// We don't want to leak logging into our user's console unless it's an error
'no-console': ['error', { allow: ['warn', 'error'] }],
},
};

8
packages/validator/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.DS_Store

View file

@ -0,0 +1,24 @@
/** @type {import("@types/prettier").Options} */
module.exports = {
printWidth: 100,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
useTabs: true,
plugins: ['../../node_modules/prettier-plugin-astro'],
overrides: [
{
files: '*.astro',
options: {
parser: 'astro',
},
},
{
files: ['.*', '*.json', '*.md', '*.toml', '*.yml'],
options: {
useTabs: false,
},
},
],
};

View file

@ -0,0 +1,92 @@
<p align="center">
<img src="https://raw.githubusercontent.com/ayoayco/astro-reactive-library/main/.github/assets/logo/min-banner.png" alt="Astro Reactive Library Logo">
<strong>Astro Reactive Validator</strong>
<br />
Set up validators for your forms easily.
<br />
<br />
<img src="https://img.shields.io/npm/v/@astro-reactive/validator" />
<img src="https://img.shields.io/npm/l/@astro-reactive/validator" />
<img src="https://img.shields.io/npm/dt/@astro-reactive/validator" />
<img src="https://img.shields.io/librariesio/release/npm/@astro-reactive/validator" />
<br />
<br />
</p>
## Installation
In your [Astro](https://astro.build) project:
```
npm i @astro-reactive/validator @astro-reactive/form
```
## Usage
Use in an Astro page:
```astro
---
import { FormControl, FormGroup } from "@astro-reactive/form/core";
import Form from "@astro-reactive/form";
import { Validators } from "@astro-reactive/validator";
const form = new FormGroup([
{
name: "username",
label: "Username",
validators: [Validators.required],
},
{
name: "email",
label: "Email",
validators: [Validators.email, Validators.required],
},
{
name: "password",
label: "Password",
type: "password",
validators: [Validators.required, Validators.minLength(8)],
},
]);
// set the name optionally
form.name = "Simple Form";
// you can insert a control at any point
form.controls.push(
new FormControl({
type: "checkbox",
name: "is-awesome",
label: "is Awesome?",
labelPosition: "right",
})
);
// you can get a FormControl object
const userNameControl = form.get("username");
// you can set values dynamically
userNameControl?.setValue("RAMOOOON");
form.get('is-awesome')?.setValue("checked");
---
<!--
the `formGroups` attribute can take a single FormGroup
or an array of FormGroup for multiple fieldsets;
we do a single for now in this example
-->
<Form showValidationHints={true} formGroups={form} />
```
# Screenshots
Result of example above:
![Screen Shot 2022-10-15 at 1 31 11 PM](https://user-images.githubusercontent.com/4262489/195984173-c19e8cf0-bc55-41d5-8267-e3de44c6bf64.png)
# Validators available
1. `Validators.min(limit)` - checks if number value is greater than or equal to limit
1. `Validators.max(limit)` - checks if number value is less than or equal to limit
1. `Validators.required` - checks if value is empty
1. `Validators.requiredChecked` - checks if value is "checked"
1. `Validators.email` - checks if value is a valid email
1. `Validators.minLength(limit)` - checks if value length is greater than or equal to limit
1. `Validators.maxLength(limit)` - checks if value length is less than or equal to limit

View file

@ -0,0 +1,3 @@
## 0.0.1
- Validators
- validator functions

View file

@ -0,0 +1,35 @@
---
import type { HookType } from './core';
export interface Props {
hook?: HookType;
displayErrorMessages?: boolean;
}
const { hook = 'all', displayErrorMessages = false } = Astro.props;
---
<input hidden name="hook" id="hook" value={hook} />
<input
hidden
name="displayErrorMessages"
id="displayErrorMessages"
value={displayErrorMessages.toString()}
/>
<script>
// TODO: selectors should by unique IDs generated by our library
// TODO: implement type guards pls 😱
// TODO: create deserializer util
// TODO: handle hooks / when to attach event listeners
// const form = document.querySelector('form');
import { clearErrors, validate } from './core';
// const hook: HookType = (document.getElementById('hook') as HTMLInputElement).value as HookType;
const inputs = [...document.querySelectorAll('form input')] as HTMLInputElement[];
inputs?.forEach((input) => {
input.addEventListener('blur', validate);
input.addEventListener('input', clearErrors);
});
</script>

View file

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

View file

@ -0,0 +1,152 @@
import type { ValidationResult } from './validator.types';
import { Validators } from './validators';
export function validate(event: FocusEvent) {
// NOTE: event target attribute names are converted to lowercase
const element = event.target as HTMLInputElement;
const attributeNames = element?.getAttributeNames() || [];
const validatorAttirbutes = attributeNames.filter((attribute) =>
attribute.includes('data-validator-')
);
const value = element.value;
const validityArray: ValidationResult[] = validatorAttirbutes
.map((validator) => validator.replace('data-', ''))
.map((validator): ValidationResult => {
// insert logic for each validator
// TODO: implement a map of functions,validator
if (validator === Validators.min()) {
const limit = parseInt(element.getAttribute('data-validator-min') || '0', 10);
return validateMin(value, limit);
}
if (validator === Validators.max()) {
const limit = parseInt(element.getAttribute('data-validator-min') || '0', 10);
return validateMax(value, limit);
}
if (validator === Validators.required) {
return validateRequired(value);
}
if (validator === Validators.requiredChecked) {
return validateRequiredChecked(value);
}
if (validator === Validators.email) {
return validateEmail(value);
}
if (validator === Validators.minLength()) {
const limit = parseInt(element.getAttribute('data-validator-minlength') || '0', 10);
return validateMinLength(value, limit);
}
if (validator === Validators.maxLength()) {
const limit = parseInt(element.getAttribute('data-validator-maxlength') || '0', 10);
return validateMaxLength(value, limit);
}
return true;
});
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) {
const element = event.target as HTMLInputElement;
element.parentElement?.setAttribute('data-validator-haserrors', 'false');
element.setAttribute('data-validator-haserrors', 'false');
}
function validateMin(value: string, limit: number): ValidationResult {
const isValid = parseInt(value, 10) >= limit;
if (!isValid) {
return {
error: 'min',
limit: limit,
};
}
return true;
}
function validateMax(value: string, limit: number): ValidationResult {
const isValid = parseInt(value, 10) <= limit;
if (!isValid) {
return {
error: 'max',
limit: limit,
};
}
return true;
}
function validateRequired(value: string): ValidationResult {
const isValid = !!value;
if (!isValid) {
return {
error: 'required',
};
}
return true;
}
function validateRequiredChecked(value: string): ValidationResult {
const isValid = value === 'checked';
if (!isValid) {
return {
error: 'requiredChecked',
};
}
return true;
}
// TODO: review regexp vulnerability
function validateEmail(value: string): ValidationResult {
const isValid = String(value)
.toLowerCase()
.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
);
if (!isValid) {
return {
error: 'email',
};
}
return true;
}
function validateMinLength(value: string, limit: number): ValidationResult {
const isValid = value.length >= limit;
if (!isValid) {
return {
error: 'minLength',
limit: limit,
};
}
return true;
}
function validateMaxLength(value: string, limit: number): ValidationResult {
const isValid = value.length <= limit;
if (!isValid) {
return {
error: 'minLength',
limit: limit,
};
}
return true;
}

View file

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

View file

@ -0,0 +1,50 @@
// improvement: implement min&max inclusive/exclusive
export class Validators {
static min(min?: number): string {
const label = 'validator-min';
if (min !== undefined) {
return `${label}:${min}`;
}
return label;
}
static max(max?: number): string {
const label = 'validator-max';
if (max !== undefined) {
return `${label}:${max}`;
}
return label;
}
static get required(): string {
return `validator-required`;
}
static get requiredChecked(): string {
return `validator-required:checked`;
}
static get email(): string {
return `validator-email`;
}
static minLength(minLength?: number): string {
const label = 'validator-minlength';
if (minLength !== undefined) {
return `${label}:${minLength}`;
}
return label;
}
static maxLength(maxLength?: number): string {
const label = 'validator-maxlength';
if (maxLength !== undefined) {
return `${label}:${maxLength}`;
}
return label;
}
// static pattern(pattern: string | RegExp): string {
// return `validator-pattern:${pattern}`;
// }
}

View file

@ -0,0 +1,3 @@
import Validator from './Validator.astro';
export default Validator;
export * from './core';

View file

@ -0,0 +1,62 @@
{
"name": "@astro-reactive/validator",
"description": "Form validation library for Astro 🔥",
"version": "0.0.1",
"repository": "https://github.com/ayoayco/astro-reactive-library",
"homepage": "https://github.com/ayoayco/astro-reactive-library#readme",
"author": {
"name": "Ayo Ayco",
"email": "ayo@ayco.io",
"url": "https://ayco.io"
},
"type": "module",
"exports": {
".": "./index.ts"
},
"files": [
"core/",
"Validator.astro",
"index.ts"
],
"keywords": [
"astro-component"
],
"scripts": {
"test": "mocha --parallel --timeout 15000",
"test:watch": "mocha --watch --parallel --timeout 15000",
"format": "prettier -w .",
"lint": "eslint . --ext .ts,.js",
"lint:fix": "eslint --fix . --ext .ts,.js",
"build": "tsc --noEmit"
},
"devDependencies": {
"@types/chai": "^4.3.3",
"@types/eslint": "^8.4.6",
"@types/mocha": "^10.0.0",
"@types/node": "^18.7.18",
"@types/prettier": "^2.7.0",
"@typescript-eslint/eslint-plugin": "^5.37.0",
"@typescript-eslint/parser": "^5.37.0",
"astro": "^1.5.0",
"astro-component-tester": "^0.6.0",
"chai": "^4.3.6",
"eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"mocha": "^10.0.0",
"prettier": "^2.7.1",
"prettier-plugin-astro": "^0.5.4",
"typescript": "^4.8.3"
},
"peerDependencies": {
"astro": "^1.5.0"
},
"main": "index.js",
"directories": {
"test": "test"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/ayoayco/astro-reactive-library/issues"
}
}

View file

@ -0,0 +1,18 @@
import { expect } from 'chai';
import { describe, beforeEach, it } from 'mocha';
import { getComponentOutput } from 'astro-component-tester';
describe('Example Tests', () => {
let component;
beforeEach(() => {
component = undefined;
});
describe('Component test', async () => {
it('example component should not be empty', async () => {
component = await getComponentOutput('./Validator.astro');
expect(component.html).not.to.equal('\n');
});
});
});

View file

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