feat(form): implement TextArea control (#169)

This commit is contained in:
Woramat Ngamkham 2022-11-01 06:21:39 +07:00 committed by GitHub
parent c38e5dfbe3
commit f93f8b6484
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 192 additions and 4 deletions

View file

@ -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";

View file

@ -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<InputType, "checkbox" | "radio" | "submit" | "button">;
}
export interface Checkbox extends ControlBase {
type: "checkbox";
checked: boolean;
@ -56,6 +60,13 @@ export interface Dropdown extends Omit<ControlBase, "value"> {
options: string[] | ControlOption[];
}
export interface TextArea extends ControlBase {
type: "textarea";
value?: string;
rows?: number;
cols?: number;
}
export interface ControlOption {
label: string;
value: string;

View file

@ -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' ? (
<DropdownControl control={control as Dropdown} readOnly={readOnly} />
) :
control.type === 'textarea' ? (
<TextAreaControl control={control as TextArea} readOnly={readOnly} />
) : (
<Input control={control} readOnly={readOnly} />
)

View file

@ -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<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,
};
}, {});
---
<textarea
name={control.name}
id={control.name}
placeholder={control?.placeholder}
rows={control?.rows ?? 3}
cols={control?.cols ?? 21}
data-label={control?.label}
data-label-position={control?.labelPosition}
readonly={readOnly || null}
{...validatorAttributes}
>
{ control.value }
</textarea>

View file

@ -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;

View file

@ -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 = '<textarea';
const props = {
control: {
name: 'FAKE NAME',
type: 'textarea',
},
};
// act
component = await getComponentOutput('./components/controls/TextArea.astro', props);
const actualResult = cleanString(component.html);
// assert
expect(actualResult).to.contain(expectedHTML);
});
it('Should render a textarea with label', async () => {
// 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);
});
});