html-form-field 0.3.1 → 0.4.1
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/dist/html-form-field.cjs +57 -14
- package/dist/html-form-field.min.js +1 -1
- package/dist/html-form-field.mjs +57 -14
- package/package.json +1 -1
- package/src/form-field.ts +54 -15
- package/types/html-form-field.d.ts +29 -0
package/dist/html-form-field.cjs
CHANGED
|
@@ -147,6 +147,10 @@ class OptionItem extends BaseFormItem {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
150
154
|
const DELIM = ","; // or use Unit Separator `\x1F` instead
|
|
151
155
|
|
|
152
156
|
const isString = (v) => ("string" === typeof v);
|
|
@@ -170,20 +174,29 @@ const getNodeList = ({form, name}) => {
|
|
|
170
174
|
const selector = `input[name=${safeName}], textarea[name=${safeName}], button[name=${safeName}], select[name=${safeName}]`;
|
|
171
175
|
|
|
172
176
|
const nodeList = form.querySelectorAll(selector);
|
|
173
|
-
if (!nodeList.length) {
|
|
174
|
-
|
|
175
|
-
return [form]
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
throw new Error(`Not found: name=${safeName}`)
|
|
177
|
+
if (!nodeList.length && isHTMLElement(form) && form.matches(selector)) {
|
|
178
|
+
return [form]
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
// No throw on a 0-match result here: a freshly rendered form may not yet
|
|
182
|
+
// contain the controls (dynamic radio/checkbox groups, lazy-populated
|
|
183
|
+
// <select>, etc.). The error surfaces lazily from `items()` instead.
|
|
181
184
|
return nodeList
|
|
182
185
|
};
|
|
183
186
|
|
|
184
|
-
|
|
187
|
+
// Detach `change` listeners from elements that were part of the previous
|
|
188
|
+
// `_fields` snapshot. Pair this with `addEventListeners` on the new
|
|
189
|
+
// snapshot inside `reload()` so a control that dropped out of the
|
|
190
|
+
// selector (renamed, removed from the DOM, or moved out of the form)
|
|
191
|
+
// stops firing this field's handlers.
|
|
192
|
+
const removeEventListeners = (nodeList, handler) => {
|
|
185
193
|
for (const node of nodeList) {
|
|
186
194
|
node.removeEventListener("change", handler);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const addEventListeners = (nodeList, handler) => {
|
|
199
|
+
for (const node of nodeList) {
|
|
187
200
|
node.addEventListener("change", handler);
|
|
188
201
|
}
|
|
189
202
|
};
|
|
@@ -222,6 +235,11 @@ class FormBridgeImpl {
|
|
|
222
235
|
|
|
223
236
|
|
|
224
237
|
|
|
238
|
+
// `null` when the most recent getNodeList() returned zero matches.
|
|
239
|
+
// Methods that need item-level access (items, value, current, etc.)
|
|
240
|
+
// surface the error lazily; closest() still works because it walks
|
|
241
|
+
// from `_fields[0]`, which is simply `undefined` in this state.
|
|
242
|
+
|
|
225
243
|
|
|
226
244
|
|
|
227
245
|
constructor(options = {} ) {
|
|
@@ -234,9 +252,9 @@ class FormBridgeImpl {
|
|
|
234
252
|
triggerOnChange(this);
|
|
235
253
|
};
|
|
236
254
|
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
this._items = formItemList(this,
|
|
255
|
+
const fields = this._fields = Array.from(getNodeList(options));
|
|
256
|
+
addEventListeners(fields, _onChange);
|
|
257
|
+
this._items = fields.length ? formItemList(this, fields) : null;
|
|
240
258
|
|
|
241
259
|
const defaults = options.defaults;
|
|
242
260
|
if (defaults) {
|
|
@@ -296,16 +314,41 @@ class FormBridgeImpl {
|
|
|
296
314
|
}
|
|
297
315
|
|
|
298
316
|
reload() {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
// Detach handlers from the previous snapshot first — `getNodeList()`
|
|
318
|
+
// can now legitimately return fewer (or zero) controls, and any
|
|
319
|
+
// node that drops out of the selector must stop firing this
|
|
320
|
+
// field's `change` listener.
|
|
321
|
+
removeEventListeners(this._fields, this._onChange);
|
|
322
|
+
const fields = this._fields = Array.from(getNodeList(this.options));
|
|
323
|
+
addEventListeners(fields, this._onChange);
|
|
324
|
+
this._items = fields.length ? formItemList(this, fields) : null;
|
|
303
325
|
}
|
|
304
326
|
|
|
305
327
|
items() {
|
|
328
|
+
if (!this._items) {
|
|
329
|
+
throw new Error(`Not found: name=${JSON.stringify(this.name)}`)
|
|
330
|
+
}
|
|
306
331
|
return this._items
|
|
307
332
|
}
|
|
308
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Walk up from the field's first element (inclusive) and return the
|
|
336
|
+
* closest ancestor matching the selector. Useful for reaching the
|
|
337
|
+
* container element when the field is a `<select>` (so you can
|
|
338
|
+
* append `<option>` elements before calling `reload()`) or when
|
|
339
|
+
* the field is part of a checkbox/radio group inside a wrapper.
|
|
340
|
+
*
|
|
341
|
+
* For multi-element fields (checkbox/radio groups), the walk starts
|
|
342
|
+
* from the first element only — callers that need a different
|
|
343
|
+
* starting point can use `items().at(i)?.node.closest(selector)`.
|
|
344
|
+
*
|
|
345
|
+
* Returns `null` when no ancestor matches, matching the contract
|
|
346
|
+
* of `Element.closest()`.
|
|
347
|
+
*/
|
|
348
|
+
closest(selector) {
|
|
349
|
+
return _nullishCoalesce(_optionalChain([this, 'access', _ => _._fields, 'access', _2 => _2[0], 'optionalAccess', _3 => _3.closest, 'call', _4 => _4(selector)]), () => ( null))
|
|
350
|
+
}
|
|
351
|
+
|
|
309
352
|
toggle(value, checked) {
|
|
310
353
|
let result;
|
|
311
354
|
const delim = this.options.delim || DELIM;
|
|
@@ -1 +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
|
|
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 l(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 l 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 c=e=>"string"==typeof e,h=e=>null==e?"":c(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);return!i.length&&"function"==typeof e.matches&&e.matches(n)?[e]:i},d=(e,t)=>{for(const s of e)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=this._fields=Array.from(u(e));d(n,s),this._items=n.length?i(this,n):null;const o=e.defaults;if(o)for(const e of o)if(this.setValue(e))break}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&&c(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(){((e,t)=>{for(const s of e)s.removeEventListener("change",t)})(this._fields,this._onChange);const e=this._fields=Array.from(u(this.options));d(e,this._onChange),this._items=e.length?i(this,e):null}items(){if(!this._items)throw new Error(`Not found: name=${JSON.stringify(this.name)}`);return this._items}closest(e){return t=function(e){let t,s=e[0],n=1;for(;n<e.length;){const i=e[n],o=e[n+1];if(n+=2,("optionalAccess"===i||"optionalCall"===i)&&null==s)return;"access"===i||"optionalAccess"===i?(t=s,s=o(s)):"call"!==i&&"optionalCall"!==i||(s=o((...e)=>s.call(t,...e)),t=void 0)}return s}([this,"access",e=>e._fields,"access",e=>e[0],"optionalAccess",e=>e.closest,"call",t=>t(e)]),s=()=>null,null!=t?t:s();var t,s}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}({});
|
package/dist/html-form-field.mjs
CHANGED
|
@@ -145,6 +145,10 @@ class OptionItem extends BaseFormItem {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
148
152
|
const DELIM = ","; // or use Unit Separator `\x1F` instead
|
|
149
153
|
|
|
150
154
|
const isString = (v) => ("string" === typeof v);
|
|
@@ -168,20 +172,29 @@ const getNodeList = ({form, name}) => {
|
|
|
168
172
|
const selector = `input[name=${safeName}], textarea[name=${safeName}], button[name=${safeName}], select[name=${safeName}]`;
|
|
169
173
|
|
|
170
174
|
const nodeList = form.querySelectorAll(selector);
|
|
171
|
-
if (!nodeList.length) {
|
|
172
|
-
|
|
173
|
-
return [form]
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
throw new Error(`Not found: name=${safeName}`)
|
|
175
|
+
if (!nodeList.length && isHTMLElement(form) && form.matches(selector)) {
|
|
176
|
+
return [form]
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// No throw on a 0-match result here: a freshly rendered form may not yet
|
|
180
|
+
// contain the controls (dynamic radio/checkbox groups, lazy-populated
|
|
181
|
+
// <select>, etc.). The error surfaces lazily from `items()` instead.
|
|
179
182
|
return nodeList
|
|
180
183
|
};
|
|
181
184
|
|
|
182
|
-
|
|
185
|
+
// Detach `change` listeners from elements that were part of the previous
|
|
186
|
+
// `_fields` snapshot. Pair this with `addEventListeners` on the new
|
|
187
|
+
// snapshot inside `reload()` so a control that dropped out of the
|
|
188
|
+
// selector (renamed, removed from the DOM, or moved out of the form)
|
|
189
|
+
// stops firing this field's handlers.
|
|
190
|
+
const removeEventListeners = (nodeList, handler) => {
|
|
183
191
|
for (const node of nodeList) {
|
|
184
192
|
node.removeEventListener("change", handler);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const addEventListeners = (nodeList, handler) => {
|
|
197
|
+
for (const node of nodeList) {
|
|
185
198
|
node.addEventListener("change", handler);
|
|
186
199
|
}
|
|
187
200
|
};
|
|
@@ -220,6 +233,11 @@ class FormBridgeImpl {
|
|
|
220
233
|
|
|
221
234
|
|
|
222
235
|
|
|
236
|
+
// `null` when the most recent getNodeList() returned zero matches.
|
|
237
|
+
// Methods that need item-level access (items, value, current, etc.)
|
|
238
|
+
// surface the error lazily; closest() still works because it walks
|
|
239
|
+
// from `_fields[0]`, which is simply `undefined` in this state.
|
|
240
|
+
|
|
223
241
|
|
|
224
242
|
|
|
225
243
|
constructor(options = {} ) {
|
|
@@ -232,9 +250,9 @@ class FormBridgeImpl {
|
|
|
232
250
|
triggerOnChange(this);
|
|
233
251
|
};
|
|
234
252
|
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
this._items = formItemList(this,
|
|
253
|
+
const fields = this._fields = Array.from(getNodeList(options));
|
|
254
|
+
addEventListeners(fields, _onChange);
|
|
255
|
+
this._items = fields.length ? formItemList(this, fields) : null;
|
|
238
256
|
|
|
239
257
|
const defaults = options.defaults;
|
|
240
258
|
if (defaults) {
|
|
@@ -294,16 +312,41 @@ class FormBridgeImpl {
|
|
|
294
312
|
}
|
|
295
313
|
|
|
296
314
|
reload() {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
315
|
+
// Detach handlers from the previous snapshot first — `getNodeList()`
|
|
316
|
+
// can now legitimately return fewer (or zero) controls, and any
|
|
317
|
+
// node that drops out of the selector must stop firing this
|
|
318
|
+
// field's `change` listener.
|
|
319
|
+
removeEventListeners(this._fields, this._onChange);
|
|
320
|
+
const fields = this._fields = Array.from(getNodeList(this.options));
|
|
321
|
+
addEventListeners(fields, this._onChange);
|
|
322
|
+
this._items = fields.length ? formItemList(this, fields) : null;
|
|
301
323
|
}
|
|
302
324
|
|
|
303
325
|
items() {
|
|
326
|
+
if (!this._items) {
|
|
327
|
+
throw new Error(`Not found: name=${JSON.stringify(this.name)}`)
|
|
328
|
+
}
|
|
304
329
|
return this._items
|
|
305
330
|
}
|
|
306
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Walk up from the field's first element (inclusive) and return the
|
|
334
|
+
* closest ancestor matching the selector. Useful for reaching the
|
|
335
|
+
* container element when the field is a `<select>` (so you can
|
|
336
|
+
* append `<option>` elements before calling `reload()`) or when
|
|
337
|
+
* the field is part of a checkbox/radio group inside a wrapper.
|
|
338
|
+
*
|
|
339
|
+
* For multi-element fields (checkbox/radio groups), the walk starts
|
|
340
|
+
* from the first element only — callers that need a different
|
|
341
|
+
* starting point can use `items().at(i)?.node.closest(selector)`.
|
|
342
|
+
*
|
|
343
|
+
* Returns `null` when no ancestor matches, matching the contract
|
|
344
|
+
* of `Element.closest()`.
|
|
345
|
+
*/
|
|
346
|
+
closest(selector) {
|
|
347
|
+
return _nullishCoalesce(_optionalChain([this, 'access', _ => _._fields, 'access', _2 => _2[0], 'optionalAccess', _3 => _3.closest, 'call', _4 => _4(selector)]), () => ( null))
|
|
348
|
+
}
|
|
349
|
+
|
|
307
350
|
toggle(value, checked) {
|
|
308
351
|
let result;
|
|
309
352
|
const delim = this.options.delim || DELIM;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "html-form-field",
|
|
3
3
|
"description": "Unified interface for HTML form fields with synchronized binding to object properties",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"author": "@kawanet",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/kawanet/html-form-field/issues"
|
package/src/form-field.ts
CHANGED
|
@@ -26,20 +26,29 @@ const getNodeList = <T>({form, name}: {form: ParentNode, name: NS.StringKeys<T>}
|
|
|
26
26
|
const selector = `input[name=${safeName}], textarea[name=${safeName}], button[name=${safeName}], select[name=${safeName}]`
|
|
27
27
|
|
|
28
28
|
const nodeList = form.querySelectorAll<NS.FieldElement>(selector)
|
|
29
|
-
if (!nodeList.length) {
|
|
30
|
-
|
|
31
|
-
return [form] as NS.FieldElement[]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
throw new Error(`Not found: name=${safeName}`)
|
|
29
|
+
if (!nodeList.length && isHTMLElement(form) && form.matches(selector)) {
|
|
30
|
+
return [form] as NS.FieldElement[]
|
|
35
31
|
}
|
|
36
32
|
|
|
33
|
+
// No throw on a 0-match result here: a freshly rendered form may not yet
|
|
34
|
+
// contain the controls (dynamic radio/checkbox groups, lazy-populated
|
|
35
|
+
// <select>, etc.). The error surfaces lazily from `items()` instead.
|
|
37
36
|
return nodeList
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
// Detach `change` listeners from elements that were part of the previous
|
|
40
|
+
// `_fields` snapshot. Pair this with `addEventListeners` on the new
|
|
41
|
+
// snapshot inside `reload()` so a control that dropped out of the
|
|
42
|
+
// selector (renamed, removed from the DOM, or moved out of the form)
|
|
43
|
+
// stops firing this field's handlers.
|
|
44
|
+
const removeEventListeners = (nodeList: Iterable<NS.FieldElement>, handler: FieldEventHandler) => {
|
|
41
45
|
for (const node of nodeList) {
|
|
42
46
|
node.removeEventListener("change", handler)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const addEventListeners = (nodeList: Iterable<NS.FieldElement>, handler: FieldEventHandler) => {
|
|
51
|
+
for (const node of nodeList) {
|
|
43
52
|
node.addEventListener("change", handler)
|
|
44
53
|
}
|
|
45
54
|
}
|
|
@@ -77,7 +86,12 @@ class FormBridgeImpl<T = any> implements FormField<T> {
|
|
|
77
86
|
readonly options: FormFieldOptions<T>
|
|
78
87
|
readonly name: FormField<T>["name"]
|
|
79
88
|
|
|
80
|
-
private
|
|
89
|
+
private _fields: NS.FieldElement[]
|
|
90
|
+
// `null` when the most recent getNodeList() returned zero matches.
|
|
91
|
+
// Methods that need item-level access (items, value, current, etc.)
|
|
92
|
+
// surface the error lazily; closest() still works because it walks
|
|
93
|
+
// from `_fields[0]`, which is simply `undefined` in this state.
|
|
94
|
+
private _items: NS.FormItem[] | null
|
|
81
95
|
private readonly _onChange: FieldEventHandler
|
|
82
96
|
|
|
83
97
|
constructor(options: FormFieldOptions<T> = {} as FormFieldOptions<T>) {
|
|
@@ -90,9 +104,9 @@ class FormBridgeImpl<T = any> implements FormField<T> {
|
|
|
90
104
|
triggerOnChange(this)
|
|
91
105
|
}
|
|
92
106
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
this._items = formItemList(this,
|
|
107
|
+
const fields = this._fields = Array.from(getNodeList(options))
|
|
108
|
+
addEventListeners(fields, _onChange)
|
|
109
|
+
this._items = fields.length ? formItemList(this, fields) : null
|
|
96
110
|
|
|
97
111
|
const defaults = options.defaults
|
|
98
112
|
if (defaults) {
|
|
@@ -152,16 +166,41 @@ class FormBridgeImpl<T = any> implements FormField<T> {
|
|
|
152
166
|
}
|
|
153
167
|
|
|
154
168
|
reload(): void {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
169
|
+
// Detach handlers from the previous snapshot first — `getNodeList()`
|
|
170
|
+
// can now legitimately return fewer (or zero) controls, and any
|
|
171
|
+
// node that drops out of the selector must stop firing this
|
|
172
|
+
// field's `change` listener.
|
|
173
|
+
removeEventListeners(this._fields, this._onChange)
|
|
174
|
+
const fields = this._fields = Array.from(getNodeList(this.options))
|
|
175
|
+
addEventListeners(fields, this._onChange)
|
|
176
|
+
this._items = fields.length ? formItemList(this, fields) : null
|
|
159
177
|
}
|
|
160
178
|
|
|
161
179
|
items(): NS.FormItem[] {
|
|
180
|
+
if (!this._items) {
|
|
181
|
+
throw new Error(`Not found: name=${JSON.stringify(this.name)}`)
|
|
182
|
+
}
|
|
162
183
|
return this._items
|
|
163
184
|
}
|
|
164
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Walk up from the field's first element (inclusive) and return the
|
|
188
|
+
* closest ancestor matching the selector. Useful for reaching the
|
|
189
|
+
* container element when the field is a `<select>` (so you can
|
|
190
|
+
* append `<option>` elements before calling `reload()`) or when
|
|
191
|
+
* the field is part of a checkbox/radio group inside a wrapper.
|
|
192
|
+
*
|
|
193
|
+
* For multi-element fields (checkbox/radio groups), the walk starts
|
|
194
|
+
* from the first element only — callers that need a different
|
|
195
|
+
* starting point can use `items().at(i)?.node.closest(selector)`.
|
|
196
|
+
*
|
|
197
|
+
* Returns `null` when no ancestor matches, matching the contract
|
|
198
|
+
* of `Element.closest()`.
|
|
199
|
+
*/
|
|
200
|
+
closest<E extends Element = Element>(selector: string): E | null {
|
|
201
|
+
return this._fields[0]?.closest<E>(selector) ?? null
|
|
202
|
+
}
|
|
203
|
+
|
|
165
204
|
toggle(value: string, checked?: boolean): boolean {
|
|
166
205
|
let result: boolean
|
|
167
206
|
const delim = this.options.delim || DELIM
|
|
@@ -141,6 +141,26 @@ declare namespace formField {
|
|
|
141
141
|
* or `undefined` if no item has the given value.
|
|
142
142
|
*/
|
|
143
143
|
itemOf(value: string): FormItem | undefined
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Walk up from the field's first element (inclusive) and return the closest
|
|
147
|
+
* ancestor that matches `selector`, mirroring `Element.closest()`.
|
|
148
|
+
*
|
|
149
|
+
* The primary use case is reaching the container element so you can mutate
|
|
150
|
+
* the DOM and then call `reload()`. For example, to populate an initially
|
|
151
|
+
* empty `<select>`:
|
|
152
|
+
*
|
|
153
|
+
* ```ts
|
|
154
|
+
* const cityField = formField({form, name: "city"})
|
|
155
|
+
* const select = cityField.closest<HTMLSelectElement>("select")
|
|
156
|
+
* select.add(new Option("Tokyo", "tokyo"))
|
|
157
|
+
* cityField.reload()
|
|
158
|
+
* ```
|
|
159
|
+
*
|
|
160
|
+
* For multi-element fields (checkbox/radio groups), the walk starts from
|
|
161
|
+
* the first element only. Returns `null` if no ancestor matches.
|
|
162
|
+
*/
|
|
163
|
+
closest<E extends Element = Element>(selector: string): E | null
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
interface FormItem<E extends ItemElement = ItemElement> {
|
|
@@ -177,4 +197,13 @@ export type FormField<T = any> = formField.FormField<T>
|
|
|
177
197
|
|
|
178
198
|
export type FormFieldOptions<T = any> = formField.FormFieldOptions<T>
|
|
179
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Construct a `FormField` bound to the form control(s) with the given `name`.
|
|
202
|
+
*
|
|
203
|
+
* Construction itself does not throw when zero controls match — the resulting
|
|
204
|
+
* field can still call `closest()` to locate the surrounding container so the
|
|
205
|
+
* caller can populate a dynamic group and then `reload()`. Methods that
|
|
206
|
+
* actually need an item (`items()`, `value`, `current()`, `toggle()`, …) throw
|
|
207
|
+
* `Not found: name=...` lazily until at least one control is matched.
|
|
208
|
+
*/
|
|
180
209
|
export const formField: <T = any>(options: FormFieldOptions<T>) => FormField<T>
|