feat(form): add Dropdown control (#158)
This commit is contained in:
parent
5a96395280
commit
304cad0151
9 changed files with 229 additions and 14 deletions
|
@ -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";
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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} />
|
||||||
)
|
)
|
||||||
|
|
42
packages/form/components/controls/Dropdown.astro
Normal file
42
packages/form/components/controls/Dropdown.astro
Normal 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>
|
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
152
packages/form/test/Dropdown.astro.test.js
Normal file
152
packages/form/test/Dropdown.astro.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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: {
|
||||||
|
|
Loading…
Reference in a new issue