feat(form): add Dropdown control (#158)

This commit is contained in:
Woramat Ngamkham 2022-10-29 00:48:26 +07:00 committed by GitHub
parent 5a96395280
commit 304cad0151
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 229 additions and 14 deletions

View file

@ -39,6 +39,13 @@ const form = new FormGroup([
{ label: "Disagree", value: "no" }, { label: "Disagree", value: "no" },
], ],
}, },
{
name: "size",
label: "Size",
type: "dropdown",
options: ["S", "M", "L", "XL", "XXL"],
placeholder: "-- Please choose an option --",
},
]); ]);
form.name = "Simple Form"; form.name = "Simple Form";

View file

@ -2,7 +2,7 @@
* `ControlType` determines the type of form control * `ControlType` determines the type of form control
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types
*/ */
export type ControlType = export type InputType =
| "text" | "text"
| "checkbox" | "checkbox"
| "radio" | "radio"
@ -25,6 +25,8 @@ export type ControlType =
| "url" | "url"
| "week"; | "week";
export type ControlType = InputType | "dropdown";
export interface ControlBase { export interface ControlBase {
name: string; name: string;
id?: string; id?: string;
@ -34,7 +36,7 @@ export interface ControlBase {
labelPosition?: "right" | "left"; labelPosition?: "right" | "left";
placeholder?: string; placeholder?: string;
validators?: string[]; // TODO: implement validator type validators?: string[]; // TODO: implement validator type
options?: string[] | RadioOption[]; // prevent build failed options?: string[] | ControlOption[];
} }
export interface Checkbox extends ControlBase { export interface Checkbox extends ControlBase {
@ -45,10 +47,16 @@ export interface Checkbox extends ControlBase {
export interface Radio extends Omit<ControlBase, "value"> { export interface Radio extends Omit<ControlBase, "value"> {
type: "radio"; type: "radio";
value?: string; value?: string;
options: string[] | RadioOption[]; options: string[] | ControlOption[];
} }
export interface RadioOption { export interface Dropdown extends Omit<ControlBase, "value"> {
type: "dropdown";
value?: string;
options: string[] | ControlOption[];
}
export interface ControlOption {
label: string; label: string;
value: string; value: string;
} }

View file

@ -2,8 +2,9 @@
/** /**
* DEFAULT CONTROL COMPONENT * DEFAULT CONTROL COMPONENT
*/ */
import type { Radio } from '@astro-reactive/common'; import type { Radio, Dropdown } 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 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';
@ -25,6 +26,9 @@ const hasErrors: boolean | null = !!control.errors?.length;
{ {
control.type === 'radio' ? ( control.type === 'radio' ? (
<RadioGroup control={control as Radio} /> <RadioGroup control={control as Radio} />
) :
control.type === 'dropdown' ? (
<DropdownControl control={control as Dropdown} />
) : ( ) : (
<Input control={control} /> <Input control={control} />
) )

View file

@ -0,0 +1,42 @@
---
/**
* DROPDOWN COMPONENT
*/
import type { Dropdown, ControlOption } from '@astro-reactive/common';
export interface Props {
control: Dropdown;
}
const { control } = Astro.props;
const options = control.options.map((option: string | ControlOption) => {
if (typeof option === 'string') {
return {
label: option,
value: option,
};
}
return option;
});
---
<select name={control.name} id={control.name}>
{
control?.placeholder && (
<option value="" disabled selected={!control?.value}>
{control.placeholder}
</option>
)
}
{
options.map((option: ControlOption) => (
<option
value={option.value}
selected={option.value === control.value}
>
{option.label}
</option>
))
}
</select>

View file

@ -2,6 +2,7 @@
/** /**
* DEFAULT INPUT COMPONENT * DEFAULT INPUT COMPONENT
*/ */
import type { InputType } from '@astro-reactive/common';
import type { FormControl } from '../../core/form-control'; import type { FormControl } from '../../core/form-control';
export interface Props { export interface Props {
@ -29,7 +30,7 @@ const validatorAttributes: Record<string, string> = validators?.reduce((prev, va
<input <input
name={control.name} name={control.name}
id={control.name} id={control.name}
type={control.type} type={control.type as InputType}
value={control.value?.toString()} value={control.value?.toString()}
checked={control.value === 'checked'} checked={control.value === 'checked'}
placeholder={control.placeholder} placeholder={control.placeholder}

View file

@ -2,7 +2,7 @@
/** /**
* RADIO GROUP COMPONENT * RADIO GROUP COMPONENT
*/ */
import type { Radio } from '@astro-reactive/common'; import type { Radio, ControlOption } from '@astro-reactive/common';
export interface Props { export interface Props {
control: Radio; control: Radio;
@ -10,7 +10,7 @@ export interface Props {
const { control } = Astro.props; const { control } = Astro.props;
const options = control.options.map((option) => { const options = control.options.map((option: string | ControlOption) => {
if (typeof option === 'string') { if (typeof option === 'string') {
return { return {
label: option, label: option,
@ -22,7 +22,7 @@ const options = control.options.map((option) => {
--- ---
{ {
options.map((option) => ( options.map((option: ControlOption) => (
<div class="radio-option"> <div class="radio-option">
<input <input
type="radio" type="radio"

View file

@ -4,17 +4,18 @@ import type {
ControlBase, ControlBase,
ControlType, ControlType,
Radio, Radio,
RadioOption, Dropdown,
ControlOption,
Submit, Submit,
ValidationError, ValidationError,
} from '@astro-reactive/common'; } from '@astro-reactive/common';
export type ControlConfig = ControlBase | Checkbox | Radio | Submit | Button; export type ControlConfig = ControlBase | Checkbox | Radio | Submit | Button | Dropdown;
export class FormControl { export class FormControl {
private _name = ''; private _name = '';
private _type: ControlType = 'text'; private _type: ControlType = 'text';
private _value?: string | number | null | string[] | RadioOption[]; private _value?: string | number | null | string[] | ControlOption[];
private _label = ''; private _label = '';
private _labelPosition?: 'right' | 'left' = 'left'; private _labelPosition?: 'right' | 'left' = 'left';
private _isValid = true; private _isValid = true;
@ -22,7 +23,7 @@ export class FormControl {
private _placeholder: string | null = null; private _placeholder: string | null = null;
private _validators: string[] = []; private _validators: string[] = [];
private _errors: ValidationError[] = []; private _errors: ValidationError[] = [];
private _options: string[] | RadioOption[] = []; private _options: string[] | ControlOption[] = [];
private validate: (value: string, validators: string[]) => ValidationError[] = ( private validate: (value: string, validators: string[]) => ValidationError[] = (
value: string, value: string,

View file

@ -0,0 +1,152 @@
import { expect } from 'chai';
import { describe, beforeEach, it } from 'mocha';
import { getComponentOutput } from 'astro-component-tester';
import { cleanString } from './utils/index.js';
describe('Dropdown.astro test', () => {
let component;
beforeEach(() => {
component = undefined;
});
it('Should render all dropdown string[] options', async () => {
// arrange
const expectedOptions = 3;
const element = /<option/g;
const props = {
control: {
label: 'FAKE LABEL',
name: 'FAKE NAME',
type: 'dropdown',
options: ['one', 'two', 'three'],
},
showValidationHints: true,
};
// act
component = await getComponentOutput('./components/controls/Dropdown.astro', props);
const actualResult = cleanString(component.html);
const matches = actualResult.match(element) || [];
// assert
expect(matches.length).to.equal(expectedOptions);
});
it('Should render all dropdown ControlOption[] options', async () => {
// arrange
const expectedOptions = 3;
const element = /<option/g;
const props = {
control: {
label: 'FAKE LABEL',
name: 'FAKE NAME',
type: 'dropdown',
options: [
{
label: 'one',
value: 'one',
},
{
label: 'two',
value: 'two',
},
{
label: 'three',
value: 'three',
},
],
},
showValidationHints: true,
};
// act
component = await getComponentOutput('./components/controls/Dropdown.astro', props);
const actualResult = cleanString(component.html);
const matches = actualResult.match(element) || [];
// assert
expect(matches.length).to.equal(expectedOptions);
});
it('Should render a dropdown with placeholder', async () => {
// arrange
const expectedResult = '<optionvalue=""disabledselected="true">TEST</option>';
const expectedOptions = 4;
const element = /<option/g;
const props = {
control: {
label: 'FAKE LABEL',
name: 'FAKE NAME',
type: 'dropdown',
placeholder: 'TEST',
options: ['one', 'two', 'three'],
},
showValidationHints: true,
};
// act
component = await getComponentOutput('./components/controls/Dropdown.astro', props);
const actualResult = cleanString(component.html);
const matches = actualResult.match(element) || [];
// assert
expect(matches.length).to.equal(expectedOptions);
expect(actualResult).to.contain(expectedResult);
});
it('Should select a selected dropdown option from string[] correctly', async () => {
const expectedResult = '<optionvalue="two"selected="true">';
const props = {
control: {
label: 'FAKE LABEL',
name: 'FAKE NAME',
type: 'dropdown',
value: 'two',
options: ['one', 'two', 'three'],
},
showValidationHints: true,
};
// act
component = await getComponentOutput('./components/controls/Dropdown.astro', props);
const actualResult = cleanString(component.html);
// assert
expect(actualResult).to.contain(expectedResult);
});
it('Should select a selected dropdown option from ControlOption[] correctly', async () => {
const expectedResult = '<optionvalue="three"selected="true">';
const props = {
control: {
label: 'FAKE LABEL',
name: 'FAKE NAME',
type: 'dropdown',
value: 'three',
options: [
{
value: 'one',
label: '1',
},
{
value: 'two',
label: '2',
},
{
value: 'three',
label: '3',
},
],
},
showValidationHints: true,
};
// act
component = await getComponentOutput('./components/controls/Dropdown.astro', props);
const actualResult = cleanString(component.html);
// assert
expect(actualResult).to.contain(expectedResult);
});
});

View file

@ -54,7 +54,7 @@ describe('RadioGroup.astro test', () => {
expect(actualResult).to.contain(expectedResult); expect(actualResult).to.contain(expectedResult);
}); });
it('Should render a checked radio option from RadioOption[] correctly', async () => { it('Should render a checked radio option from ControlOption[] correctly', async () => {
const expectedResult = '<inputtype="radio"name="FAKENAME"value="three"checked="true">'; const expectedResult = '<inputtype="radio"name="FAKENAME"value="three"checked="true">';
const props = { const props = {
control: { control: {