Skip to content

Commit 22eb1d0

Browse files
committed
✨ Add Form component
1 parent 53fc602 commit 22eb1d0

File tree

9 files changed

+512
-1
lines changed

9 files changed

+512
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ import { Accordion } from 'webcoreui/react'
291291
- [ErrorPage](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/ErrorPage)
292292
- [ExpandableTable](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/ExpandableTable)
293293
- [FAQ](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/FAQ)
294+
- [Form](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/Form)
294295
- [GridWithIcons](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/GridWithIcons)
295296
- [Hero](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/Hero)
296297
- [Icon](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/Icon)

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default [
3737
'eqeqeq': 'error',
3838
'for-direction': 'error',
3939
'func-call-spacing': 'error',
40-
'indent': ['error', 4],
40+
'indent': ['error', 4, { 'SwitchCase': 1 }],
4141
'jsx-quotes': 'error',
4242
'key-spacing': 'error',
4343
'keyword-spacing': 'error',

src/blocks/Form/Form.astro

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
import {
3+
Button,
4+
Checkbox,
5+
Input,
6+
Radio,
7+
Select,
8+
Slider,
9+
Switch,
10+
Textarea
11+
} from 'webcoreui/astro'
12+
13+
import type { FormProps } from './form'
14+
15+
interface Props extends FormProps {}
16+
17+
const {
18+
fields,
19+
gap,
20+
className,
21+
...rest
22+
} = Astro.props
23+
24+
const classes = [
25+
'grid',
26+
gap || 'md',
27+
className
28+
]
29+
---
30+
31+
<form class:list={classes} {...rest}>
32+
{fields.map(field => {
33+
switch (field.type) {
34+
case 'button': return (
35+
<Button type="submit" {...(({ label, type, ...rest }) => rest)(field)}>{field.label}</Button>
36+
)
37+
case 'checkbox': return <Checkbox {...field} />
38+
case 'radio': return <Radio {...field} />
39+
case 'select': return <Select {...field} />
40+
case 'slider': return <Slider {...field} />
41+
case 'switch': return <Switch {...field} />
42+
case 'textarea': return <Textarea {...field} />
43+
default: return <Input {...field} />
44+
}
45+
})}
46+
</form>

src/blocks/Form/Form.svelte

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<script lang="ts">
2+
import { classNames } from 'webcoreui'
3+
import {
4+
Button,
5+
Checkbox,
6+
Input,
7+
Radio,
8+
Select,
9+
Slider,
10+
Switch,
11+
Textarea
12+
} from 'webcoreui/svelte'
13+
14+
import type { FormProps } from './form'
15+
16+
const {
17+
fields,
18+
gap,
19+
className,
20+
...rest
21+
}: FormProps = $props()
22+
23+
const classes = classNames([
24+
'grid',
25+
gap || 'md',
26+
className
27+
])
28+
</script>
29+
30+
<form class={classes} {...rest}>
31+
{#each fields as field}
32+
{#if field.type === 'button'}
33+
<Button {...(({ label, type, ...rest }) => rest)(field)} type="submit">
34+
{field.label}
35+
</Button>
36+
{:else if field.type === 'checkbox'}
37+
<Checkbox {...field} />
38+
{:else if field.type === 'radio'}
39+
<Radio {...field} />
40+
{:else if field.type === 'select'}
41+
<Select {...field} />
42+
{:else if field.type === 'slider'}
43+
<Slider {...field} />
44+
{:else if field.type === 'switch'}
45+
<Switch {...field} />
46+
{:else if field.type === 'textarea'}
47+
<Textarea {...field} />
48+
{:else}
49+
<Input {...field} />
50+
{/if}
51+
{/each}
52+
</form>

src/blocks/Form/Form.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from 'react'
2+
import { classNames } from 'webcoreui'
3+
import {
4+
Button,
5+
Checkbox,
6+
Input,
7+
Radio,
8+
Select,
9+
Slider,
10+
Switch,
11+
Textarea
12+
} from 'webcoreui/react'
13+
14+
import type { FormProps } from './form'
15+
16+
const Form = ({
17+
fields,
18+
gap,
19+
className,
20+
...rest
21+
}: FormProps) => {
22+
const classes = classNames([
23+
'grid',
24+
gap || 'md',
25+
className
26+
])
27+
28+
return (
29+
<form className={classes} {...rest}>
30+
{fields.map(field => {
31+
switch (field.type) {
32+
case 'button': return (
33+
<Button type="submit" {...(({ label, type, ...rest }) => rest)(field)}>{field.label}</Button>
34+
)
35+
case 'checkbox': return <Checkbox {...field} />
36+
case 'radio': return <Radio {...field} />
37+
case 'select': return <Select {...field} />
38+
case 'slider': return <Slider {...field} />
39+
case 'switch': return <Switch {...field} />
40+
case 'textarea': return <Textarea {...field} />
41+
default: return <Input {...field} />
42+
}
43+
})}
44+
</form>
45+
)
46+
}
47+
48+
export default Form

src/blocks/Form/form.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { Gap } from 'webcoreui'
2+
import type {
3+
ButtonProps,
4+
CheckboxProps,
5+
InputProps,
6+
RadioProps,
7+
SelectProps,
8+
SliderProps,
9+
SwitchProps,
10+
TextareaProps
11+
} from 'webcoreui/astro'
12+
13+
export type FormField =
14+
| ({ type: 'button', label: string } & ButtonProps)
15+
| ({ type: 'checkbox' } & CheckboxProps)
16+
| ({ type: 'radio' } & RadioProps)
17+
| ({ type: 'select' } & SelectProps)
18+
| ({ type: 'slider' } & SliderProps)
19+
| ({ type: 'switch' } & SwitchProps)
20+
| ({ type: 'textarea' } & TextareaProps)
21+
| ({ type: InputProps['type'] } & Omit<InputProps, 'type'>)
22+
23+
export type FormProps = {
24+
fields: FormField[]
25+
gap?: Gap
26+
className?: string
27+
id?: string
28+
name?: string
29+
action?: string
30+
method?: 'post' | 'get' | 'dialog'
31+
noValidate?: boolean
32+
target?: ButtonProps['target']
33+
enctype?: 'application/x-www-form-urlencoded'
34+
| 'multipart/form-data'
35+
|'text/plain'
36+
}
37+

src/blocks/Form/useForm.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { get } from 'webcoreui'
2+
3+
export type FormActions = {
4+
validationRules: Record<string, boolean>
5+
isPreventDefault: boolean
6+
onErrorCallback: ((invalidFields: string[]) => void) | null
7+
preventDefault: () => FormActions
8+
getInput: (field: string) => HTMLInputElement | null
9+
getInputValue: (field: string) => string | null
10+
getInputValues: () => Record<string, string>
11+
update: (field: string, value: string) => FormActions
12+
setValidations: (validationRules: Record<string, boolean>) => FormActions
13+
isValidForm: () => boolean,
14+
onChange: (callback: (formValues: Record<string, string>) => void) => FormActions
15+
onSubmit: (callback: (formValues: Record<string, string>) => void) => FormActions
16+
onError: (callback: (invalidFields: string[]) => void) => FormActions
17+
}
18+
19+
export const useForm = (selector: string): FormActions | null => {
20+
const form = get(selector) as HTMLFormElement
21+
22+
if (!form) {
23+
return null
24+
}
25+
26+
const missingNameFields = Array.from(form.elements).filter(element =>
27+
element instanceof HTMLInputElement
28+
|| element instanceof HTMLSelectElement
29+
|| element instanceof HTMLTextAreaElement
30+
).filter(element => !element.name)
31+
32+
if (missingNameFields.length) {
33+
// eslint-disable-next-line no-console
34+
console.error([
35+
'name attribute is missing for the following elements:\n\n',
36+
missingNameFields.map(el => el.outerHTML).join('\n\n'),
37+
'\n\nForm values will not be picked up.'
38+
].join(' '))
39+
}
40+
41+
return {
42+
validationRules: {},
43+
isPreventDefault: false,
44+
onErrorCallback: null,
45+
preventDefault() {
46+
this.isPreventDefault = true
47+
48+
return this
49+
},
50+
getInput(field) {
51+
return form.querySelector(`[name=${field}]`)
52+
},
53+
getInputValue(field) {
54+
return String(new FormData(form).get(field))
55+
},
56+
getInputValues() {
57+
const formData = new FormData(form)
58+
const formValues: Record<string, string> = {}
59+
60+
for (const [key, value] of formData.entries()) {
61+
formValues[key] = String(value)
62+
}
63+
64+
return formValues
65+
},
66+
update(field, value) {
67+
const input = form.querySelector(`[name=${field}]`) as HTMLInputElement
68+
69+
if (input) {
70+
input.value = value
71+
}
72+
73+
return this
74+
},
75+
setValidations(validationRules) {
76+
this.validationRules = validationRules
77+
78+
return this
79+
},
80+
isValidForm() {
81+
return Object.values(this.validationRules).every(key => key)
82+
},
83+
onChange(callback) {
84+
form.addEventListener('input', () => {
85+
callback?.(this.getInputValues())
86+
})
87+
88+
return this
89+
},
90+
onSubmit(callback) {
91+
form.addEventListener('submit', event => {
92+
if (this.isPreventDefault) {
93+
event.preventDefault()
94+
}
95+
96+
if (this.isValidForm()) {
97+
callback?.(this.getInputValues())
98+
}
99+
100+
if (!this.isValidForm() && this.onErrorCallback) {
101+
const invalidFields = Object
102+
.keys(this.validationRules)
103+
.filter(key => !this.validationRules[key])
104+
105+
this.onErrorCallback(invalidFields)
106+
}
107+
})
108+
109+
return this
110+
},
111+
onError(callback) {
112+
this.onErrorCallback = callback
113+
114+
return this
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)