html-form-field 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/html-form-field.cjs +353 -0
- package/dist/html-form-field.min.js +1 -0
- package/dist/html-form-field.mjs +351 -0
- package/dist/package.json +1 -0
- package/package.json +64 -0
- package/src/form-field.ts +207 -0
- package/src/form-item.ts +148 -0
- package/src/index.ts +5 -0
- package/types/html-form-field.d.ts +186 -0
package/src/form-item.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type {FormField, formField as NS} from "../types/html-form-field.d.ts"
|
|
2
|
+
|
|
3
|
+
const triggerOnWrite = (field: FormField) => {
|
|
4
|
+
const onWrite = field.options.onWrite
|
|
5
|
+
if (onWrite) onWrite(field)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const checkableMap: Record<string, boolean> = {
|
|
9
|
+
option: true,
|
|
10
|
+
radio: true,
|
|
11
|
+
checkbox: true,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type UpperCaseTagNameMap = { [K in Uppercase<keyof HTMLElementTagNameMap>]: HTMLElementTagNameMap[Lowercase<K>] }
|
|
15
|
+
|
|
16
|
+
const isElementOf = <N extends keyof UpperCaseTagNameMap>(v: Node, tagName: N): v is UpperCaseTagNameMap[N] => ((v as HTMLElement).tagName === tagName)
|
|
17
|
+
|
|
18
|
+
export const formItemList = (field: FormField, nodeList: Iterable<NS.FieldElement>) => {
|
|
19
|
+
const list: NS.FormItem[] = []
|
|
20
|
+
|
|
21
|
+
for (const node of nodeList) {
|
|
22
|
+
if (isElementOf(node, "SELECT")) {
|
|
23
|
+
for (const option of node.options) {
|
|
24
|
+
list.push(new OptionItem(field, option))
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
list.push(new InputItem(field, node))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return list
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
abstract class BaseFormItem<E extends NS.ItemElement> implements NS.FormItem<E> {
|
|
35
|
+
protected readonly field: FormField
|
|
36
|
+
abstract readonly checkable: boolean
|
|
37
|
+
readonly node: E
|
|
38
|
+
|
|
39
|
+
protected constructor(field: FormField, node: E) {
|
|
40
|
+
this.field = field
|
|
41
|
+
this.node = node
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get value() {
|
|
45
|
+
return this.node.value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
set value(value: string) {
|
|
49
|
+
this.setValue(value)
|
|
50
|
+
triggerOnWrite(this.field)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setValue(value: string) {
|
|
54
|
+
this.node.value = value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get checked(): boolean {
|
|
58
|
+
return this.getChecked()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected abstract getChecked(): boolean
|
|
62
|
+
|
|
63
|
+
set checked(checked: boolean) {
|
|
64
|
+
const prev = this.checked
|
|
65
|
+
if (prev !== checked) {
|
|
66
|
+
this.setChecked(checked)
|
|
67
|
+
triggerOnWrite(this.field)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
abstract setChecked(checked: boolean): void
|
|
72
|
+
|
|
73
|
+
get disabled() {
|
|
74
|
+
return this.node.disabled
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
set disabled(disabled: boolean) {
|
|
78
|
+
this.node.disabled = disabled
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
abstract get label(): string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class InputItem<E extends HTMLInputElement | HTMLTextAreaElement | HTMLButtonElement> extends BaseFormItem<E> {
|
|
85
|
+
readonly checkable: boolean
|
|
86
|
+
|
|
87
|
+
constructor(entry: FormField, node: E) {
|
|
88
|
+
super(entry, node)
|
|
89
|
+
this.checkable = checkableMap[node.type]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected getChecked() {
|
|
93
|
+
return (this.node as HTMLInputElement).checked
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setChecked(checked: boolean) {
|
|
97
|
+
(this.node as HTMLInputElement).checked = checked
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get label() {
|
|
101
|
+
const node = this.node
|
|
102
|
+
const label = node.closest("label")
|
|
103
|
+
const siblings = label && label.querySelectorAll(node.tagName)
|
|
104
|
+
|
|
105
|
+
if (siblings && siblings.length === 1) {
|
|
106
|
+
const text = pickText(label)
|
|
107
|
+
if (text) return text
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const labels = node.labels
|
|
111
|
+
for (const label of labels) {
|
|
112
|
+
const text = pickText(label)
|
|
113
|
+
if (text) return text
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const text = pickText(node)
|
|
117
|
+
if (text) return text
|
|
118
|
+
|
|
119
|
+
function pickText(node: HTMLElement) {
|
|
120
|
+
const text = node.innerText || node.textContent
|
|
121
|
+
if (text) return text.trim()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
class OptionItem<E extends HTMLOptionElement> extends BaseFormItem<E> {
|
|
127
|
+
readonly checkable: boolean
|
|
128
|
+
|
|
129
|
+
constructor(entry: FormField, node: E) {
|
|
130
|
+
super(entry, node)
|
|
131
|
+
this.checkable = true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
protected getChecked() {
|
|
135
|
+
return this.node.selected
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setChecked(checked: boolean) {
|
|
139
|
+
// Note: Setting false may trigger auto-selection of the first option on select-one
|
|
140
|
+
this.node.selected = checked
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get label() {
|
|
144
|
+
const node = this.node
|
|
145
|
+
const text = node.label || node.textContent
|
|
146
|
+
if (text) return text.trim()
|
|
147
|
+
}
|
|
148
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* html-form-field - Unified interface for HTML form fields with synchronized binding to object properties
|
|
3
|
+
*/
|
|
4
|
+
declare namespace formField {
|
|
5
|
+
type FieldElement = HTMLInputElement | HTMLTextAreaElement | HTMLButtonElement | HTMLSelectElement
|
|
6
|
+
|
|
7
|
+
type ItemElement = HTMLInputElement | HTMLTextAreaElement | HTMLButtonElement | HTMLOptionElement
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract string keys of T
|
|
11
|
+
* - If T is undefined: string (allow any name)
|
|
12
|
+
* - If T is an object: union of keys K where T[K] extends string (strict)
|
|
13
|
+
*/
|
|
14
|
+
type StringKeys<T> = [T] extends [undefined]
|
|
15
|
+
? string
|
|
16
|
+
: { [K in keyof T]: K extends string ? (T[K] extends string ? K : never) : never }[keyof T];
|
|
17
|
+
|
|
18
|
+
type OnWrite<T> = FormFieldOptions<T>["onWrite"]
|
|
19
|
+
|
|
20
|
+
type OnChange<T> = FormFieldOptions<T>["onChange"]
|
|
21
|
+
|
|
22
|
+
interface FormFieldOptions<T = any> {
|
|
23
|
+
/**
|
|
24
|
+
* form element or container element which includes field elements
|
|
25
|
+
*/
|
|
26
|
+
form: HTMLFormElement | FieldElement | ParentNode
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* name of the field element
|
|
30
|
+
* - When `bindTo` is supplied, TypeScript infers `T` from that object and `name` is restricted
|
|
31
|
+
* to keys of the bound object whose value type is `string`.
|
|
32
|
+
* - When `bindTo` is not supplied (or `T` is not inferred), any string is allowed.
|
|
33
|
+
*/
|
|
34
|
+
name: StringKeys<T>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Object to synchronize form values.
|
|
38
|
+
* Only properties matching field names obtained via item() will have getters/setters defined.
|
|
39
|
+
* The getter always references the value of the DOM field,
|
|
40
|
+
* and the setter updates the DOM and triggers onWrite.
|
|
41
|
+
* Other properties are not affected.
|
|
42
|
+
*/
|
|
43
|
+
bindTo?: T
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Called when the value is changed by user interaction,
|
|
47
|
+
* or when assigning to a bound object's property.
|
|
48
|
+
* Fires before onChange.
|
|
49
|
+
*/
|
|
50
|
+
onWrite?: (field: FormField<T>) => void
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Called when the value is changed by user interaction (change event).
|
|
54
|
+
* Not triggered by setter assignments. Fires after onWrite.
|
|
55
|
+
*/
|
|
56
|
+
onChange?: (item: FormField<T>) => void
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* delimiter for multiple values (for checkboxes, multi-select)
|
|
60
|
+
* Default: `,` (comma). Use Unit Separator `\x1F` instead,
|
|
61
|
+
* if your values may include commas to reduce collisions.
|
|
62
|
+
*/
|
|
63
|
+
delim?: string
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Default values to attempt at initialization (in order).
|
|
67
|
+
* For checkbox/radio/select fields, tries the first value; if no matching option exists,
|
|
68
|
+
* falls back to the next value, and so on until a match is found.
|
|
69
|
+
*/
|
|
70
|
+
defaults?: string[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface FormField<T = any> {
|
|
74
|
+
readonly options: FormFieldOptions<T>
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* name of the field element (input, select, textarea)
|
|
78
|
+
*/
|
|
79
|
+
readonly name: StringKeys<T>
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* getter/setter of the value.
|
|
83
|
+
* setter triggers onWrite handler.
|
|
84
|
+
* Note: even for checkbox groups / multiple-select, the exposed type is string.
|
|
85
|
+
* Multiple values are represented as a single string joined by options.delim (default ',').
|
|
86
|
+
*/
|
|
87
|
+
value: string
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* rebind the field element (input, select, textarea).
|
|
91
|
+
* Call this when the set of option elements changes (for example when checkboxes,
|
|
92
|
+
* radio buttons, or select <option> elements are added or removed) so that the
|
|
93
|
+
* FormField re-scans and rebinds its items to reflect the current DOM.
|
|
94
|
+
*/
|
|
95
|
+
reload(): void
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* list of all items (including disabled items)
|
|
99
|
+
* - for checkbox, radio, select: returns all options
|
|
100
|
+
* - for other types: returns all item(s)
|
|
101
|
+
*/
|
|
102
|
+
items(): FormItem[]
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* list of active items (excluding disabled items)
|
|
106
|
+
* - for checkbox, radio: returns checked and not-disabled items
|
|
107
|
+
* - for select: returns selected and not-disabled options
|
|
108
|
+
* - for other types: returns all not-disabled item(s)
|
|
109
|
+
*/
|
|
110
|
+
current(): FormItem[]
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* select/deselect an option (for checkbox, multi-select)
|
|
114
|
+
*/
|
|
115
|
+
toggle(value: string, checked?: boolean): boolean
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* check if an option is selected (for checkbox, multi-select)
|
|
119
|
+
*/
|
|
120
|
+
has(value: string): boolean
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Shortcut to access an item by index (operates on items()).
|
|
124
|
+
* Equivalent to items().at(index). Returns undefined if out of range.
|
|
125
|
+
*/
|
|
126
|
+
itemAt(index: number): FormItem | undefined
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Shortcut to access an item by value (operates on items()).
|
|
130
|
+
* Returns the first FormItem whose value equals the given value, or undefined if not found.
|
|
131
|
+
*/
|
|
132
|
+
itemOf(value: string): FormItem | undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface FormItem<E extends ItemElement = ItemElement> {
|
|
136
|
+
/**
|
|
137
|
+
* underlying HTML element (input, option, textarea)
|
|
138
|
+
*/
|
|
139
|
+
readonly node: E
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* getter/setter of the `value` property
|
|
143
|
+
* setter triggers onWrite handler
|
|
144
|
+
*/
|
|
145
|
+
value: string
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* method to set the `value` property silently
|
|
149
|
+
* does not trigger onWrite handler
|
|
150
|
+
*/
|
|
151
|
+
setValue(value: string): void
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* whether the option is checkable (radio, checkbox, select)
|
|
155
|
+
*/
|
|
156
|
+
readonly checkable: boolean
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* whether the option is selected (radio, checkbox, select)
|
|
160
|
+
* setter triggers onWrite handler
|
|
161
|
+
*/
|
|
162
|
+
checked: boolean | undefined
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* method to set the `checked` property silently
|
|
166
|
+
* does not trigger onWrite handler
|
|
167
|
+
*/
|
|
168
|
+
setChecked(checked: boolean): void
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* getter/setter of the `disabled` property
|
|
172
|
+
*/
|
|
173
|
+
disabled: boolean
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* text content of the option (if applicable)
|
|
177
|
+
*/
|
|
178
|
+
readonly label: string | undefined
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type FormField<T = any> = formField.FormField<T>
|
|
183
|
+
|
|
184
|
+
export type FormFieldOptions<T = any> = formField.FormFieldOptions<T>
|
|
185
|
+
|
|
186
|
+
export const formField: <T = any>(options: FormFieldOptions<T>) => FormField<T>
|