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.
@@ -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,5 @@
1
+ export {formField} from "./form-field.ts"
2
+ export type {FormField, FormFieldOptions} from "../types/html-form-field.d.ts"
3
+ import type {formField as NS} from "../types/html-form-field.d.ts"
4
+
5
+ export type formField = typeof NS
@@ -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>