diff --git a/apps/demo/src/pages/index.astro b/apps/demo/src/pages/index.astro index ee2e23c..dd43a48 100644 --- a/apps/demo/src/pages/index.astro +++ b/apps/demo/src/pages/index.astro @@ -34,8 +34,8 @@ const form = new FormGroup([ label: "Agreement", type: "radio", value: [ - { label: "Agree", value: "yes" }, - { label: "Disagree", value: "no", labelPosition: "right" }, + { label: "Agree", value: "yes", checked: true }, + { label: "Disagree", value: "no" }, ], }, ]); @@ -61,9 +61,12 @@ form.get("is-awesome")?.setValue("checked"); // setting an invalid value will cause errors as server-rendered form.get("email")?.setValue("invalid-email"); + +// switch between light and dark mode +const theme = "dark"; --- - + @@ -71,8 +74,15 @@ form.get("email")?.setValue("invalid-email"); Astro - + +

Astro Reactive Form

-
+ + diff --git a/apps/demo/src/pages/pizza-delivery.astro b/apps/demo/src/pages/pizza-delivery.astro index 28f4021..7a154bb 100644 --- a/apps/demo/src/pages/pizza-delivery.astro +++ b/apps/demo/src/pages/pizza-delivery.astro @@ -98,6 +98,7 @@ infoForm.name = "Customer Info"; Astro +

Pizza Form Demo

{ value: string[] | RadioOption[]; } -export interface RadioOption extends Omit { +export interface RadioOption { label: string; value: string; checked?: boolean; diff --git a/packages/form/Form.astro b/packages/form/Form.astro deleted file mode 100644 index c3d97f6..0000000 --- a/packages/form/Form.astro +++ /dev/null @@ -1,84 +0,0 @@ ---- -import type { Submit, Radio, RadioOption } from 'common/types'; -import { FormGroup, FormControl } from './core'; -import FieldSet from './components/FieldSet.astro'; -import Field from './components/Field.astro'; - -export interface Props { - formGroups: FormGroup | FormGroup[]; - submitControl?: Submit; - theme?: 'light' | 'dark'; - showValidationHints?: boolean; -} - -const { submitControl, formGroups: form, theme, showValidationHints = false } = Astro.props; - -const formTheme = theme ?? 'light'; -const formName = Array.isArray(form) ? null : form?.name || null; ---- - - - { - Array.isArray(form) - ? form?.map((group) =>
) - : form?.controls.map((control) => - control.type === 'radio' ? ( - [ - , - ...(control as Radio)?.value?.map((v: string | RadioOption) => ( - - )), - ] - ) : ( - - ) - ) - } - { - submitControl && ( - - ) - } - - - diff --git a/packages/form/components/Errors.astro b/packages/form/components/Errors.astro new file mode 100644 index 0000000..322ed78 --- /dev/null +++ b/packages/form/components/Errors.astro @@ -0,0 +1,17 @@ +--- +import type { ValidationError } from 'common/types'; + +export interface Props { + errors: ValidationError[]; +} + +const { errors = [] } = Astro.props; +--- + +{ + errors.map((error) => ( +
  • + {error.error} {error.limit} +
  • + )) +} diff --git a/packages/form/components/Field.astro b/packages/form/components/Field.astro index 461d50b..6003755 100644 --- a/packages/form/components/Field.astro +++ b/packages/form/components/Field.astro @@ -1,98 +1,34 @@ --- +/** + * DEFAULT CONTROL COMPONENT + */ +import type { Radio } from 'common/types'; import type { FormControl } from '../core/form-control'; +import Input from './controls/Input.astro'; +import RadioGroup from './controls/RadioGroup.astro'; +import Errors from './Errors.astro'; +import Label from './Label.astro'; export interface Props { control: FormControl; showValidationHints: boolean; - showErrors?: boolean; - showOnlyLabel?: boolean; + showErrors?: boolean; // feature flag for showing validation errors } -const { control, showValidationHints, showErrors = false, showOnlyLabel = false } = Astro.props; +const { control, showValidationHints, showErrors = false } = Astro.props; -const { validators = [] } = control; - -const isRequired: boolean = showValidationHints && validators.includes('validator-required'); - -const hasErrors: boolean = showValidationHints && !!control.errors?.length; - -const validatorAttributes: Record = 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, - }; - }, - - {} -); +const hasErrors: boolean | null = !!control.errors?.length; --- -
    - { - control.label && control.labelPosition === 'left' && ( - - ) - } - - { - !showOnlyLabel && ( - - ) - } - - { - showErrors && - hasErrors && - control.errors.map((error) => ( -
  • - {error.error} {error.limit} -
  • - )) - } - - { - control.label && control.labelPosition === 'right' && ( - - ) - } +
    +
    - - diff --git a/packages/form/components/Form.astro b/packages/form/components/Form.astro new file mode 100644 index 0000000..8f57b57 --- /dev/null +++ b/packages/form/components/Form.astro @@ -0,0 +1,58 @@ +--- +import type { Submit } from 'common/types'; +import { FormGroup, FormControl } from '../core'; +import FieldSet from './FieldSet.astro'; +import Field from './Field.astro'; + +export interface Props { + formGroups: FormGroup | FormGroup[]; + submitControl?: Submit; + theme?: 'light' | 'dark'; + showValidationHints?: boolean; +} + +const { submitControl, formGroups = [], theme, showValidationHints = false } = Astro.props; + +const formTheme = theme ?? 'light'; +const formName = Array.isArray(formGroups) ? null : formGroups?.name || null; +--- + +
    + { + Array.isArray(formGroups) + ? formGroups?.map((group) => ( +
    + )) + : formGroups?.controls.map((control) => ( + + )) + } + { + submitControl && ( + + ) + } + + + + + diff --git a/packages/form/components/Label.astro b/packages/form/components/Label.astro new file mode 100644 index 0000000..be6f703 --- /dev/null +++ b/packages/form/components/Label.astro @@ -0,0 +1,36 @@ +--- +import type { FormControl } from '../core'; + +export interface Props { + control: FormControl; + showValidationHints: boolean; + showErrors?: boolean; // feature flag for showing validation errors +} + +const { control, showValidationHints } = Astro.props; + +const { validators = [] } = control; + +const isRequired: boolean = showValidationHints && validators.includes('validator-required'); +--- + + { + control.label && control.labelPosition === 'left' && ( + + ) + } + + + + { + control.label && control.labelPosition === 'right' && ( + + ) + } + diff --git a/packages/form/components/controls/Input.astro b/packages/form/components/controls/Input.astro new file mode 100644 index 0000000..db834fe --- /dev/null +++ b/packages/form/components/controls/Input.astro @@ -0,0 +1,40 @@ +--- +/** + * DEFAULT INPUT COMPONENT + */ +import type { FormControl } from '../../core/form-control'; + +export interface Props { + control: FormControl; +} + +const { control } = Astro.props; + +const { validators = [] } = control; + +const hasErrors: boolean = !!control.errors?.length; + +const validatorAttributes: Record = 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, + }; +}, {}); +--- + + diff --git a/packages/form/components/controls/RadioGroup.astro b/packages/form/components/controls/RadioGroup.astro new file mode 100644 index 0000000..1961b18 --- /dev/null +++ b/packages/form/components/controls/RadioGroup.astro @@ -0,0 +1,31 @@ +--- +/** + * RADIO GROUP COMPONENT + */ +import type { Radio } from 'common/types'; + +export interface Props { + control: Radio; +} + +const { control } = Astro.props; + +const options = control.value.map((option) => { + if (typeof option === 'string') { + return { + label: option, + value: option, + }; + } + return option; +}); +--- + +{ + options.map((option) => ( +
    + {' '} + {option.label} +
    + )) +} diff --git a/packages/form/core/form-control.ts b/packages/form/core/form-control.ts index 638b36e..5360f69 100644 --- a/packages/form/core/form-control.ts +++ b/packages/form/core/form-control.ts @@ -13,15 +13,14 @@ export type ControlConfig = ControlBase | Checkbox | Radio | Submit | Button; export class FormControl { private _name = ''; - private _id = ''; private _type: ControlType = 'text'; private _value?: string | number | null | string[] | RadioOption[]; - private _label?: string; + private _label = ''; private _labelPosition?: 'right' | 'left' = 'left'; private _isValid = true; private _isPristine = true; - private _placeholder?: string; - private _validators?: string[]; + private _placeholder: string | null = null; + private _validators: string[] = []; private _errors: ValidationError[] = []; private validate: (value: string, validators: string[]) => ValidationError[] = ( @@ -34,18 +33,25 @@ export class FormControl { }; constructor(private config: ControlConfig) { - const { name, id, type, value, label, labelPosition, placeholder, validators = [] } = config; + const { + name, + type = 'text', + value = null, + label = '', + labelPosition = 'left', + placeholder = null, + validators = [], + } = config; + this._name = name; - this._id = id ?? name; - this._type = type ?? 'text'; - this._value = value ?? null; - this._label = label ?? ''; - this._labelPosition = labelPosition ?? 'left'; - this._placeholder = placeholder ?? ''; + this._type = type; + this._value = value; + this._label = label; + this._labelPosition = labelPosition; + this._placeholder = placeholder; 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; @@ -53,6 +59,7 @@ export class FormControl { const valueStr: string = this._value?.toString() || ''; this._errors = this.validate(valueStr, validators); } else { + // if user did not install the validator, then errors should be empty this._errors = []; } }); @@ -62,10 +69,6 @@ export class FormControl { return this._name; } - get id() { - return this._id; - } - get type() { return this._type; } diff --git a/packages/form/index.ts b/packages/form/index.ts index 0b332f4..9b8a12b 100644 --- a/packages/form/index.ts +++ b/packages/form/index.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -import Form from './Form.astro'; +import Form from './components/Form.astro'; export default Form; export * from './core'; diff --git a/packages/form/package.json b/packages/form/package.json index 33fe571..8459e1a 100644 --- a/packages/form/package.json +++ b/packages/form/package.json @@ -16,7 +16,6 @@ "files": [ "core/", "components/", - "Form.astro", "index.ts" ], "keywords": [ diff --git a/packages/form/test/Field.astro.test.js b/packages/form/test/Field.astro.test.js index fe31c18..5ff26b2 100644 --- a/packages/form/test/Field.astro.test.js +++ b/packages/form/test/Field.astro.test.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { beforeEach, describe, it } from 'mocha'; import { getComponentOutput } from 'astro-component-tester'; +import { cleanString } from './utils/index.js'; describe('Field.astro test', () => { let component; @@ -29,28 +30,6 @@ describe('Field.astro test', () => { expect(actualResult).to.contain(expectedLabel); }); - it('Should render only label', async () => { - // arrange - const expectedLabel = 'TestLabel'; - const props = { - control: { - label: expectedLabel, - name: 'username', - labelPosition: 'left', - }, - showValidationHints: false, - showOnlyLabel: true, - }; - - // act - component = await getComponentOutput('./components/Field.astro', props); - const actualResult = cleanString(component.html); - - // assert - expect(actualResult).to.contain(expectedLabel); - expect(actualResult).to.not.contain('input'); - }); - it('Should render required fields with asterisk in label when showValidationHints is true', async () => { // arrange const expectedLabel = 'TestLabel'; @@ -75,7 +54,7 @@ describe('Field.astro test', () => { it('Should server-render validation error attributes', async () => { // arrange - const expectedClass = 'has-errors'; + const expectedResult = 'data-validator-haserrors="true"'; const props = { control: { label: 'FAKE LABEL', @@ -97,10 +76,6 @@ describe('Field.astro test', () => { const actualResult = cleanString(component.html); // assert - expect(actualResult).to.contain(expectedClass); + expect(actualResult).to.contain(expectedResult); }); }); - -function cleanString(str) { - return str.replace(/\s/g, ''); -} diff --git a/packages/form/test/FieldSet.astro.test.js b/packages/form/test/FieldSet.astro.test.js index 2bbcd78..879ee39 100644 --- a/packages/form/test/FieldSet.astro.test.js +++ b/packages/form/test/FieldSet.astro.test.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { beforeEach, describe, it } from 'mocha'; import { getComponentOutput } from 'astro-component-tester'; +import { cleanString } from './utils/index.js'; describe('FieldSet.astro test', () => { let component; @@ -36,7 +37,3 @@ describe('FieldSet.astro test', () => { expect(actualResult).to.contain(`${expectedName}`); }); }); - -function cleanString(str) { - return str.replace(/\s/g, ''); -} diff --git a/packages/form/test/Form.astro.test.js b/packages/form/test/Form.astro.test.js index 6e390c6..d514485 100644 --- a/packages/form/test/Form.astro.test.js +++ b/packages/form/test/Form.astro.test.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { describe, beforeEach, it } from 'mocha'; import { getComponentOutput } from 'astro-component-tester'; +import { cleanString } from './utils/index.js'; describe('Form.astro test', () => { let component; @@ -15,7 +16,7 @@ describe('Form.astro test', () => { const expectedCount = 0; const element = /
    /g; const props = { formGroups: undefined }; - component = await getComponentOutput('./Form.astro', props); + component = await getComponentOutput('./components/Form.astro', props); // act const actualResult = cleanString(component.html); @@ -30,7 +31,7 @@ describe('Form.astro test', () => { const expectedCount = 0; const element = /
    /g; const props = { formGroups: [] }; - component = await getComponentOutput('./Form.astro', props); + component = await getComponentOutput('./components/Form.astro', props); // act const actualResult = cleanString(component.html); @@ -54,7 +55,7 @@ describe('Form.astro test', () => { ], }; const props = { formGroups: Array(expectedCount).fill(fakeFormGroup) }; - component = await getComponentOutput('./Form.astro', props); + component = await getComponentOutput('./components/Form.astro', props); // act const actualResult = cleanString(component.html); @@ -78,7 +79,7 @@ describe('Form.astro test', () => { ], }; const props = { formGroups: fakeFormGroup }; - component = await getComponentOutput('./Form.astro', props); + component = await getComponentOutput('./components/Form.astro', props); // act const actualResult = cleanString(component.html); @@ -89,7 +90,3 @@ describe('Form.astro test', () => { }); }); }); - -function cleanString(str) { - return str.replace(/\s/g, ''); -} diff --git a/packages/form/test/RadioGroup.astro.test.js b/packages/form/test/RadioGroup.astro.test.js new file mode 100644 index 0000000..ba0f74c --- /dev/null +++ b/packages/form/test/RadioGroup.astro.test.js @@ -0,0 +1,34 @@ +import { expect } from 'chai'; +import { describe, beforeEach, it } from 'mocha'; +import { getComponentOutput } from 'astro-component-tester'; +import { cleanString } from './utils/index.js'; + +describe('RadioGroup.astro test', () => { + let component; + + beforeEach(() => { + component = undefined; + }); + it('Should render all radio options', async () => { + // arrange + const expectedOptions = 3; + const element = /radio-option/g; + const props = { + control: { + label: 'FAKE LABEL', + name: 'FAKE NAME', + type: 'radio', + value: ['one', 'two', 'three'], + }, + showValidationHints: true, + }; + + // act + component = await getComponentOutput('./components/controls/RadioGroup.astro', props); + const actualResult = cleanString(component.html); + const matches = actualResult.match(element) || []; + + // assert + expect(matches.length).to.equal(expectedOptions); + }); +}); diff --git a/packages/form/test/utils/index.js b/packages/form/test/utils/index.js new file mode 100644 index 0000000..fb97686 --- /dev/null +++ b/packages/form/test/utils/index.js @@ -0,0 +1,3 @@ +export function cleanString(str) { + return str.replace(/\s/g, ''); +}