Skip to content

Commit d499bc3

Browse files
committed
✨ Add useSmartFill to useForm hook
1 parent 4c05079 commit d499bc3

File tree

3 files changed

+132
-1
lines changed

3 files changed

+132
-1
lines changed

src/blocks/Form/useForm.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type FormActions = {
1111
update: (field: string, value: string | boolean) => FormActions
1212
setValidations: (validationRules: Record<string, boolean>) => FormActions
1313
isValidForm: () => boolean,
14+
useSmartFill: (options?: { patterns?: Record<string, RegExp>, inputs?: Record<string, string> }) => FormActions
1415
onChange: (callback: (formValues: Record<string, string>) => void) => FormActions
1516
onSubmit: (callback: (formValues: Record<string, string>) => void) => FormActions
1617
onError: (callback: (invalidFields: string[]) => void) => FormActions
@@ -84,6 +85,113 @@ export const useForm = (selector: string): FormActions | null => {
8485
isValidForm() {
8586
return Object.values(this.validationRules).every(key => key)
8687
},
88+
useSmartFill({ patterns, inputs } = {}) {
89+
const textPatterns = {
90+
email: /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/gm,
91+
phone: /\b(?:\+?\d{1,3}[\s-]?)?(?:\(?\d{2,4}\)?[\s-]?)?\d{3,4}[\s-]?\d{3,4}\b/gm,
92+
postal: /^[0-9]{4,5}(?:-[0-9]{4})?$/gm,
93+
date: /\b\d{1,4}[-/.]\d{1,2}[-/.]\d{1,4}\b/gm,
94+
url: /\bhttps?:\/\/[^\s/$.?#].[^\s]*\b/gm,
95+
...(patterns || {})
96+
}
97+
98+
const fieldHints: Record<string, string[]> = {
99+
email: ['mail', 'email', 'eaddress'],
100+
phone: ['phone', 'mobile', 'tel', 'cell'],
101+
postal: ['zip', 'postal', 'postcode'],
102+
date: ['date', 'dob', 'birth', 'day'],
103+
url: ['url', 'website', 'link']
104+
}
105+
106+
const typeMap = {
107+
email: 'email',
108+
url: 'url',
109+
date: 'date',
110+
tel: 'phone'
111+
} as const
112+
113+
const parseText = (text: string) => {
114+
const extracted: Record<string, string> = {}
115+
116+
for (const [key, regex] of Object.entries(textPatterns)) {
117+
const match = text.trim().match(regex)
118+
119+
if (match) {
120+
extracted[key] = match[0].trim()
121+
}
122+
}
123+
124+
return extracted
125+
}
126+
127+
const inferFieldType = (name: string, type: string) => {
128+
const lowered = name.toLowerCase().replace(/[_\-\d]/g, '')
129+
130+
for (const [key, mappedName] of Object.entries(inputs || {})) {
131+
if (mappedName.toLowerCase() === name.toLowerCase()) {
132+
return key
133+
}
134+
}
135+
136+
if (type in typeMap) {
137+
return typeMap[type as keyof typeof typeMap]
138+
}
139+
140+
for (const [key, hints] of Object.entries(fieldHints)) {
141+
if (hints.some(hint => lowered.includes(hint))) {
142+
return key
143+
}
144+
}
145+
146+
return null
147+
}
148+
149+
form.addEventListener('paste', (event: ClipboardEvent) => {
150+
const target = event.target
151+
152+
if (!(target instanceof HTMLInputElement)) {
153+
return
154+
}
155+
156+
const pastedText = event.clipboardData?.getData('text') || ''
157+
158+
if (pastedText?.length < 5) {
159+
return
160+
}
161+
162+
const extracted = parseText(pastedText)
163+
164+
if (!Object.keys(extracted).length) {
165+
return
166+
}
167+
168+
let autoFilled = false
169+
170+
Array.from(form.elements).forEach(element => {
171+
if (element instanceof HTMLInputElement
172+
|| element instanceof HTMLSelectElement
173+
|| element instanceof HTMLTextAreaElement
174+
) {
175+
const inferred = inferFieldType(element.name, element.type)
176+
177+
if (inferred && extracted[inferred]) {
178+
const inputEvent = new Event('input', { bubbles: true })
179+
180+
element.value = extracted[inferred]
181+
element.dispatchEvent(inputEvent)
182+
183+
autoFilled = true
184+
}
185+
}
186+
})
187+
188+
if (autoFilled) {
189+
event.preventDefault()
190+
}
191+
})
192+
193+
return this
194+
},
87195
onChange(callback) {
88196
form.addEventListener('input', () => {
89197
callback?.(this.getInputValues())

src/pages/blocks/form.astro

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ const getThemedContactForm = (theme: ButtonProps['theme']): FormProps['fields']
2727
{ type: 'textarea', label: 'Message', name: 'message' },
2828
{ type: 'button', label: 'Send', theme }
2929
]
30+
31+
const getUserForm = (): FormProps['fields'] => [
32+
{ type: 'email', label: 'E-mail', name: 'email' },
33+
{ type: 'tel', label: 'Phone', name: 'phone' },
34+
{ type: 'url', label: 'Site', name: 'site' },
35+
{ type: 'number', label: 'Zip', name: 'zip' },
36+
{ type: 'date', label: 'DOB', name: 'dob' },
37+
{ type: 'textarea', label: 'Message', name: 'message' },
38+
{ type: 'button', label: 'Send' }
39+
]
3040
---
3141

3242
<Layout>
@@ -165,10 +175,22 @@ const getThemedContactForm = (theme: ButtonProps['theme']): FormProps['fields']
165175

166176
<Card title="Form with useForm hook">
167177
<section.component
168-
fields={getThemedContactForm(undefined)}
178+
fields={getUserForm()}
169179
id="form"
170180
/>
171181
</Card>
182+
183+
<Card>
184+
<span class="muted">Copy the following text and paste it in the form next to it to test smart fill with the <code>useForm</code> hook.</span>
185+
186+
<ul>
187+
<li>jane@doe.com</li>
188+
<li>+1 555-123-4567</li>
189+
<li>2025-01-01</li>
190+
<li>https://jane@example.com</li>
191+
<li>90210</li>
192+
</ul>
193+
</Card>
172194
</div>
173195
))}
174196

@@ -184,6 +206,7 @@ const getThemedContactForm = (theme: ButtonProps['theme']): FormProps['fields']
184206

185207
if (form) {
186208
form.preventDefault()
209+
.useSmartFill()
187210
.update('email', 'john@doe.com')
188211
.update('message', 'You can update values dynamically')
189212
.onChange(formValues => {
64.2 KB
Loading

0 commit comments

Comments
 (0)