diff --git a/apps/demo/src/pages/index.astro b/apps/demo/src/pages/index.astro index 79be3e5..88f9d5d 100644 --- a/apps/demo/src/pages/index.astro +++ b/apps/demo/src/pages/index.astro @@ -48,6 +48,12 @@ const form = new FormGroup([ options: ["S", "M", "L", "XL", "XXL"], placeholder: "-- Please choose an option --", }, + { + name: "comment", + label: "Feedback", + type: "textarea", + value: "Nice!" + }, ]); form.name = "Simple Form"; diff --git a/packages/common/types/control.types.ts b/packages/common/types/control.types.ts index 423b79b..fb8f22b 100644 --- a/packages/common/types/control.types.ts +++ b/packages/common/types/control.types.ts @@ -25,7 +25,7 @@ export type InputType = | "url" | "week"; -export type ControlType = InputType | "dropdown"; +export type ControlType = InputType | "dropdown" | "textarea"; export interface ControlBase { name: string; @@ -39,6 +39,10 @@ export interface ControlBase { options?: string[] | ControlOption[]; } +export interface TextInput extends ControlBase { + type: Exclude; +} + export interface Checkbox extends ControlBase { type: "checkbox"; checked: boolean; @@ -56,6 +60,13 @@ export interface Dropdown extends Omit { options: string[] | ControlOption[]; } +export interface TextArea extends ControlBase { + type: "textarea"; + value?: string; + rows?: number; + cols?: number; +} + export interface ControlOption { label: string; value: string; diff --git a/packages/form/components/Field.astro b/packages/form/components/Field.astro index 082dc68..f6b479a 100644 --- a/packages/form/components/Field.astro +++ b/packages/form/components/Field.astro @@ -2,9 +2,10 @@ /** * DEFAULT CONTROL COMPONENT */ -import type { Radio, Dropdown } from '@astro-reactive/common'; +import type { Radio, Dropdown, TextArea } from '@astro-reactive/common'; import type { FormControl } from '../core/form-control'; import DropdownControl from './controls/Dropdown.astro'; +import TextAreaControl from './controls/TextArea.astro'; import Input from './controls/Input.astro'; import RadioGroup from './controls/RadioGroup.astro'; import Errors from './Errors.astro'; @@ -30,6 +31,9 @@ const hasErrors: boolean | null = !!control.errors?.length; ) : control.type === 'dropdown' ? ( + ) : + control.type === 'textarea' ? ( + ) : ( ) diff --git a/packages/form/components/controls/TextArea.astro b/packages/form/components/controls/TextArea.astro new file mode 100644 index 0000000..b1f9c2e --- /dev/null +++ b/packages/form/components/controls/TextArea.astro @@ -0,0 +1,40 @@ +--- +/** + * TEXT AREA COMPONENT + */ +import type { TextArea } from '@astro-reactive/common'; + +export interface Props { + control: TextArea; + readOnly?: boolean; +} + +const { control, readOnly = false } = Astro.props; + +const { validators = [] } = control; + +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/core/form-control.ts b/packages/form/core/form-control.ts index 07d80c2..28fef8d 100644 --- a/packages/form/core/form-control.ts +++ b/packages/form/core/form-control.ts @@ -1,16 +1,17 @@ import type { Button, Checkbox, - ControlBase, ControlType, + TextInput, Radio, Dropdown, ControlOption, Submit, ValidationError, + TextArea, } from '@astro-reactive/common'; -export type ControlConfig = ControlBase | Checkbox | Radio | Submit | Button | Dropdown; +export type ControlConfig = TextInput | Checkbox | Radio | Submit | Button | Dropdown | TextArea; export class FormControl { private _name = ''; @@ -24,6 +25,8 @@ export class FormControl { private _validators: string[] = []; private _errors: ValidationError[] = []; private _options: string[] | ControlOption[] = []; + private _rows: number | null = null; + private _cols: number | null = null; private validate: (value: string, validators: string[]) => ValidationError[] = ( value: string, @@ -55,6 +58,12 @@ export class FormControl { this._validators = validators; this._options = options; + if (config.type === 'textarea') { + const { rows = null, cols = null } = config; + this._rows = rows; + this._cols = cols; + } + // TODO: implement independence // form should try to import validator, // but handle error when it's not installed @@ -115,6 +124,14 @@ export class FormControl { return this._options; } + get rows() { + return this._rows; + } + + get cols() { + return this._cols; + } + setValue(value: string) { this._value = value; this._isPristine = false; diff --git a/packages/form/test/TextArea.astro.test.js b/packages/form/test/TextArea.astro.test.js new file mode 100644 index 0000000..27677d9 --- /dev/null +++ b/packages/form/test/TextArea.astro.test.js @@ -0,0 +1,110 @@ +import { expect } from 'chai'; +import { describe, beforeEach, it } from 'mocha'; +import { getComponentOutput } from 'astro-component-tester'; +import { cleanString } from './utils/index.js'; + +describe('TextArea.astro test', () => { + let component; + + beforeEach(() => { + component = undefined; + }); + + it('Should render a textarea', async () => { + // arrange + const expectedHTML = ' { + // arrange + const expectedLabel = 'TestLabel'; + const props = { + control: { + label: 'TestLabel', + name: 'FAKE NAME', + type: 'textarea', + }, + }; + + // act + component = await getComponentOutput('./components/controls/TextArea.astro', props); + const actualResult = cleanString(component.html); + + // assert + expect(actualResult).to.contain(expectedLabel); + }); + + it('Should render a textarea with placeholder', async () => { + // arrange + const expectedAttribute = 'placeholder="test"'; + const props = { + control: { + label: 'FAKE LABEL', + name: 'FAKE NAME', + type: 'textarea', + placeholder: 'test', + }, + }; + + // act + component = await getComponentOutput('./components/controls/TextArea.astro', props); + const actualResult = cleanString(component.html); + + // assert + expect(actualResult).to.contain(expectedAttribute); + }); + + it('Should render a textarea with initial value', async () => { + // arrange + const expectedText = 'hello,world!'; + const props = { + control: { + label: 'FAKE LABEL', + name: 'FAKE NAME', + type: 'textarea', + placeholder: 'test', + value: 'hello,world!', + }, + }; + + // act + component = await getComponentOutput('./components/controls/TextArea.astro', props); + const actualResult = cleanString(component.html); + + // assert + expect(actualResult).to.contain(expectedText); + }); + + it('Should render a required textarea with data required attribute when showValidationHints is true', async () => { + // arrange + const expectedAttribute = 'data-validator-required="true"'; + const props = { + control: { + label: 'FAKE LABEL', + name: 'FAKE NAME', + type: 'textarea', + validators: ['validator-required'], + }, + }; + + // act + component = await getComponentOutput('./components/controls/TextArea.astro', props); + const actualResult = cleanString(component.html); + + // assert + expect(actualResult).to.contain(expectedAttribute); + }); +});