feat(form): implement TextArea control (#169)
This commit is contained in:
parent
c38e5dfbe3
commit
f93f8b6484
6 changed files with 192 additions and 4 deletions
|
@ -48,6 +48,12 @@ const form = new FormGroup([
|
||||||
options: ["S", "M", "L", "XL", "XXL"],
|
options: ["S", "M", "L", "XL", "XXL"],
|
||||||
placeholder: "-- Please choose an option --",
|
placeholder: "-- Please choose an option --",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "comment",
|
||||||
|
label: "Feedback",
|
||||||
|
type: "textarea",
|
||||||
|
value: "Nice!"
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
form.name = "Simple Form";
|
form.name = "Simple Form";
|
||||||
|
|
|
@ -25,7 +25,7 @@ export type InputType =
|
||||||
| "url"
|
| "url"
|
||||||
| "week";
|
| "week";
|
||||||
|
|
||||||
export type ControlType = InputType | "dropdown";
|
export type ControlType = InputType | "dropdown" | "textarea";
|
||||||
|
|
||||||
export interface ControlBase {
|
export interface ControlBase {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -39,6 +39,10 @@ export interface ControlBase {
|
||||||
options?: string[] | ControlOption[];
|
options?: string[] | ControlOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TextInput extends ControlBase {
|
||||||
|
type: Exclude<InputType, "checkbox" | "radio" | "submit" | "button">;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Checkbox extends ControlBase {
|
export interface Checkbox extends ControlBase {
|
||||||
type: "checkbox";
|
type: "checkbox";
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
|
@ -56,6 +60,13 @@ export interface Dropdown extends Omit<ControlBase, "value"> {
|
||||||
options: string[] | ControlOption[];
|
options: string[] | ControlOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TextArea extends ControlBase {
|
||||||
|
type: "textarea";
|
||||||
|
value?: string;
|
||||||
|
rows?: number;
|
||||||
|
cols?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ControlOption {
|
export interface ControlOption {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
/**
|
/**
|
||||||
* DEFAULT CONTROL COMPONENT
|
* 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 type { FormControl } from '../core/form-control';
|
||||||
import DropdownControl from './controls/Dropdown.astro';
|
import DropdownControl from './controls/Dropdown.astro';
|
||||||
|
import TextAreaControl from './controls/TextArea.astro';
|
||||||
import Input from './controls/Input.astro';
|
import Input from './controls/Input.astro';
|
||||||
import RadioGroup from './controls/RadioGroup.astro';
|
import RadioGroup from './controls/RadioGroup.astro';
|
||||||
import Errors from './Errors.astro';
|
import Errors from './Errors.astro';
|
||||||
|
@ -30,6 +31,9 @@ const hasErrors: boolean | null = !!control.errors?.length;
|
||||||
) :
|
) :
|
||||||
control.type === 'dropdown' ? (
|
control.type === 'dropdown' ? (
|
||||||
<DropdownControl control={control as Dropdown} readOnly={readOnly} />
|
<DropdownControl control={control as Dropdown} readOnly={readOnly} />
|
||||||
|
) :
|
||||||
|
control.type === 'textarea' ? (
|
||||||
|
<TextAreaControl control={control as TextArea} readOnly={readOnly} />
|
||||||
) : (
|
) : (
|
||||||
<Input control={control} readOnly={readOnly} />
|
<Input control={control} readOnly={readOnly} />
|
||||||
)
|
)
|
||||||
|
|
40
packages/form/components/controls/TextArea.astro
Normal file
40
packages/form/components/controls/TextArea.astro
Normal 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>
|
|
@ -1,16 +1,17 @@
|
||||||
import type {
|
import type {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
ControlBase,
|
|
||||||
ControlType,
|
ControlType,
|
||||||
|
TextInput,
|
||||||
Radio,
|
Radio,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
ControlOption,
|
ControlOption,
|
||||||
Submit,
|
Submit,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
|
TextArea,
|
||||||
} from '@astro-reactive/common';
|
} 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 {
|
export class FormControl {
|
||||||
private _name = '';
|
private _name = '';
|
||||||
|
@ -24,6 +25,8 @@ export class FormControl {
|
||||||
private _validators: string[] = [];
|
private _validators: string[] = [];
|
||||||
private _errors: ValidationError[] = [];
|
private _errors: ValidationError[] = [];
|
||||||
private _options: string[] | ControlOption[] = [];
|
private _options: string[] | ControlOption[] = [];
|
||||||
|
private _rows: number | null = null;
|
||||||
|
private _cols: number | null = null;
|
||||||
|
|
||||||
private validate: (value: string, validators: string[]) => ValidationError[] = (
|
private validate: (value: string, validators: string[]) => ValidationError[] = (
|
||||||
value: string,
|
value: string,
|
||||||
|
@ -55,6 +58,12 @@ export class FormControl {
|
||||||
this._validators = validators;
|
this._validators = validators;
|
||||||
this._options = options;
|
this._options = options;
|
||||||
|
|
||||||
|
if (config.type === 'textarea') {
|
||||||
|
const { rows = null, cols = null } = config;
|
||||||
|
this._rows = rows;
|
||||||
|
this._cols = cols;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: implement independence
|
// TODO: implement independence
|
||||||
// form should try to import validator,
|
// form should try to import validator,
|
||||||
// but handle error when it's not installed
|
// but handle error when it's not installed
|
||||||
|
@ -115,6 +124,14 @@ export class FormControl {
|
||||||
return this._options;
|
return this._options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get rows() {
|
||||||
|
return this._rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
get cols() {
|
||||||
|
return this._cols;
|
||||||
|
}
|
||||||
|
|
||||||
setValue(value: string) {
|
setValue(value: string) {
|
||||||
this._value = value;
|
this._value = value;
|
||||||
this._isPristine = false;
|
this._isPristine = false;
|
||||||
|
|
110
packages/form/test/TextArea.astro.test.js
Normal file
110
packages/form/test/TextArea.astro.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue