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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Yusuke Kawasaki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # html-form-field
2
+
3
+ Unified interface for HTML form fields with synchronized binding to object properties
4
+
5
+ [![Node.js CI](https://github.com/kawanet/html-form-field/workflows/Node.js%20CI/badge.svg)](https://github.com/kawanet/html-form-field/actions/)
6
+ [![npm version](https://img.shields.io/npm/v/html-form-field)](https://www.npmjs.com/package/html-form-field)
7
+ [![gzip size](https://img.badgesize.io/https://cdn.jsdelivr.net/npm/html-form-field/dist/html-form-field.min.js?compression=gzip)](https://cdn.jsdelivr.net/npm/html-form-field/dist/html-form-field.min.js)
8
+
9
+ - Unified getter/setter for text inputs, checkboxes, radio buttons, and select elements
10
+ - Two-way binding between form fields and object properties (synchronized updates in both directions)
11
+ - Built-in change detection with `onChange` and `onWrite` callbacks
12
+ - Small browser build: [html-form-field.min.js](https://cdn.jsdelivr.net/npm/html-form-field/dist/html-form-field.min.js) under 4KB minified, under 2KB gzipped
13
+ - Full TypeScript support - [html-form-field.d.ts](https://github.com/kawanet/html-form-field/blob/main/types/html-form-field.d.ts) for detailed specifications
14
+
15
+ ## SYNOPSIS
16
+
17
+ ```typescript
18
+ import {formField} from "html-form-field"
19
+
20
+ interface Context {
21
+ nickname: string
22
+ email: string
23
+ favo: string
24
+ }
25
+
26
+ const form = document.querySelector("form")
27
+
28
+ const ctx = {} as Context
29
+
30
+ formField({form, bindTo: ctx, name: "nickname"})
31
+
32
+ console.log(ctx.nickname) // reads from form field
33
+
34
+ ctx.nickname = "John" // updates form field
35
+ ```
36
+
37
+ #### HTML Example
38
+
39
+ ```html
40
+
41
+ <form>
42
+ <ul>
43
+ <li>Nickname: <input type="text" name="nickname" value="Alice"></li>
44
+ <li>Email: <input type="email" name="email" value="alice@example.com"></li>
45
+ <li>Favorites:
46
+ <label><input type="checkbox" name="favo" value="tech">Tech</label>
47
+ <label><input type="checkbox" name="favo" value="travel">Travel</label>
48
+ <label><input type="checkbox" name="favo" value="trading">Trading</label>
49
+ </li>
50
+ </ul>
51
+ </form>
52
+ ```
53
+
54
+ #### Value Access
55
+
56
+ ```js
57
+ const email = formField({form, name: "email"})
58
+
59
+ console.log(email.value) // current value
60
+
61
+ email.value = "john@example.com" // update value
62
+ ```
63
+
64
+ #### Multiple Selections
65
+
66
+ ```js
67
+ const favo = formField({form, name: "favo", delim: ","})
68
+
69
+ favo.toggle("tech") // toggle checkbox
70
+
71
+ favo.toggle("travel", true)
72
+
73
+ favo.toggle("trading", false)
74
+
75
+ console.log(favo.has("travel")) // check if selected
76
+
77
+ // Shortcut to item by index. Equivalent to items().at(index))
78
+ const firstItem = favo.itemAt(0)
79
+ console.log(firstItem.checked)
80
+
81
+ // Shortcut to item by value. Equivalent to items().find(v => v.value === value)
82
+ const travelItem = favo.itemOf("travel")
83
+ console.log(travelItem.checked)
84
+ ```
85
+
86
+ #### Change Handling and Default Values
87
+
88
+ ```js
89
+ formField({
90
+ form,
91
+ bindTo: ctx,
92
+ name: "email",
93
+ onWrite: ({name, value}) => sessionStorage.setItem(name, value),
94
+ onChange: ({name, value}) => submitForm(),
95
+ defaults: [sessionStorage.getItem("email")],
96
+ })
97
+ ```
98
+
99
+ ## LINKS
100
+
101
+ - https://www.npmjs.com/package/html-form-field
102
+ - https://github.com/kawanet/html-form-field
@@ -0,0 +1,353 @@
1
+ 'use strict';
2
+
3
+ const triggerOnWrite$1 = (field) => {
4
+ const onWrite = field.options.onWrite;
5
+ if (onWrite) onWrite(field);
6
+ };
7
+
8
+ const checkableMap = {
9
+ option: true,
10
+ radio: true,
11
+ checkbox: true,
12
+ };
13
+
14
+
15
+
16
+ const isElementOf = (v, tagName) => ((v ).tagName === tagName);
17
+
18
+ const formItemList = (field, nodeList) => {
19
+ const list = [];
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
+ class BaseFormItem {
35
+
36
+
37
+
38
+
39
+ constructor(field, node) {
40
+ this.field = field;
41
+ this.node = node;
42
+ }
43
+
44
+ get value() {
45
+ return this.node.value
46
+ }
47
+
48
+ set value(value) {
49
+ this.setValue(value);
50
+ triggerOnWrite$1(this.field);
51
+ }
52
+
53
+ setValue(value) {
54
+ this.node.value = value;
55
+ }
56
+
57
+ get checked() {
58
+ return this.getChecked()
59
+ }
60
+
61
+
62
+
63
+ set checked(checked) {
64
+ const prev = this.checked;
65
+ if (prev !== checked) {
66
+ this.setChecked(checked);
67
+ triggerOnWrite$1(this.field);
68
+ }
69
+ }
70
+
71
+
72
+
73
+ get disabled() {
74
+ return this.node.disabled
75
+ }
76
+
77
+ set disabled(disabled) {
78
+ this.node.disabled = disabled;
79
+ }
80
+
81
+
82
+ }
83
+
84
+ class InputItem extends BaseFormItem {
85
+
86
+
87
+ constructor(entry, node) {
88
+ super(entry, node);
89
+ this.checkable = checkableMap[node.type];
90
+ }
91
+
92
+ getChecked() {
93
+ return (this.node ).checked
94
+ }
95
+
96
+ setChecked(checked) {
97
+ (this.node ).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) {
120
+ const text = node.innerText || node.textContent;
121
+ if (text) return text.trim()
122
+ }
123
+ }
124
+ }
125
+
126
+ class OptionItem extends BaseFormItem {
127
+
128
+
129
+ constructor(entry, node) {
130
+ super(entry, node);
131
+ this.checkable = true;
132
+ }
133
+
134
+ getChecked() {
135
+ return this.node.selected
136
+ }
137
+
138
+ setChecked(checked) {
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
+ }
149
+
150
+ const DELIM = ","; // or use Unit Separator `\x1F` instead
151
+
152
+ const isString = (v) => ("string" === typeof v);
153
+
154
+ const noNullString = (v) => (v == null) ? "" : isString(v) ? v : String(v);
155
+
156
+ const splitString = (v, delim) => (v == null) ? [] : Array.isArray(v) ? v.map(noNullString) : noNullString(v).split(delim);
157
+
158
+ const isHTMLElement = (v) => ("function" === typeof (v ).matches);
159
+
160
+ const formField = (options) => {
161
+ return new FormBridgeImpl(options)
162
+ };
163
+
164
+ const getNodeList = ({form, name}) => {
165
+ const safeName = JSON.stringify(name);
166
+ if (!name) {
167
+ throw new Error(`Invalid name=${safeName}`)
168
+ }
169
+
170
+ const selector = `input[name=${safeName}], textarea[name=${safeName}], button[name=${safeName}], select[name=${safeName}]`;
171
+
172
+ const nodeList = form.querySelectorAll(selector);
173
+ if (!nodeList.length) {
174
+ if (isHTMLElement(form) && form.matches(selector)) {
175
+ return [form]
176
+ }
177
+
178
+ throw new Error(`Not found: name=${safeName}`)
179
+ }
180
+
181
+ return nodeList
182
+ };
183
+
184
+ const updateEventListener = (nodeList, handler) => {
185
+ for (const node of nodeList) {
186
+ node.removeEventListener("change", handler);
187
+ node.addEventListener("change", handler);
188
+ }
189
+ };
190
+
191
+ const triggerOnWrite = (field) => {
192
+ const onWrite = field.options.onWrite;
193
+ if (onWrite) onWrite(field);
194
+ };
195
+
196
+ const triggerOnChange = (field) => {
197
+ const onChange = field.options.onChange;
198
+ if (onChange) onChange(field);
199
+ };
200
+
201
+ const applyBindTo = (bindTo, name, field) => {
202
+ if (!bindTo) return // nothing to be bound
203
+
204
+ delete bindTo[name ];
205
+
206
+ const getValue = () => field.value;
207
+
208
+ const setValue = (value) => {
209
+ field.value = value;
210
+ };
211
+
212
+ Object.defineProperty(bindTo, name, {
213
+ get: getValue,
214
+ set: setValue,
215
+ enumerable: true,
216
+ configurable: true,
217
+ });
218
+ };
219
+
220
+ class FormBridgeImpl {
221
+
222
+
223
+
224
+
225
+
226
+
227
+ constructor(options = {} ) {
228
+ this.options = options;
229
+ const name = this.name = options.name;
230
+ applyBindTo(options.bindTo, name, this);
231
+
232
+ const _onChange = this._onChange = () => {
233
+ triggerOnWrite(this);
234
+ triggerOnChange(this);
235
+ };
236
+
237
+ const nodeList = getNodeList(options);
238
+ updateEventListener(nodeList, _onChange);
239
+ const items = this._items = formItemList(this, nodeList);
240
+
241
+ const defaults = options.defaults;
242
+ if (defaults) {
243
+ if (items[0].checkable) {
244
+ for (const v of defaults) {
245
+ if (this.setValue(v)) break
246
+ }
247
+ } else {
248
+ this.setValue(defaults);
249
+ }
250
+ }
251
+ }
252
+
253
+ get value() {
254
+ const values = this.current().map(v => v.value);
255
+ if (values.length > 1) {
256
+ const delim = this.options.delim || DELIM;
257
+ return values.join(delim)
258
+ } else {
259
+ return values[0]
260
+ }
261
+ }
262
+
263
+ set value(value) {
264
+ this.setValue(value);
265
+ triggerOnWrite(this);
266
+ }
267
+
268
+ setValue(value) {
269
+ const items = this.items();
270
+
271
+ if (items.length === 1 && !items[0].checkable && isString(value)) {
272
+ value = [value];
273
+ }
274
+
275
+ const delim = this.options.delim || DELIM;
276
+ const values = splitString(value, delim);
277
+ let i = 0;
278
+ let assigned = 0;
279
+
280
+ for (const item of items) {
281
+ if (item.checkable) {
282
+ const checked = values.includes(item.value);
283
+ if (checked) assigned++;
284
+ if (item.checked !== checked) {
285
+ item.setChecked(checked);
286
+ }
287
+ } else {
288
+ const value = values[i++];
289
+ const isNull = (value == null);
290
+ item.setValue(isNull ? "" : value);
291
+ if (!isNull) assigned++;
292
+ }
293
+ }
294
+
295
+ return !!assigned
296
+ }
297
+
298
+ current() {
299
+ return this.items().filter(v => (!v.disabled && (!v.checkable || v.checked)))
300
+ }
301
+
302
+ reload() {
303
+ const nodeList = getNodeList(this.options);
304
+ const _onChange = this._onChange;
305
+ updateEventListener(nodeList, _onChange);
306
+ this._items = formItemList(this, nodeList);
307
+ }
308
+
309
+ items() {
310
+ return this._items
311
+ }
312
+
313
+ toggle(value, checked) {
314
+ let result;
315
+ const delim = this.options.delim || DELIM;
316
+ const values = splitString(value, delim);
317
+
318
+ for (const item of this.items()) {
319
+ if (values.includes(item.value)) {
320
+ if (checked != null) {
321
+ result = checked;
322
+ } else {
323
+ result = !item.checked;
324
+ }
325
+ item.checked = result;
326
+ }
327
+ }
328
+
329
+ return result
330
+ }
331
+
332
+ has(value) {
333
+ return !!this.current().find(v => v.value === value)
334
+ }
335
+
336
+ /**
337
+ * Shortcut to access an item by index (operates on items()).
338
+ * Equivalent to items().at(index). Returns undefined if out of range.
339
+ */
340
+ itemAt(index) {
341
+ return this.items().at(index)
342
+ }
343
+
344
+ /**
345
+ * Shortcut to access an item by value (operates on items()).
346
+ * Returns the first FormItem whose value equals the given value, or undefined if not found.
347
+ */
348
+ itemOf(value) {
349
+ return this.items().find(v => v.value === value)
350
+ }
351
+ }
352
+
353
+ exports.formField = formField;
@@ -0,0 +1 @@
1
+ var formField=function(e){"use strict";const t=e=>{const t=e.options.onWrite;t&&t(e)},s={option:!0,radio:!0,checkbox:!0},n=(e,t)=>e.tagName===t,i=(e,t)=>{const s=[];for(const i of t)if(n(i,"SELECT"))for(const t of i.options)s.push(new c(e,t));else s.push(new r(e,i));return s};class o{constructor(e,t){this.field=e,this.node=t}get value(){return this.node.value}set value(e){this.setValue(e),t(this.field)}setValue(e){this.node.value=e}get checked(){return this.getChecked()}set checked(e){this.checked!==e&&(this.setChecked(e),t(this.field))}get disabled(){return this.node.disabled}set disabled(e){this.node.disabled=e}}class r extends o{constructor(e,t){super(e,t),this.checkable=s[t.type]}getChecked(){return this.node.checked}setChecked(e){this.node.checked=e}get label(){const e=this.node,t=e.closest("label"),s=t&&t.querySelectorAll(e.tagName);if(s&&1===s.length){const e=o(t);if(e)return e}const n=e.labels;for(const e of n){const t=o(e);if(t)return t}const i=o(e);if(i)return i;function o(e){const t=e.innerText||e.textContent;if(t)return t.trim()}}}class c extends o{constructor(e,t){super(e,t),this.checkable=!0}getChecked(){return this.node.selected}setChecked(e){this.node.selected=e}get label(){const e=this.node,t=e.label||e.textContent;if(t)return t.trim()}}const l=e=>"string"==typeof e,h=e=>null==e?"":l(e)?e:String(e),a=(e,t)=>null==e?[]:Array.isArray(e)?e.map(h):h(e).split(t),u=({form:e,name:t})=>{const s=JSON.stringify(t);if(!t)throw new Error(`Invalid name=${s}`);const n=`input[name=${s}], textarea[name=${s}], button[name=${s}], select[name=${s}]`,i=e.querySelectorAll(n);if(!i.length){if("function"==typeof e.matches&&e.matches(n))return[e];throw new Error(`Not found: name=${s}`)}return i},d=(e,t)=>{for(const s of e)s.removeEventListener("change",t),s.addEventListener("change",t)},f=e=>{const t=e.options.onWrite;t&&t(e)};class m{constructor(e={}){this.options=e;const t=this.name=e.name;((e,t,s)=>{if(!e)return;delete e[t],Object.defineProperty(e,t,{get:()=>s.value,set:e=>{s.value=e},enumerable:!0,configurable:!0})})(e.bindTo,t,this);const s=this._onChange=()=>{f(this),(e=>{const t=e.options.onChange;t&&t(e)})(this)},n=u(e);d(n,s);const o=this._items=i(this,n),r=e.defaults;if(r)if(o[0].checkable){for(const e of r)if(this.setValue(e))break}else this.setValue(r)}get value(){const e=this.current().map(e=>e.value);if(e.length>1){const t=this.options.delim||",";return e.join(t)}return e[0]}set value(e){this.setValue(e),f(this)}setValue(e){const t=this.items();1===t.length&&!t[0].checkable&&l(e)&&(e=[e]);const s=this.options.delim||",",n=a(e,s);let i=0,o=0;for(const e of t)if(e.checkable){const t=n.includes(e.value);t&&o++,e.checked!==t&&e.setChecked(t)}else{const t=n[i++],s=null==t;e.setValue(s?"":t),s||o++}return!!o}current(){return this.items().filter(e=>!e.disabled&&(!e.checkable||e.checked))}reload(){const e=u(this.options),t=this._onChange;d(e,t),this._items=i(this,e)}items(){return this._items}toggle(e,t){let s;const n=this.options.delim||",",i=a(e,n);for(const e of this.items())i.includes(e.value)&&(s=null!=t?t:!e.checked,e.checked=s);return s}has(e){return!!this.current().find(t=>t.value===e)}itemAt(e){return this.items().at(e)}itemOf(e){return this.items().find(t=>t.value===e)}}return e.formField=e=>new m(e),"undefined"!=typeof module&&(module.exports=e),e.formField}({});